Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions changelog.d/8930-elf-archive-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Fixed ELF links that died with `undefined reference to
<futures_channel::mpsc::SenderTask>::notify` out of a well-known wrapper
archive when a compiled package pulled in `bundled-streams`.

GNU `ld` scans each archive on the command line exactly once, left to right,
and the perry archive block is mutually recursive: the wrapper archives listed
*before* perry-stdlib resolve nothing (the user objects reference only stdlib
symbols), every wrapper member is pulled from the repeat *after* stdlib on
references stdlib itself opened, and those members then reference back into
stdlib — which `ld` has already walked past. That stayed invisible until
shared-dependency pruning correctly dropped a bundled member because stdlib
exports it. The archive block is now wrapped in `-Wl,--start-group` /
`-Wl,--end-group` on ELF targets so the linker re-scans it to a fixed point.
Symbol precedence is unchanged and non-ELF link lines are untouched; a link
that already resolved produces a byte-identical executable.
6 changes: 6 additions & 0 deletions changelog.d/9289-scrypt-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix a silent security downgrade in `node:crypto` scrypt: the callback form now
forwards `N`/`cost`, `r`/`blockSize`, `p`/`parallelization`, and `maxmem`
instead of always deriving with Node's defaults. Both callback and synchronous
forms now reject invalid combinations and insufficient `maxmem` with a
Node-compatible `RangeError`, rather than silently substituting the weaker
default work factor.
21 changes: 10 additions & 11 deletions crates/perry-codegen/src/expr/calls/crypto_kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,13 @@ pub(crate) fn arm_crypto_scrypt(
let pwd_box = lower_expr(ctx, &args[0])?;
let salt_box = lower_expr(ctx, &args[1])?;
let len_box = lower_expr(ctx, &args[2])?;
let cb_expr = if args.len() >= 5 {
let _ = lower_expr(ctx, &args[3])?;
&args[4]
let (opts_box, cb_expr) = if args.len() >= 5 {
(lower_expr(ctx, &args[3])?, &args[4])
} else {
&args[3]
(
double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)),
&args[3],
)
};
let cb_box = lower_expr(ctx, cb_expr)?;
let blk = ctx.block();
Expand All @@ -144,6 +146,7 @@ pub(crate) fn arm_crypto_scrypt(
(I64, &pwd_handle),
(I64, &salt_handle),
(DOUBLE, &len_box),
(DOUBLE, &opts_box),
(DOUBLE, &cb_box),
],
))
Expand Down Expand Up @@ -243,27 +246,23 @@ pub(crate) fn arm_crypto_scrypt_sync(
let salt_box = lower_expr(ctx, &args[1])?;
let keylen_box = lower_expr(ctx, &args[2])?;
let opts_box = if args.len() >= 4 {
Some(lower_expr(ctx, &args[3])?)
lower_expr(ctx, &args[3])?
} else {
None
double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
};
// #2013/#3146: node validates keylen as an integer in [0, 2^31-1].
emit_validate_integer_arg(ctx, &keylen_box, "keylen", 0.0, i32::MAX as f64);
let blk = ctx.block();
let pwd_handle = unbox_to_i64(blk, &pwd_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let opts_handle = match &opts_box {
Some(b) => unbox_to_i64(blk, b),
None => "0".to_string(),
};
let buf_handle = blk.call(
I64,
"js_crypto_scrypt_bytes",
&[
(I64, &pwd_handle),
(I64, &salt_handle),
(DOUBLE, &keylen_box),
(I64, &opts_handle),
(DOUBLE, &opts_box),
],
);
Ok(nanbox_pointer_inline(blk, &buf_handle))
Expand Down
9 changes: 6 additions & 3 deletions crates/perry-codegen/src/lower_call/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1061,12 +1061,14 @@ pub(super) fn lower_builtin_new<'a>(
// `new Response(res.body, res)` header re-wrap — is drained to its
// bytes instead of stringified to its numeric stream handle.
// Non-stream bodies coerce exactly as get_raw_string_ptr did.
let body_ptr = if !args.is_empty() {
let (body_ptr, body_is_string) = if !args.is_empty() {
let v = lower_expr(ctx, &args[0])?;
let blk = ctx.block();
blk.call(I64, "js_response_body_init_ptr", &[(DOUBLE, &v)])
let is_string = blk.call(I32, "js_nanbox_is_any_string", &[(DOUBLE, &v)]);
let body_ptr = blk.call(I64, "js_response_body_init_ptr", &[(DOUBLE, &v)]);
(body_ptr, is_string)
} else {
"0".to_string()
("0".to_string(), "0".to_string())
};

// Default init: status=200, statusText=null, headers=0
Expand Down Expand Up @@ -1164,6 +1166,7 @@ pub(super) fn lower_builtin_new<'a>(
(DOUBLE, &status_val),
(I64, &status_text_ptr),
(DOUBLE, &headers_handle),
(I32, &body_is_string),
],
);
// Response handle is a plain numeric f64 (response-registry id).
Expand Down
29 changes: 29 additions & 0 deletions crates/perry-codegen/src/lower_call/field_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,10 +693,39 @@ pub(crate) fn apply_field_initializers_recursive(
None => init_pairs.push((field.name.clone(), init, field.is_private)),
}
}
// #8962: an IMPORTED class installs nothing here. Its whole
// field-initializer phase — public field writes, private-field adds AND
// the shared private brand — is baked into the defining module's
// standalone `<prefix>__<class>_constructor`, which `codegen/method.rs`
// emits for exactly that reason ("At the `new ImportedClass(...)` call
// site, `lower_new` applies initializers against the imported class
// stub — which has none"). That premise holds for FIELDS because the
// stub flattens every field to `is_private: false` with `init: None`,
// so the worst this loop could do was write `undefined` into a slot the
// real constructor overwrites moments later.
//
// It does NOT hold for the private BRAND. The stub copies private
// METHOD and accessor names verbatim (it needs them to resolve dispatch
// symbols), and `has_private_instance_brand` is defined purely over
// `#`-prefixed method/getter/setter names — so a stub answers `true` and
// this site emitted `js_private_brand_add` at the importing module's
// `new`, on top of the one the defining module's constructor emits.
// Installing a class's brand twice on one object is the observable
// error PrivateMethodOrAccessorAdd requires, so the runtime threw
// "Cannot initialize private elements twice on the same object" out of
// `new Hono()` — any imported class with a private method or accessor,
// whether constructed directly or reached as an ancestor through
// `AncestorsOnly`.
//
// Suppressing BOTH flags (not just the brand) is what restores the
// `continue` below for a stub whose only private elements are methods:
// for a stub the two predicates are the same question, since its fields
// are never private.
let (class_has_private_elements, class_has_private_brand) = ctx
.classes
.get(&class_name_in_chain)
.copied()
.filter(|class| !class.is_imported_stub())
.map(|class| {
(
class.has_private_instance_elements(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pub fn declare_phase1(module: &mut LlModule) {
module.declare_function("js_nanbox_string", DOUBLE, &[I64]);
module.declare_function("js_nanbox_pointer", DOUBLE, &[I64]);
module.declare_function("js_nanbox_get_pointer", I64, &[DOUBLE]);
module.declare_function("js_nanbox_is_any_string", I32, &[DOUBLE]);
module.declare_function(
"js_native_handle_new_owned",
DOUBLE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,9 @@ pub(crate) fn declare_data_stores(module: &mut LlModule) {
module.declare_function("js_crypto_random_nonce", I64, &[]);
module.declare_function("js_crypto_scrypt", I64, &[I64, I64, DOUBLE]);
// crypto.scryptSync(password, salt, keylen, options?) -> Buffer. The 4th
// arg is the NaN-unboxed options-object pointer (0 = none).
module.declare_function("js_crypto_scrypt_bytes", I64, &[I64, I64, DOUBLE, I64]);
// arg is the full NaN-boxed options value so validation can distinguish
// objects, primitives, and undefined.
module.declare_function("js_crypto_scrypt_bytes", I64, &[I64, I64, DOUBLE, DOUBLE]);
// crypto.generateKeyPairSync(type, options) -> { publicKey, privateKey }.
module.declare_function("js_crypto_generate_key_pair_sync", DOUBLE, &[I64, I64]);
module.declare_function(
Expand Down
8 changes: 4 additions & 4 deletions crates/perry-codegen/src/runtime_decls/strings_part2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,11 +491,11 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
DOUBLE,
&[I64, I64, I64, I64, DOUBLE, DOUBLE],
);
module.declare_function("js_crypto_scrypt_bytes", I64, &[I64, I64, DOUBLE, I64]);
module.declare_function("js_crypto_scrypt_bytes", I64, &[I64, I64, DOUBLE, DOUBLE]);
module.declare_function(
"js_crypto_scrypt_async",
DOUBLE,
&[I64, I64, DOUBLE, DOUBLE],
&[I64, I64, DOUBLE, DOUBLE, DOUBLE],
);
module.declare_function("js_crypto_sign_rsa_sha256", I64, &[I64, I64, DOUBLE]);
module.declare_function("js_crypto_sign_async", DOUBLE, &[I64, I64, DOUBLE, DOUBLE]);
Expand Down Expand Up @@ -944,8 +944,8 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
// property access (`request.url` where `request: any`) routes through
// `js_object_get_field_by_name`'s strip-tag path → `HANDLE_PROPERTY_DISPATCH`.
// ──────────────────────────────────────────────────────────────────
// new Response(body_ptr, status, status_text_ptr, headers_handle) -> f64
module.declare_function("js_response_new", DOUBLE, &[I64, DOUBLE, I64, DOUBLE]);
// new Response(body_ptr, status, status_text_ptr, headers, body_is_string) -> f64
module.declare_function("js_response_new", DOUBLE, &[I64, DOUBLE, I64, DOUBLE, I32]);
// Normalize a Request/Response subclass object (for example NextResponse)
// to its native Fetch registry handle; bare handles pass through.
module.declare_function("js_fetch_unwrap_handle", DOUBLE, &[DOUBLE]);
Expand Down
14 changes: 9 additions & 5 deletions crates/perry-ext-fetch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,21 +1179,22 @@ pub extern "C" fn js_headers_entries(handle: f64) -> f64 {
// ── Response advanced ─────────────────────────────────────────────

/// `new Response(body, init)` — stores body string + status + statusText
/// + headers. The `headers_handle` arg matches perry-stdlib's 4-arg shape
/// (declared in `crates/perry-codegen/src/runtime_decls.rs:1045`); a
/// 3-arg version dropped the codegen-supplied headers handle on the
/// + headers. The arguments match perry-stdlib's constructor ABI
/// (declared in `crates/perry-codegen/src/runtime_decls/strings_part2.rs`);
/// a 3-arg version dropped the codegen-supplied headers handle on the
/// floor — `fetchRes.headers.forEach(...)` then iterated an empty map.
///
/// # Safety
/// All string pointers must be null or Perry-runtime `StringHeader`s;
/// `headers_handle` must be 0.0 / TAG_UNDEFINED or a valid handle id
/// returned by `js_headers_new`.
/// returned by `js_headers_new`; `body_is_string` is a boolean i32.
#[no_mangle]
pub unsafe extern "C" fn js_response_new(
body_ptr: *const StringHeader,
status: f64,
status_text_ptr: *const StringHeader,
headers_handle: f64,
body_is_string: i32,
) -> f64 {
let body_opt = read_str(body_ptr);
let body_present = body_opt.is_some();
Expand Down Expand Up @@ -1224,7 +1225,7 @@ pub unsafe extern "C" fn js_response_new(
));
}
let headers_id = handle_id(headers_handle);
let headers = if headers_id != 0 {
let mut headers = if headers_id != 0 {
HEADERS_HANDLES
.lock()
.unwrap()
Expand All @@ -1234,6 +1235,9 @@ pub unsafe extern "C" fn js_response_new(
} else {
HeadersStore::default()
};
if body_is_string != 0 && !headers.has("content-type") {
headers.set("content-type", "text/plain;charset=UTF-8");
}
store_response(FetchResponse {
status,
status_text,
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-ext-fetch/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ fn response_static_json() {
assert_eq!(status, 200.0);
}

#[test]
fn response_string_body_gets_default_content_type() {
let body = alloc_string("hello");
let response = unsafe { js_response_new(body.as_raw(), 0.0, std::ptr::null(), 0.0, 1) };
let headers = js_response_get_headers(response);
let key = alloc_string("content-type");
let value_ptr = unsafe { js_headers_get(headers, key.as_raw()) };
let value = perry_ffi::read_string(unsafe { JsString::from_raw(value_ptr) }).expect("header");
assert_eq!(value, "text/plain;charset=UTF-8");
}

// #1688: request.text()/.json()/.arrayBuffer() were unimplemented. The
// FFIs build a JsPromise (runtime symbols unavailable in the unittest
// binary, as with every other promise-returning fetch FFI), so this
Expand Down
21 changes: 21 additions & 0 deletions crates/perry-hir/src/ir/decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,27 @@ pub struct Class {
}

impl Class {
/// True for the metadata-only stub `compile_module` synthesizes for a class
/// IMPORTED from another module (`perry-codegen/src/codegen/mod.rs`, "Build
/// a stub Class with the minimum fields the codegen needs").
///
/// A stub is a NAME TABLE, not a class: it carries member names so the
/// importing module can resolve dispatch symbols, and carries no bodies, no
/// field initializers and no constructor. Everything a construction
/// actually *does* — field initializers, private-field adds, the private
/// brand — is baked into the defining module's standalone
/// `<prefix>__<class>_constructor` instead (`codegen/method.rs`,
/// `is_constructor_method`), precisely because the stub has none of it.
///
/// `id == 0` is the marker: the driver hands out class ids from 1
/// (`run_pipeline.rs`: "Start at 1, 0 is reserved for \"no parent\"") and
/// every local class takes its id from `LoweringContext::fresh_class`, so
/// the stub built at `codegen/mod.rs` ("id: 0, // imported — no local
/// ClassId") is the only `Class` in a module's class table with id 0.
pub fn is_imported_stub(&self) -> bool {
self.id == 0
}

/// Whether construction installs any instance-private element.
pub fn has_private_instance_elements(&self) -> bool {
self.fields.iter().any(|field| field.is_private)
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-hir/src/lower/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ mod expr_call;
pub(crate) mod expr_function;
pub(crate) use expr_function::capture_function_source;
mod expr_member;
pub(crate) use expr_member::{private_storage_property, wrap_private_guard, PRIV_OP_WRITE};
pub(crate) use expr_member::{
private_storage_property, wrap_private_guard, PRIV_OP_READ, PRIV_OP_WRITE,
};
mod expr_misc;
mod expr_new;
mod expr_new_builtins;
Expand Down
48 changes: 45 additions & 3 deletions crates/perry-hir/src/lower_patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//! parameter destructuring, and other pattern-related utilities.

use crate::ir::*;
use crate::lower::{lower_expr, LoweringContext};
use crate::lower::{
lower_expr, private_storage_property, wrap_private_guard, LoweringContext, PRIV_OP_READ,
};
use crate::lower_types::*;
use crate::types::{LocalId, Type};
use anyhow::{anyhow, Result};
Expand Down Expand Up @@ -244,9 +246,49 @@ pub(crate) fn lower_assign_target_to_expr(
Ok(Expr::IndexGet { object, index })
}
ast::MemberProp::PrivateName(private) => {
let property = format!("#{}", private.name);
// #8968: the READ half of a compound / logical assignment
// to a private member must lower exactly like an ordinary
// `this.#x` read in `expr_member/member_tail.rs` — brand-
// guard the receiver, and address the slot by its MANGLED
// storage key.
//
// This arm used to build `PropertyGet { property: "#x" }`
// from the source spelling. A private FIELD does not live
// under that key: `private_storage_property` stores it as
// `#<perry:private-value:{class_id}:#x>`, a mangling that
// exists precisely so a private field cannot collide with
// an ordinary computed property such as `obj["#x"]`. So
// the read missed every time and produced `undefined`,
// while the WRITE half — lowered through
// `lower_expr_assignment`, which does use the mangled key
// — landed correctly. Nothing threw; the value was simply
// wrong:
//
// this.#n += 1 // NaN, not n + 1
// this.#s += "b" // "undefinedb"
// this.#v ||= d // ALWAYS stored d (read was falsy)
// this.#v ??= d // ALWAYS stored d (read was nullish)
// this.#v &&= d // NEVER stored (read was falsy)
//
// hono is the reported case (#8968): its
// `get res() { return this.#res ||= new Response(null, …) }`
// meant every read of `c.res` replaced the finalized
// response with a fresh empty 200, so an unmatched route
// answered `200 ""` and a registered `app.notFound()`
// handler had its 404 thrown away after it had already run.
//
// The guard is also what makes the mangled key resolvable
// for a private METHOD or ACCESSOR: `js_private_guard`
// pushes the access hint that the runtime
// `private_member_get_by_name` consumes. It restores the
// spec-required TypeError for the read of a set-only
// private accessor (`set #x(v) {}` then `obj.#x ||= 1`)
// and for a compound assignment on a foreign receiver.
let private_name = format!("#{}", private.name);
let object = wrap_private_guard(ctx, object, &private_name, PRIV_OP_READ);
let property = private_storage_property(ctx, &private_name);
Ok(Expr::PropertyGet {
byte_offset: 0,
byte_offset: member.span.lo.0,
object,
property,
})
Expand Down
Loading