Skip to content

bundle: a damaged executable refuses instead of starting the REPL (#882) - #899

Merged
InauguralPhysicist merged 1 commit into
mainfrom
fix/882-bundle-corrupt
Aug 5, 2026
Merged

bundle: a damaged executable refuses instead of starting the REPL (#882)#899
InauguralPhysicist merged 1 commit into
mainfrom
fix/882-bundle-corrupt

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

A bundle is the one artifact of this project designed to be copied and downloaded, so truncation is its normal failure mode. And a truncated bundle was indistinguishable from a plain interpreter: no magic at EOF → selfexec returned 0 → main() carried on with no script argument → REPLexit 0.

./myapp && echo "deployed"      # printed "deployed" for a corrupt binary

That contradicts the contract's "never report success on failure", while the tape layer next door already refuses every damaged input with exit 3 (#411).

Why this needed a format change, not a check

Nothing at EOF can separate a truncated bundle from a plain interpreter once the trailer is gone — that's the whole difficulty. So the archive gained a 24-byte head magic at its start (fmt 1 → 2), which survives truncation and is the only thing that can answer "was this ever a bundle?".

state before now
no head, no trailer (plain interpreter) REPL REPL — unchanged
head + valid trailer runs runs — unchanged
head, trailer missing/unreadable REPL, exit 0 refuse, exit 3
valid trailer, no head at its offset garbage/torn refuse, exit 3
trailer names an unreadable format message, then REPL, exit 0 refuse, exit 3

That last row wasn't in the issue — the fmt-mismatch path printed "re-bundle on this version" and then returned 0, dropping into the REPL at exit 0 anyway.

Two implementation notes, both load-bearing

The head magic is stored XOR-obfuscated in the runtime. Its plaintext must not appear in the image's rodata — otherwise every plain eigenscript start finds this very constant inside itself and declares itself a damaged bundle. That would have bricked the interpreter, so tests/test_bundle.sh now asserts the plain binary both runs a script and opens the REPL.

The scan is gated to the REPL path. Finding a head without a trailer needs a linear scan, so it runs only when startup would otherwise open the REPL (no script argument). eigenscript script.eigs — the hot path, and every one of the suite's thousands of invocations — pays nothing. The documented consequence: a damaged bundle invoked with arguments still behaves as an interpreter. The shipped form (./myapp) is the case that matters and is covered.

Verification

intact            bundled hello / 536              exit=0
truncated -50     bundle: archive header found but the trailer is missing…   exit=3
trailer zeroed    bundle: archive header found but the trailer is missing…   exit=3
plain binary      REPL starts, script runs         exit=0

tests/test_bundle.sh goes 8 → 13 checks. Existing test 6 (entry-level tear) now tears past the head magic, so it keeps exercising the entry-header misparse it was written for rather than degenerating into a head mismatch.

  • Release suite: 3792/3792
  • ASan + UBSan, detect_leaks=1: 3790/3790, leak tally 0

docs/BUNDLE.md documents the fmt-2 layout, the decision table, and both implementation constraints.

Closes #882

🤖 Generated with Claude Code

A bundle is the one artifact of this project designed to be COPIED and
DOWNLOADED, so truncation is its normal failure mode — and a truncated
bundle was indistinguishable from a plain interpreter. read_trailer found
no magic at EOF, selfexec returned 0, main() carried on with no script
argument, opened the REPL, and exited 0:

    ./myapp && echo "deployed"      # printed "deployed" for a corrupt binary

That contradicts LANGUAGE_CONTRACT.md's "never report success on
failure", and the tape layer next door already refuses every damaged
input with exit 3 (#411).

Nothing at EOF can separate a truncated bundle from a plain interpreter
once the trailer is gone, so the archive gained a 24-byte head magic at
its START (fmt 1 -> 2), which survives truncation. Startup refuses with
exit 3 on three states that all used to exit 0:

  - head present, trailer missing or unreadable (truncated/damaged)
  - valid trailer whose named offset has no head
  - trailer naming a format this binary cannot read (previously printed
    a message and then fell through to the REPL anyway)

Two implementation notes, both load-bearing:

The head magic is stored XOR-obfuscated in the runtime so its plaintext
never appears in the image's rodata. Without that, every plain
`eigenscript` start would find this very constant inside itself and
declare itself a damaged bundle — the fix would have bricked the
interpreter. tests/test_bundle.sh asserts the plain binary still runs a
script AND still opens the REPL, because that failure mode is total.

Finding the head without a trailer needs a linear scan, so it runs ONLY
when startup would otherwise open the REPL (no script argument).
`eigenscript script.eigs` — the hot path, and every one of the suite's
thousands of invocations — pays nothing. The documented consequence is
that a damaged bundle invoked WITH arguments still behaves as an
interpreter; the shipped form (`./myapp`) is the case that matters.

Existing test 6 (entry-level tear) now tears past the head magic so it
keeps exercising the entry-header misparse it was written for rather
than becoming a head mismatch.

Suite 3792/3792 release, 3790/3790 ASan+UBSan with detect_leaks=1, leak
tally 0. Bundle section: 13/13.

Closes #882

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 5, 2026 23:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the --bundle executable format and startup detection so that corrupted/truncated bundles refuse to run (exit 3) instead of silently starting the REPL and exiting 0, aligning bundle behavior with the project’s “never report success on failure” contract.

Changes:

  • Bumped bundle archive format to v2 and added a 24-byte head magic at the start of the appended archive to detect truncated bundles.
  • Updated bundle self-exec logic to refuse on missing/invalid trailer when head magic is present, and to refuse on format mismatch instead of falling through to the REPL.
  • Expanded bundle test coverage and documented the new format + damaged-bundle decision table; added changelog entry.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/bundle.c Adds fmt=2 head-magic marker + refusal paths (exit 3) for damaged/truncated bundles; validates head at trailer-named offset.
tests/test_bundle.sh Extends the bundle test suite to cover truncation, in-place trailer corruption, and no-false-positive checks for the plain interpreter.
docs/BUNDLE.md Documents fmt=2 layout, damaged-bundle refusal behavior, and the REPL-path-only scan constraint.
CHANGELOG.md Records the behavioral fix and format bump for corrupted bundles (#882).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/bundle.c
Comment on lines +75 to +78
static void bundle_head_magic(unsigned char out[BUNDLE_HEAD_LEN]) {
for (size_t i = 0; i < BUNDLE_HEAD_LEN; i++)
out[i] = (unsigned char)(BUNDLE_HEAD_OBF[i] ^ 0x5A);
}
Comment thread src/bundle.c
}
}

fseek(f, (long)(off + BUNDLE_HEAD_LEN), SEEK_SET);
Comment thread docs/BUNDLE.md

A torn archive (misparsing entry headers) refuses with exit 3 rather
than running garbage. There are no per-entry checksums — a flipped bit
inside one file's *data* is out of scope for fmt 1; the attached tape
@InauguralPhysicist
InauguralPhysicist merged commit df8f8be into main Aug 5, 2026
3 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the fix/882-bundle-corrupt branch August 5, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A corrupted --bundle executable silently starts the REPL and exits 0 — a truncated single-file artifact is indistinguishable from a successful run

2 participants