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.
386 lines
15 KiB
C
386 lines
15 KiB
C
/*
|
|
* 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
|
|
* size + base-materialisation off the base's tnode KIND, enumerating only
|
|
* N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode
|
|
* N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz
|
|
* stayed at the default 8 and the base fell to the wide-header fallback —
|
|
* it materialised the full {ptr,len,cap} header and indexed with an 8-byte
|
|
* stride + a full-word MOVQ load. So `s[1]` over a global str read 8 bytes
|
|
* at ptr+8 instead of the single byte at ptr+1 (cstage emits MOVZBQ).
|
|
* cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the RESOLVED
|
|
* base TYPE (`idx_eff(lhs->type)->sub->size`), uniform across local/global/
|
|
* str/slice/ptr — so it was already correct. LOCAL str/slice index was also
|
|
* already byte-id-clean (esz resolves off the local's tnode).
|
|
*
|
|
* THE FIX (#10): align cgindex's global-resolution arm UP to cstage's uniform
|
|
* type-driven dispatch — the same template the sister fn `cgslice` already
|
|
* uses (generic globaltn, esz = elemsizeofc(c, tn), base = N_TARRAY ? LEAQ :
|
|
* MOVQ name(SB)). A global str/slice now resolves esz=1 off the type table
|
|
* and routes through the existing isglobalptr emission (MOVQ name(SB),BX;
|
|
* ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage.
|
|
*
|
|
* EACH ROW CARRIES BOTH DIMENSIONS (802 model):
|
|
* (a) cstage `ww build` + run, asserting the exit — pins that the converged
|
|
* asm reads the correct element value, not a ptr word.
|
|
* (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (rule-10). A
|
|
* stride-8 regression of the fix re-diverges wwstage from cstage here.
|
|
*
|
|
* BYTE-ID SCOPE PER ROW:
|
|
* Most rows compare the FULL `.s`. The global-slice row compares the TEXT
|
|
* section only (lines before the first DATA/GLOBL directive): a bare
|
|
* module-level `let g: []u8;` decl emits a divergent zero-header DATAW in
|
|
* wwstage that cstage omits — a SEPARATE, pre-existing data-emission gap
|
|
* (module-level slice static-init, task #7/#18 family) orthogonal to the
|
|
* index read. The TEXT comparison still pins the index codegen exactly
|
|
* (where a stride-8 regression manifests: MOVZBQ vs IMULQ $8 + MOVQ), so
|
|
* the #10 fix is fully gated; only the unrelated DATA noise is excluded.
|
|
* (Same kind of orthogonal block 802 documents for its slice rows.)
|
|
*
|
|
* GATE POLARITY: must stay GREEN. A wrong exit means a global str/slice index
|
|
* regressed back to a ptr-word read; a byte-id FAIL means the stages diverged.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
|
|
static int
|
|
runwait(const char *cmd)
|
|
{
|
|
int rc = system(cmd);
|
|
if (rc == -1) return -1;
|
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
|
return -1;
|
|
}
|
|
|
|
struct row { const char *label; const char *src; int want_exit; int text_only; };
|
|
|
|
static const struct row rows[] = {
|
|
/* the exact #10 repro: a global str "ABC", s[1] == 'B' == 66.
|
|
* Pre-fix this read an 8-byte word at ptr+8 (wide-header stride). */
|
|
{ "gstr_mid",
|
|
"package main;\n"
|
|
"let s: str = \"ABC\";\n"
|
|
"export fn main() i32 = { return s[1]: i32; };\n", 66, 0 },
|
|
/* first element s[0] == 'A' == 65 — guards an off-by-one in the
|
|
* index scale (a wide stride would land elsewhere). */
|
|
{ "gstr_first",
|
|
"package main;\n"
|
|
"let s: str = \"ABC\";\n"
|
|
"export fn main() i32 = { return s[0]: i32; };\n", 65, 0 },
|
|
/* last element s[2] == 'C' == 67. */
|
|
{ "gstr_last",
|
|
"package main;\n"
|
|
"let s: str = \"ABC\";\n"
|
|
"export fn main() i32 = { return s[2]: i32; };\n", 67, 0 },
|
|
/* two global-str indices summed (65 + 66 == 131) — pins the byte
|
|
* load width: a full-word load would carry the high bytes. */
|
|
{ "gstr_sum",
|
|
"package main;\n"
|
|
"let s: str = \"ABC\";\n"
|
|
"export fn main() i32 = { return s[0]: i32 + s[1]: i32; };\n", 131, 0 },
|
|
/* global slice index: g[1] == 20. The bare `let g: []u8;` decl emits
|
|
* a divergent zero-header DATAW (orthogonal #7/#18 data gap), so this
|
|
* row is TEXT-only byte-id; cstage runtime pins the value, TEXT-id
|
|
* pins the index read. g is assigned at runtime to dodge the SEPARATE
|
|
* module-level slice-literal static-init gap (which fails to link in
|
|
* BOTH stages identically). */
|
|
{ "gslice_mid",
|
|
"package main;\n"
|
|
"let g: []u8;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
|
|
" g = a;\n"
|
|
" return g[1]: i32;\n"
|
|
"};\n", 20, 1 },
|
|
/* global slice with a WIDTH>1, SIGNED element: g[1] - g[0] == 20 - (-5)
|
|
* == 25. Pins that the global path resolves esz=4 off the type table
|
|
* (not the default 8) AND sign-extends (MOVSXD) — a stride-8 regression
|
|
* re-diverges the TEXT (IMULQ $8 + MOVQ word vs IMULQ $4 + MOVSXD). Same
|
|
* bare-decl DATAW data-gap as gslice_mid, so TEXT-only. */
|
|
{ "gislice_mid",
|
|
"package main;\n"
|
|
"let g: []i32;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let a: [3]i32 = [-5, 20, 30];\n"
|
|
" 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",
|
|
"package main;\n"
|
|
"export fn main() i32 = { let s: str = \"ABC\"; return s[1]: i32; };\n",
|
|
66, 0 },
|
|
/* REGRESSION PIN: local slice index (already clean). g[2] == 30. */
|
|
{ "lslice_last",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
|
|
" let g: []u8 = a;\n"
|
|
" return g[2]: i32;\n"
|
|
"};\n", 30, 0 },
|
|
/* REGRESSION PIN: local array index (already clean). a[1] == 20. */
|
|
{ "larr_mid",
|
|
"package main;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
|
|
" return a[1]: i32;\n"
|
|
"};\n", 20, 0 },
|
|
{ NULL, NULL, 0, 0 }
|
|
};
|
|
|
|
/* A directive line that begins the DATA/GLOBL section. The TEXT-only
|
|
* compare stops at the first such line (the index codegen lives entirely
|
|
* in the TEXT segment above it). */
|
|
static int
|
|
isdataline(const char *ln)
|
|
{
|
|
return strncmp(ln, "DATA", 4) == 0 || strncmp(ln, "GLOBL", 5) == 0;
|
|
}
|
|
|
|
/* Byte-compare two .s files. With text_only, both files are truncated at
|
|
* the first DATA/GLOBL line before comparison (orthogonal data-section
|
|
* divergence excluded; see the BYTE-ID SCOPE note above). */
|
|
static int
|
|
asm_eq(const char *a, const char *b, int text_only)
|
|
{
|
|
FILE *fa = fopen(a, "rb");
|
|
FILE *fb = fopen(b, "rb");
|
|
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
|
|
int rc = 0;
|
|
char la[4096], lb[4096];
|
|
for (;;) {
|
|
char *ra = fgets(la, sizeof la, fa);
|
|
char *rb = fgets(lb, sizeof lb, fb);
|
|
if (text_only && ra && isdataline(la)) ra = NULL;
|
|
if (text_only && rb && isdataline(lb)) rb = NULL;
|
|
if (ra == NULL && rb == NULL) break;
|
|
if (ra == NULL || rb == NULL) { rc = -1; break; }
|
|
if (strcmp(la, lb) != 0) { rc = -1; break; }
|
|
}
|
|
fclose(fa); fclose(fb);
|
|
return rc;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
const char *bin = getenv("BIN");
|
|
if (!bin) bin = "out/bin";
|
|
char absbin[1024];
|
|
if (bin[0] != '/') {
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
|
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
|
bin = absbin;
|
|
}
|
|
|
|
char w6c[1100], w6c_ww[1100];
|
|
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
|
|
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
|
|
if (access(w6c_ww, X_OK) != 0) {
|
|
fprintf(stderr, "globalidx: w6c_ww missing — cannot run the "
|
|
"cs==ww byte-id gate\n");
|
|
return 1;
|
|
}
|
|
|
|
int n = 0, fail = 0;
|
|
for (int i = 0; rows[i].src; i++, n++) {
|
|
char src[64];
|
|
snprintf(src, sizeof src, "/tmp/wwgi_%d_%d.ww", getpid(), i);
|
|
FILE *f = fopen(src, "wb");
|
|
if (f == NULL) { fail++; continue; }
|
|
fputs(rows[i].src, f);
|
|
fclose(f);
|
|
|
|
/* (a) cstage build + run in a scratch dir. */
|
|
char tmpdir[64];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwgi_%d_d_%d", getpid(), i);
|
|
mkdir(tmpdir, 0755);
|
|
|
|
char cmd[2048];
|
|
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
|
|
tmpdir, bin, src);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "row[%s]: cstage build failed\n",
|
|
rows[i].label);
|
|
fail++;
|
|
unlink(src); rmdir(tmpdir);
|
|
continue;
|
|
}
|
|
|
|
char outbin[128];
|
|
const char *base = strrchr(src, '/');
|
|
base = base ? base + 1 : src;
|
|
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
|
char *dot = strrchr(outbin, '.');
|
|
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
|
|
|
|
int got = runwait(outbin);
|
|
if (got != rows[i].want_exit) {
|
|
fprintf(stderr, "row[%s]: cstage exit %d, want %d\n",
|
|
rows[i].label, got, rows[i].want_exit);
|
|
fail++;
|
|
}
|
|
unlink(outbin); rmdir(tmpdir);
|
|
|
|
/* (b) cs==ww byte-id gate. */
|
|
char cs_s[64], ws_s[64];
|
|
snprintf(cs_s, sizeof cs_s, "/tmp/wwgi_%d_%d_cs.s", getpid(), i);
|
|
snprintf(ws_s, sizeof ws_s, "/tmp/wwgi_%d_%d_ww.s", getpid(), i);
|
|
|
|
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
|
|
fail++; unlink(src); continue;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
|
|
w6c_ww, ws_s, src);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "row[%s]: w6c_ww failed\n", rows[i].label);
|
|
fail++; unlink(src); unlink(cs_s); continue;
|
|
}
|
|
if (asm_eq(cs_s, ws_s, rows[i].text_only) != 0) {
|
|
fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER%s "
|
|
"(rule-10 byte-id violation)\n", rows[i].label,
|
|
rows[i].text_only ? " (TEXT section)" : "");
|
|
fail++;
|
|
}
|
|
unlink(src); unlink(cs_s); unlink(ws_s);
|
|
}
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "%d/%d globalidx tests failed\n", fail, n);
|
|
return 1;
|
|
}
|
|
printf("globalidx: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n);
|
|
return 0;
|
|
}
|