Skip to content

stage1: exec stage2 from a sealed memfd, config on stdin#13

Merged
HarryR merged 1 commit into
mainfrom
memfd-sealed-exec
Jul 6, 2026
Merged

stage1: exec stage2 from a sealed memfd, config on stdin#13
HarryR merged 1 commit into
mainfrom
memfd-sealed-exec

Conversation

@HarryR

@HarryR HarryR commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Follows the boot-path security review. Removes the last on-disk artifacts from the stage2 launch and tightens the "measured == executed" invariant.

What changes

  • Sealed, pathless exec. The payload is loaded into an anonymous memfd, sealed with F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL, and launched with execveat(fd, "", AT_EMPTY_PATH). Previously we wrote it to /tmp/stage2.exe and exec'd that path, so the measured bytes and the executed bytes were two different objects with a window in between. Now the thing measured into PCR 14, the thing sealed immutable, and the thing executed are one kernel object. MFD_EXEC is requested where supported (Linux 6.3+ hardened kernels default new memfds to non-executable), with an EINVAL fallback for older kernels.

  • Config on stdin. The raw user-data JSON is delivered on stdin from a second memfd (dup2'd to fd 0), replacing /tmp/stage2-config.json. stdin is a universal channel that needs no extra-fd convention (which trips up runtimes like Bun/Node single-file executables), and as an in-memory file it has no pipe-size limit / no deadlock. No stage2 artifact is left on any named path.

example-stage2 now reads its config from stdin; the README documents the new contract. Config remains unmeasured by design (PCR 14 is code-only), consistent with the existing measurement model.

Verification

Full chain (stage0 -> UKI -> stage1 -> example-stage2) boots and powers off cleanly in both sha256 and ed25519 modes; the payload runs from the sealed memfd (argv[0]=stage2) and prints the config JSON received on stdin. Workspace tests pass; no new warnings.

🤖 Generated with Claude Code

Replace the write-to-/tmp-then-exec path with an in-memory, pathless
launch:

- Load the payload into an anonymous memfd, seal it (F_SEAL_WRITE plus
  SHRINK/GROW/SEAL), and execveat() it directly. The bytes measured into
  PCR 14 are now immutable and are exactly what runs; there is no named,
  writable inode to swap between measurement and exec, which closes the
  measure/exec TOCTOU. MFD_EXEC is requested where supported, with an
  EINVAL fallback for pre-6.3 kernels.

- Deliver the config (the raw user-data JSON) on stdin from a second
  memfd instead of /tmp/stage2-config.json. stdin needs no extra-fd
  convention (which trips up runtimes like Bun/Node single-file
  executables) and, being an in-memory file, has no pipe-size limit.
  No stage2 artifact is left on any named path.

example-stage2 now reads its config from stdin; README documents the
contract. Config stays unmeasured by design (code-only PCR 14).

Verified: full chain boots and powers off in both sha256 and ed25519
modes, with the config JSON received on stdin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR HarryR merged commit 5a4a4f3 into main Jul 6, 2026
4 checks passed
@HarryR HarryR deleted the memfd-sealed-exec branch July 6, 2026 07:46
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.

1 participant