zsh: one word in a cross-compile cache, and every $(...) hung

`X=$(echo hi)` never returned. It has been in docs/history/kernel.md section 7
as "mksh's job control still doesn't reliably progress ... root cause not yet
isolated" with three suspects, and it was none of them, and it was not the
kernel at all.

ports/zsh/Makefile carries a block of `zsh_cv_*` answers for the checks
autoconf cannot run when cross-compiling. One of them was

    zsh_cv_sys_killesrch=no

which reads like a reassuring property and is in fact the answer to the
question configure asks: *does kill(pid, 0) return ESRCH correctly?* `no` makes
configure emit BROKEN_KILL_ESRCH, and Src/zsh_system.h answers that with

    #ifdef BROKEN_KILL_ESRCH
    #undef ESRCH
    #define ESRCH EINVAL
    #endif

zsh's waitforpid() ends its wait on `errno != ESRCH` (Src/jobs.c:1569). With
ESRCH redefined to EINVAL (22) and the kernel correctly returning ESRCH (3),
that condition was true for ever: the shell sat in sigsuspend waiting for a
child it had already reaped. An instrumented zsh says the whole thing in one
line -- kill returned -1, errno is 3, and the constant it is compared against
is 22:

    WFPkill -1 3 22

**Autoconf's own cross-compilation default for that check is `yes`.** Deleting
the line would have been correct. Writing it down as "a fact about SchOS" is
what got it wrong, and the block's comment now says to read each name as the
question rather than as a property.

Verified: `X=$(echo inline)`, `$(/bin/echo external)`, backticks, nested
`$(echo $(echo deep))`, a slow child, and `$(ls /bin | wc -l)` all return.
mksh -- as /bin/sh -- was never affected.

Two corrections to yesterday's note, both confident and both wrong:

  - "Not zsh, mksh hangs the same way" was false. mksh installs as /bin/sh,
    so that test was a `command not found` whose output scrolled past. Had the
    binary been named right, this would have localised to zsh's build in one
    step instead of a day.
  - cmd/sigtest had already proven the kernel's SIGCHLD/sigsuspend contract,
    and a musl-linked probe had already shown kill(pid,0) returning -1 with
    errno == ESRCH == 3 and zsh's own loop condition evaluating false. Both
    results were right; both were read as "so it must be subtler in the
    kernel". What they actually pointed at was the one place the probe and the
    shell differed -- how each was compiled.

The lesson is narrow enough to state: when a probe that reproduces a program's
exact syscall sequence behaves correctly and the program does not, stop looking
under the syscalls and look at the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-22 00:29:39 +02:00
commit b012060699
4 changed files with 112 additions and 108 deletions

View file

@ -98,6 +98,21 @@ ZSH_LIBS := -lncursesw $(MUSL)/lib/libc.a $(LIBGCC) $(MUSL)/lib/libc.a \
# Answers configure cannot work out without running a program on the target.
# Each one is a fact about SchOS, so each one is written down here rather than
# guessed by autoconf's cross-compilation defaults.
#
# **Read each name as the question configure asks, not as a property.** These
# are `zsh_cv_*` cache entries for AC_CACHE_CHECK, and the value answers the
# *check's* wording. `zsh_cv_sys_killesrch` is "does kill(pid, 0) return ESRCH
# correctly", so `yes` is the healthy answer and `no` means broken.
#
# It was `no` here, and that single word cost every `$(command substitution)`
# in the shell. `no` makes configure emit BROKEN_KILL_ESRCH, and zsh_system.h
# answers that with `#undef ESRCH / #define ESRCH EINVAL`. zsh's waitforpid()
# ends its wait on `errno != ESRCH`; with ESRCH redefined to EINVAL (22) and
# the kernel correctly returning ESRCH (3), that test was true for ever and
# the shell waited on a child it had already reaped. Every `X=$(...)` hung.
#
# The cruel part: autoconf's own cross-compilation default for this one is
# `yes`. Leaving the line out entirely would have been correct.
ZSH_CACHE := \
zsh_cv_shared_environ=yes \
zsh_cv_shared_tgetent=no \
@ -107,7 +122,7 @@ ZSH_CACHE := \
zsh_cv_func_realpath_accepts_null=yes \
zsh_cv_sys_path_dev_fd=/dev/fd \
zsh_cv_sys_fifo=yes \
zsh_cv_sys_killesrch=no \
zsh_cv_sys_killesrch=yes \
zsh_cv_sys_signal_handler=void \
zsh_cv_prog_tcsetpgrp=yes \
ac_cv_func_mmap_fixed_mapped=yes \