Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 50 additions & 3 deletions src/bundle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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
Expand All @@ -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;
Comment on lines +130 to +133
}

static int bundle_scan_for_head(FILE *f) {
unsigned char want[BUNDLE_HEAD_LEN];
bundle_head_magic(want);
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions tests/test_bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +167 to +178

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)" ;;
Expand Down
Loading