From 7a6b67feccfd7cdbffb7aaac27cd00ffb38b4923 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 14 Jun 2026 12:36:36 +0900 Subject: [PATCH] w6l: size -L/-l/input arrays by argc, not a fixed 64 (F-C) libdirs, lflags and inputs were fixed 64-slot arrays written with no bound check; the 65th -L/-l flag (or input) wrote past the allocation -> heap corruption. Size all three by argc instead, the true upper bound since each argv slot yields at most one entry, mirroring cstage cmd/w6l/main.c:63-67 (calloc(argc, ...)). Drop the now-dead maxinputs "too many inputs" cap -- cstage has none, and argc-sizing makes it unreachable. Regenerates the w6l combined.ww. Table-driven 632 test reaches a lib only via the Nth -L (N in {1,64,65,100,128}, both stages); pre-fix the nflags=65 row fails (slot one past the 64-array). --- Makefile | 5 + selfhost/cmd/w6l/main.combined.ww | 20 ++-- selfhost/cmd/w6l/main.ww | 20 ++-- test/wcc/632_w6l_manyflags.c | 151 ++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 test/wcc/632_w6l_manyflags.c diff --git a/Makefile b/Makefile index fa5d758f..2116501a 100644 --- a/Makefile +++ b/Makefile @@ -233,6 +233,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6c $(BIN)/test_w6a $(BIN)/test_w6a_parsenum \ $(BIN)/test_dataw $(BIN)/test_datar \ $(BIN)/test_w6l $(BIN)/test_data_link \ + $(BIN)/test_w6l_manyflags \ $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ $(BIN)/test_at_test $(BIN)/test_selfimport $(BIN)/test_missingpkg \ @@ -638,6 +639,10 @@ $(BIN)/test_datar: test/wcc/520_datar.c $(BIN)/w6a $(BIN)/w6l | $(BIN) $(BIN)/test_w6l: test/wcc/600_w6l.c $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_w6l_manyflags: test/wcc/632_w6l_manyflags.c $(BIN)/w6c $(BIN)/w6a \ + $(BIN)/w6l $(BIN)/w6l_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_data_link: test/wcc/620_data_link.c $(BIN)/w6a $(BIN)/w6l | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index 673d2c84..9e6120f2 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -5226,15 +5226,16 @@ fn isso(path: *u8) i32 = { export fn main(argc: i32, argv: **u8) i32 = { let outpath: *u8 = nil; - let maxinputs: i32 = 64; - let inputs: []*u8 = alloc([], maxinputs: u64)!; - inputs.len = maxinputs; + // argc is the upper bound on flags/inputs (one each per argv slot), + // mirroring cstage's calloc(argc, ...). No fixed cap. + let inputs: []*u8 = alloc([], argc: u64)!; + inputs.len = argc; let ninputs: i32 = 0; - let libdirs: []*u8 = alloc([], maxinputs: u64)!; - libdirs.len = maxinputs; + let libdirs: []*u8 = alloc([], argc: u64)!; + libdirs.len = argc; let nlibdirs: i32 = 0; - let lflags: []*u8 = alloc([], maxinputs: u64)!; - lflags.len = maxinputs; + let lflags: []*u8 = alloc([], argc: u64)!; + lflags.len = argc; let nlflags: i32 = 0; let i: i32 = 1; @@ -5292,11 +5293,6 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; };}; } else { - if (ninputs >= maxinputs) { - let m: str = "w6l: too many inputs\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; inputs[ninputs] = a; ninputs += 1; };};};}; diff --git a/selfhost/cmd/w6l/main.ww b/selfhost/cmd/w6l/main.ww index 00320423..37c8f65d 100644 --- a/selfhost/cmd/w6l/main.ww +++ b/selfhost/cmd/w6l/main.ww @@ -176,15 +176,16 @@ fn isso(path: *u8) i32 = { export fn main(argc: i32, argv: **u8) i32 = { let outpath: *u8 = nil; - let maxinputs: i32 = 64; - let inputs: []*u8 = alloc([], maxinputs: u64)!; - inputs.len = maxinputs; + // argc is the upper bound on flags/inputs (one each per argv slot), + // mirroring cstage's calloc(argc, ...). No fixed cap. + let inputs: []*u8 = alloc([], argc: u64)!; + inputs.len = argc; let ninputs: i32 = 0; - let libdirs: []*u8 = alloc([], maxinputs: u64)!; - libdirs.len = maxinputs; + let libdirs: []*u8 = alloc([], argc: u64)!; + libdirs.len = argc; let nlibdirs: i32 = 0; - let lflags: []*u8 = alloc([], maxinputs: u64)!; - lflags.len = maxinputs; + let lflags: []*u8 = alloc([], argc: u64)!; + lflags.len = argc; let nlflags: i32 = 0; let i: i32 = 1; @@ -242,11 +243,6 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; };}; } else { - if (ninputs >= maxinputs) { - let m: str = "w6l: too many inputs\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; inputs[ninputs] = a; ninputs += 1; };};};}; diff --git a/test/wcc/632_w6l_manyflags.c b/test/wcc/632_w6l_manyflags.c new file mode 100644 index 00000000..94c18515 --- /dev/null +++ b/test/wcc/632_w6l_manyflags.c @@ -0,0 +1,151 @@ +/* + * 632_w6l_manyflags — w6l -L/-l arrays must be sized by argc, not a + * fixed 64-slot cap (drain F-C). The 65th -L used to write past the + * allocation: `libdirs[nlibdirs] = ...` with no bound check, corrupting + * the heap and dropping the flag. + * + * Behavioural discriminator: make a -L past slot 64 LOAD-BEARING. We + * build libfoo.a in a real directory and reach it ONLY via the Nth -L, + * preceded by N-1 junk dirs. w6l errors `cannot find -lfoo` unless it + * can locate the archive, so a successful link (exit 0 + ET_EXEC) proves + * the Nth -L was honoured. N is a table {1, 64, 65, 100, 128}; pre-fix, + * N=65 (the slot exactly one past the 64-element array) reliably fails + * to round-trip and the link errors — verified by rebuilding the pre-fix + * w6l_ww. Both stages are driven for parity. + */ +#include +#include +#include +#include +#include + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +/* link `obj` with linker `lnk`, resolving -lfoo through `libdir` placed + * as the Nth (last) -L after nflags-1 junk dirs. Returns the linker's + * exit code; on success also fails (-1) if the output isn't an ET_EXEC + * ELF — proving the archive was actually located and linked. */ +static int +linkwith(const char *lnk, const char *obj, const char *libdir, int nflags) +{ + char exe[64]; + snprintf(exe, sizeof exe, "/tmp/wwfc_%d_x", getpid()); + + char junk[8192]; + size_t off = 0; + for (int i = 1; i < nflags; i++) + off += snprintf(junk + off, sizeof junk - off, + " -L/nonexist/wwfc_d%d", i); + + char cmd[16384]; + snprintf(cmd, sizeof cmd, + "%s -o %s %s%s -L%s -lfoo 2>/dev/null", + lnk, exe, obj, junk, libdir); + int rc = runwait(cmd); + if (rc != 0) { unlink(exe); return rc; } + + FILE *f = fopen(exe, "rb"); + if (!f) return -1; + unsigned char hdr[20]; + int ok = fread(hdr, 1, sizeof hdr, f) == sizeof hdr; + fclose(f); + unlink(exe); + if (!ok || memcmp(hdr, "\x7f""ELF", 4) != 0) return -1; + unsigned short etype = (unsigned short)hdr[16] + | ((unsigned short)hdr[17] << 8); + if (etype != 2) return -1; /* ET_EXEC */ + return 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + char src[64], asmf[64], obj[64]; + char fsrc[64], fasm[64], fobj[64]; + char libdir[64], lib[128], cmd[1024]; + int pid = getpid(); + snprintf(src, sizeof src, "/tmp/wwfc_%d_m.ww", pid); + snprintf(asmf, sizeof asmf, "/tmp/wwfc_%d_m.s", pid); + snprintf(obj, sizeof obj, "/tmp/wwfc_%d_m.o", pid); + snprintf(fsrc, sizeof fsrc, "/tmp/wwfc_%d_f.ww", pid); + snprintf(fasm, sizeof fasm, "/tmp/wwfc_%d_f.s", pid); + snprintf(fobj, sizeof fobj, "/tmp/wwfc_%d_f.o", pid); + snprintf(libdir, sizeof libdir, "/tmp/wwfc_%d_lib", pid); + snprintf(lib, sizeof lib, "%s/libfoo.a", libdir); + + /* a standalone main.o (no undefs) + an unrelated archived foo.o. */ + FILE *f = fopen(src, "wb"); + fputs("fn main() i32 = { return 42; };", f); + fclose(f); + f = fopen(fsrc, "wb"); + fputs("export fn foo() i32 = { return 7; };", f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s", bin, asmf, src); + if (runwait(cmd) != 0) { fprintf(stderr, "w6c main failed\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s", bin, obj, asmf); + if (runwait(cmd) != 0) { fprintf(stderr, "w6a main failed\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s", bin, fasm, fsrc); + if (runwait(cmd) != 0) { fprintf(stderr, "w6c foo failed\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s", bin, fobj, fasm); + if (runwait(cmd) != 0) { fprintf(stderr, "w6a foo failed\n"); return 1; } + snprintf(cmd, sizeof cmd, "mkdir -p %s && ar rcs %s %s", libdir, lib, fobj); + if (runwait(cmd) != 0) { fprintf(stderr, "ar failed\n"); return 1; } + + int counts[] = { 1, 64, 65, 100, 128 }; + int fail = 0; + for (int i = 0; i < (int)(sizeof counts / sizeof counts[0]); i++) { + int n = counts[i]; + char wlnk[2048], clnk[2048]; + snprintf(wlnk, sizeof wlnk, "%s/w6l_ww", bin); + snprintf(clnk, sizeof clnk, "%s/w6l", bin); + int wrc = linkwith(wlnk, obj, libdir, n); + int crc = linkwith(clnk, obj, libdir, n); + if (wrc != 0) { + fprintf(stderr, "FAIL: w6l_ww nflags=%d link rc=%d " + "(65th-style -L not honoured → heap overflow)\n", + n, wrc); + fail++; + } + if (crc != 0) { + fprintf(stderr, "FAIL: w6l (cstage) nflags=%d link rc=%d\n", + n, crc); + fail++; + } + } + + snprintf(cmd, sizeof cmd, "rm -rf %s %s %s %s %s %s %s", + src, asmf, obj, fsrc, fasm, fobj, libdir); + (void)runwait(cmd); + + if (fail) { + fprintf(stderr, "w6l_manyflags: %d failure(s)\n", fail); + return 1; + } + printf("w6l_manyflags: -L past slot 64 honoured by both stages " + "(1/64/65/100/128 flags)\n"); + return 0; +}