Skip to content

Commit 88aa369

Browse files
committed
Fix the exec optimisation mess (re: 17ebfbf, 6701bb3, f0b0b67)
This commit supersedes @lijog's Solaris patch 280-23332860 (see 17ebfbf) as this is a more general fix that makes the patch redundant. Of course its associated regression tests stay. Reproducer script: trap 'echo SIGUSR1 received' USR1 sh -c 'kill -s USR1 $PPID' Run as a normal script. Expected behaviour: prints "SIGUSR1 received" Actual behaviour: the shell invoking the script terminates. Oops. As of 6701bb3, ksh again allows an exec-without-fork optimisation for the last command in a script. So the 'sh' command gets the same PID as the script, therefore its parent PID ($PPID) is the invoking script and not the script itself, which has been overwritten in working memory. This shows that, if there are traps set, the exec optimisation is incorrect as the expected process is not signalled. While 6701bb3 reintroduced this problem for scripts, this has always been an issue for certain other situations: forked command substitutions, background subshells, and -c option argument scripts. This commit fixes it in all those cases. In sh_exec(), case TFORK, the optimisation (flagged in no_fork) was only blocked for SIGINT and for the EXIT and ERR pseudosignals. That is wrong. It should be blocked for all signal and pseudosignal traps, except DEBUG (which is run before the command) and SIGKILL and SIGSTOP (which cannot be trapped). (I've also tested the behaviour of other shells. One shell, mksh, never does an exec optimisation, even if no traps are set. I don't know if that is intentional or not. I suppose it is possible that a script might expect to receive a signal without trapping it first, and they could conceivably be affected the same way by this exec optimisation. But the ash variants (e.g. Busybox ash, dash, FreeBSD sh), as well as bash, yash and zsh, all do act like this, so the behaviour is very widespread. This commit makes ksh act like them.) Multiple files: - Remove the sh.errtrap, sh.exittrap and sh.end_fn flags and their associated code from the superseded Solaris patch. src/cmd/ksh93/include/shell.h: - Add a scoped sh.st.trapdontexec flag for sh_exec() to disable exec-without-fork optimisations. It should be in the sh.st scope struct because it needs to be reset in subshell scopes. src/cmd/ksh93/bltins/trap.c: b_trap(): - Set sh.st.trapdontexec if any trap is set and non-empty (an empty trap means ignore the signal, which is inherited by an exec'd process, so the optimisation is fine in that case). - Only clear sh.st.trapdontexec if we're not in a ksh function scope; unlike subshells, ksh functions fall back to parent traps if they don't trap a signal themselves, so a ksh function's parent traps also need to disable the exec optimisation. src/cmd/ksh93/sh/fault.c: sh_sigreset(): - Introduce a new -1 mode for sh_funscope() to use, which acts like mode 0 except it does not clear sh.st.trapdontexec. This avoids clearing sh.st.trapdontexec for ksh functions scopes (see above). - Otherwise, clear sh.st.trapdontexec whenever traps are reset. src/cmd/ksh93/sh/xec.c: check_exec_optimization(): - Consolidate all the exec optimisation logic into this function, including the logic from the no_fork flag in sh_exec()/TFORK. - In the former no_fork flag logic, replace the three checks for SIGINT, ERR and EXIT traps with a single check for the sh.st.trapdontexec flag.
1 parent f0b0b67 commit 88aa369

File tree

8 files changed

+123
-43
lines changed

8 files changed

+123
-43
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Any uppercase BUG_* names are modernish shell bug IDs.
1010
the last component command was an external command and the pipeline was
1111
the last command in a background subshell.
1212

13+
- When any trap except DEBUG, KILL or STOP is set to a non-empty command,
14+
the last command in a script or forked subshell will no longer avoid forking
15+
before executing; this optimization incorrectly bypassed the traps.
16+
1317
2022-06-15:
1418

1519
- Fixed a bug where converting an indexed array into an associative array in

src/cmd/ksh93/bltins/trap.c

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,6 @@ int b_trap(int argc,char *argv[],Shbltin_t *context)
136136
sh.trapnote = 0;
137137

138138
}
139-
if(sig == SH_ERRTRAP)
140-
{
141-
if(clear)
142-
sh.errtrap = 0;
143-
else if(!sh.fn_depth || sh.end_fn)
144-
sh.errtrap = 1;
145-
}
146139
continue;
147140
}
148141
if(sig > sh.sigmax)
@@ -159,8 +152,6 @@ int b_trap(int argc,char *argv[],Shbltin_t *context)
159152
else if(clear)
160153
{
161154
sh_sigclear(sig);
162-
if(sig == 0)
163-
sh.exittrap = 0;
164155
if(dflag)
165156
signal(sig,SIG_DFL);
166157
}
@@ -180,8 +171,39 @@ int b_trap(int argc,char *argv[],Shbltin_t *context)
180171
sh.st.trapcom[sig] = (sh.sigflag[sig]&SH_SIGOFF) ? Empty : sh_strdup(action);
181172
if(arg && arg != Empty)
182173
free(arg);
183-
if(sig == 0 && (!sh.fn_depth || sh.end_fn))
184-
sh.exittrap = 1;
174+
}
175+
}
176+
/*
177+
* Set a flag for sh_exec() to disable exec-without-fork optimizations if any trap is set and non-empty.
178+
* (In ksh functions, there may be parent scope traps, so do not reset to 0 if in a ksh function.)
179+
*/
180+
if(sh.fn_depth==0)
181+
sh.st.trapdontexec = 0;
182+
if(!sh.st.trapdontexec)
183+
{
184+
/* EXIT and real signals */
185+
for(sig=0; sig<=sh.sigmax; sig++)
186+
{
187+
/* these cannot be trapped */
188+
if(sig==SIGKILL || sig==SIGSTOP)
189+
continue;
190+
if(sh.st.trapcom[sig] && *sh.st.trapcom[sig])
191+
{
192+
sh.st.trapdontexec++;
193+
break;
194+
}
195+
}
196+
}
197+
if(!sh.st.trapdontexec)
198+
{
199+
/* other pseudosignals -- exclude DEBUG as it is executed before the command */
200+
for(sig=0; sig<SH_DEBUGTRAP; sig++)
201+
{
202+
if(sh.st.trap[sig] && *sh.st.trap[sig])
203+
{
204+
sh.st.trapdontexec++;
205+
break;
206+
}
185207
}
186208
}
187209
}

src/cmd/ksh93/include/shell.h

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ struct sh_scoped
204204
short opterror;
205205
int ioset;
206206
unsigned short trapmax;
207+
char trapdontexec; /* stop exec optimization if any non-DEBUG/SIGKILL/SIGSTOP trap is set and non-empty */
207208
char *trap[SH_DEBUGTRAP+1];
208209
char **otrap;
209210
char **trapcom;
@@ -335,9 +336,9 @@ struct Shell_s
335336
int inuse_bits;
336337
struct argnod *envlist;
337338
struct dolnod *arglist;
338-
int fn_depth;
339+
int fn_depth; /* scoped ksh-style function call depth */
339340
int fn_reset;
340-
int dot_depth;
341+
int dot_depth; /* dot-script and POSIX function call depth */
341342
int hist_depth;
342343
int xargmin;
343344
int xargmax;
@@ -381,9 +382,6 @@ struct Shell_s
381382
char *mathnodes;
382383
char *bltin_dir;
383384
struct Regress_s*regress;
384-
char exittrap;
385-
char errtrap;
386-
char end_fn;
387385
#if SHOPT_FILESCAN
388386
char *cur_line;
389387
#endif

src/cmd/ksh93/sh/fault.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,17 @@ void sh_sigdone(void)
315315
* Restore to default signals
316316
* Free the trap strings if mode is non-zero
317317
* If mode>1 then ignored traps cause signal to be ignored
318+
* If mode==-1 we're entering a new function scope in sh_funscope()
318319
*/
319320
void sh_sigreset(register int mode)
320321
{
321322
register char *trap;
322323
register int flag, sig=sh.st.trapmax;
324+
/* do not reset sh.st.trapdontexec in a new ksh function scope as parent traps will still be active */
325+
if(mode < 0)
326+
mode = 0;
327+
else
328+
sh.st.trapdontexec = 0;
323329
while(sig-- > 0)
324330
{
325331
if(trap=sh.st.trapcom[sig])

src/cmd/ksh93/sh/init.c

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,9 +1527,6 @@ Shell_t *sh_init(register int argc,register char *argv[], Shinit_f userinit)
15271527
astintercept(&sh.bltindata,1);
15281528
if(sh.userinit=userinit)
15291529
(*userinit)(&sh, 0);
1530-
sh.exittrap = 0;
1531-
sh.errtrap = 0;
1532-
sh.end_fn = 0;
15331530
error_info.exit = sh_exit;
15341531
#ifdef BUILD_DTKSH
15351532
{
@@ -1714,9 +1711,6 @@ int sh_reinit(char *argv[])
17141711
sh.inpipe = sh.outpipe = 0;
17151712
job_clear();
17161713
job.in_critical = 0;
1717-
sh.exittrap = 0;
1718-
sh.errtrap = 0;
1719-
sh.end_fn = 0;
17201714
/* update ${.sh.pid}, $$, $PPID */
17211715
sh.ppid = sh.current_pid;
17221716
sh.current_pid = sh.pid = getpid();

src/cmd/ksh93/sh/main.c

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,6 @@ static void exfile(register Sfio_t *iop,register int fno)
467467
error_info.line = 1;
468468
sh.inlineno = 1;
469469
sh.binscript = 0;
470-
sh.exittrap = 0;
471-
sh.errtrap = 0;
472-
sh.end_fn = 0;
473470
if(sfeof(iop))
474471
goto eof_or_error;
475472
/* command loop */

src/cmd/ksh93/sh/xec.c

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -909,10 +909,17 @@ static Namval_t *enter_namespace(Namval_t *nsp)
909909
/*
910910
* Check whether to execve(2) the final command or make its redirections permanent.
911911
*/
912-
static int check_exec_optimization(struct ionod *iop)
912+
static int check_exec_optimization(int type, int execflg, int execflg2, struct ionod *iop)
913913
{
914-
if(sh.subshell || sh.exittrap || sh.errtrap)
914+
if(type&(FAMP|FPOU)
915+
|| !(execflg && sh.fn_depth==0 || execflg2)
916+
|| sh.st.trapdontexec
917+
|| sh.subshell
918+
|| ((struct checkpt*)sh.jmplist)->mode==SH_JMPEVAL
919+
|| (pipejob && (sh_isstate(SH_MONITOR) || sh_isoption(SH_PIPEFAIL) || sh_isstate(SH_TIMING))))
920+
{
915921
return(0);
922+
}
916923
/* '<>;' (IOREWRITE) redirections are incompatible with exec */
917924
while(iop && !(iop->iofile & IOREWRITE))
918925
iop = iop->ionxt;
@@ -1133,7 +1140,7 @@ int sh_exec(register const Shnode_t *t, int flags)
11331140
int tflags = 1;
11341141
if(np && nv_isattr(np,BLT_DCL))
11351142
tflags |= 2;
1136-
if(execflg && !check_exec_optimization(io))
1143+
if(execflg && !check_exec_optimization(type,execflg,execflg2,io))
11371144
execflg = 0;
11381145
if(argn==0)
11391146
{
@@ -1506,15 +1513,7 @@ int sh_exec(register const Shnode_t *t, int flags)
15061513
int pipes[3];
15071514
if(sh.subshell)
15081515
sh_subtmpfile();
1509-
no_fork = !(type&(FAMP|FPOU))
1510-
&& !sh.subshell
1511-
&& !(sh.st.trapcom[SIGINT] && *sh.st.trapcom[SIGINT])
1512-
&& !sh.st.trapcom[0]
1513-
&& !sh.st.trap[SH_ERRTRAP]
1514-
&& ((struct checkpt*)sh.jmplist)->mode!=SH_JMPEVAL
1515-
&& (execflg && sh.fn_depth==0 || execflg2)
1516-
&& !(pipejob && (sh_isstate(SH_MONITOR) || sh_isoption(SH_PIPEFAIL) || sh_isstate(SH_TIMING)));
1517-
if(no_fork)
1516+
if(no_fork = check_exec_optimization(type,execflg,execflg2,t->fork.forkio))
15181517
job.parent=parent=0;
15191518
else
15201519
{
@@ -1799,7 +1798,7 @@ int sh_exec(register const Shnode_t *t, int flags)
17991798
jmpval = sigsetjmp(buffp->buff,0);
18001799
if(jmpval==0)
18011800
{
1802-
if(execflg && !check_exec_optimization(t->fork.forkio))
1801+
if(execflg && !check_exec_optimization(type,execflg,execflg2,t->fork.forkio))
18031802
{
18041803
execflg = 0;
18051804
flags &= ~sh_state(SH_NOFORK);
@@ -3100,7 +3099,7 @@ int sh_funscope(int argn, char *argv[],int(*fun)(void*),void *arg,int execflg)
31003099
}
31013100
if(!fun && sh_isoption(SH_FUNCTRACE) && sh.st.trap[SH_DEBUGTRAP] && *sh.st.trap[SH_DEBUGTRAP])
31023101
save_debugtrap = sh_strdup(sh.st.trap[SH_DEBUGTRAP]);
3103-
sh_sigreset(0);
3102+
sh_sigreset(-1);
31043103
if(save_debugtrap)
31053104
sh.st.trap[SH_DEBUGTRAP] = save_debugtrap;
31063105
argsav = sh_argnew(argv,&saveargfor);
@@ -3117,8 +3116,6 @@ int sh_funscope(int argn, char *argv[],int(*fun)(void*),void *arg,int execflg)
31173116
nv_putval(SH_PATHNAMENOD,sh.st.filename,NV_NOFREE);
31183117
nv_putval(SH_FUNNAMENOD,sh.st.funname,NV_NOFREE);
31193118
}
3120-
if((execflg & sh_state(SH_NOFORK)))
3121-
sh.end_fn = 1;
31223119
jmpval = sigsetjmp(buffp->buff,0);
31233120
if(jmpval == 0)
31243121
{
@@ -3169,7 +3166,6 @@ int sh_funscope(int argn, char *argv[],int(*fun)(void*),void *arg,int execflg)
31693166
sh.st = *prevscope;
31703167
sh.topscope = (Shscope_t*)prevscope;
31713168
nv_getval(sh_scoped(IFSNOD));
3172-
sh.end_fn = 0;
31733169
if(nsig)
31743170
{
31753171
for (isig = 0; isig < nsig; ++isig)

src/cmd/ksh93/tests/basic.sh

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,14 +928,77 @@ AIX | SunOS)
928928
esac
929929
930930
# ======
931+
# Test exec optimization of last command in script or subshell
932+
931933
(
932-
ulimit -t unlimited 2>/dev/null
934+
ulimit -t unlimited 2>/dev/null # fork subshell
933935
print "${.sh.pid:-$("$SHELL" -c 'echo "$PPID"')}" # fallback for pre-93u+m ksh without ${.sh.pid}
934936
"$SHELL" -c 'print "$$"'
935937
) >out
936938
pid1= pid2=
937939
{ read pid1 && read pid2; } <out && let "pid1 == pid2" \
938940
|| err_exit "last command in forked subshell not exec-optimized ($pid1 != $pid2)"
939941
942+
got=$(
943+
ulimit -t unlimited 2>/dev/null # fork subshell
944+
print "${.sh.pid:-$("$SHELL" -c 'echo "$PPID"')}" # fallback for pre-93u+m ksh without ${.sh.pid}
945+
"$SHELL" -c 'print "$$"'
946+
)
947+
pid1= pid2=
948+
{ read pid1 && read pid2; } <<<$got && let "pid1 == pid2" \
949+
|| err_exit "last command in forked comsub not exec-optimized ($pid1 != $pid2)"
950+
951+
cat >script <<\EOF
952+
echo $$
953+
sh -c 'echo $$'
954+
EOF
955+
"$SHELL" script >out
956+
pid1= pid2=
957+
{ read pid1 && read pid2; } <out && let "pid1 == pid2" \
958+
|| err_exit "last command in script not exec-optimized ($pid1 != $pid2)"
959+
960+
for sig in EXIT ERR ${ kill -l; }
961+
do
962+
case $sig in
963+
KILL | STOP)
964+
# cannot be trapped
965+
continue ;;
966+
esac
967+
968+
# the following is tested in a background subshell because ksh before 2022-06-18 didn't
969+
# do exec optimization on the last external command in a forked non-background subshell
970+
(
971+
trap + "$sig" # unadvertised (still sort of broken) feature: unignore signal
972+
trap : "$sig"
973+
print "${.sh.pid:-$("$SHELL" -c 'echo "$PPID"')}" # fallback for pre-93u+m ksh without ${.sh.pid}
974+
"$SHELL" -c 'print "$$"'
975+
) >out &
976+
wait
977+
pid1= pid2=
978+
{ read pid1 && read pid2; } <out && let "pid1 != pid2" \
979+
|| err_exit "last command in forked subshell exec-optimized in spite of $sig trap ($pid1 == $pid2)"
980+
981+
got=$(
982+
ulimit -t unlimited 2>/dev/null # fork subshell
983+
trap + "$sig" # unadvertised (still sort of broken) feature: unignore signal
984+
trap : "$sig"
985+
print "${.sh.pid:-$("$SHELL" -c 'echo "$PPID"')}" # fallback for pre-93u+m ksh without ${.sh.pid}
986+
"$SHELL" -c 'print "$$"'
987+
)
988+
pid1= pid2=
989+
{ read pid1 && read pid2; } <<<$got && let "pid1 != pid2" \
990+
|| err_exit "last command in forked comsub exec-optimized in spite of $sig trap ($pid1 == $pid2)"
991+
992+
cat >script <<-EOF
993+
trap ":" $sig
994+
echo \$\$
995+
sh -c 'echo \$\$'
996+
EOF
997+
"$SHELL" script >out
998+
pid1= pid2=
999+
{ read pid1 && read pid2; } <out && let "pid1 != pid2" \
1000+
|| err_exit "last command in script exec-optimized in spite of $sig trap ($pid1 == $pid2)"
1001+
done
1002+
9401003
# ======
9411004
exit $((Errors<125?Errors:125))

0 commit comments

Comments
 (0)