From 33ec0fb1ac621e77fd09efd9a48095ed6ff8deb1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 22:08:11 +0900 Subject: [PATCH] w6c+w6c_ww: (*p)[i] deref base materializes the array ADDRESS (#61 C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both stages SEGV'd identically (byte-id-blind): cgun's TK_STAR emitted a scalar MOVQ (AX),AX for an array pointee, so the index consumed a[0]'s VALUE as its base — a wild deref. An array value IS its address everywhere in this cgen (#270-1a), so the ARRAY pointee now takes the same skip as the #185 *fn deref in both stages: `*p` leaves AX = p's value, and every consumer that materializes a complex index base via cgexpr(base) — N_INDEX read fallback, cgassign store/compound, TK_AMP, N_SLICE — gets the array address from the one deref choke-point. wwstage additionally joins the N_UN-TK_STAR base to the stamped-tinfo esz arms (cgindex / cgassign store + compound / TK_AMP &(*p)[i]) where cstage reads base->type uniformly: without it, esz fell to the 8B default and a narrow element would mis-stride the moment the base started materializing (cs!=ww only reachable post-choke-point-fix, which is why it rides this commit). 949_ptrarr_index_run grows the deref_* rows: read (8B/4B/param-base), write (8B / 1B+neighbor-guards), compound — runtime + byte-id, the only nets that can see a both-stages-identical miscompile. --- cmd/w6c/cgen.c | 12 ++++- selfhost/cmd/w6c/main.combined.ww | 37 +++++++++++++--- selfhost/cmd/wcc/cgenexpr.ww | 37 +++++++++++++--- selfhost/cmd/wwdump/main.combined.ww | 37 +++++++++++++--- test/wcc/949_ptrarr_index_run.c | 66 ++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 20 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index de4f3b7f..07f7fd52 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3976,11 +3976,19 @@ cgexpr(Cg *c, Node *n, Local *locals) * load the first instruction word and CALL * would segfault on that junk. Mirror * ref/harec/src/check.c expr_call's - * STORAGE_POINTER→STORAGE_FUNCTION skip. */ + * STORAGE_POINTER→STORAGE_FUNCTION skip. + * #61 C: same skip for an ARRAY pointee — an + * array value IS its address everywhere in + * this cgen (#270-1a), so `*p` on `*[N]T` + * leaves AX = p's value. The scalar load + * below pulled a[0]'s VALUE and `(*p)[i]` + * then dereferenced it as the index base — + * a wild pointer, SIGSEGV on both stages. */ Type *rt = n->type; Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt; - if (ru && ru->kind == TY_FN) + if (ru && (ru->kind == TY_FN + || ru->kind == TY_ARRAY)) break; } /* f64/f32 result rides X0 (SSE), not AX — an integer diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e199fbf2..05d7a073 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -21930,7 +21930,8 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { // `s.ptr[i]` / struct-field index: stride is the // checker-stamped element tinfo's natural size, the // same idiom as the N_INDEX-base arm below (#60/#72). @@ -21939,6 +21940,10 @@ fn cgindex(c: *cgen, n: *node) void = { // so a signed-narrow field element sign-extends on load // (loadopsz keys on (signed,sz)); cstage's fldloadop reads // it from the element type — align ww up (#255). + // N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo + // source — cstage reads base->type uniformly; without + // this arm esz fell to the 8B default (wrong stride for + // narrow elements once the deref base materializes). let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; elem_isarray = tinfoisarray(dt); @@ -21997,7 +22002,9 @@ fn cgindex(c: *cgen, n: *node) void = { // cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without // this an N_DOT-base tagged element fell to the scalar // loadopsz path and silently dropped the tag/payload-high word. - if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) { + // N_UN deref base joins for the same reason (#61 C). + if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX + || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { if (typeistagged(dt)) { @@ -24489,12 +24496,17 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { // `&p.ptr[i]`: stride is the checker-stamped // element tinfo's natural size, mirroring // cgindex's N_DOT arm so &p.ptr[i] and // p.ptr[i] agree. cstage idx_eff(base->type) // ->sub->size (cmd/w6c/cgen.c:3517-18). #72. + // N_UN deref base (`&(*p)[i]`, #61 C): same + // stamped source; cstage reads base->type + // uniformly. let dt: *tinfo = opnd.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; }; };}; @@ -24583,9 +24595,15 @@ fn cgun(c: *cgen, n: *node) void = { // would load the first instruction word and a subsequent // CALL would segfault. Mirror ref/harec/src/check.c // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. + // #61 C: same skip for an ARRAY pointee — an array value IS + // its address everywhere in this cgen (#270-1a), so `*p` on + // `*[N]T` leaves AX = p's value. The scalar load below + // pulled a[0]'s VALUE and `(*p)[i]` then dereferenced it as + // the index base — a wild pointer, SIGSEGV on both stages. let rti: *tinfo = n.type_: *tinfo; for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; + if (rti != nil && rti.kind == tykind.TY_ARRAY) { 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 @@ -27232,13 +27250,16 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { // lhs.type_ is the checker-stamped element tinfo // of the N_INDEX: esz is its natural size and the // tagged-element gate (below) reads the same // .type_ — same idiom as cgindex's n.type_ read // (#60/#72). cstage idx_eff(base->type)->sub->size - // (cmd/w6c/cgen.c:3517-18). + // (cmd/w6c/cgen.c:3517-18). N_UN deref base + // (`(*p)[i] = v`, #61 C): same stamped source. let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { @@ -27677,7 +27698,11 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { + // N_UN deref base (`(*p)[i] OP= v`, #61 C): + // same stamped source as the store arm. let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 0ed19a69..492ccc91 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1558,7 +1558,8 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { // `s.ptr[i]` / struct-field index: stride is the // checker-stamped element tinfo's natural size, the // same idiom as the N_INDEX-base arm below (#60/#72). @@ -1567,6 +1568,10 @@ fn cgindex(c: *cgen, n: *node) void = { // so a signed-narrow field element sign-extends on load // (loadopsz keys on (signed,sz)); cstage's fldloadop reads // it from the element type — align ww up (#255). + // N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo + // source — cstage reads base->type uniformly; without + // this arm esz fell to the 8B default (wrong stride for + // narrow elements once the deref base materializes). let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; elem_isarray = tinfoisarray(dt); @@ -1625,7 +1630,9 @@ fn cgindex(c: *cgen, n: *node) void = { // cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without // this an N_DOT-base tagged element fell to the scalar // loadopsz path and silently dropped the tag/payload-high word. - if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) { + // N_UN deref base joins for the same reason (#61 C). + if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX + || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { if (typeistagged(dt)) { @@ -4117,12 +4124,17 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { // `&p.ptr[i]`: stride is the checker-stamped // element tinfo's natural size, mirroring // cgindex's N_DOT arm so &p.ptr[i] and // p.ptr[i] agree. cstage idx_eff(base->type) // ->sub->size (cmd/w6c/cgen.c:3517-18). #72. + // N_UN deref base (`&(*p)[i]`, #61 C): same + // stamped source; cstage reads base->type + // uniformly. let dt: *tinfo = opnd.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; }; };}; @@ -4211,9 +4223,15 @@ fn cgun(c: *cgen, n: *node) void = { // would load the first instruction word and a subsequent // CALL would segfault. Mirror ref/harec/src/check.c // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. + // #61 C: same skip for an ARRAY pointee — an array value IS + // its address everywhere in this cgen (#270-1a), so `*p` on + // `*[N]T` leaves AX = p's value. The scalar load below + // pulled a[0]'s VALUE and `(*p)[i]` then dereferenced it as + // the index base — a wild pointer, SIGSEGV on both stages. let rti: *tinfo = n.type_: *tinfo; for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; + if (rti != nil && rti.kind == tykind.TY_ARRAY) { 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 @@ -6860,13 +6878,16 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { // lhs.type_ is the checker-stamped element tinfo // of the N_INDEX: esz is its natural size and the // tagged-element gate (below) reads the same // .type_ — same idiom as cgindex's n.type_ read // (#60/#72). cstage idx_eff(base->type)->sub->size - // (cmd/w6c/cgen.c:3517-18). + // (cmd/w6c/cgen.c:3517-18). N_UN deref base + // (`(*p)[i] = v`, #61 C): same stamped source. let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { @@ -7305,7 +7326,11 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { + // N_UN deref base (`(*p)[i] OP= v`, #61 C): + // same stamped source as the store arm. let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 17573dde..bec47cea 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -21930,7 +21930,8 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { // `s.ptr[i]` / struct-field index: stride is the // checker-stamped element tinfo's natural size, the // same idiom as the N_INDEX-base arm below (#60/#72). @@ -21939,6 +21940,10 @@ fn cgindex(c: *cgen, n: *node) void = { // so a signed-narrow field element sign-extends on load // (loadopsz keys on (signed,sz)); cstage's fldloadop reads // it from the element type — align ww up (#255). + // N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo + // source — cstage reads base->type uniformly; without + // this arm esz fell to the 8B default (wrong stride for + // narrow elements once the deref base materializes). let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; elem_isarray = tinfoisarray(dt); @@ -21997,7 +22002,9 @@ fn cgindex(c: *cgen, n: *node) void = { // cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without // this an N_DOT-base tagged element fell to the scalar // loadopsz path and silently dropped the tag/payload-high word. - if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) { + // N_UN deref base joins for the same reason (#61 C). + if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX + || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { if (typeistagged(dt)) { @@ -24489,12 +24496,17 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { // `&p.ptr[i]`: stride is the checker-stamped // element tinfo's natural size, mirroring // cgindex's N_DOT arm so &p.ptr[i] and // p.ptr[i] agree. cstage idx_eff(base->type) // ->sub->size (cmd/w6c/cgen.c:3517-18). #72. + // N_UN deref base (`&(*p)[i]`, #61 C): same + // stamped source; cstage reads base->type + // uniformly. let dt: *tinfo = opnd.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; }; };}; @@ -24583,9 +24595,15 @@ fn cgun(c: *cgen, n: *node) void = { // would load the first instruction word and a subsequent // CALL would segfault. Mirror ref/harec/src/check.c // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. + // #61 C: same skip for an ARRAY pointee — an array value IS + // its address everywhere in this cgen (#270-1a), so `*p` on + // `*[N]T` leaves AX = p's value. The scalar load below + // pulled a[0]'s VALUE and `(*p)[i]` then dereferenced it as + // the index base — a wild pointer, SIGSEGV on both stages. let rti: *tinfo = n.type_: *tinfo; for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; + if (rti != nil && rti.kind == tykind.TY_ARRAY) { 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 @@ -27232,13 +27250,16 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { // lhs.type_ is the checker-stamped element tinfo // of the N_INDEX: esz is its natural size and the // tagged-element gate (below) reads the same // .type_ — same idiom as cgindex's n.type_ read // (#60/#72). cstage idx_eff(base->type)->sub->size - // (cmd/w6c/cgen.c:3517-18). + // (cmd/w6c/cgen.c:3517-18). N_UN deref base + // (`(*p)[i] = v`, #61 C): same stamped source. let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { @@ -27677,7 +27698,11 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - } else { if (base.kind == nkind.N_DOT) { + } else { if (base.kind == nkind.N_DOT + || (base.kind == nkind.N_UN + && base.op == tkind.TK_STAR)) { + // N_UN deref base (`(*p)[i] OP= v`, #61 C): + // same stamped source as the store arm. let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { diff --git a/test/wcc/949_ptrarr_index_run.c b/test/wcc/949_ptrarr_index_run.c index 500530c0..de54c9aa 100644 --- a/test/wcc/949_ptrarr_index_run.c +++ b/test/wcc/949_ptrarr_index_run.c @@ -32,6 +32,18 @@ * param / local / cast bases, read / write / compound routes, * neighbor-corruption guards, the &p[i] pointer-difference (B), and * the live consumer's mix-in-place shape (siphash round). + * + * C. `(*p)[i]` explicit deref + index — BOTH stages SEGV'd + * identically (byte-id-blind): cgun's TK_STAR materialized an + * 8-byte SCALAR load of a[0]'s value and the index used that + * VALUE as its base — a wild deref. Fixed in both stages at the + * deref choke-point: an ARRAY pointee takes the #185 *fn skip + * (an array value IS its address, #270-1a), so `*p` leaves AX = + * p's value and every index/addr-of/store route through + * `cgexpr(base)` materializes the array address for free. The + * deref_* rows pin read / write / compound at 8B and narrow + * widths (the narrow rows also pin the wwstage N_UN-base + * stamped-tinfo esz arm against the 8B default). */ #include #include @@ -261,6 +273,60 @@ static const struct row rows[] = { " if (v[3] != 11u64) { return 4; };\n" " return 0;\n" "};\n", 0 }, + /* C: (*p)[i] read, 8B elem (ken p7 — SEGV'd both stages). */ + { "deref_rd_u64", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u64 = [100u64, 101u64, 102u64, 103u64];\n" + " let p: *[4]u64 = &a;\n" + " return (*p)[1]: i32;\n" + "};\n", 101 }, + /* C: (*p)[i] read, 4B elem — pins the N_UN-base esz arm (the 8B + * default would mis-stride once the base materializes). */ + { "deref_rd_u32", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u32 = [11u32, 22u32, 33u32, 44u32];\n" + " let p: *[4]u32 = &a;\n" + " return (*p)[2]: i32;\n" + "};\n", 33 }, + /* C: (*p)[i] read through a param base. */ + { "deref_rd_param", + "package main;\n" + "fn rd(p: *[4]u64) u64 = {\n" + " return (*p)[1];\n" + "};\n" + "export fn main() i32 = {\n" + " let a: [4]u64 = [100u64, 101u64, 102u64, 103u64];\n" + " return rd(&a): i32;\n" + "};\n", 101 }, + /* C: (*p)[i] write, 8B elem. */ + { "deref_wr_u64", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u64 = [1u64, 2u64, 3u64, 4u64];\n" + " let p: *[4]u64 = &a;\n" + " (*p)[1] = 7u64;\n" + " return (a[0] + a[1] + a[2]): i32;\n" + "};\n", 11 }, + /* C: (*p)[i] write, 1B elem, neighbor guards. */ + { "deref_wr_u8", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " let p: *[4]u8 = &a;\n" + " (*p)[1] = 9u8;\n" + " return (a[0] + a[1] + a[2]): i32;\n" + "};\n", 13 }, + /* C: (*p)[i] compound. */ + { "deref_compound_u64", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u64 = [100u64, 101u64, 102u64, 103u64];\n" + " let p: *[4]u64 = &a;\n" + " (*p)[1] += 5u64;\n" + " return a[1]: i32;\n" + "};\n", 106 }, { NULL, NULL, 0 } };