w6c+w6c_ww: (*p)[i] deref base materializes the array ADDRESS (#61 C)

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.
This commit is contained in:
2026-06-04 22:08:11 +09:00
parent eea3e197c2
commit 33ec0fb1ac
5 changed files with 169 additions and 20 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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 <stdio.h>
#include <stdlib.h>
@@ -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 }
};