From 4cd5ef6d53872bac9d917e1517f2e5fee5206e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 5 Aug 2026 18:07:32 +0200 Subject: [PATCH 1/2] fix(codegen): root the URL setters and forEach operands; url_main.rs is clean Closes the last two shapes in this file. The nine URL setters share one match arm, so a single fix covers pathname/search/hash/protocol/hostname/port/username/password/href: the receiver was unboxed to a raw heap pointer and then `value` lowered -- arbitrary user code -- before the runtime call used it. forEach had TWO windows, not one: the receiver crossed the `callback` lowering, and both it and the callback crossed `this_arg`'s. All operands are rooted together, `this_arg` included when present. Arm-aware audit of the whole file now reports: 9 guards, each with exactly one top-level release 0 remaining raw-pointer-across-lowering sites The earlier scan's "9 remaining" included three false positives -- it crossed match-arm boundaries and paired a receiver with the NEXT arm's lower_expr. Entries/Keys/Values use their receiver immediately. URL-family gap tests byte-identical to node; a setters+forEach repro with allocating arguments matches node and is clean under PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FORCE_EVACUATE=1. --- crates/perry-codegen/src/expr/url_main.rs | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/perry-codegen/src/expr/url_main.rs b/crates/perry-codegen/src/expr/url_main.rs index 45b0ba58b7..94b9a9fdae 100644 --- a/crates/perry-codegen/src/expr/url_main.rs +++ b/crates/perry-codegen/src/expr/url_main.rs @@ -158,11 +158,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::UrlSetHref { .. } => "js_url_set_href", _ => unreachable!(), }; - let url_v = lower_expr(ctx, url)?; + // Same window as the URLSearchParams family (#7462/#7463), and it + // covers all nine setters at once: `url_handle` is a raw heap + // pointer and lowering `value` runs arbitrary user code that can + // collect. Root both, unbox from the reloaded receiver. + let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[url, value])?; + let (url_v, val_v) = (vals[0].clone(), vals[1].clone()); let url_handle = unbox_to_i64(ctx.block(), &url_v); - let val_v = lower_expr(ctx, value)?; ctx.block() .call_void(runtime_fn, &[(I64, &url_handle), (DOUBLE, &val_v)]); + super::temp_root::temp_root_release(ctx, operand_guard); // Assignment expression evaluates to the value on the RHS. Ok(val_v) } @@ -533,11 +538,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { callback, this_arg, } => { - let p_v = lower_expr(ctx, params)?; + // Two windows here, not one: `p_ptr` crosses the `callback` + // lowering, and both it and `cb_v` cross `this_arg`'s. Root every + // operand together and unbox from the reloaded receiver. + let mut operand_exprs: Vec<&Expr> = vec![params, callback]; + if let Some(this_arg) = this_arg { + operand_exprs.push(this_arg); + } + let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &operand_exprs)?; + let (p_v, cb_v) = (vals[0].clone(), vals[1].clone()); let p_ptr = unbox_to_i64(ctx.block(), &p_v); - let cb_v = lower_expr(ctx, callback)?; - let this_v = if let Some(this_arg) = this_arg { - lower_expr(ctx, this_arg)? + let this_v = if this_arg.is_some() { + vals[2].clone() } else { double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) }; @@ -545,6 +557,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_url_search_params_for_each", &[(I64, &p_ptr), (DOUBLE, &cb_v), (DOUBLE, &this_v)], ); + super::temp_root::temp_root_release(ctx, operand_guard); Ok(ctx .block() .bitcast_i64_to_double(crate::nanbox::TAG_UNDEFINED_I64)) From f693dc0593a586db6f5bd3b1bfe8c76011bcaa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 5 Aug 2026 18:07:57 +0200 Subject: [PATCH 2/2] docs: changelog fragment for 7464 --- changelog.d/7464-url-setters-rooting.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7464-url-setters-rooting.md diff --git a/changelog.d/7464-url-setters-rooting.md b/changelog.d/7464-url-setters-rooting.md new file mode 100644 index 0000000000..81bc2605b4 --- /dev/null +++ b/changelog.d/7464-url-setters-rooting.md @@ -0,0 +1 @@ +- **The nine URL setters and `URLSearchParams.forEach` no longer hold their receiver across user code.** The setters share one match arm, so a single fix covers `pathname`/`search`/`hash`/`protocol`/`hostname`/`port`/`username`/`password`/`href`: the receiver was unboxed to a raw heap pointer before `value` lowered. `forEach` had two windows — the receiver crossed the `callback` lowering, and both it and the callback crossed `this_arg`'s. All operands are now rooted together. An arm-aware audit of `url_main.rs` reports 9 guards each with exactly one top-level release and **0** remaining raw-pointer-across-lowering sites; the earlier "9 remaining" count included three false positives from a scan that crossed match-arm boundaries. 12/12 URL-family gap tests byte-identical to node; clean under forced evacuation. (#7464)