Skip to content

GC: a class expression with a static { … } block SIGSEGVs under PERRY_GC_MOVING_LOOP_POLLS=1 (static-this cell is not a rewritten root) #7201

Description

@proggeramlug

Summary

A class expression carrying a static { … } block SIGSEGVs (exit=139) under PERRY_GC_MOVING_LOOP_POLLS=1 when the block's body allocates. Clean under the shipped default. A variant that captures an outer value does not crash but computes the wrong static value 4 times in 400.

This is a residual site of the #7154 class that #7192 did not close, and 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: "q" });
  return a.length;
}
function make(): any {
  return class {
    static k: number = 1;
    static {
      (this as any).viaBlock = churn();
    }
  };
}
let bad = 0;
for (let r = 0; r < 400; r++) {
  const C: any = make();
  if (C.k !== 1) bad++;
  if (C.viaBlock !== 600) bad++;
}
console.log("bad", bad);
node --experimental-strip-types  →  bad 0
perry, default                   →  bad 0
perry, PERRY_GC_MOVING_LOOP_POLLS=1 (compile AND run)  →  exit=139, no output

Reproduced identically at 73a9084ea, so inherited, not caused by #7184/#7192.

A capturing variant (make(tag) with churn() + tag) does not crash and reports viaBlock 4 wrong out of 400 — that one already pushed a root before #7198 via the !captured_args.is_empty() clause, which is the evidence that the class object is not the stale thing here.

Where it is

Not the class object. #7198 adds !block_fns.is_empty() to Expr::ClassExprFresh's protection predicate, and the emitted IR confirms the object is now temp-rooted at allocation and re-read before every use:

%r18 = call i64 @js_object_alloc(i32 3, i32 1)
call void @js_object_mark_class(i64 %r18)
call void @js_class_object_pin_parent(i64 %r18, i32 3)
%r19 = call i32 @js_gc_temp_root_push(i64 %r18)
...
%r33 = call i64 @js_gc_temp_root_get(i32 %r19)      ; re-read before arming
%r35 = <nanbox %r33>
call void @js_static_this_arm_value(double %r35)
%r36 = call double @perry_static_..._init_0()        ; the block body — user code
%r37 = call i64 @js_gc_temp_root_get(i32 %r19)      ; re-read after

The crash survives that. What is still stale is the value js_static_this_arm_value parks in the runtime's one-shot static-this cell: the compiled block body reads it back through js_static_this_resolve, and if the object relocates between the arm and the resolve — or between the resolve and the body's this.x = … stores — the cell hands out a from-space address. That cell needs to be a registered, rewritten root (mark and rewrite, like RuntimeHandleScope at gc/roots/runtime_handles.rs:258-275), not a plain thread-local word.

crates/perry-runtime/src/object/... js_static_this_arm_value / js_static_this_resolve are the entry points.

Why it matters

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

Refs #7154, #7161, #7192, #685.

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