From 99028e9f709ee44f7c730d2883e31e37adab4d57 Mon Sep 17 00:00:00 2001 From: Martijn Dekker Date: Mon, 23 Mar 2026 22:45:04 +0000 Subject: [PATCH] Fix bug in comsubs involving nested functions with redirects Port of upstream commit 99028e9 for ksh 1.0.6: add sh_redirect() comsub fork workaround using flag==1/2 or string-buffer stdout (sfset & SF_STRING; 1.0.6 sfio uses SF_STRING where newer trees use SFIO_STRING). 1.0.6 lacked the io.c block present in newer branches. Upstream: https://github.com/ksh93/ksh/commit/99028e9f709ee44f7c730d2883e31e37adab4d57 --- src/cmd/ksh93/sh/io.c | 27 +++++++++++ src/cmd/ksh93/tests/subshell.sh | 66 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/src/cmd/ksh93/sh/io.c b/src/cmd/ksh93/sh/io.c index 130b0368..ccea238a 100644 --- a/src/cmd/ksh93/sh/io.c +++ b/src/cmd/ksh93/sh/io.c @@ -1135,6 +1135,33 @@ int sh_redirect(struct ionod *iop, int flag) clexec = 1; if(iop) traceon = sh_trace(NULL,0); + /* + * A command substitution will hang on exit, writing infinite '\0', if, + * within it, standard output (FD 1) is redirected for a built-in command + * that calls sh_subfork(), or redirected permanently using 'exec' or + * 'redirect'. This forking workaround is necessary to avoid that bug. + * For shared-state comsubs, forking is incorrect, so error out then. + * TODO: actually fix the bug and remove this workaround. + */ + if(sh.subshell && sh.comsub && (flag==1 || flag==2 || (sfset(sfstdout,0,0) & SF_STRING))) + { + struct ionod *i; + for(i = iop; i; i = i->ionxt) + { + if((i->iofile & IOUFD) != 1) + continue; + if(!sh.subshare) + { + sh_subfork(); + break; + } + if(flag==1 || flag==2) + { + errormsg(SH_DICT,ERROR_exit(1),"cannot redirect stdout inside shared-state comsub"); + UNREACHABLE(); + } + } + } for(;iop;iop=iop->ionxt) { iof=iop->iofile; diff --git a/src/cmd/ksh93/tests/subshell.sh b/src/cmd/ksh93/tests/subshell.sh index 0708c2ba..77c423d8 100755 --- a/src/cmd/ksh93/tests/subshell.sh +++ b/src/cmd/ksh93/tests/subshell.sh @@ -1183,5 +1183,71 @@ exp='some output' [[ $got == "$exp" ]] || err_exit 'command substitution did not catch output' \ "(expected $(printf %q "$exp"), got $(printf %q "$got"))" +# ====== +# Command substitution loses stdout after nested function redirects stdout to /dev/null + +got=$( + function echo_devnull { + echo "DEVNULL" >/dev/null + } + function func { + echo_devnull >&2 + echo "FUNC" + } + OUT=$(func) + echo "OUT: ${OUT}" +) +exp='OUT: FUNC' +[[ $got == "$exp" ]] || err_exit "comsub nested redirect test 1a (expected $(printf %q "$exp"), got $(printf %q "$got"))" + +got=$( + function echo_devnull { + echo "DEVNULL" >/dev/null + } + function func { + echo_devnull >&2 + echo "FUNC" + } + OUT=${ func; } + echo "OUT: ${OUT}" +) +exp='OUT: FUNC' +[[ $got == "$exp" ]] || err_exit "comsub nested redirect test 1b (expected $(printf %q "$exp"), got $(printf %q "$got"))" + +got=$( + function echo_devnull { + ulimit -c 0 + } + function func { + echo_devnull >&2 + echo "FUNC" + } + OUT=$(func) + echo "OUT: ${OUT}" +) +exp='OUT: FUNC' +[[ $got == "$exp" ]] || err_exit "comsub nested redirect test 2a (expected $(printf %q "$exp"), got $(printf %q "$got"))" + +got=$( + function echo_devnull { + ulimit -c 0 + } + function func { + echo_devnull >&2 + echo "FUNC" + } + OUT=${ func; } + echo "OUT: ${OUT}" +) +exp='OUT: FUNC' +[[ $got == "$exp" ]] || err_exit "comsub nested redirect test 2b (expected $(printf %q "$exp"), got $(printf %q "$got"))" + +got=$( + OUT=$(ulimit -c 0 >&2; echo OK) + echo "OUT: ${OUT}" +) +exp='OUT: OK' +[[ $got == "$exp" ]] || err_exit "comsub nested redirect test 3 (expected $(printf %q "$exp"), got $(printf %q "$got"))" + # ====== exit $((Errors<125?Errors:125))