Skip to content

fix(procsub): =(cmd) takes a fresh temp name, never one a dead shell left - #160

Merged
LESdylan merged 1 commit into
developfrom
fix/eqsub-unique-temp
Sep 26, 2026
Merged

LESdylan merged 1 commit into
developfrom
fix/eqsub-unique-temp

Conversation

@LESdylan

Copy link
Copy Markdown
Member

What & why

zsh_glob_test's eqsub/reads-back failed in a local pty gate run: cat =(echo hi) printed nothing, exited 0 and reported no error. It happens about once in 150 runs, on develop and on a 3.1.0 build as well.

strace of a failing run:

openat("/tmp/hsh2222-0", O_RDWR|O_CREAT|O_EXCL) = -1 EEXIST

Root cause.

  • The temp name was /tmp/hsh<pid>-<n>.
  • A =(cmd) made inside $( ) outlives its subshell, so /tmp keeps files from processes long gone: 88 of them in this container, 2 more after every suite run.
  • A later shell that got the same pid found its name taken, and the O_EXCL open failed.
  • create_procsub_file then returned NULL and the word vanished without any message. cat ran with no argument and read its stdin instead.

Change (src/platform/posix/procsub_file.c):

  • The name now comes from mkstemp, as TMP_DIR/hsh.XXXXXX. It is still exclusive and mode 0600, and a name that is already taken is retried instead of being fatal.
  • If the file cannot be created at all, the shell says so (=(...): cannot create a temporary file: <reason>) instead of silently dropping the word.

This is the flake mentioned in #157's verification notes.

How I verified it

  • Reproduced first, made deterministic. tests/zsh_procsub_test.py gains the collision itself: the script creates /tmp/hsh$$-0 and /tmp/hsh$$-1 first, then runs cat =(echo hi) </dev/null. develop prints nothing; this branch prints hi.
  • The original loop, cat =(echo hi) 300 times, goes from 1 or 2 empty runs to 0.
  • Local gates on this commit (ASan debug build):
    • golden tests/tester: 5363/5363;
    • tests/run_scripts.sh against bash --posix: 125/125;
    • verify_alloc.sh: identical output on both heaps;
    • alloc_stress.sh: all clean;
    • tests/pty_suite.sh: 111 ok, 6 skipped, 4 failed, with zsh_procsub_test and zsh_glob_test among the passes. None of the four failures is this change:
      • prompt_compat_matrix, prompt_drift_matrix and prompt_jobs_badge expect the non-root %/$ prompt and get #, because the container runs as root. They fail the same way on develop, and CI runs them as a normal user.
      • hxp_framework_test hit the 420 s per-file limit on this 4-core container. develop's binary takes 440 s for it on the same machine, and CI's runners finish it inside the limit.
  • norminette is OK on the touched file.

Notes / trade-offs

  • The name no longer contains the pid, so a leftover file can't be matched to its shell by name alone. Nothing in src/ parsed that name. Cleanup is unchanged: the session still removes every file it recorded.
  • Not fixed here: a =(cmd) made inside $( ) still leaves its file in /tmp when that subshell exits. This PR makes those leftovers harmless; removing them is a separate change, in how a subshell hands off its cleanup list.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG


Generated by Claude Code

…left

zsh_glob_test's eqsub/reads-back failed in a local pty gate run:

    cat =(echo hi)      -> no output, status 0, no error

About one run in 150 does this on develop, and on a 3.1.0 build too.
strace of a failing run:

    openat("/tmp/hsh2222-0", O_RDWR|O_CREAT|O_EXCL) = -1 EEXIST

The name was /tmp/hsh<pid>-<n>. A =(cmd) made inside $( ) outlives its
subshell, so /tmp keeps such files from processes long gone (88 of them
here, 2 more on every run of the suite). A later shell given the same
pid found its name taken. The O_EXCL open failed, create_procsub_file
returned NULL, the word vanished without a word, and `cat` ran with no
argument and read its stdin instead.

The name now comes from mkstemp: TMP_DIR/hsh.XXXXXX, still O_EXCL and
0600, and a taken name is retried rather than fatal. If the file cannot
be created at all, the shell says so ("=(...): cannot create a
temporary file: <reason>") instead of dropping the word silently.

tests/zsh_procsub_test.py gains the collision itself, made
deterministic: the script creates /tmp/hsh$$-0 and -1 first, then runs
`cat =(echo hi) </dev/null`. develop prints nothing; this prints hi.
The original loop, `cat =(echo hi)` 300 times, goes from 1 or 2 empty
runs to 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG
@LESdylan
LESdylan merged commit 0959d48 into develop Sep 26, 2026
36 checks passed
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.

2 participants