Skip to content

zsh: sigsuspend autoconf probe fails in new darwin stdenv sandbox #513543

Description

@jwiegley

Nixpkgs version

  • Unstable (26.05)

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:

  1. 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.)
  2. The stuck zsh has no child in ps (running or zombie); the $(...) subprocess already exited.
  3. lsof -p <pid> shows the command-sub pipe FD with no peer.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    0.kind: bugSomething is broken6.topic: darwinRunning or building packages on Darwin

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions