From 195ffaf503440713a0560af987f2e2a1ef2397b1 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Wed, 5 Aug 2026 19:33:44 -0500 Subject: [PATCH] bundle: the head magic must survive the optimizer (#882 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The head magic is stored XOR-obfuscated so its plaintext never lands in the runtime image — otherwise every plain `eigenscript` start finds the constant inside itself and refuses to run. That obfuscation is only as good as the compiler's willingness to leave it alone. gcc -O2 did not fold it. clang did: it constant-folded the XOR loop and materialized the plaintext in rodata, so on macOS the interpreter detected itself as a damaged bundle and exited 3. CI's macos-latest job caught it — 16 REPL checks, 6 CLI checks and the bundle suite's own "plain interpreter still starts the REPL" all red. Exactly the failure mode the obfuscation exists to prevent, on the platform I cannot build for locally. Two changes: - the XOR key is `volatile`, so the loop cannot be folded away; - a scan hit is only believed when a well-formed entry header follows it ([u32 path_len][printable path][u64 size] that fits the file), so a coincidental magic match anywhere in future rodata cannot make the interpreter refuse itself. And a gate that does not depend on trusting any of that: tests/test_bundle.sh now greps the BUILT interpreter for the plaintext magic and fails if it is there. That catches the fold at test time on any toolchain, present or future, rather than waiting for a platform job to go red. Validated both ways — the check passes on the real binary and fires on a file containing the magic. Suite 3805/3805; bundle section 13 -> 14 checks. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 8 ++++++- src/bundle.c | 53 +++++++++++++++++++++++++++++++++++++++--- tests/run_all_tests.sh | 2 +- tests/test_bundle.sh | 21 +++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f17593..e252e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -332,7 +332,13 @@ All notable changes to EigenScript are documented here. that a damaged bundle invoked *with* arguments still behaves as an interpreter; the shipped form (`./myapp`) is the case that matters. `tests/test_bundle.sh` covers all four states including the - no-false-positive checks on the plain binary. + no-false-positive checks on the plain binary — plus a **fold guard** + that greps the built interpreter for the plaintext magic, because the + obfuscation is only as good as the compiler's willingness to leave it + alone: gcc -O2 did not fold it, **clang did**, and CI's macOS job went + red with every REPL check failing at exit 3. The key is `volatile` + now, and a coincidental magic match is additionally rejected unless a + well-formed entry header follows it. - **vm_run_bytecode/sandbox_run: an assembled chunk's temporal opcodes now turn history recording on themselves (#831).** `g_trace_hist` was diff --git a/src/bundle.c b/src/bundle.c index d23f720..22c74c7 100644 --- a/src/bundle.c +++ b/src/bundle.c @@ -50,6 +50,7 @@ #define TRAILER_SIZE 24 #define BUNDLE_HEAD_LEN 24 + /* #882: a head magic at the START of the archive region, so a bundle whose * trailer is gone is still recognizable AS a bundle. * @@ -72,9 +73,18 @@ static const unsigned char BUNDLE_HEAD_OBF[BUNDLE_HEAD_LEN] = { 0x77, 0x1b, 0x08, 0x19, 0x12, 0x13, 0x0c, 0x1f, 0x77, 0x2c, 0x68, 0x5a }; +/* The key is `volatile` on purpose. With a plain constant, an optimizer is + * free to constant-fold this whole loop and materialize the PLAINTEXT in + * rodata — which is exactly what it must not do, because then the runtime + * image contains the magic and every plain `eigenscript` start finds it + * inside itself and refuses to run. gcc -O2 did not fold it; clang did, and + * CI's macOS job caught it as "plain interpreter still starts the REPL" + * failing with exit 3. A volatile read cannot be folded away. */ static void bundle_head_magic(unsigned char out[BUNDLE_HEAD_LEN]) { + static volatile unsigned char key = 0x5A; + unsigned char k = key; for (size_t i = 0; i < BUNDLE_HEAD_LEN; i++) - out[i] = (unsigned char)(BUNDLE_HEAD_OBF[i] ^ 0x5A); + out[i] = (unsigned char)(BUNDLE_HEAD_OBF[i] ^ k); } /* Refuse loudly, exit 3 — the same contract the tape layer uses for a damaged @@ -98,6 +108,31 @@ static int own_exe_path(const char *argv0, char *out, size_t cap) { * interpreter start, which is interactive, so an ~800 KB read is invisible. * Takes the FIRST occurrence: the writer emits it immediately after the * runtime image, so anything matching later is inside the payload. */ +/* Does a plausible archive entry header start at `off`? The writer emits + * [u32 path_len][path bytes, no NUL][u64 size][bytes], so a real archive head + * is followed by a small length, a printable relative path, and a size that + * fits in the file. Used to reject a coincidental magic match. */ +static int bundle_entry_header_plausible(FILE *f, long off) { + if (fseek(f, 0, SEEK_END) != 0) return 0; + long fsz = ftell(f); + if (fsz < 0 || off < 0 || off >= fsz) return 0; + if (fseek(f, off, SEEK_SET) != 0) return 0; + + uint32_t plen; + if (fread(&plen, 4, 1, f) != 1) return 0; + if (plen == 0 || plen > 2048) return 0; + + char rel[2049]; + if (fread(rel, 1, plen, f) != plen) return 0; + for (uint32_t i = 0; i < plen; i++) + if (rel[i] < 0x20 || (unsigned char)rel[i] > 0x7e) return 0; + + uint64_t size; + if (fread(&size, 8, 1, f) != 1) return 0; + if (size > (uint64_t)fsz) return 0; + return 1; +} + static int bundle_scan_for_head(FILE *f) { unsigned char want[BUNDLE_HEAD_LEN]; bundle_head_magic(want); @@ -112,8 +147,20 @@ static int bundle_scan_for_head(FILE *f) { size_t have = carry + got; if (have >= BUNDLE_HEAD_LEN) { for (size_t i = 0; i + BUNDLE_HEAD_LEN <= have; i++) - if (buf[i] == want[0] && memcmp(buf + i, want, BUNDLE_HEAD_LEN) == 0) - return 1; + if (buf[i] == want[0] && memcmp(buf + i, want, BUNDLE_HEAD_LEN) == 0) { + /* Belt and braces after the clang fold above: a match is + * only believed if a well-formed first entry header + * follows it. A stray occurrence in some future rodata + * blob will not be followed by a plausible + * [u32 path_len][printable path][u64 size], so it cannot + * make the interpreter refuse itself. */ + long at = ftell(f); + if (at < 0) return 0; + long head_end = at - (long)(have - i) + BUNDLE_HEAD_LEN; + int believable = bundle_entry_header_plausible(f, head_end); + if (fseek(f, at, SEEK_SET) != 0) return 0; + if (believable) return 1; + } } if (got == 0) return 0; carry = (have >= BUNDLE_HEAD_LEN - 1) ? BUNDLE_HEAD_LEN - 1 : have; diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index e873a94..c61ddfe 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -1661,7 +1661,7 @@ echo "" # [42g] --bundle (#413): single-file distribution — script + eigs_modules + # stdlib in one executable; tape-attached bundles replay byte-identically. -echo "[42g] Bundle (8 checks)" +echo "[42g] Bundle (14 checks)" BN_OUTPUT=$(bash "$TESTS_DIR/test_bundle.sh" 2>&1) BN_PASS=$(echo "$BN_OUTPUT" | grep -c "PASS:" || true) BN_FAIL=$(echo "$BN_OUTPUT" | grep -c "FAIL:" || true) diff --git a/tests/test_bundle.sh b/tests/test_bundle.sh index 3a069b2..a6250c4 100644 --- a/tests/test_bundle.sh +++ b/tests/test_bundle.sh @@ -156,6 +156,27 @@ POUT=$("$EIGS" "$APP/plain.eigs" 2>&1); RC=$? && ok "plain interpreter runs a script normally" \ || fail "plain interpreter runs a script normally" "rc=$RC out=$POUT" +# ---- 10b. THE regression gate for the above. The head magic is stored +# XOR-obfuscated so its plaintext is never in the runtime image; if a compiler +# constant-folds that obfuscation away, the plaintext lands in rodata and every +# plain `eigenscript` start finds the magic inside itself and refuses with +# exit 3. That is not hypothetical: gcc -O2 did not fold it, clang did, and +# CI's macOS job went red with "plain interpreter still starts the REPL" +# failing. The key is `volatile` now, but assert the OUTCOME rather than trust +# the compiler — this catches it at test time on any toolchain. +if command -v python3 >/dev/null 2>&1; then + if python3 -c " +import sys +plain = b'\\x7fEIGS-BUNDLE-ARCHIVE-v2\\x00' +sys.exit(0 if plain not in open('$EIGS','rb').read() else 1) +"; then + ok "head magic plaintext is NOT in the interpreter image (fold guard)" + else + fail "head magic plaintext is NOT in the interpreter image (fold guard)" \ + "the obfuscation was constant-folded — every plain start will refuse itself" + fi +fi + ROUT=$(echo 'print of "repl-ok"' | "$EIGS" 2>&1); RC=$? case "$ROUT" in *repl-ok*) ok "plain interpreter still starts the REPL (no false refusal)" ;;