cgen: load full 24B header on whole str/slice deref-by-value
`let s: str = *h` (a str/slice loaded by value through *str / *[]T)
fell through the N_UN deref arm to the scalar load, emitting a single
MOVQ that read only the 8B .ptr and left .len/.cap from stale registers,
so len(*p) returned garbage. Both stages emitted byte-identical wrong
code, so the self-compile byte-id gate was blind to it. Add a str/slice
arm that loads the full {ptr,len,cap} via cgslicehdr when the chased
pointee is TY_STR/TY_SLICE.
Surfaced by the codegen miscompile hunt (finding C1b). Pinned by
test/lang/deref_hdr_test.ww, which interposes a different-sized decoy
header so the test reddens when the arm is reverted.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
79
test/lang/deref_hdr_test.ww
Normal file
79
test/lang/deref_hdr_test.ww
Normal file
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user