Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/7462-searchparams-receiver-rooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Six `URLSearchParams` lowerings held the receiver across arbitrary user code.** `Get`, `Has`, `Set`, `Append`, `Delete` and `GetAll` each unboxed the params object to a raw heap pointer and then lowered the `name` argument — which runs user code that can collect — before using it, the same shape as #7453 and confirmed in emitted IR. Moving the unbox below the lowering does not fix it: the NaN-boxed value is the same pointer with a tag on it, and equally invisible to the collector. Both operands now lower through `lower_exprs_rooted` and the receiver is unboxed from the reloaded value, with the guard released after the consuming call. 12/12 URL and URLSearchParams gap tests byte-identical to the Node oracle; clean under forced evacuation. Five further sites of this shape remain in `url_main.rs` with different structures. (#7462)
72 changes: 60 additions & 12 deletions crates/perry-codegen/src/expr/url_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}

Expr::UrlSearchParamsGet { params, name } => {
let p_v = lower_expr(ctx, params)?;
// #7453's shape: `p_ptr` — and the tagged `p_v` it is masked from —
// is a heap pointer, and lowering `name` runs arbitrary user code
// that can collect. Root both operands first and unbox from the
// reloaded receiver, so neither crosses the window in a register.
// The guard lives to the end of the arm: every use below is a use
// of one of the two rooted values.
let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[params, name])?;
let (p_v, n_v) = (vals[0].clone(), vals[1].clone());
let p_ptr = unbox_to_i64(ctx.block(), &p_v);
let n_v = lower_expr(ctx, name)?;
let str_ptr = ctx.block().call(
I64,
"js_url_search_params_get",
&[(I64, &p_ptr), (DOUBLE, &n_v)],
);
// Released after the consuming call, which itself allocates.
super::temp_root::temp_root_release(ctx, operand_guard);
// Runtime returns a null pointer when the key is absent;
// JS expects `null` in that case, not an empty string.
let blk = ctx.block();
Expand All @@ -321,9 +329,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
name,
value,
} => {
let p_v = lower_expr(ctx, params)?;
// #7453's shape: `p_ptr` — and the tagged `p_v` it is masked from —
// is a heap pointer, and lowering `name` runs arbitrary user code
// that can collect. Root both operands first and unbox from the
// reloaded receiver, so neither crosses the window in a register.
// The guard lives to the end of the arm: every use below is a use
// of one of the two rooted values.
let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[params, name])?;
let (p_v, n_v) = (vals[0].clone(), vals[1].clone());
let p_ptr = unbox_to_i64(ctx.block(), &p_v);
let n_v = lower_expr(ctx, name)?;
// Runtime returns 0.0 / 1.0 as a plain f64 — not NaN-boxed.
// Translate to TAG_TRUE / TAG_FALSE so `typeof` and strict-eq
// behave correctly.
Expand All @@ -341,6 +355,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &p_ptr), (DOUBLE, &n_v)],
)
};
// Released after the consuming call, which itself allocates.
super::temp_root::temp_root_release(ctx, operand_guard);
let blk = ctx.block();
let is_true = blk.fcmp("une", &raw, &double_literal(0.0));
let tagged = blk.select(
Expand All @@ -358,14 +374,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
name,
value,
} => {
let p_v = lower_expr(ctx, params)?;
// #7453's shape: `p_ptr` — and the tagged `p_v` it is masked from —
// is a heap pointer, and lowering `name` runs arbitrary user code
// that can collect. Root both operands first and unbox from the
// reloaded receiver, so neither crosses the window in a register.
// The guard lives to the end of the arm: every use below is a use
// of one of the two rooted values.
let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[params, name])?;
let (p_v, n_v) = (vals[0].clone(), vals[1].clone());
let p_ptr = unbox_to_i64(ctx.block(), &p_v);
let n_v = lower_expr(ctx, name)?;
let val_v = lower_expr(ctx, value)?;
ctx.block().call_void(
"js_url_search_params_set",
&[(I64, &p_ptr), (DOUBLE, &n_v), (DOUBLE, &val_v)],
);
// Released after the consuming call, which itself allocates.
super::temp_root::temp_root_release(ctx, operand_guard);
Ok(ctx
.block()
.bitcast_i64_to_double(crate::nanbox::TAG_UNDEFINED_I64))
Expand All @@ -376,14 +400,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
name,
value,
} => {
let p_v = lower_expr(ctx, params)?;
// #7453's shape: `p_ptr` — and the tagged `p_v` it is masked from —
// is a heap pointer, and lowering `name` runs arbitrary user code
// that can collect. Root both operands first and unbox from the
// reloaded receiver, so neither crosses the window in a register.
// The guard lives to the end of the arm: every use below is a use
// of one of the two rooted values.
let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[params, name])?;
let (p_v, n_v) = (vals[0].clone(), vals[1].clone());
let p_ptr = unbox_to_i64(ctx.block(), &p_v);
let n_v = lower_expr(ctx, name)?;
let val_v = lower_expr(ctx, value)?;
ctx.block().call_void(
"js_url_search_params_append",
&[(I64, &p_ptr), (DOUBLE, &n_v), (DOUBLE, &val_v)],
);
// Released after the consuming call, which itself allocates.
super::temp_root::temp_root_release(ctx, operand_guard);
Ok(ctx
.block()
.bitcast_i64_to_double(crate::nanbox::TAG_UNDEFINED_I64))
Expand All @@ -394,9 +426,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
name,
value,
} => {
let p_v = lower_expr(ctx, params)?;
// #7453's shape: `p_ptr` — and the tagged `p_v` it is masked from —
// is a heap pointer, and lowering `name` runs arbitrary user code
// that can collect. Root both operands first and unbox from the
// reloaded receiver, so neither crosses the window in a register.
// The guard lives to the end of the arm: every use below is a use
// of one of the two rooted values.
let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[params, name])?;
let (p_v, n_v) = (vals[0].clone(), vals[1].clone());
let p_ptr = unbox_to_i64(ctx.block(), &p_v);
let n_v = lower_expr(ctx, name)?;
if let Some(v_expr) = value {
let v_v = lower_expr(ctx, v_expr)?;
ctx.block().call_void(
Expand All @@ -408,6 +446,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
"js_url_search_params_delete",
&[(I64, &p_ptr), (DOUBLE, &n_v)],
);
// Released after the consuming call, which itself allocates.
super::temp_root::temp_root_release(ctx, operand_guard);
}
Ok(ctx
.block()
Expand Down Expand Up @@ -484,16 +524,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}

Expr::UrlSearchParamsGetAll { params, name } => {
let p_v = lower_expr(ctx, params)?;
// #7453's shape: `p_ptr` — and the tagged `p_v` it is masked from —
// is a heap pointer, and lowering `name` runs arbitrary user code
// that can collect. Root both operands first and unbox from the
// reloaded receiver, so neither crosses the window in a register.
// The guard lives to the end of the arm: every use below is a use
// of one of the two rooted values.
let (vals, operand_guard) = super::temp_root::lower_exprs_rooted(ctx, &[params, name])?;
let (p_v, n_v) = (vals[0].clone(), vals[1].clone());
let p_ptr = unbox_to_i64(ctx.block(), &p_v);
let n_v = lower_expr(ctx, name)?;
// Returns f64 with the raw array pointer bit-cast in; the runtime
// does not NaN-box it, so tag it here with POINTER_TAG.
let raw_f64 = ctx.block().call(
DOUBLE,
"js_url_search_params_get_all",
&[(I64, &p_ptr), (DOUBLE, &n_v)],
);
// Released after the consuming call, which itself allocates.
super::temp_root::temp_root_release(ctx, operand_guard);
let bits = ctx.block().bitcast_double_to_i64(&raw_f64);
Ok(nanbox_pointer_inline(ctx.block(), &bits))
}
Expand Down
Loading