From 675a0368cdb297003a253aa611cd849635e910d6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 13 Jun 2026 10:17:51 +0900 Subject: [PATCH] w6l: dynamic e_entry rebases with the actual text offset, both stages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the dynamic section pushed the header past the first page, the entry point kept its first-page address — every sufficiently large dynamic binary SIGSEGV'd into the headers. Recompute e_entry as entry - 0x1000 + text_off in both stages (ELF: the entry must point into .text wherever it lands). Both stages move in one commit: one ELF contract; the 989_dynentry gate pins the field and the run. --- Makefile | 14 +++ cmd/w6l/dynout.c | 9 +- selfhost/cmd/w6l/dynout.ww | 8 +- selfhost/cmd/w6l/main.combined.ww | 8 +- test/wcc/989_dynentry_run.c | 186 ++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 test/wcc/989_dynentry_run.c diff --git a/Makefile b/Makefile index c4d9dbe5..581e37c0 100644 --- a/Makefile +++ b/Makefile @@ -265,6 +265,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_enumcap_run \ $(BIN)/test_wwdumpgate_run \ $(BIN)/test_datargate_run \ + $(BIN)/test_dynentry_run \ $(BIN)/test_ampfncollide_run \ $(BIN)/test_trycallcollide_run \ $(BIN)/test_gunsigned_run \ @@ -846,6 +847,19 @@ $(BIN)/test_datargate_run: test/wcc/989_datargate_run.c \ $(BIN)/w6a $(BIN)/w6a_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_dynentry_run (#63, F15 c6): w6l's dynamic e_entry is rebased onto the +# actual .text file offset once the dynamic headers overflow the first page, +# instead of the stale base+0x1000+val (which mapped into the headers → +# SIGSEGV). BOTH stages (#263). Builds + runs ~160-dyn-sym binaries on both +# driver twins and asserts exit 42 + byte-identical output (rule-10). Skips +# without libc. See the test header. +$(BIN)/test_dynentry_run: test/wcc/989_dynentry_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_ampfncollide_run (#4, c2): `&fn` synthesis (unoptype TK_AMP + # assignableaddrfn) prefers the current module's fn when a same-leaf fn is # declared in a later module. Builds/rejects on BOTH driver twins (rule-10). diff --git a/cmd/w6l/dynout.c b/cmd/w6l/dynout.c index 2c759996..355889cb 100644 --- a/cmd/w6l/dynout.c +++ b/cmd/w6l/dynout.c @@ -589,7 +589,14 @@ l_emit_dyn_elf(Lnk *l, FILE *f, u64 base, u64 entry) eh.e_type = ET_EXEC; eh.e_machine = EM_X86_64; eh.e_version = EV_CURRENT; - eh.e_entry = entry; + /* #63: main computed `entry` as base + 0x1000 + val assuming .text sits + * at file offset 0x1000, but text_off above is recomputed and overflows + * 0x1000 once the dynamic headers exceed the first page (e.g. ~100 dyn + * syms). Rebase e_entry onto the actual text_off so it points at the + * real _start instead of into the headers. e_entry is a virtual address; + * PT_LOAD #1 maps file offset 0 at `base`, so the entry VA tracks + * text_off. Both stages fixed together (#263). */ + eh.e_entry = entry - 0x1000 + text_off; eh.e_phoff = ehdr_sz; eh.e_ehsize = sizeof(Ehdr); eh.e_phentsize = sizeof(Phdr); diff --git a/selfhost/cmd/w6l/dynout.ww b/selfhost/cmd/w6l/dynout.ww index 8f2f1e6c..61e7d12a 100644 --- a/selfhost/cmd/w6l/dynout.ww +++ b/selfhost/cmd/w6l/dynout.ww @@ -661,7 +661,13 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { dwr16(filebuf.ptr, 16u64, ET_EXEC_D); dwr16(filebuf.ptr, 18u64, EM_X86_64_D); dwr32(filebuf.ptr, 20u64, EV_CURRENT_D); - dwr64(filebuf.ptr, 24u64, entry); + // #63: main computed `entry` as BASE + CODE_VA_OFF (== PAGE) + val + // assuming .text sits at file offset 0x1000, but textoff above is + // recomputed and overflows 0x1000 once the dynamic headers exceed the + // first page (e.g. ~100 dyn syms). Rebase e_entry onto the actual textoff + // so it points at the real _start instead of into the headers. Both + // stages (#263). PAGE is dynout-local; equals main.ww's CODE_VA_OFF. + dwr64(filebuf.ptr, 24u64, entry - PAGE + textoff); dwr64(filebuf.ptr, 32u64, ehdrsz); dwr64(filebuf.ptr, 40u64, 0u64); dwr32(filebuf.ptr, 48u64, 0u32); diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index 2e049e2d..1594d7c0 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -4754,7 +4754,13 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { dwr16(filebuf.ptr, 16u64, ET_EXEC_D); dwr16(filebuf.ptr, 18u64, EM_X86_64_D); dwr32(filebuf.ptr, 20u64, EV_CURRENT_D); - dwr64(filebuf.ptr, 24u64, entry); + // #63: main computed `entry` as BASE + CODE_VA_OFF (== PAGE) + val + // assuming .text sits at file offset 0x1000, but textoff above is + // recomputed and overflows 0x1000 once the dynamic headers exceed the + // first page (e.g. ~100 dyn syms). Rebase e_entry onto the actual textoff + // so it points at the real _start instead of into the headers. Both + // stages (#263). PAGE is dynout-local; equals main.ww's CODE_VA_OFF. + dwr64(filebuf.ptr, 24u64, entry - PAGE + textoff); dwr64(filebuf.ptr, 32u64, ehdrsz); dwr64(filebuf.ptr, 40u64, 0u64); dwr32(filebuf.ptr, 48u64, 0u32); diff --git a/test/wcc/989_dynentry_run.c b/test/wcc/989_dynentry_run.c new file mode 100644 index 00000000..d71a538d --- /dev/null +++ b/test/wcc/989_dynentry_run.c @@ -0,0 +1,186 @@ +/* + * 989_dynentry_run (#63, F15 c6) — w6l's dynamic e_entry must track the actual + * .text file offset once the dynamic headers overflow the first page. + * + * THE BUG (BOTH stages, cat-A #263, real ELF ABI): main computed the entry VA + * as base + 0x1000 + entry->val assuming .text sits at file offset 0x1000, but + * the dynamic emitter recomputes text_off = alignup(headers_end, page) and + * wrote the stale entry into e_entry unchanged. With ~100+ dynamic symbols the + * dynsym/dynstr/hash/versym/verneed/rela.plt headers push past 0x1000, so + * .text lands at 0x2000+ while e_entry still pointed at base+0x1000+val — a + * mapped R+X page of header bytes. The linker exits 0; the binary SIGSEGVs. + * Byte-id gates can't see it (both stages identically wrong); test 992 covers + * only static links and 810_dyn uses a handful of syms. THE FIX (both stages): + * rebase e_entry = entry - 0x1000 + text_off (cmd/w6l/dynout.c + + * selfhost/cmd/w6l/dynout.ww). + * + * row | dyn syms | result (cs==ww) + * ------+----------+------------------------------------------- + * big | ~160 | exit 42 (was SIGSEGV 139 both stages), byte-id + * small | 1 | exit 42 (control, headers fit page), byte-id + * + * big was RED pre-c6 on BOTH stages (e_entry into the headers → SIGSEGV). + * small pins the headers-fit-page path stays correct and byte-identical. + * + * Generates the dyn-sym set from `nm -D ` (default-versioned text + * exports) so it adapts to whatever glibc is present; skips (passing) when + * libc / nm are unavailable or too few symbols to overflow the first page. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +/* Harvest up to `max` default-versioned libc text exports into syms[][]. + * Returns the count, or -1 if nm/libc unavailable. */ +static int +harvest_syms(const char *libc, char syms[][32], int max) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, + "nm -D %s 2>/dev/null | grep ' T ' | grep '@@GLIBC_2' " + "| grep -v PRIVATE | sed 's/.* //; s/@@.*//' " + "| grep -E '^[a-z_][a-zA-Z0-9_]{2,18}$' | sort -u", libc); + FILE *p = popen(cmd, "r"); + if (!p) return -1; + int n = 0; + char line[128]; + while (n < max && fgets(line, sizeof line, p)) { + line[strcspn(line, "\n")] = '\0'; + if (line[0] == '\0') continue; + snprintf(syms[n], 32, "%s", line); + n++; + } + pclose(p); + return n; +} + +/* Build with `driver` into /main, run, return exit code (or -1 on + * build failure). Sets *outbin to the produced binary path. */ +static int +build_run(const char *driver, const char *src, const char *libdir, + const char *tmpdir, char *outbin, size_t obsz) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cd %s && %s build -o %s/main %s -L %s -l c " + "2>/dev/null", tmpdir, driver, tmpdir, src, libdir); + if (runwait(cmd) != 0) return -1; + snprintf(outbin, obsz, "%s/main", tmpdir); + return runwait(outbin); +} + +int +main(void) +{ + const char *libc = NULL, *libdir = NULL; + if (access("/usr/lib/libc.so.6", 0) == 0) { libc = "/usr/lib/libc.so.6"; libdir = "/usr/lib"; } + else if (access("/lib/x86_64-linux-gnu/libc.so.6", 0) == 0) { libc = "/lib/x86_64-linux-gnu/libc.so.6"; libdir = "/lib/x86_64-linux-gnu"; } + else if (access("/lib64/libc.so.6", 0) == 0) { libc = "/lib64/libc.so.6"; libdir = "/lib64"; } + if (!libc) { puts("dynentry: no libc.so.6 — skipping"); return 0; } + + static char syms[200][32]; + int ns = harvest_syms(libc, syms, 200); + if (ns < 130) { + printf("dynentry: only %d libc syms harvested (<130, can't overflow " + "first page) — skipping\n", ns); + return 0; + } + + 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 cdrv[1100], wdrv[1100]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + int have_ww = (access(wdrv, X_OK) == 0); + + /* ---- big: ~160 gated dead PLT calls, return 42 ---- */ + char bigsrc[80]; + snprintf(bigsrc, sizeof bigsrc, "/tmp/dynent_big_%d.ww", getpid()); + FILE *f = fopen(bigsrc, "wb"); + if (!f) return 1; + fputs("package main;\n", f); + for (int i = 0; i < ns; i++) + fprintf(f, "@symbol(\"%s\") fn s%03d() void;\n", syms[i], i); + fputs("let g: i32 = 0;\nexport fn main() i32 = {\n", f); + for (int i = 0; i < ns; i++) + fprintf(f, "\tif (g != 0) { s%03d(); };\n", i); + fputs("\treturn 42;\n};\n", f); + fclose(f); + + /* ---- small: 1 dyn sym, headers fit the page ---- */ + char smallsrc[80]; + snprintf(smallsrc, sizeof smallsrc, "/tmp/dynent_small_%d.ww", getpid()); + f = fopen(smallsrc, "wb"); + if (!f) return 1; + fputs("package main;\n" + "@symbol(\"getpid\") fn cgetpid() i32;\n" + "let g: i32 = 0;\n" + "export fn main() i32 = { if (g != 0) { cgetpid(); }; return 42; };\n", + f); + fclose(f); + + struct { const char *label; const char *src; } cases[] = { + { "big", bigsrc }, { "small", smallsrc }, + }; + + int fail = 0, total = 0; + for (int ci = 0; ci < 2; ci++) { + total++; + char ctd[80], wtd[80], cob[160], wob[160]; + snprintf(ctd, sizeof ctd, "/tmp/dynent_c_%d_%d", getpid(), ci); + snprintf(wtd, sizeof wtd, "/tmp/dynent_w_%d_%d", getpid(), ci); + mkdir(ctd, 0755); mkdir(wtd, 0755); + + int crc = build_run(cdrv, cases[ci].src, libdir, ctd, cob, sizeof cob); + if (crc != 42) { + fprintf(stderr, "dynentry[cstage][%s]: exit %d want 42 " + "(e_entry into headers? #63)\n", cases[ci].label, crc); + fail++; + } + if (have_ww) { + int wrc = build_run(wdrv, cases[ci].src, libdir, wtd, wob, sizeof wob); + if (wrc != 42) { + fprintf(stderr, "dynentry[wwstage][%s]: exit %d want 42 " + "(e_entry into headers? #63)\n", cases[ci].label, wrc); + fail++; + } + char cmp[400]; + snprintf(cmp, sizeof cmp, "cmp -s %s %s", cob, wob); + if (runwait(cmp) != 0) { + fprintf(stderr, "dynentry[%s]: cs/ww binary differ (#63)\n", + cases[ci].label); + fail++; + } + unlink(wob); + } + unlink(cob); + rmdir(ctd); rmdir(wtd); + } + + unlink(bigsrc); unlink(smallsrc); + + if (fail) { + fprintf(stderr, "dynentry_run: %d/%d check(s) failed\n", fail, total); + return 1; + } + printf("dynentry_run: %d/%d ok (%d dyn syms)\n", total, total, ns); + return 0; +}