From f2b47087a2ba47f476d1be89883d5eebb8acf53b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 14 May 2026 00:21:53 +0900 Subject: [PATCH] wcc: &s.len / &s.cap typed as *i64 (closes #13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TK_AMP early-exit on slice/str .len/.cap pseudo-fields now returns *i64 instead of legacy *i32. Slice ABI is 24B fixed; LEAQ at the slot was already correct, only the pointer typing was wrong — store-width flips from MOVL to MOVQ via the existing primsize-from-tnode path. &s.ptr untouched (already **T). --- cmd/wcc/check.c | 17 ++++++++ test/wcc/690_amp_dot.c | 96 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 6a4bff89..a61f891a 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -623,6 +623,23 @@ cunop(Checker *c, Node *n) type_name(c->a, t)); return t->sub; case TK_AMP: /* address-of */ + /* Slice/str pseudo-fields .len/.cap surface as i32 but live + * in 8B-aligned slots in the header (ptr@0, len@8, cap@16). + * Address-of must be typed *i64 so deref-write hits the full + * slot; otherwise *&s.len = N stores 4B (MOVL) and the upper + * 4B leak from whatever the prior MOVQ store of s.len left + * behind. */ + if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && + n->lhs->str && + (strcmp(n->lhs->str, "len") == 0 || + strcmp(n->lhs->str, "cap") == 0)) { + Type *bt = n->lhs->lhs->type; + Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + if (bu && bu->kind == TY_PTR) bu = bu->sub; + if (bu && bu->kind == TY_NAMED) bu = bu->under; + if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) + return type_ptr(c->a, ty_i64); + } return type_ptr(c->a, t); default: return err(c, n->pos, "unsupported unary %s", tokname(n->op)); diff --git a/test/wcc/690_amp_dot.c b/test/wcc/690_amp_dot.c index 5f95e697..d35634cd 100644 --- a/test/wcc/690_amp_dot.c +++ b/test/wcc/690_amp_dot.c @@ -102,8 +102,10 @@ static const struct row rows[] = { 55 }, /* The motivating idiom: write through `&s.len` on a slice header * to truncate without re-allocating. Mirrors the Hare slice- - * header poke pattern that bufio will eventually want. Test: - * fill a slice's len to 5, then *&s.len = 0; assert s.len == 0. */ + * header poke pattern that bufio will eventually want. The slice + * header's .len slot is 8B even though the surface type is i32, + * so `&s.len` is `*i64` (matches storage); deref-store hits all + * 8B, and the i64 load reads back the value the user wrote. */ { "amp_dot_slice_len_writethrough", "fn main() i32 = {\n" " let arr: [4]u8;\n" @@ -111,11 +113,97 @@ static const struct row rows[] = { " s.ptr = &arr[0];\n" " s.len = 4;\n" " s.cap = 4;\n" - " let q: *i32 = &s.len;\n" - " *q = 0;\n" + " let q: *i64 = &s.len;\n" + " *q = 0i64;\n" " return s.len: i32;\n" "};\n", 0 }, + /* Width-stress for `&s.len`: write a value whose lower-32B + * differs from upper-32B and confirm the upper bytes don't + * leak from the prior 8B store of `s.len = 4`. Before the fix, + * `&s.len` was `*i32` and `*q = v` lowered to MOVL, leaving + * the upper 4B at whatever the MOVQ store of 4 left there + * (zero — pass by accident). With a non-zero stale upper or a + * fresh write that fills both halves, the i64 read of s.len + * exposes the mismatch. We write 0xFFFF_FFFF_FFFF_FFFF and + * return 1 iff s.len reads back as -1 (i64). */ + { "amp_dot_slice_len_width_stress", + "fn main() i32 = {\n" + " let arr: [4]u8;\n" + " let s: []u8;\n" + " s.ptr = &arr[0];\n" + " s.len = 4;\n" + " s.cap = 4;\n" + " let q: *i64 = &s.len;\n" + " *q = -1i64;\n" + " let v: i64 = s.len: i64;\n" + " if (v == -1i64) { return 1; };\n" + " return 0;\n" + "};\n", + 1 }, + /* Same shape for `&s.cap`. cap lives at +16 in the slice header + * and was the second pseudo-field broken by the same width bug. */ + { "amp_dot_slice_cap_width_stress", + "fn main() i32 = {\n" + " let arr: [4]u8;\n" + " let s: []u8;\n" + " s.ptr = &arr[0];\n" + " s.len = 4;\n" + " s.cap = 4;\n" + " let q: *i64 = &s.cap;\n" + " *q = -1i64;\n" + " let v: i64 = s.cap: i64;\n" + " if (v == -1i64) { return 1; };\n" + " return 0;\n" + "};\n", + 1 }, + /* Same shape for str.len. str header is (ptr, len) with len at + * +8, also 8B storage. */ + { "amp_dot_str_len_width_stress", + "fn main() i32 = {\n" + " let s: str = \"abcd\";\n" + " let q: *i64 = &s.len;\n" + " *q = -1i64;\n" + " let v: i64 = s.len: i64;\n" + " if (v == -1i64) { return 1; };\n" + " return 0;\n" + "};\n", + 1 }, + /* `&s.ptr` write-through. ptr lives at +0 and is `*T` (not a + * pseudo-i32), so `&s.ptr` types as `**T` — already the right + * width pre-fix. Pin it so a future regression doesn't slip the + * other way. */ + { "amp_dot_slice_ptr_writethrough", + "fn main() i32 = {\n" + " let arr: [2]u8;\n" + " arr[0] = 13; arr[1] = 0;\n" + " let s: []u8;\n" + " s.len = 2; s.cap = 2;\n" + " let pp: **u8 = &s.ptr;\n" + " *pp = &arr[0];\n" + " return s.ptr[0]: i32;\n" + "};\n", + 13 }, + /* Chained `&w.s.len` — slice WRAPPED in a struct. The pseudo- + * field width override gates on the leaf .len/.cap with base + * type TY_SLICE/TY_STR; for `&w.s.len` the base of the leaf + * dot is `w.s` (type []u8), so the override must still fire + * and the deref-store must hit all 8B at the wrapped slice + * header's len slot (offset of .s + 8). */ + { "amp_dot_slice_field_len_width", + "type wrap = struct { s: []u8, x: i32 };\n" + "fn main() i32 = {\n" + " let arr: [4]u8;\n" + " let w: wrap;\n" + " w.s.ptr = &arr[0];\n" + " w.s.len = 4; w.s.cap = 4;\n" + " let q: *i64 = &w.s.len;\n" + " *q = -1i64;\n" + " let v: i64 = w.s.len: i64;\n" + " if (v == -1i64) { return 1; };\n" + " return 0;\n" + "};\n", + 1 }, /* &o.s.ptr — slice-FIELD of a struct: combines shape 1 (walk * into struct field at .s) and shape 3 (slice pseudo-tail * .ptr at offset 0 of the header). Proves the spine walker's