Skip to content

GC: { ...src } with an accessor source SIGSEGVs under PERRY_GC_MOVING_LOOP_POLLS=1 #7200

Description

@proggeramlug

Summary

{ ...src, k: v } where src carries an accessor SIGSEGVs (exit=139) under PERRY_GC_MOVING_LOOP_POLLS=1. Clean under the shipped default (polls off since #7161). A lighter variant of the same program does not crash but silently loses the copied accessor value 10 times in 400.

This is one of the residual sites of the #7154 class that #7184 and #7192 did not close, and it is a hard blocker for reverting #7161.

Reproducer

function churn(): number {
  const a: any[] = [];
  for (let i = 0; i < 600; i++) a.push({ i: i, s: "z" });
  return a.length;
}
const seen: any[] = [];
for (let r = 0; r < 400; r++) {
  const src: any = { plain: 5 };
  Object.defineProperty(src, "hot", { enumerable: true, get: function () { return churn(); } });
  const out: any = { ...src, tail: 7 };
  if (out.hot !== 600) seen.push(r + ":" + String(out.hot) + ":" + typeof out.hot);
}
console.log("bad", seen.length, seen.slice(0, 6).join(" "));
node --experimental-strip-types  →  bad 0
perry, default                   →  bad 0
perry, PERRY_GC_MOVING_LOOP_POLLS=1 (compile AND run)  →  exit=139, no output

Without the string building in the loop (console.log("plain", bp, "hot", bh, "tail", bt) counting each field separately) it does not crash, and reports plain 0 hot 10 tail 0 — so the destination object is intact and it is specifically the accessor's copied value that is lost.

Reproduced identically at 73a9084ea (before #7184 and #7192), so it is inherited, not caused by either.

Where it is

Not Expr::ObjectSpread — that arm is reached only from a JSX spread attribute (crates/perry-hir/src/jsx.rs:67, its sole construction site). Since #809 an object literal containing a spread lowers to a source-ordered IIFE built on js_object_assign_one (crates/perry-hir/src/lower/expr_object.rs:844), and that is the path here.

The accumulator in Expr::ObjectAssign's lowering (crates/perry-codegen/src/expr/logical_collections.rs) is threaded through a bare SSA register across each lower_expr(ctx, src) and across each js_object_assign_one call:

let mut acc = ctx.block().call(DOUBLE, "js_object_assign_validate_target", &[(DOUBLE, &target_box)]);
for src in sources {
    let src_box = lower_expr(ctx, src)?;
    acc = ctx.block().call(DOUBLE, "js_object_assign_one", &[(DOUBLE, &acc), (DOUBLE, &src_box)]);
}

js_object_assign_one reads every own key of the source, so an accessor there runs arbitrary user code inside the helper, which under polls can reach a back-edge poll and an evacuating minor with the caller's frame live. That is the same shape #7192 fixed for Expr::Object/ObjectSpread/ClassExprFresh, applied to the arm the syntax actually takes.

The evidence that the object is fine but the accessor's value is not points one layer further in — at the runtime helper's own handling of the value the getter returned — so the fix may be on both sides. js_object_assign_one's RuntimeHandleScope usage is the place to look.

Why it matters

{ ...props } is ubiquitous. Until this is closed, PERRY_GC_MOVING_LOOP_POLLS=1 cannot become the default again, i.e. #7161 cannot be reverted and #7019's minor-GC RSS win stays reverted.

Refs #7154, #7161, #7192, #809.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions