From 6d73a644ef0de8d3c671107b28b6647805bbe4e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 11:06:43 +0000 Subject: [PATCH] fix(procsub): =(cmd) takes a fresh temp name, never one a dead shell 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-. 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: ") 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) Claude-Session: https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG --- src/platform/posix/procsub_file.c | 20 ++++++++++++++------ tests/zsh_procsub_test.py | 11 +++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/platform/posix/procsub_file.c b/src/platform/posix/procsub_file.c index ae892936..c80095e3 100644 --- a/src/platform/posix/procsub_file.c +++ b/src/platform/posix/procsub_file.c @@ -13,7 +13,9 @@ #include "shell.h" #include "libft.h" #include "sys.h" -#include +#include +#include +#include #include #include @@ -68,18 +70,24 @@ static int run_into_fd(t_shell *state, const char *cmd, int fd) } /* Create the temp file and record it so the session cleans it up. Returns - the path (owned by the caller), or NULL. */ + the path (owned by the caller), or NULL. + The name comes from mkstemp, not from the pid: a =(cmd) made inside + $( ) outlives its subshell, so /tmp keeps files from processes long + gone, and a later shell given the same pid found its name taken. The + O_EXCL open failed, the word vanished without a word, and `cat =(...)` + read its stdin instead. mkstemp keeps O_EXCL's safety and retries a + taken name itself. */ char *create_procsub_file(t_shell *state, const char *cmd) { t_procsub_entry entry; char buf[64]; int fd; - ft_snprintf(buf, sizeof(buf), "%s/hsh%d-%d", TMP_DIR, - (int)getpid(), (int)state->proc_subs.len); - fd = open(buf, O_RDWR | O_CREAT | O_EXCL, 0600); + ft_strlcpy(buf, TMP_DIR "/hsh.XXXXXX", sizeof(buf)); + fd = mkstemp(buf); if (fd < 0) - return (NULL); + return (ft_eprintf("%s: =(...): cannot create a temporary file: " + "%s\n", state->ctx, strerror(errno)), NULL); if (cmd && *cmd && run_into_fd(state, cmd, fd) < 0) return (close(fd), unlink(buf), NULL); if (!cmd || !*cmd) diff --git a/tests/zsh_procsub_test.py b/tests/zsh_procsub_test.py index 6bec051a..6f40104f 100644 --- a/tests/zsh_procsub_test.py +++ b/tests/zsh_procsub_test.py @@ -118,6 +118,17 @@ def temp_file_cases(): check("temp: path was printed", bool(re.match(r"^/.*", path)), True) if path: check("temp: cleaned up after exit", os.path.exists(path), False) + # A file left in /tmp by another process must never block a new one. + # The name used to be /tmp/hsh-, opened O_EXCL: a leftover + # from an earlier process that had the same pid (a =(cmd) made inside + # $( ) outlives its subshell) failed the open, the word vanished, and + # `cat` read its stdin instead -- no error, status 0. It surfaced as + # zsh_glob_test's eqsub/reads-back failing about once in 150 runs. + out, _ = run_zsh(': > /tmp/hsh$$-0; : > /tmp/hsh$$-1\n' + 'cat =(echo hi)