You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cc auth-error path: Cannot read properties of undefined (reading 'def') where node reports Not logged in #9417
With #9407 making --input-format stream-json functional, the unauthenticated path now runs — and diverges:
node : assistant/result text = "Not logged in · Please run /login"
error: authentication_failed, terminal_reason: completed
perry: assistant/result text = "Cannot read properties of undefined (reading 'def')"
terminal_reason: model_error
Record shape, record count and exit code all match — only the content differs, so this is a genuine downstream defect rather than a framing problem: something reads .def off an undefined value on cc's auth-error path under perry.
A user who is simply logged out is told about an internal property access instead, and the result is classified as model_error rather than authentication_failed — which would also mislead any automated retry logic keying on that field.
Uncovered by #9407; filed separately because it is a different bug from the three that PR fixes.
Root-caused. .def is zod v4's _zod.def, and the defect is a codegen precise-root gap — not an auth-path bug at all.
What .def is
.def is the $ZodType definition record ({type, shape, checks, catchall, …}) that zod's $constructor (b1 in the bundle) stores on every schema's non-enumerable _zod internals bag. 70 .def occurrences in the bundle; essentially all are q._zod.def. The container that should have held it is q._zod on a zod schema object.
Proven, not inferred: gdb on a PERRY_KEEP_SYMBOLS=1 build caught the fault with rdi → "extend" and rsi = 6. That is ZodObject.extend (q.extend=(_)=>K4.extend(q,_)), whose first statement after its guard is {...q._zod.def, …}. cc reaches it because it converts every tool's zod inputSchema to JSON Schema while building the request — node does this too; node simply gets to the SDK's x-api-key auth error afterwards. So the auth path is a red herring: this is a request-build-path defect that any run reaches.
The upstream divergence
An evacuating young-gen minor moved the object and the holder kept the pre-collection address.
Nothing crashes at the move. js_object_get_own_field_or_undef fails its obj_type == GC_TYPE_OBJECT check on the recycled cell and returns TAG_UNDEFINED, so the read answers undefined silently and the next member access throws — naming a property several steps downstream of the actual defect. That is why the symptom looked like an auth-path data bug.
PERRY_GC_PROTECT_FROMSPACE=1 faults directly on the stale use: "This address is RETIRED FROM-SPACE… user_ptr=…, obj_type=2, size=296" — a 296-byte GC_TYPE_OBJECT, i.e. a zod schema instance — at cmpb $0x2,-0x8(%rbx), the GC-header read.
In perry_closure_cli_2_1_112_js__57014 (base 0xf65b40, 3584 bytes): the receiver is stored to -0x50(%rbp), the argument object literal is built by call …AnonShape_508648973f25c6bf_constructor at +0xd2a, the receiver is reloaded from -0x50(%rbp) at +0xd50, and the probe is called at +0xd6f.
That function contains zero js_shadow_slot_bind / js_shadow_frame_enter calls — while the binary holds 16211 of them — yet it allocates (two object-literal constructors, js_object_alloc_class_inline_keys_stamped, js_array_from_values) and makes 13 method calls. So collect_pointer_typed_locals returned empty for it, enable_shadow_frame(0) ran, and the .extend receiver lives in a plain %rbp slot the collector never sees and never rewrites.
Residual root cause = a codegen precise-root gap, in crates/perry-codegen/src/collectors/pointer_locals.rs and codegen/closure.rs:612 — the same family as the in-tree a_coalesced_optional_chain_read_keeps_its_shadow_slot regression test. Fixing it needs the HIR shape of that closure, which is separate work in a different subsystem, so I am leaving this issue open rather than closing it against a partial fix.
Both strong priors were wrong
Error-name enumerability (#9440) is unrelated — no error object is serialized here; the message is the divergence. Tag ambiguity (0x7FFE/0x7FFC) is unrelated — the value is a genuine object pointer, just a retired one.
What was fixed
PR #9444 lands a real, independently demonstrated defect found on the way: invoke_accessor_getter/Setter held prev (and three sibling values) as bare Rust locals across user code that allocates, installing a retired from-space address as the caller's this. Its fixture reproduces cc's exact message deterministically, with no GC knobs. It is not this bug — base vs fixed cc binaries show no measurable change (8/8 vs 8/8 in one batch; the rate swings with machine load), and PERRY_GC_MOVING_LOOP_POLLS=0 still corrects the fixed binary 6/6.
Reproduction artifacts
Kept on the build host at /root/claude-9417/: app_sym (base) and app_fix cc binaries with symbols, symbolize.py and gdbloop.sh for reproducing the backtrace.
Fix is up: PR #9479. cc-level: 49/62 bad runs on base → 0/74 on the fixed binary, all 74 reaching node's Not logged in.
One correction to my earlier diagnosis comment, because it would mislead the next person: the "zero js_shadow_slot_bind calls ⇒ collector returned empty" inference was a vacuity trap — under the shipping native-roots lowering, binds are deleted and roots live in .perry_gcmap statepoints, so zero binds is what a correctly rooted function looks like (only 3 of 141,217 cc functions have any). native_root_coverage/mod.rs's own module doc warns about exactly this. The stack map shows the function was rooted fine; what was missing was one slot — an expression temporary, the receiver of a named-property dynamic dispatch, held unrooted across its own argument list (gc-rooting-invariant.md case 3, fixed for computed keys in #7210(3), never for named dispatch). Blast radius 14,002 cc functions (9.9%).
collect_pointer_locals and closure.rs:612 are correct code; nothing in the shadow-frame machinery was at fault.
Leaving this open until #9479 merges; it closes via the PR.
Activity
proggeramlug commentedon Sep 1, 2026
Root-caused.
.defis zod v4's_zod.def, and the defect is a codegen precise-root gap — not an auth-path bug at all.What
.defis.defis the$ZodTypedefinition record ({type, shape, checks, catchall, …}) that zod's$constructor(b1in the bundle) stores on every schema's non-enumerable_zodinternals bag. 70.defoccurrences in the bundle; essentially all areq._zod.def. The container that should have held it isq._zodon a zod schema object.Proven, not inferred: gdb on a
PERRY_KEEP_SYMBOLS=1build caught the fault withrdi→"extend"andrsi = 6. That isZodObject.extend(q.extend=(_)=>K4.extend(q,_)), whose first statement after its guard is{...q._zod.def, …}. cc reaches it because it converts every tool's zodinputSchemato JSON Schema while building the request — node does this too; node simply gets to the SDK'sx-api-keyauth error afterwards. So the auth path is a red herring: this is a request-build-path defect that any run reaches.The upstream divergence
An evacuating young-gen minor moved the object and the holder kept the pre-collection address.
Nothing crashes at the move.
js_object_get_own_field_or_undeffails itsobj_type == GC_TYPE_OBJECTcheck on the recycled cell and returnsTAG_UNDEFINED, so the read answersundefinedsilently and the next member access throws — naming a property several steps downstream of the actual defect. That is why the symptom looked like an auth-path data bug.Evidence:
PERRY_GC_MOVING_LOOP_POLLS=0→ 6/6 correct.PERRY_GC_SCAVENGE_NURSERY_MB=64/512→ 0 bad. Baseline → 4/6 bad.PERRY_GC_PROTECT_FROMSPACE=1faults directly on the stale use: "This address is RETIRED FROM-SPACE… user_ptr=…, obj_type=2, size=296" — a 296-byteGC_TYPE_OBJECT, i.e. a zod schema instance — atcmpb $0x2,-0x8(%rbx), the GC-header read.js_object_get_field_ic_miss→get_field_by_name_object_tail→builtin_reflection_accessor_read→invoke_accessor_getter→ zod'sK4.defineLazy(q,"shape",…)getter → 4 JS frames → the faulting probe.The residual root cause
In
perry_closure_cli_2_1_112_js__57014(base0xf65b40, 3584 bytes): the receiver is stored to-0x50(%rbp), the argument object literal is built bycall …AnonShape_508648973f25c6bf_constructorat +0xd2a, the receiver is reloaded from-0x50(%rbp)at +0xd50, and the probe is called at +0xd6f.That function contains zero
js_shadow_slot_bind/js_shadow_frame_entercalls — while the binary holds 16211 of them — yet it allocates (two object-literal constructors,js_object_alloc_class_inline_keys_stamped,js_array_from_values) and makes 13 method calls. Socollect_pointer_typed_localsreturned empty for it,enable_shadow_frame(0)ran, and the.extendreceiver lives in a plain%rbpslot the collector never sees and never rewrites.Residual root cause = a codegen precise-root gap, in
crates/perry-codegen/src/collectors/pointer_locals.rsandcodegen/closure.rs:612— the same family as the in-treea_coalesced_optional_chain_read_keeps_its_shadow_slotregression test. Fixing it needs the HIR shape of that closure, which is separate work in a different subsystem, so I am leaving this issue open rather than closing it against a partial fix.Both strong priors were wrong
Error-
nameenumerability (#9440) is unrelated — no error object is serialized here; the message is the divergence. Tag ambiguity (0x7FFE/0x7FFC) is unrelated — the value is a genuine object pointer, just a retired one.What was fixed
PR #9444 lands a real, independently demonstrated defect found on the way:
invoke_accessor_getter/Setterheldprev(and three sibling values) as bare Rust locals across user code that allocates, installing a retired from-space address as the caller'sthis. Its fixture reproduces cc's exact message deterministically, with no GC knobs. It is not this bug — base vs fixed cc binaries show no measurable change (8/8 vs 8/8 in one batch; the rate swings with machine load), andPERRY_GC_MOVING_LOOP_POLLS=0still corrects the fixed binary 6/6.Reproduction artifacts
Kept on the build host at
/root/claude-9417/:app_sym(base) andapp_fixcc binaries with symbols,symbolize.pyandgdbloop.shfor reproducing the backtrace.fix(runtime): an accessor call must not corrupt the caller's `this` (…
changelog: PerryTS#9417 dispatch-receiver roots
proggeramlug commentedon Sep 2, 2026
Fix is up: PR #9479. cc-level: 49/62 bad runs on base → 0/74 on the fixed binary, all 74 reaching node's
Not logged in.One correction to my earlier diagnosis comment, because it would mislead the next person: the "zero
js_shadow_slot_bindcalls ⇒ collector returned empty" inference was a vacuity trap — under the shipping native-roots lowering, binds are deleted and roots live in.perry_gcmapstatepoints, so zero binds is what a correctly rooted function looks like (only 3 of 141,217 cc functions have any).native_root_coverage/mod.rs's own module doc warns about exactly this. The stack map shows the function was rooted fine; what was missing was one slot — an expression temporary, the receiver of a named-property dynamic dispatch, held unrooted across its own argument list (gc-rooting-invariant.mdcase 3, fixed for computed keys in #7210(3), never for named dispatch). Blast radius 14,002 cc functions (9.9%).collect_pointer_localsandclosure.rs:612are correct code; nothing in the shadow-frame machinery was at fault.Leaving this open until #9479 merges; it closes via the PR.
fix(codegen): root the dynamic-dispatch receiver across its argument …
fix(codegen): PropertySet rejections honour sloppy mode (#9459); a wr…
fix(runtime): root every saved implicit-`this` across the user code i…
fix(runtime): root every saved implicit-`this` across the user code i…