Skip to content

fix(trace): tape-record file_exists / ls / mkdir / getcwd / exe_path (#585)#668

Merged
InauguralPhysicist merged 1 commit into
mainfrom
fix/trace-fs-nondet-585
Jul 18, 2026
Merged

fix(trace): tape-record file_exists / ls / mkdir / getcwd / exe_path (#585)#668
InauguralPhysicist merged 1 commit into
mainfrom
fix/trace-fs-nondet-585

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #585

Five filesystem builtins predated the trace tape and were neither recorded nor replay-blocked, so under EIGS_REPLAY they ran live. A program branching on file_exists of p replayed against the current filesystem, not the recorded one — a tape recorded on one machine silently diverged on another. The same closed-world hole #471 closed for args and #148 covered for proc_*, and it sat visibly next to the already-taped is_dir (#576) in the same BUILTINS.md table.

Reproduction (before)

record file_exists("probe") with the file present  -> 1
delete the file
replay                                              -> 0   # ran live

Fix

Each is wrapped with the TAKE/RECORD pair — their live paths do real work (fopen, readdir list construction, a syscall) before the return value exists, so TRACE_NONDET_RET alone would still run that work under replay. The early TAKE short-circuits before any fs access and serves the recorded value.

mkdir — the design decision

mkdir is a write whose return (a success bit) is filesystem-dependent. It's Recorded, not #148-non-replayable, because that bit is pinnable by the tape (unlike a subprocess fd). Under EIGS_REPLAY the TAKE short-circuits before mkdir(2), so the recorded bit is served and the directory is not created a second time — replay doesn't re-run the write side effect, the same rule as the subprocess/audio boundary.

Oracle (transcript, both tiers)

test_replay.sh records with the fs in one state, mutates it, and replays — the recorded answer must win:

builtin record mutate replay
file_exists present → 1 delete file 1 (JIT + EIGS_JIT_OFF)
ls 2 entries delete them 2
mkdir success → 1 remove dir 1, dir not re-created
getcwd dir A replay from / A

exe_path uses the same getcwd pattern but is invariant between record and replay (same interpreter binary), so it's documented rather than mutation-tested.

Consumer gates

The reason the trace-tape skill mandates checking them — one consumer calls these:

  • liferaft test/replay.sh: unaffected (calls none of the five), still N=1 (argv only).
  • tidelog test/replay.sh: calls file_exists; byte-for-byte identical record vs replay. The change improves its determinism — file_exists no longer runs live under replay.

Docs

docs/TRACE.md lists the five under Recorded Builtins with mkdir's write-side-effect note; docs/BUILTINS.md marks each trace-recorded, closing the side-by-side inconsistency the issue flagged.

Gates

  • Release suite 3090/3090
  • ASan + UBSan detect_leaks=1 3094/3094, leak tally 0
  • test_replay.sh 23/23 including strict-mode (exit 3 on name mismatch)

🤖 Generated with Claude Code

…585)

These five fs builtins predated the trace tape and were neither recorded nor
replay-blocked, so under EIGS_REPLAY they ran LIVE: a program branching on
`file_exists of p` replayed against the current filesystem, not the recorded
one — a tape recorded on one machine silently diverged on another. Same
closed-world hole #471 closed for `args` and #148 covered for proc_*. The
inconsistency was visible next to the already-taped is_dir (#576) and
read_line (#558) in the same BUILTINS.md table.

Each is now wrapped with the TAKE/RECORD pair (their live paths do real work —
fopen, readdir bulk-list construction, a syscall — before the return exists,
so TRACE_NONDET_RET alone would run the live work under replay). Under
EIGS_REPLAY the early TAKE short-circuits before any fs access and serves the
recorded value.

mkdir is the design call: a WRITE whose return (a success bit) is
filesystem-dependent. It is Recorded rather than #148-non-replayable because
that bit IS pinnable by the tape (unlike a subprocess fd), and under replay
the TAKE short-circuits before mkdir(2) — so the recorded bit is served and
the directory is NOT created a second time. Replay does not re-run the write
side effect, the same rule as the subprocess/audio boundary.

Oracle: test_replay.sh records with the fs in one state, mutates it, and
replays; the recorded answer must win (a live probe would now disagree).
Verified JIT on and EIGS_JIT_OFF. mkdir additionally asserts the directory is
not re-created on replay. exe_path uses the same getcwd pattern but is
invariant between record and replay (same interpreter), so it is documented
rather than mutation-tested.

docs/TRACE.md lists the five under Recorded Builtins with mkdir's
write-side-effect note; docs/BUILTINS.md marks each trace-recorded, closing
the side-by-side inconsistency the issue flagged.

Consumer gates green: liferaft/test/replay.sh (unaffected, N=1 argv only),
tidelog/test/replay.sh (calls file_exists; byte-for-byte identical).

Suite 3090/3090 release, 3094/3094 ASan+UBSan (detect_leaks=1, tally 0);
test_replay.sh 23/23 incl. strict-mode.
@InauguralPhysicist
InauguralPhysicist merged commit 6dac849 into main Jul 18, 2026
18 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the fix/trace-fs-nondet-585 branch July 18, 2026 06: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.

file_exists / ls / mkdir / getcwd / exe_path are untraced nondet reads — silently diverge under EIGS_REPLAY

1 participant