Finding
eigs_bundle_selfexec (src/bundle.c:316) treats a missing or damaged trailer as "this is not a bundle":
if (!read_trailer(f, &off, &count)) { fclose(f); return 0; }
main() then continues as an ordinary eigenscript invocation with no script argument — which starts the interactive REPL. So a corrupted bundle does not fail; it prints a banner and exits 0.
Reproduction
v0.38.0, 078e759. Build a bundle, damage it, run it with stdin closed (the CI / service / container-ENTRYPOINT case):
$ eigenscript --bundle app.eigs ./myapp
bundled app.eigs + 76 file(s) -> ./myapp # 1,491,683 bytes
$ ./myapp </dev/null ; echo "exit=$?"
bundled hello
536
exit=0
$ cp myapp trunc && truncate -s -50 trunc # remove 50 bytes
$ ./trunc </dev/null ; echo "exit=$?"
EigenScript 0.38.0
Type 'exit' or Ctrl-D to quit.
exit=0
Truncation amount is irrelevant — 50, 500, 5 000, and 50 000 bytes removed all produce the same banner and exit=0. Zeroing the last 40 bytes (corrupting the magic in place, without changing the file size) does too.
Interactively it is worse in a different way: with a terminal attached it sits at the REPL prompt waiting for input, so a container ENTRYPOINT or a cron job hangs indefinitely instead of failing.
Why this is worth fixing
A bundle is the one artifact of this project designed to be distributed — copied, downloaded, shipped in an image. Partial downloads, truncated copies, and interrupted builds are the normal failure modes for such a file, and the exit code is what every automated consumer keys on:
./myapp && echo "deployed" # prints "deployed" for a corrupt binary
It also contradicts the project's own stated rule. LANGUAGE_CONTRACT.md:56-58:
Programs never continue past an unrecovered error or report success on failure.
And it is inconsistent with how the sibling on-disk format is handled. I tested the tape paths in the same session and they are exemplary — every damaged input is refused loudly with exit 3:
$ eigenscript --step bad.tape
step: tape has no version header — … refusing to step (docs/TRACE.md) exit=3
$ EIGS_REPLAY=badver.tape eigenscript app.eigs
trace: tape format v99, this binary reads v2 — refusing to replay … exit=3
The tape layer refuses; the bundle layer — the artifact that actually gets shipped to other machines — silently degrades.
Why the current check cannot distinguish the cases
The magic-at-EOF probe conflates two states that need different answers:
- a plain
eigenscript interpreter (no trailer) → REPL is correct;
- a damaged bundle (trailer truncated away or zeroed) → should refuse.
Both look identical to read_trailer. Note, though, that they are trivially separable by size in every realistic case: the stock interpreter is ~808 KB, while the damaged bundles above are all ~1.49 MB — the archive payload is still there, only the trailer is gone.
Suggested fix
Write a head magic at the start of the appended archive region (immediately after the runtime image) and record the image size in the trailer. Startup then decides:
- no head magic → plain interpreter → REPL (unchanged behavior);
- head magic present and trailer valid → run the bundle (unchanged);
- head magic present but trailer missing/invalid → corrupt bundle: refuse with a clear message and a non-zero exit, matching the tape layer's contract.
That covers truncation (head magic survives, trailer does not) and in-place corruption (head magic survives, trailer fails to validate), while leaving the stock binary's behavior untouched. A length or checksum field over the archive region would additionally catch damage in the middle of the payload, which none of the probes above exercise.
Related: src/bundle.c:47-66,309-316, docs/BUNDLE.md, #411 (the tape refusal contract this should mirror).
Finding
eigs_bundle_selfexec(src/bundle.c:316) treats a missing or damaged trailer as "this is not a bundle":main()then continues as an ordinaryeigenscriptinvocation with no script argument — which starts the interactive REPL. So a corrupted bundle does not fail; it prints a banner and exits 0.Reproduction
v0.38.0,
078e759. Build a bundle, damage it, run it with stdin closed (the CI / service / container-ENTRYPOINT case):Truncation amount is irrelevant — 50, 500, 5 000, and 50 000 bytes removed all produce the same banner and
exit=0. Zeroing the last 40 bytes (corrupting the magic in place, without changing the file size) does too.Interactively it is worse in a different way: with a terminal attached it sits at the REPL prompt waiting for input, so a container ENTRYPOINT or a cron job hangs indefinitely instead of failing.
Why this is worth fixing
A bundle is the one artifact of this project designed to be distributed — copied, downloaded, shipped in an image. Partial downloads, truncated copies, and interrupted builds are the normal failure modes for such a file, and the exit code is what every automated consumer keys on:
It also contradicts the project's own stated rule.
LANGUAGE_CONTRACT.md:56-58:And it is inconsistent with how the sibling on-disk format is handled. I tested the tape paths in the same session and they are exemplary — every damaged input is refused loudly with exit 3:
The tape layer refuses; the bundle layer — the artifact that actually gets shipped to other machines — silently degrades.
Why the current check cannot distinguish the cases
The magic-at-EOF probe conflates two states that need different answers:
eigenscriptinterpreter (no trailer) → REPL is correct;Both look identical to
read_trailer. Note, though, that they are trivially separable by size in every realistic case: the stock interpreter is ~808 KB, while the damaged bundles above are all ~1.49 MB — the archive payload is still there, only the trailer is gone.Suggested fix
Write a head magic at the start of the appended archive region (immediately after the runtime image) and record the image size in the trailer. Startup then decides:
That covers truncation (head magic survives, trailer does not) and in-place corruption (head magic survives, trailer fails to validate), while leaving the stock binary's behavior untouched. A length or checksum field over the archive region would additionally catch damage in the middle of the payload, which none of the probes above exercise.
Related:
src/bundle.c:47-66,309-316,docs/BUNDLE.md,#411(the tape refusal contract this should mirror).