Nixpkgs version
Describe the bug
zsh hangs in __sigsuspend on darwin after PR #508474
Both of my macOS machines started hanging in interactive zsh sometime in the last few days. Fresh Terminal, type pwd, get the output, and the next prompt never draws. The shell still accepts keystrokes, but Ctrl-C does nothing and Enter does nothing. New ssh sessions hang the same way. Bash is fine. zsh -c 'echo OK' (no .zshrc) is fine. zsh -ic 'echo OK' reliably hangs.
After digging through it I'm fairly sure this is a regression from #508474 (darwin: migrate source releases from apple-sdk to darwin). The build still succeeds, but the resulting zsh binary embeds a race-prone signal-handling path that loses SIGCHLD wakeups.
Steps to reproduce
On a darwin host rebuilt against post-#508474 nixpkgs, with any non-empty interactive .zshrc:
zsh -c 'echo OK' # works (skips .zshrc)
zsh -ic 'echo OK' # hangs
bash -ic 'echo OK' # works (control)
The hang triggers on the first $(...) reaping race during init or precmd. Common triggering call sites I've observed:
eval "$(starship init zsh)" during init
eval "$(direnv hook zsh)" in precmd
$(hostname -f 2>/dev/null) inside iTerm2 shell-integration's per-prompt hook
Expected behaviour
I expected the shell to operate as normal, so that after executing pwd I receive a following shell prompt. Instead, the next shell prompt never comes and I cannot interact with my machine any further using zsh.
Screenshots
No response
Relevant log output
Additional context
What's actually wrong
zsh's configure.ac (around line 2334) does an AC_RUN_IFELSE test: fork, have the child exit, then call sigsuspend() with an empty mask and check whether the SIGCHLD handler observed the child. On the new stdenv that test program returns nonzero inside the build sandbox, so autoconf records zsh_cv_sys_sigsuspend=no and defines BROKEN_POSIX_SIGSUSPEND. Src/signals.c:signal_suspend() then compiles a sigprocmask(SIG_UNBLOCK,…) + pause() fallback instead of the atomic sigsuspend(&saved_mask). That fallback has a race window between unmask and pause; when a $(...) command-substitution child exits, the SIGCHLD fires its handler in that window and updates zsh's job-table bookkeeping, then pause() waits forever for another signal that never comes.
The nix log output for both builds is the cleanest evidence:
pre-#508474 build: checking if POSIX sigsuspend() works... yes
post-#508474 build: checking if POSIX sigsuspend() works... no
And the symbol-import diff between the resulting binaries:
pre: nm -u zsh | grep -E '_(sigsuspend|pause)$' -> _sigsuspend
post: nm -u zsh | grep -E '_(sigsuspend|pause)$' -> _pause
Same source, same upstream version (5.9), different code path compiled in.
Diagnostic checks
If you're staring at a wedged zsh and want to confirm:
sample <pid> shows signal_suspend → pause → __sigsuspend. The pause frame above __sigsuspend is the broken code path. (macOS implements pause(3) internally as sigsuspend(0), so the kernel-side symbol shows up either way; the userspace frames are what distinguish the two paths.)
- The stuck zsh has no child in
ps (running or zombie); the $(...) subprocess already exited.
lsof -p <pid> shows the command-sub pipe FD with no peer.
nm -u $(command -v zsh) | grep -E '_(sigsuspend|pause)$' prints _pause. A working zsh prints _sigsuspend. This is the most direct check and works without attaching to a running process.
Workaround
I'm running this overlay on both machines and it produces a working zsh:
zsh = prev.zsh.overrideAttrs (old: prev.lib.optionalAttrs prev.stdenv.isDarwin {
preConfigure = (old.preConfigure or "") + ''
export zsh_cv_sys_sigsuspend=yes
'';
});
After rebuild, nix log says ... (cached) yes, the binary imports _sigsuspend instead of _pause, and zsh -ic 'echo OK' returns in under a second.
What I don't know
I haven't bisected why the autoconf test fails inside the new sandbox. The same runtime check works correctly on the user-side machine running the resulting binary -- _sigsuspend is in libSystem.B.dylib and behaves as POSIX requires. So the bug isn't in macOS or in zsh; it's a sandbox-vs-real-kernel discrepancy that causes autoconf to bake the wrong code path into the binary. Plausible candidates: sandbox profile rewriting SIGCHLD delivery between sandbox-internal processes, feature-test macros from the new SDK headers altering sigaction flag defaults, or fork-accounting differences in the new builder bash. I haven't proved any of these.
It's worth thinking about whether other AC_RUN_IFELSE probes in other darwin packages are quietly failing the same way and downgrading to inferior fallbacks. The zsh case is loud because the result is a hang.
Suggested fix
The cheapest unblock is to add the zsh_cv_sys_sigsuspend=yes override to nixpkgs' zsh package as a darwin-conditional preConfigure, same shape as the workaround above. That fixes every affected user immediately. The cost is skipping a runtime probe whose answer we already know on darwin.
The more durable fix is figuring out what changed about the sandbox-side signal-delivery in #508474 and making the runtime test return the right answer. That's preferable long-term, but the cache override is straightforward and I'm happy to send a PR if maintainers want it.
Environment
- Two
aarch64-darwin hosts, macOS 26.4.1 (build 25E253), kernel Darwin 25.4.0 / xnu-12377.101.15~1
- nix-darwin + home-manager + flakes
- Boundary on each host: rebuild that swapped zsh from
2sbihdb…-zsh-5.9 to 80qadsn…-zsh-5.9 (same upstream version, different hash).
- Closure diff at the boundary:
apple-sdk +354 MiB, plus zsh + pcre2 + ncurses + libiconv all rebuilt with same versions and new hashes -- the canonical fingerprint of an stdenv-level rebuild.
System metadata
- system:
"aarch64-darwin"
- host os:
Darwin 25.4.0, macOS 26.4.1
- multi-user?:
yes
- sandbox:
no
- version:
nix-env (Determinate Nix 3.11.2) 2.31.1
- nixpkgs:
/nix/store/i2gsp87gqp16whm9mw0ybk9n84zir01x-source
Are you using nix-darwin?
Yes, I am using nix-darwin.
Notify maintainers
cc @reckenrode (who landed #508474), @NixOS/darwin-maintainers
Note for maintainers: Please tag this issue in your pull request description. (i.e. Resolves #ISSUE.)
I assert that this issue is relevant for Nixpkgs
Is this issue important to you?
Add a 👍 reaction to issues you find important.
Nixpkgs version
Describe the bug
zsh hangs in
__sigsuspendon darwin after PR #508474Both of my macOS machines started hanging in interactive zsh sometime in the last few days. Fresh Terminal, type
pwd, get the output, and the next prompt never draws. The shell still accepts keystrokes, but Ctrl-C does nothing and Enter does nothing. Newsshsessions hang the same way. Bash is fine.zsh -c 'echo OK'(no.zshrc) is fine.zsh -ic 'echo OK'reliably hangs.After digging through it I'm fairly sure this is a regression from #508474 (
darwin: migrate source releases from apple-sdk to darwin). The build still succeeds, but the resulting zsh binary embeds a race-prone signal-handling path that loses SIGCHLD wakeups.Steps to reproduce
On a darwin host rebuilt against post-#508474 nixpkgs, with any non-empty interactive
.zshrc:The hang triggers on the first
$(...)reaping race during init or precmd. Common triggering call sites I've observed:eval "$(starship init zsh)"during initeval "$(direnv hook zsh)"in precmd$(hostname -f 2>/dev/null)inside iTerm2 shell-integration's per-prompt hookExpected behaviour
I expected the shell to operate as normal, so that after executing
pwdI receive a following shell prompt. Instead, the next shell prompt never comes and I cannot interact with my machine any further using zsh.Screenshots
No response
Relevant log output
Additional context
What's actually wrong
zsh's
configure.ac(around line 2334) does anAC_RUN_IFELSEtest: fork, have the child exit, then callsigsuspend()with an empty mask and check whether the SIGCHLD handler observed the child. On the new stdenv that test program returns nonzero inside the build sandbox, so autoconf recordszsh_cv_sys_sigsuspend=noand definesBROKEN_POSIX_SIGSUSPEND.Src/signals.c:signal_suspend()then compiles asigprocmask(SIG_UNBLOCK,…) + pause()fallback instead of the atomicsigsuspend(&saved_mask). That fallback has a race window between unmask and pause; when a$(...)command-substitution child exits, the SIGCHLD fires its handler in that window and updates zsh's job-table bookkeeping, thenpause()waits forever for another signal that never comes.The
nix logoutput for both builds is the cleanest evidence:And the symbol-import diff between the resulting binaries:
Same source, same upstream version (5.9), different code path compiled in.
Diagnostic checks
If you're staring at a wedged zsh and want to confirm:
sample <pid>showssignal_suspend → pause → __sigsuspend. Thepauseframe above__sigsuspendis the broken code path. (macOS implementspause(3)internally assigsuspend(0), so the kernel-side symbol shows up either way; the userspace frames are what distinguish the two paths.)ps(running or zombie); the$(...)subprocess already exited.lsof -p <pid>shows the command-sub pipe FD with no peer.nm -u $(command -v zsh) | grep -E '_(sigsuspend|pause)$'prints_pause. A working zsh prints_sigsuspend. This is the most direct check and works without attaching to a running process.Workaround
I'm running this overlay on both machines and it produces a working zsh:
After rebuild,
nix logsays... (cached) yes, the binary imports_sigsuspendinstead of_pause, andzsh -ic 'echo OK'returns in under a second.What I don't know
I haven't bisected why the autoconf test fails inside the new sandbox. The same runtime check works correctly on the user-side machine running the resulting binary --
_sigsuspendis inlibSystem.B.dyliband behaves as POSIX requires. So the bug isn't in macOS or in zsh; it's a sandbox-vs-real-kernel discrepancy that causes autoconf to bake the wrong code path into the binary. Plausible candidates: sandbox profile rewriting SIGCHLD delivery between sandbox-internal processes, feature-test macros from the new SDK headers alteringsigactionflag defaults, or fork-accounting differences in the new builder bash. I haven't proved any of these.It's worth thinking about whether other
AC_RUN_IFELSEprobes in other darwin packages are quietly failing the same way and downgrading to inferior fallbacks. The zsh case is loud because the result is a hang.Suggested fix
The cheapest unblock is to add the
zsh_cv_sys_sigsuspend=yesoverride to nixpkgs'zshpackage as a darwin-conditionalpreConfigure, same shape as the workaround above. That fixes every affected user immediately. The cost is skipping a runtime probe whose answer we already know on darwin.The more durable fix is figuring out what changed about the sandbox-side signal-delivery in #508474 and making the runtime test return the right answer. That's preferable long-term, but the cache override is straightforward and I'm happy to send a PR if maintainers want it.
Environment
aarch64-darwinhosts, macOS 26.4.1 (build 25E253), kernel Darwin 25.4.0 / xnu-12377.101.15~12sbihdb…-zsh-5.9to80qadsn…-zsh-5.9(same upstream version, different hash).apple-sdk +354 MiB, plus zsh + pcre2 + ncurses + libiconv all rebuilt with same versions and new hashes -- the canonical fingerprint of an stdenv-level rebuild.System metadata
"aarch64-darwin"Darwin 25.4.0, macOS 26.4.1yesnonix-env (Determinate Nix 3.11.2) 2.31.1/nix/store/i2gsp87gqp16whm9mw0ybk9n84zir01x-sourceAre you using nix-darwin?
Yes, I am using nix-darwin.
Notify maintainers
cc @reckenrode (who landed #508474), @NixOS/darwin-maintainers
Note for maintainers: Please tag this issue in your pull request description. (i.e.
Resolves #ISSUE.)I assert that this issue is relevant for Nixpkgs
Is this issue important to you?
Add a 👍 reaction to issues you find important.