Skip to content

SIGSEGV: proxy construct-trap bad-return + later instanceof/method on Proxy(class) instance (GC side-table scanner hole, #5937) #5976

Description

@TheHypnoo

Summary

A SIGSEGV (rc 139) occurs when a proxy construct trap that returns a non-object (which correctly throws a TypeError) is followed by an instanceof and a method call on an instance built via new Proxy(<class>, {}). Individually each step is fine; the crash needs the specific sequence, and it does not fire before the descriptor/proxy GC-scanner landed — so it looks like a hole in that scanner.

Minimal repro

function show(label: string, fn: () => unknown) {
  try { console.log(label, "ok", JSON.stringify(fn())); }
  catch (e: any) { console.log(label, "throw", e?.constructor?.name); }
}

// A proxy whose construct trap returns a non-object → TypeError (per spec).
const badReturn: any = new Proxy(function BadReturn() {}, { construct() { return 1 as any; } });
show("badret", () => Reflect.construct(badReturn, []));   // prints "throw TypeError"

class Thing { value: string; constructor(v: string) { this.value = v; } method() { return "m:" + this.value; } }

show("case9", () => {
  const W: any = new Proxy(Thing, {});
  const i: any = new W("x");
  return [i instanceof Thing, i.method()];   // ← SIGSEGV here
});
console.log("END");   // never reached under Perry

Perry prints badret throw TypeError then segfaults (rc 139); Node prints both lines + END.

Narrowing (all verified):

  • Remove the badReturn construct-trap-throw case → no crash.
  • Change case9 to return i.value only (drop instanceof/method) → no crash.
  • i instanceof Thing alone, or i.method() alone → no crash. Only the two together after the bad-return throw crash.
  • Replacing new Proxy(Thing, {}) with a plain new Thing(...) → no crash.

The "needs a specific op sequence + a prior throwing path + GC-triggering work" signature is a classic dangling-GC-root / stale-side-table-pointer.

Suspected cause

#5937 (fix(gc): scan descriptor/proxy side tables; root static-field globals, 53e4336) added the proxy/descriptor side-table scanners. This repro lives squarely in that path: the PROXIES side table holds ProxyEntry { target, handler } (raw NaN-box bits), the bad-return construct throws mid-trap, and the later Proxy(class) construct + instanceof/method triggers a minor GC whose scan of the proxy/descriptor tables appears to leave a stale/rewritten pointer that the subsequent access dereferences. Likely an incomplete rewrite (a target/handler slot, or a descriptor owner key, not fixed up after the throwing trap).

Provenance

Reproduces on origin/main (e2815e3), rc 139 — NOT specific to any feature branch. Surfaces the node-suite case test-parity/node-suite/object/reflect-proxy-construct.ts (object module 24→23). Does NOT reproduce at the pre-#5937 baseline bfa0f258d (object 24/24), so it was introduced between that point and current main — the #5937 scanner is the prime suspect.

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