diff --git a/NEWS b/NEWS index 6be27e019..871d4acb3 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,18 @@ For full details, see the git log at: https://github.com/ksh93/ksh Any uppercase BUG_* names are modernish shell bug IDs. +2021-12-08: + +- Fixed: if a function returned with a status > 256 using the 'return' command + and the return value corresponded to a value that could have resulted from a + signal, and an EXIT trap was active, then the shell mistakenly issued that + signal to itself. Depending on the signal, this could cause the shell to + terminate ungracefully, e.g. 'return 267' caused SIGSEGV ("memory fault"). + +- For the 'return' built-in command, you can now freely specify any + return value that fits in a signed integer, typically a 32-bit value. + Note that $? is truncated to 8 bits when the current (sub)shell exits. + 2021-12-05: - Fixed an issue on illumos that caused some parameters in the getconf diff --git a/src/cmd/ksh93/COMPATIBILITY b/src/cmd/ksh93/COMPATIBILITY index 670d548a5..d5bc1db1b 100644 --- a/src/cmd/ksh93/COMPATIBILITY +++ b/src/cmd/ksh93/COMPATIBILITY @@ -164,6 +164,10 @@ For more details, see the NEWS file and for complete details, see the git log. 29. It is now an error for arithmetic expressions to assign an out-of-range index value to a variable of an enumeration type created with 'enum'. +30. For the 'return' built-in command, you can now freely specify any + return value that fits in a signed integer, typically a 32-bit value. + Note that $? is truncated to 8 bits when the current (sub)shell exits. + ____________________________________________________________________________ KSH-93 VS. KSH-88 diff --git a/src/cmd/ksh93/bltins/cflow.c b/src/cmd/ksh93/bltins/cflow.c index 32a459b0e..0771b0686 100644 --- a/src/cmd/ksh93/bltins/cflow.c +++ b/src/cmd/ksh93/bltins/cflow.c @@ -46,11 +46,10 @@ #endif int b_return(register int n, register char *argv[],Shbltin_t *context) { - register char *arg; - register Shell_t *shp = context->shp; - struct checkpt *pp = (struct checkpt*)shp->jmplist; - const char *options = (**argv=='r'?sh_optreturn:sh_optexit); - while((n = optget(argv,options))) switch(n) + /* 'return' outside of function, dotscript and profile behaves like 'exit' */ + char do_exit = **argv=='e' || sh.fn_depth==0 && sh.dot_depth==0 && !sh_isstate(SH_PROFILE); + NOT_USED(context); + while((n = optget(argv, **argv=='e' ? sh_optexit : sh_optreturn))) switch(n) { case ':': if(!strmatch(argv[opt_info.index],"[+-]+([0-9])")) @@ -66,17 +65,27 @@ done: errormsg(SH_DICT,ERROR_usage(2),"%s",optusage((char*)0)); UNREACHABLE(); } - pp->mode = (**argv=='e'?SH_JMPEXIT:SH_JMPFUN); argv += opt_info.index; - n = (arg = *argv) ? (int)strtol(arg, (char**)0, 10) : shp->savexit; - if(n<0 || n==256 || n > SH_EXITMASK+shp->gd->sigmax+1) - n &= ((unsigned int)n)&SH_EXITMASK; - /* return outside of function, dotscript and profile is exit */ - if(shp->fn_depth==0 && shp->dot_depth==0 && !sh_isstate(SH_PROFILE)) - pp->mode = SH_JMPEXIT; - shp->savexit = n; - sh_exit((pp->mode == SH_JMPEXIT) ? (n & SH_EXITMASK) : n); - return(1); + if(*argv) + { + long l = strtol(*argv, NIL(char**), 10); + if(do_exit) + n = (int)(l & SH_EXITMASK); /* exit: apply bitmask before conversion to avoid undefined int overflow */ + else if((long)(n = (int)l) != l) /* return: convert to int and check for overflow (should be safe enough) */ + { + errormsg(SH_DICT,ERROR_warn(0),"%s: out of range",*argv); + n = 128; /* overflow is undefined, so use a consistent status for this */ + } + } + else + { + n = sh.savexit; /* no argument: pass down $? */ + if(do_exit) + n &= SH_EXITMASK; + } + ((struct checkpt*)sh.jmplist)->mode = do_exit ? SH_JMPEXIT : SH_JMPFUN; + sh_exit(sh.savexit = n); + UNREACHABLE(); } diff --git a/src/cmd/ksh93/data/builtins.c b/src/cmd/ksh93/data/builtins.c index 691a944c4..79b17f833 100644 --- a/src/cmd/ksh93/data/builtins.c +++ b/src/cmd/ksh93/data/builtins.c @@ -637,7 +637,7 @@ const char sh_optexec[] = ; const char sh_optexit[] = -"[-1c?\n@(#)$Id: exit (AT&T Research) 1999-07-07 $\n]" +"[-1c?\n@(#)$Id: exit (ksh 93u+m) 2021-12-08 $\n]" "[--catalog?" SH_DICT "]" "[+NAME?exit - exit the current shell]" "[+DESCRIPTION?\bexit\b is shell special built-in that causes the " @@ -647,10 +647,10 @@ const char sh_optexit[] = "\n" "\n[n]\n" "\n" -"[+EXIT STATUS?If \an\a is specified, the exit status is the least significant " - "eight bits of the value of \an\a. Otherwise, the exit status is the " - "exit status of preceding command. When invoked inside a trap, the " - "preceding command means the command that invoked the trap.]" +"[+EXIT STATUS?The exit status is the least significant eight bits of the " + "value of \an\a (if specified) or of the exit status of the preceding " + "command. If \bexit\b is invoked inside a trap, the preceding command " + "means the command that invoked the trap.]" "[+SEE ALSO?\bbreak\b(1), \breturn\b(1)]" ; @@ -1515,25 +1515,28 @@ const char sh_optredirect[] = ; const char sh_optreturn[] = -"[-1c?\n@(#)$Id: return (AT&T Research) 1999-07-07 $\n]" +"[-1c?\n@(#)$Id: return (ksh 93u+m) 2021-12-08 $\n]" "[--catalog?" SH_DICT "]" "[+NAME?return - return from a function or dot script ]" "[+DESCRIPTION?\breturn\b is a shell special built-in that causes the " - "function or dot script that invokes it to exit. " - "If \breturn\b is invoked outside of a function or dot script " - "it is equivalent to \bexit\b.]" + "function, dot script or profile script that invokes it to exit. " + "If \breturn\b is invoked outside of one of these, it behaves " + "exactly like \bexit\b(1); see its manual page.]" "[+?If \breturn\b is invoked inside a function defined with the \bfunction\b " "reserved word syntax, then any \bEXIT\b trap set within the " - "then function will be invoked in the context of the caller " + "function will be invoked in the context of the caller " "before the function returns.]" "[+?If \an\a is given, it will be used to set the exit status.]" "\n" "\n[n]\n" "\n" -"[+EXIT STATUS?If \an\a is specified, the exit status is the least significant " - "eight bits of the value of \an\a. Otherwise, the exit status is the " - "exit status of preceding command.]" -"[+SEE ALSO?\bbreak\b(1), \bexit\b(1)]" +"[+EXIT STATUS?If \an\a is not specified, the exit status is that of the " + "preceding command. Otherwise, it is the value \an\a as a signed " + "integer. An out-of-range value produces a warning and an exit " + "status of 128. The range can be shown using \bgetconf INT_MIN\b " + "and \bgetconf INT_MAX\b. When the current (sub)shell exits, " + "the exit status is truncated to 8 bits as in \bexit\b.]" +"[+SEE ALSO?\bbreak\b(1), \bexit\b(1), \bgetconf\b(1)]" ; diff --git a/src/cmd/ksh93/include/version.h b/src/cmd/ksh93/include/version.h index b762d4ada..6c1ae927f 100644 --- a/src/cmd/ksh93/include/version.h +++ b/src/cmd/ksh93/include/version.h @@ -21,7 +21,7 @@ #define SH_RELEASE_FORK "93u+m" /* only change if you develop a new ksh93 fork */ #define SH_RELEASE_SVER "1.0.0-beta.2" /* semantic version number: https://semver.org */ -#define SH_RELEASE_DATE "2021-12-05" /* must be in this format for $((.sh.version)) */ +#define SH_RELEASE_DATE "2021-12-08" /* must be in this format for $((.sh.version)) */ #define SH_RELEASE_CPYR "(c) 2020-2021 Contributors to ksh " SH_RELEASE_FORK /* Scripts sometimes field-split ${.sh.version}, so don't change amount of whitespace. */ diff --git a/src/cmd/ksh93/sh.1 b/src/cmd/ksh93/sh.1 index ba945b1de..caf0953e5 100644 --- a/src/cmd/ksh93/sh.1 +++ b/src/cmd/ksh93/sh.1 @@ -1523,10 +1523,21 @@ the command. .TP .B ? -The decimal value returned by the last executed command. +The exit status returned by the last executed command. Its meaning depends +on the command or function that defines it, but there are conventions that +other commands often depend on: zero typically means 'success' or 'true', +one typically means 'non-success' or 'false', and a value greater than one +typically indicates some kind of error. Only the 8 least significant bits of +\f3$?\fP (values 0 to 255) are preserved when the exit status is passed on +to a parent process, but within the same (sub)shell environment, it is a +signed integer value with a range of possible values as shown by the +commands \f3getconf INT_MIN\fP and \f3getconf INT_MAX\fP. Shell functions +that run in the current environment may return status values in this range. .TP .B $ -The process id of this shell. +The process ID of the main shell process. Note that this value will not +change in a subshell, even if the subshell runs in a different process. +See also \f3.sh.pid\fP. .TP .B _ Initially, the value of @@ -6060,17 +6071,15 @@ then this command persistently modifies file descriptors as in Causes the shell to exit with the exit status specified by .IR n . -The value will be the least significant 8 bits of the specified status. -If +The value will be the least significant 8 bits of .I n\^ -is omitted, then the exit status is that of the last command executed. -An end-of-file will also cause the shell to exit -except for a -shell which has the +(if specified) or of the exit status of the last command executed. +An end-of-file will also cause the shell to exit, +except for an interactive shell that has the .B ignoreeof -option (see +option turned on (see .B set -below) turned on. +below). .TP \(dg\(dd \f3export\fP \*(OK \f3\-p\fP \*(CK \*(OK \f2name\^\fP\*(OK\f3=\fP\f2value\^\fP\*(CK \*(CK .\|.\|. If @@ -6959,25 +6968,20 @@ redirected to themselves as part of the invocation (e.g. \fB4>&4\fR) or if the \fBposix\fR option is set. .TP \(dg \f3return\fP \*(OK \f2n\^\fP \*(CK -Causes a shell -.I function -or -\f3\|.\fP -script to return -to the invoking script -with the exit status specified by +Causes a shell function, dot script (see \f3\|.\fP and \f3source\fP), +or profile script to return to the invoking shell environment with the +exit status specified by .IR n . -The value will be the least significant 8 bits of the specified status. +This status value can use the full signed integer range as shown by the +commands \f3getconf INT_MIN\fP and \f3getconf INT_MAX\fP. A value +outside that range will produce a warning and an exit status of 128. If .I n\^ -is omitted, then the return status is that of the last command executed. +is omitted, then the value of \f3$?\fP is assumed, i.e., the exit +status of the last command executed is passed on. If .B return -is invoked while not in a -.I function -or a -\f3\|.\fP -script, +is invoked while not in a function, dot script, or profile script, then it behaves the same as .BR exit . .TP @@ -7169,7 +7173,7 @@ Same as .BR \-H . .TP 8 .B ignoreeof -The shell will not exit on end-of-file. +An interactive shell will not exit on end-of-file. The command .B exit must be used. diff --git a/src/cmd/ksh93/sh/subshell.c b/src/cmd/ksh93/sh/subshell.c index 72b38764a..a4d8538a2 100644 --- a/src/cmd/ksh93/sh/subshell.c +++ b/src/cmd/ksh93/sh/subshell.c @@ -871,6 +871,9 @@ Sfio_t *sh_subshell(Shell_t *shp,Shnode_t *t, volatile int flags, int comsub) srand(rp->rand_seed = sp->rand_seed); rp->rand_last = sp->rand_last; } + /* Real subshells have their exit status truncated to 8 bits by the kernel. + * Since virtual subshells should be indistinguishable, do the same here. */ + sh.exitval &= SH_EXITMASK; } shp->subshare = sp->subshare; shp->subdup = sp->subdup; diff --git a/src/cmd/ksh93/sh/xec.c b/src/cmd/ksh93/sh/xec.c index 2fadce601..0eb4b8b0e 100644 --- a/src/cmd/ksh93/sh/xec.c +++ b/src/cmd/ksh93/sh/xec.c @@ -3259,8 +3259,8 @@ int sh_funscope(int argn, char *argv[],int(*fun)(void*),void *arg,int execflg) } if(jmpval) r=shp->exitval; - if(nsig && r>SH_EXITSIG) - kill(shgd->current_pid,r&SH_EXITMASK); + if(jmpval==SH_JMPFUN && sh.lastsig) + kill(shgd->current_pid, sh.lastsig); /* pass down unhandled signal that interrupted ksh function */ if(jmpval > SH_JMPFUN) { sh_chktrap(shp); diff --git a/src/cmd/ksh93/tests/functions.sh b/src/cmd/ksh93/tests/functions.sh index 731c1a6e0..2f9561e74 100755 --- a/src/cmd/ksh93/tests/functions.sh +++ b/src/cmd/ksh93/tests/functions.sh @@ -1210,11 +1210,11 @@ function foo let 1 return $1 } -invals=(135 255 256 267 -1) -outvals=(135 255 0 267 255) -for ((i=0; i < ${#invals[@]}; i++)) -do foo ${invals[i]} - [[ $? == "${outvals[i]}" ]] || err_exit "function exit ${invals[i]} should set \$? to ${outvals[i]}" +# As of ksh 93u+m 2021-12-08, you can use arbitrary signed integer return values in the current (sub)shell. +vals=(135 255 256 267 -1 1234 -4567) +for ((i=0; i < ${#vals[@]}; i++)) +do foo ${vals[i]} + [[ $? == "${vals[i]}" ]] || err_exit "function exit ${vals[i]} should set \$? to ${vals[i]}" done function foo @@ -1262,12 +1262,15 @@ expect_status=0 # ====== # When a function unsets itself, it should not fail to be unset -$SHELL -c 'PATH=/dev/null; fn() { unset -f fn; true; }; fn' || err_exit 'unset of POSIX function in the calling stack fails' -$SHELL -c 'PATH=/dev/null; function ftest { ftest2; }; function ftest2 { unset -f ftest; }; ftest' || err_exit 'unset of ksh function in the calling stack fails' -$SHELL -c 'PATH=/dev/null; fn() { unset -f fn; true; }; fn; fn' 2> /dev/null -[[ $? != 127 ]] && err_exit 'unset of POSIX function fails when it is still running' -$SHELL -c 'PATH=/dev/null; function fn { unset -f fn; true; }; fn; fn' 2> /dev/null -[[ $? != 127 ]] && err_exit 'unset of ksh function fails when it is still running' +# https://github.com/ksh93/ksh/issues/21 +got=$( { "$SHELL" -c 'PATH=/dev/null; fn() { unset -f fn; true; }; fn'; } 2>&1 ) +(( (e=$?)==0 )) || err_exit 'unset of POSIX function in the calling stack fails' "(got status $e, $(printf %q "$got"))" +got=$( { "$SHELL" -c 'PATH=/dev/null; function ftest { ftest2; }; function ftest2 { unset -f ftest; }; ftest'; } 2>&1 ) +(( (e=$?)==0 )) || err_exit 'unset of ksh function in the calling stack fails' "(got status $e, $(printf %q "$got"))" +got=$( { "$SHELL" -c 'PATH=/dev/null; fn() { unset -f fn; true; }; fn; fn'; } 2>&1 ) +(( (e=$?)==127 )) || err_exit 'unset of POSIX function fails when it is still running' "(got status $e, $(printf %q "$got"))" +got=$( { "$SHELL" -c 'PATH=/dev/null; function fn { unset -f fn; true; }; fn; fn'; } 2>&1 ) +(( (e=$?)==127 )) || err_exit 'unset of ksh function fails when it is still running' "(got status $e, $(printf %q "$got"))" # ====== # Check if environment variables passed while invoking a function are exported diff --git a/src/cmd/ksh93/tests/signal.sh b/src/cmd/ksh93/tests/signal.sh index ec95c10ae..e9d57dee5 100755 --- a/src/cmd/ksh93/tests/signal.sh +++ b/src/cmd/ksh93/tests/signal.sh @@ -466,17 +466,22 @@ let "${actual##*$'\n'} > 128" || err_exit "child process signal did not cause ex "(got ${actual##*$'\n'})" # ====== -# Killing a non-existent job shouldn't cause a segfault. Note that `2> /dev/null` has no effect when -# there is a segfault. -$SHELL -c 'kill %% 2> /dev/null'; [[ $? == 1 ]] || err_exit $'`kill` doesn\'t handle a non-existent job correctly when passed \'%%\'' -$SHELL -c 'kill %+ 2> /dev/null'; [[ $? == 1 ]] || err_exit $'`kill` doesn\'t handle a non-existent job correctly when passed \'%+\'' -$SHELL -c 'kill %- 2> /dev/null'; [[ $? == 1 ]] || err_exit $'`kill` doesn\'t handle a non-existent job correctly when passed \'%-\'' +# Killing a non-existent job shouldn't cause a segfault. +# https://github.com/ksh93/ksh/issues/34 +for c in % + - +do got=$( { "$SHELL" -c "kill %$c"; } 2>&1 ) + [[ $? == 1 ]] || err_exit "'kill' doesn't handle a non-existent job correctly when passed '%$c'" \ + "(got $(printf %q "$got"))" +done # ====== # SIGINFO should be supported by the kill builtin on platforms that have it. if "$(whence -p kill)" -INFO $$ 2> /dev/null then - kill -INFO $$ || err_exit '`kill` cannot send SIGINFO to processes when passed `-INFO`' + got=$(kill -INFO $$ 2>&1) || err_exit '`kill` cannot send SIGINFO to processes when passed `-INFO`' \ + "(got $(printf %q "$got"))" + got=$(kill -s INFO $$ 2>&1) || err_exit '`kill` cannot send SIGINFO to processes when passed `-s INFO`' \ + "(got $(printf %q "$got"))" fi # ====== @@ -523,5 +528,30 @@ got=${got% } # rm final space ((!(e = $?))) && [[ $got == "$exp" ]] || err_exit "ksh function ignores global signal traps" \ "(got status $e$( ((e>128)) && print -n / && kill -l "$e"), $(printf %q "$got"))" +# ====== +# Signal incorrectly issued when function returns with status > 256 and EXIT trap is active +# https://github.com/ksh93/ksh/issues/364 +signum=${ kill -l SEGV; } +cat > exit267 <<-EOF # unquoted delimiter; expansion active + trap 'echo OK \$?' EXIT # This trap triggers the crash + function foo { return $((signum+256)); } + foo +EOF +exp="OK $((signum+256))" +got=$( { "$SHELL" exit267; } 2>&1 ) +(( (e=$?)==signum+128 )) && [[ $got == "$exp" ]] || err_exit "'return' with status > 256:" \ + "(expected status $((signum+128)) and $(printf %q "$exp"), got status $e and $(printf %q "$got"))" + +cat > bar <<-'EOF' + trap : EXIT + function foo { "$SHELL" -c 'kill -s SEGV $$'; } + foo 2> /dev/null + echo OK +EOF +exp="OK" +got=$( { "$SHELL" bar; } 2>&1 ) +(( (e=$?)==0 )) && [[ $got == "$exp" ]] || err_exit "segfaulting child process:" \ + "(expected status 0 and $(printf %q "$exp"), got status $e and $(printf %q "$got"))" + # ====== exit $((Errors<125?Errors:125)) diff --git a/src/cmd/ksh93/tests/subshell.sh b/src/cmd/ksh93/tests/subshell.sh index 14c2d906e..bbb21db14 100755 --- a/src/cmd/ksh93/tests/subshell.sh +++ b/src/cmd/ksh93/tests/subshell.sh @@ -1083,5 +1083,12 @@ then kill -9 $tpid err_exit 'backtick command substitution hangs on reproducer from issue 316' fi +# ====== +# Virtual subshells should clip $? to 8 bits, as real subshells get that enforced by the kernel. +# (Note: 'ulimit' will reliably fork a virtual subshell into a real one.) +e1=$( (f() { return 267; }; f); echo $? ) +e2=$( (ulimit -t unlimited 2>/dev/null; f() { return 267; }; f); echo $? ) +((e1==11 && e2==11)) || err_exit "exit status of virtual ($e1) and real ($e2) subshell should both be clipped to 8 bits (11)" + # ====== exit $((Errors<125?Errors:125))