w6c_ww/cgen: uniform tinfo esz for global str/slice addr-of + store (fix #11)

Three sibling arms of the #10 global-str/slice INDEX miscompile (23670d7,
the READ path) shared the identical N_TARRAY/N_TPTR tnode-KIND whitelist in
their global-ident resolution arm and were still LIVE and silently cs!=ww:

  - cgun  `&s[1]` / `&g[1]` (cgenexpr.ww N_INDEX addr-of) — a global str
    (tnode N_TNAME) / slice (N_TSLICE) matched neither arm, so esz stayed at
    the default 8 and the base fell to the complex-base fallback: a wide
    {ptr,len,cap} header + 8-byte stride instead of MOVQ name(SB) (.ptr) +
    ADDQ.
  - cgassign `g[1] = v` store AND `g[1] OP= v` compound (two arms) — same
    whitelist; a global slice store emitted a full-word MOVQ at an 8-byte
    stride: an 8-BYTE OUT-OF-BOUNDS WRITE past a 1-byte element (memory
    corruption) instead of MOVB at .ptr+1.

cstage (cmd/w6c/cgen.c) is the runtime-correct reference and was already
uniform across all three: esz off idx_eff(base->type)->sub->size and the
base load gated by is_arr (TY_ARRAY -> LEAQ name(SB), every other -> MOVQ
name(SB), since a str/slice's .ptr IS the symbol's first word). Align the
wwstage UP to that, mirroring the just-landed cgindex template (#10): resolve
esz via elemsizeofc with no kind gate, dispatch the base by N_TARRAY ? LEAQ :
MOVQ name(SB). The store/compound arms also resolve elemtn exactly like their
local branch (element node for ARRAY/SLICE/PTR; nil for str so tnodestoreop
picks MOVB) so a global []str store routes to the 3-word header store and the
compound arm's str/slice hard-error still fires.

Close-by-construction: the global element base/stride is now computed off the
resolved type at every wwstage index site — read (cgindex, #10), addr-of
(cgun), store + compound (cgassign) — with no remaining tnode-kind whitelist.
cgslice/cgbaselen already resolved via elemsizeofc.

803_globalidx_run extends from 9 to 18 rows: global str/slice addr-of (read
back through the pointer), global slice store AND compound store `g[i] OP= v`
(the distinct third fixed arm, with adjacent-element addends as the OOB-write
guard on both), a WIDTH>1 signed variant of each (esz=4 stride/store-width pin),
and local addr-of/store regression pins. Runtime (cstage build+run) + cs==ww
byte-id per row. combined.ww embeds (w6c + wwdump) regenerate.
This commit is contained in:
2026-06-03 15:50:54 +09:00
parent 23670d7c4e
commit a9228dabb3
4 changed files with 305 additions and 69 deletions

View File

@@ -23170,17 +23170,26 @@ fn cgun(c: *cgen, n: *node) void = {
if (tn.kind == nkind.N_TARRAY) { isarr = true; };
};
} else {
// #11: addr-of twin of the #10 cgindex read
// fix. Dispatch esz + base load off the
// global's RESOLVED type, NOT an N_TARRAY/
// N_TPTR kind whitelist — a global str (tnode
// N_TNAME) / slice (N_TSLICE) matched NEITHER
// old arm, so esz stayed at the default 8 and
// the base fell to the complex-base fallback,
// yielding a wide-stride &s[i]. cstage's
// TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform:
// esz=bu->sub->size, base is_arr?LEAQ:MOVQ
// name(SB) (a str/slice's .ptr IS the symbol's
// first word). Align UP, mirroring cgindex.
let tn: *node = letvartnode(c, base.str);
if (tn != nil) {
globalname = base.str;
esz = elemsizeofc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
};
};
@@ -24797,19 +24806,34 @@ fn cgassign(c: *cgen, n: *node) void = {
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};
@@ -25229,19 +25253,34 @@ fn cgassign(c: *cgen, n: *node) void = {
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};

View File

@@ -3446,17 +3446,26 @@ fn cgun(c: *cgen, n: *node) void = {
if (tn.kind == nkind.N_TARRAY) { isarr = true; };
};
} else {
// #11: addr-of twin of the #10 cgindex read
// fix. Dispatch esz + base load off the
// global's RESOLVED type, NOT an N_TARRAY/
// N_TPTR kind whitelist — a global str (tnode
// N_TNAME) / slice (N_TSLICE) matched NEITHER
// old arm, so esz stayed at the default 8 and
// the base fell to the complex-base fallback,
// yielding a wide-stride &s[i]. cstage's
// TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform:
// esz=bu->sub->size, base is_arr?LEAQ:MOVQ
// name(SB) (a str/slice's .ptr IS the symbol's
// first word). Align UP, mirroring cgindex.
let tn: *node = letvartnode(c, base.str);
if (tn != nil) {
globalname = base.str;
esz = elemsizeofc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
};
};
@@ -5073,19 +5082,34 @@ fn cgassign(c: *cgen, n: *node) void = {
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};
@@ -5505,19 +5529,34 @@ fn cgassign(c: *cgen, n: *node) void = {
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};

View File

@@ -23170,17 +23170,26 @@ fn cgun(c: *cgen, n: *node) void = {
if (tn.kind == nkind.N_TARRAY) { isarr = true; };
};
} else {
// #11: addr-of twin of the #10 cgindex read
// fix. Dispatch esz + base load off the
// global's RESOLVED type, NOT an N_TARRAY/
// N_TPTR kind whitelist — a global str (tnode
// N_TNAME) / slice (N_TSLICE) matched NEITHER
// old arm, so esz stayed at the default 8 and
// the base fell to the complex-base fallback,
// yielding a wide-stride &s[i]. cstage's
// TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform:
// esz=bu->sub->size, base is_arr?LEAQ:MOVQ
// name(SB) (a str/slice's .ptr IS the symbol's
// first word). Align UP, mirroring cgindex.
let tn: *node = letvartnode(c, base.str);
if (tn != nil) {
globalname = base.str;
esz = elemsizeofc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = base.str;
esz = elemsizeofc(c, tn);
};
};
};
@@ -24797,19 +24806,34 @@ fn cgassign(c: *cgen, n: *node) void = {
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};
@@ -25229,19 +25253,34 @@ fn cgassign(c: *cgen, n: *node) void = {
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
elemtn = tn.lhs;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};

View File

@@ -1,7 +1,13 @@
/*
* 803_globalidx_run — BUG #10. Runtime + cs==ww byte-id net for INDEXING
* a GLOBAL `str` / GLOBAL slice (`s[i]` / `g[i]` where s/g are module-level
* lets).
* 803_globalidx_run — BUG #10 + #11. Runtime + cs==ww byte-id net for
* the GLOBAL `str` / GLOBAL slice index family (`s/g` are module-level lets):
* #10 — the READ `s[i]` / `g[i]` (cgindex).
* #11 — the ADDR-OF `&s[i]` / `&g[i]` (cgun) and the STORE `g[i] = v`
* (cgassign). Same root as #10 (the N_TARRAY/N_TPTR kind whitelist
* in the global-resolution arm), three more sibling arms; the store
* miscompile is an 8-byte OUT-OF-BOUNDS write (full-word MOVQ instead
* of MOVB), so the store rows below also assert ADJACENT elements stay
* uncorrupted.
*
* THE BUG (wwstage WRONG, cstage correct — a rule-10 divergence):
* wwstage `cgindex` (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element
@@ -113,6 +119,119 @@ static const struct row rows[] = {
" g = a;\n"
" return g[1] - g[0];\n"
"};\n", 25, 1 },
/* #11 ADDR-OF, global str: &s[1] then read the byte through the
* pointer == 'B' == 66. Pre-#11 cgun materialised the wide {ptr,len,
* cap} header + an 8-byte stride (IMULQ $8 + LEAQ s(SB)) instead of
* MOVQ s(SB) (.ptr) + ADDQ; FULL byte-id (string-literal DATA is
* stage-identical, same as the gstr READ rows). */
{ "gstr_addr",
"package main;\n"
"let s: str = \"ABC\";\n"
"export fn main() i32 = { let p: *u8 = &s[1]; return (*p): i32; };\n",
66, 0 },
/* #11 ADDR-OF, global slice []u8: &g[1] read back == 20. Bare
* `let g: []u8;` decl → orthogonal zero-header DATAW, so TEXT-only
* (same data gap as gslice_mid; the addr codegen is all TEXT). */
{ "gslice_addr",
"package main;\n"
"let g: []u8;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" g = a;\n"
" let p: *u8 = &g[1];\n"
" return (*p): i32;\n"
"};\n", 20, 1 },
/* #11 ADDR-OF, global slice WIDTH>1: &g[1] over []i32, *p == 20.
* Pins esz=4 stride on the addr path — a stride-8 regression lands
* &g[2] (*p == 30) and re-diverges the TEXT (IMULQ $4 vs IMULQ $8).
* Same bare-decl DATAW gap → TEXT-only. */
{ "gislice_addr",
"package main;\n"
"let g: []i32;\n"
"export fn main() i32 = {\n"
" let a: [3]i32 = [-5, 20, 30];\n"
" g = a;\n"
" let p: *i32 = &g[1];\n"
" return *p;\n"
"};\n", 20, 1 },
/* #11 STORE, global slice []u8: g[1] = 99, then g[1]+g[0]+g[2] ==
* 99 + 10 + 30 == 139. The g[0]/g[2] addends are the OOB-WRITE GUARD:
* pre-#11 the store was a full-word MOVQ at an 8-byte stride, writing
* 99 at ptr+8 (8-byte OOB) and leaving g[1] == 20 (sum 60, AND clobbered
* memory). The fix stores MOVB at ptr+1; adjacent bytes intact. TEXT-
* only (bare-decl DATAW gap). */
{ "gslice_store",
"package main;\n"
"let g: []u8;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" g = a;\n"
" g[1] = 99u8;\n"
" return g[1]: i32 + g[0]: i32 + g[2]: i32;\n"
"};\n", 139, 1 },
/* #11 STORE, global slice WIDTH>1 SIGNED: g[1] = 42, then
* g[1]+g[0]+g[2] == 42 + (-5) + 30 == 67. Pins esz=4 store WIDTH +
* stride: a full-word/stride-8 regression writes 8 bytes at ptr+8
* (OOB) and re-diverges the TEXT (IMULQ $4 + MOVL vs IMULQ $8 + MOVQ).
* g[0]/g[2] are the adjacency guard. TEXT-only. */
{ "gislice_store",
"package main;\n"
"let g: []i32;\n"
"export fn main() i32 = {\n"
" let a: [3]i32 = [-5, 20, 30];\n"
" g = a;\n"
" g[1] = 42;\n"
" return g[1] + g[0] + g[2];\n"
"};\n", 67, 1 },
/* #11 COMPOUND STORE, global slice []u8: g[1] += 79 → 20+79 == 99,
* then g[1]+g[0]+g[2] == 99+10+30 == 139. Exercises the THIRD fixed
* arm (cgenexpr.ww cgassign n.op != TK_ASSIGN), a distinct code path
* from the plain-store rows above: it load-combines-stores in place.
* Pre-#11 it hit the same kind whitelist → esz=8 + a full-word RMW: an
* 8-byte OOB load AND store at ptr+8, leaving g[1] == 20 (sum 60) and
* clobbering adjacent memory. The g[0]/g[2] addends are the OOB guard.
* TEXT-only (bare-decl DATAW gap). */
{ "gslice_compound",
"package main;\n"
"let g: []u8;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" g = a;\n"
" g[1] += 79u8;\n"
" return g[1]: i32 + g[0]: i32 + g[2]: i32;\n"
"};\n", 139, 1 },
/* #11 COMPOUND STORE, global slice WIDTH>1 SIGNED: g[1] += 47 →
* (-5)+47 == 42, then g[1]+g[0]+g[2] == 42+(-25)+30 == 47. Pins the
* compound arm's esz=4 load/store WIDTH + stride on the global path —
* a stride-8/full-word RMW regression reads+writes 8 bytes at ptr+8
* (OOB) and re-diverges the TEXT (IMULQ $4 + MOVL/MOVSXD vs IMULQ $8 +
* MOVQ). g[0]/g[2] are the adjacency guard. TEXT-only. */
{ "gislice_compound",
"package main;\n"
"let g: []i32;\n"
"export fn main() i32 = {\n"
" let a: [3]i32 = [-25, -5, 30];\n"
" g = a;\n"
" g[1] += 47;\n"
" return g[1] + g[0] + g[2];\n"
"};\n", 47, 1 },
/* REGRESSION PIN: local str ADDR-OF (already clean pre-#11) — the
* fix must not perturb the local path. &s[1] read back == 66. FULL. */
{ "lstr_addr",
"package main;\n"
"export fn main() i32 = { let s: str = \"ABC\"; let p: *u8 = &s[1]; return (*p): i32; };\n",
66, 0 },
/* REGRESSION PIN: local slice STORE (already clean) — g[1] = 99 then
* g[1]+g[0]+g[2] == 139, adjacency intact. FULL byte-id (a local slice
* carries no bare-decl data gap). */
{ "lslice_store",
"package main;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" let g: []u8 = a;\n"
" g[1] = 99u8;\n"
" return g[1]: i32 + g[0]: i32 + g[2]: i32;\n"
"};\n", 139, 0 },
/* REGRESSION PIN: local str index (already byte-id-clean pre-fix) —
* the fix must not perturb the local path. s[1] == 'B' == 66. */
{ "lstr_mid",