From 1c04e281388ecb4d9d9c40c46a8df5777f973a76 Mon Sep 17 00:00:00 2001 From: Johnothan King Date: Tue, 10 Feb 2026 11:51:46 -0800 Subject: [PATCH] Don't fork the last command in a forked subshell (re: c0c7d0f4, f0b0b67d) All command substitutions, including virtual ones, set SH_NOFORK and pass it on to the sh_exec invocation. On the other hand, non-substitution subshells never set the flag, and will even unset(!) it if the previous virtual subshell set it (i.e., comsubs always use it, regular subshells never use it). This means the final command of a forked comsub is always exec-optimized, while the final command of a regular forked subshell is never exec-optimized. This is complete nonsense. Either both forms of subshell should have the optimization, or neither should. After removing the <1995 botch that removes SH_NOFORK and switching to simply turning it on in all cases, I was able to get forked non-substitution subshells to always use the ending execve optimization, without any regressions (AFAIK). Side note: Virtual subshells never terminate with execve because the function in xec.c that validates the decision for using a final execve excludes virtual subshells and shared-state command substitutions. src/cmd/ksh93/sh/subshell.c: - Remove <1995 SH_NOFORK code causing exec optimization weirdness. src/cmd/ksh93/tests/basic.sh: - Readd the exec optimization test from f0b0b67d. - Add the simplified test case from #507 as a regression test. --- src/cmd/ksh93/sh/subshell.c | 5 +---- src/cmd/ksh93/tests/basic.sh | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/cmd/ksh93/sh/subshell.c b/src/cmd/ksh93/sh/subshell.c index 6637ccf55673..d4e17c4b3b8e 100644 --- a/src/cmd/ksh93/sh/subshell.c +++ b/src/cmd/ksh93/sh/subshell.c @@ -679,13 +679,10 @@ Sfio_t *sh_subshell(Shnode_t *t, volatile int flags, int comsub) sfswap(iop,sfstdout); sfset(sfstdout,SFIO_READ,0); sh.fdstatus[1] = IOWRITE; - flags |= sh_state(SH_NOFORK); } else if(sp->prev) - { sp->pipe = sp->prev->pipe; - flags &= ~sh_state(SH_NOFORK); - } + flags |= sh_state(SH_NOFORK); if(sh.savesig < 0) { sh.savesig = 0; diff --git a/src/cmd/ksh93/tests/basic.sh b/src/cmd/ksh93/tests/basic.sh index 97dfebbc2288..880fe508aa64 100755 --- a/src/cmd/ksh93/tests/basic.sh +++ b/src/cmd/ksh93/tests/basic.sh @@ -987,6 +987,15 @@ esac # ====== # Test exec optimization of last command in script or subshell +( + ulimit -t unlimited 2>/dev/null # fork subshell + print "${.sh.pid:-$("$SHELL" -c 'echo "$PPID"')}" # fallback for pre-93u+m ksh without ${.sh.pid} + "$SHELL" -c 'print "$$"' +) >out +pid1= pid2= +{ read pid1 && read pid2; } /dev/null # fork subshell print "${.sh.pid:-$("$SHELL" -c 'echo "$PPID"')}" # fallback for pre-93u+m ksh without ${.sh.pid} @@ -996,6 +1005,29 @@ pid1= pid2= { read pid1 && read pid2; } <<<$got && let "pid1 == pid2" \ || err_exit "last command in forked comsub not exec-optimized ($pid1 != $pid2)" +# https://github.com/ksh93/ksh/issues/507 +mkdir "$tmp/subshell-optimize" +cat <<'EOF1' >"$tmp/subshell-optimize/A" +cat <<'EOF2' >B +( echo B1 ) | cat +( echo B2 ) | cat +EOF2 + +cat <<'EOF2' >C +( echo C1 ) | cat +( echo C2 ) | cat +EOF2 + +( + . ./B + . ./C +) | cat +EOF1 +exp=$'B1\nB2\nC1\nC2' +got=$(cd "$tmp/subshell-optimize"; "$SHELL" "$tmp/subshell-optimize/A") +[[ $exp == $got ]] || err_exit "last command exec optimization in virtual subshells is broken" \ + "(expected $(printf %q "$exp"), got $(printf %q "$got"))" + cat >script <<\EOF echo $$ sh -c 'echo $$'