Skip to content

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

Description

@InauguralPhysicist

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:

  1. a plain eigenscript interpreter (no trailer) → REPL is correct;
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions