diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index e82227a3..80b07ba9 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4748,6 +4748,19 @@ cgexpr(Cg *c, Node *n, Local *locals) if (ru && ru->kind == TY_TAGGED && !ru->nullable && ru->size > 8) break; + /* C1b: a whole str/slice loaded BY VALUE + * through *str / *[]T — AX (the operand value) IS + * the 24B {ptr,len,cap} header address. The scalar + * load below pulled ONLY word0 (.ptr); .len/.cap + * were then stored from stale BX/CX, so len(*p) read + * garbage — byte-id-blind on both stages. Reuse the + * same 3-word header load as the slice-FIELD / + * N_INDEX str-element arms (cgslicehdr). */ + if (ru && (ru->kind == TY_STR + || ru->kind == TY_SLICE)) { + cgslicehdr(c, D_AX); + break; + } } /* f64/f32 result rides X0 (SSE), not AX — an integer * MOVQ strands the value off the float ABI and the diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 60b27f42..62634d90 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5525,6 +5525,19 @@ fn cgun(c: *cgen, n: *syntax.node) void = { if (rti != nil && rti.kind == syntax.tykind.TY_TAGGED) { if (rti.nullable == 0 && rti.size: i32 > 8) { return; }; }; + // C1b: a whole str/slice loaded BY VALUE through *str / + // *[]T — AX (the operand value) IS the 24B {ptr,len,cap} + // header address. The scalar load below pulled ONLY word0 + // (.ptr); .len/.cap were then stored from stale BX/CX, so + // len(*p) read garbage — byte-id-blind on both stages. Reuse + // the same 3-word header load as the slice-field / cgindex + // str-element arms. + if (rti != nil) { + if (rti.kind == syntax.tykind.TY_STR || rti.kind == syntax.tykind.TY_SLICE) { + cgslicehdr(c, "AX"); + return; + }; + }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float diff --git a/test/lang/deref_hdr_test.ww b/test/lang/deref_hdr_test.ww new file mode 100644 index 00000000..65b65241 --- /dev/null +++ b/test/lang/deref_hdr_test.ww @@ -0,0 +1,79 @@ +// deref_hdr_test — runtime contract for a whole 24B str/slice header loaded +// BY VALUE through a pointer (`let s = *h` where h:*str / h:*[]T). C1b (#4): +// the N_UN TK_STAR deref arm fell through to a scalar MOVQ (AX),AX that loaded +// ONLY word0 (.ptr); .len/.cap were then stored from stale BX/CX, so len(*h) +// and cap(*h) read garbage. byte-id proves the two stages agree, NOT that the +// emitted code is correct (this gap was byte-id-identical on both stages). +// +// WHY the decoy: the pre-fix bug leaves .len/.cap in whatever BX/CX happened to +// hold. A naive `let backing="abcde"; let s=*h` PASSES even broken, because the +// literal's header-load left BX/CX = backing's own len/cap moments earlier — +// the exact register coincidence that let this class hide. Each fn interposes a +// DIFFERENT-sized decoy header right before the deref, so any stale BX/CX hold +// the decoy's len/cap (≠ backing's). Verified: with the fix reverted all four +// fns RED, with the fix all GREEN. The s[i] element reads additionally pin .ptr +// (loaded into AX) — covering the whole header on str and slice, in both the +// let-bind and fn-return shapes that propagate the bug. + +package deref_hdr_test; + +fn ret_str_deref(h: *str) str = { return *h; }; + +fn ret_slice_deref(h: *[]i32) []i32 = { return *h; }; + +@test fn str_whole_deref() void = { + // Whole str loaded by value through *str. The decoy ("xy", len 2) is the + // last header materialized before `*h`, so a pre-fix stale BX/CX hold + // 2/2, not backing's 5/5 — the .len/.cap asserts then RED on the bug. + // The s[i] byte reads stride from .ptr, pinning AX (word0) too. + let backing: str = "abcde"; + let h: *str = &backing; + let decoy: str = "xy"; + assert(decoy.len: i32 == 2); + let s: str = *h; + assert(s.len: i32 == 5); + assert(s.cap: i32 == 5); + assert(s[0]: i32 == 97); + assert(s[1]: i32 == 98); + assert(s[4]: i32 == 101); +}; + +@test fn slice_whole_deref() void = { + // Whole slice loaded by value through *[]i32 — same 3-word header load. + // Decoy (len 1) clobbers the stale-register path away from backing's 3. + let backing: []i32 = [10i32, 20i32, 30i32]; + let h: *[]i32 = &backing; + let decoy: []i32 = [1i32]; + assert(decoy.len: i32 == 1); + let s: []i32 = *h; + assert(s.len: i32 == 3); + assert(s.cap: i32 == 3); + assert(s[0]: i32 == 10); + assert(s[1]: i32 == 20); + assert(s[2]: i32 == 30); +}; + +@test fn str_deref_as_return() void = { + // `return *h` feeds the same arm into the return registers. The decoy + // ("xyz", len 3) before the call seeds BX/CX wrong, so a pre-fix callee + // that returns only .ptr surfaces 3/3 here instead of backing's 4/4. + let backing: str = "abcd"; + let h: *str = &backing; + let decoy: str = "xyz"; + assert(decoy.len: i32 == 3); + let s: str = ret_str_deref(h); + assert(s.len: i32 == 4); + assert(s.cap: i32 == 4); + assert(s[0]: i32 == 97); +}; + +@test fn slice_deref_as_return() void = { + let backing: []i32 = [7i32, 8i32]; + let h: *[]i32 = &backing; + let decoy: []i32 = [1i32, 2i32, 3i32, 4i32]; + assert(decoy.len: i32 == 4); + let s: []i32 = ret_slice_deref(h); + assert(s.len: i32 == 2); + assert(s.cap: i32 == 2); + assert(s[1]: i32 == 8); +};