1
0
Fork 0
mirror of git://git.code.sf.net/p/cdesktopenv/code synced 2025-02-25 08:34:36 +00:00
cde/src/cmd/ksh93/tests/signal.sh
Martijn Dekker 30aee65113 Fix signal/trap behaviour in ksh functions (rhbz#1454804)
Prior discussion:
https://bugzilla.redhat.com/1454804

On 2017-05-23 13:33:25 UTC, Paulo Andrade wrote:
> In previous ksh versions, when exiting the scope of a ksh
> (not posix) function, it would restore the trap table of
> the "calling context" and if the reason the function exited
> was a signal, it would call sh_fault() passing as argument
> the signal value.
>   Newer ksh checks it, but calls kill(getpid(), signal_number)
> after restoring the trap table, but only calls for SIGINT and
> SIGQUIT.
[...]
>   The old way appears to have been more appropriate, but there
> must be a reason to only pass SIGINT and SIGQUIT as it is an
> explicit patch.

The last paragraph is where I differ. This would not be the first
example of outright breakage that appeared to be added deliberately
and that 93u+m has fixed or removed, see e.g. 8477d2ce ('printf %H'
had code that deleted all multibyte characters), cefe087d, or
781f0a39. Sometimes it seems the developers added a little
experiment and then forgot all about it, so it became a misfeature.

In this instance, the correct pre-2012 ksh behaviour is still
explicitly documented in (k)sh.1: "A trap condition that is not
caught or ignored by the function causes the function to terminate
and the condition to be passed on to the caller". Meaning, if there
is no function-local trap, the signal defaults to the parent scope.
There is no language that limits this to SIGINT and SIGQUIT only.
It also makes no sense at all to do so -- signals such as SIGPIPE,
SIGTERM, or SIGSEGV need to be caught by default and to do
otherwise results in misbehaviour by default.

src/cmd/ksh93/sh/xec.c: sh_funscope():
- When resending a signal after restoring the global traps state,
  remove the spurious check that limits this to SIGINT and SIGQUIT.
- Replace it with a check for nsig!=0, as that means there were
  parent trap states to restore. Otherwise 'kill' may be called
  with an invalid signal argument, causing a crash on macOS.

src/cmd/ksh93/tests/signal.sh:
- Update a test to check that a function-local SIGTERM trap is
  triggered correctly when signalled from another process.
- Complete the tests for 3aee10d7; this bug needed fixing before
  we could test that previous fix in a ksh function scope.
- Add a test for triggering global traps from ksh functions,
  testing multiple POSIX-standard signals.
2020-09-29 03:16:39 +02:00

531 lines
15 KiB
Bash
Executable file

########################################################################
# #
# This software is part of the ast package #
# Copyright (c) 1982-2012 AT&T Intellectual Property #
# and is licensed under the #
# Eclipse Public License, Version 1.0 #
# by AT&T Intellectual Property #
# #
# A copy of the License is available at #
# http://www.eclipse.org/org/documents/epl-v10.html #
# (with md5 checksum b35adb5213ca9657e911e9befb180842) #
# #
# Information and Software Systems Research #
# AT&T Research #
# Florham Park NJ #
# #
# David Korn <dgk@research.att.com> #
# #
########################################################################
function err_exit
{
print -u2 -n "\t"
print -u2 -r ${Command}[$1]: "${@:2}"
(( Errors++ ))
}
alias err_exit='err_exit $LINENO'
Command=${0##*/}
integer Errors=0
[[ -d $tmp && -w $tmp && $tmp == "$PWD" ]] || { err\_exit "$LINENO" '$tmp not set; run this from shtests. Aborting.'; exit 1; }
unset n s t
typeset -A SIG
for s in $(kill -l)
do if ! n=$(kill -l $s 2>/dev/null)
then err_exit "'kill -l $s' failed"
elif ! t=$(kill -l $n 2>/dev/null)
then err_exit "'kill -l $n' failed"
elif [[ $s == ?(SIG)$t ]]
then SIG[${s#SIG}]=1
elif ! m=$(kill -l $t 2>/dev/null)
then err_exit "'kill -l $t' failed"
elif [[ $m != $n ]]
then err_exit "'kill -l $s' => $n, 'kill -l $n' => $t, kill -l $t => $m -- expected $n"
fi
done
(
: disabling xtrace for this test
set +x --pipefail
{
$SHELL 2> out2 <<- \EOF
g=false
trap 'print -u2 PIPED; $g && exit 0;g=true' PIPE
while :
do print hello
done
EOF
} | head > /dev/null
(( $? == 0)) || err_exit "SIGPIPE with wrong error code $?"
# The below is kind of bogus as the err_exit from a bg job is never counted. But see extra check below.
[[ $(<out2) == $'PIPED\nPIPED' ]] || err_exit 'SIGPIPE output on standard error is not correct'
) &
cop=$!
{ sleep .4; kill $cop; } 2>/dev/null &
spy=$!
if wait $cop 2>/dev/null
then kill $spy 2>/dev/null
else err_exit "pipe with --pipefail PIPE trap hangs or produced an error"
fi
wait
rm -f out2
actual=$( trap 'print -n got_child' SIGCHLD
sleep .4 &
for ((i=0; i < 4; i++))
do sleep .15
print -n $i
done)
expect=01got_child23
[[ $actual == "$expect" ]] || err_exit 'SIGCHLD not working' \
"(expected $(printf %q "$expect"), got $(printf %q "$actual"))"
# begin standalone SIGINT test generation
cat > tst <<'!'
# shell trap tests
#
# tst control script that calls tst-1, must be run by ksh
# tst-1 calls tst-2
# tst-2 calls tst-3
# tst-3 defaults or handles and discards/propagates SIGINT
#
# initial -v option lists script entry and SIGINT delivery
#
# three test options
#
# d call next script directly, otherwise via $SHELL -c
# t trap, echo, and kill self on SIGINT, otherwise x or SIGINT default if no x
# x trap, echo on SIGINT, and tst-3 exit 0, tst-2 exit, otherwise SIGINT default
# z trap, echo on SIGINT, and tst-3 exit 0, tst-2 exit 0, otherwise SIGINT default
#
# Usage: tst [-v] [-options] shell-to-test ...
# "trap + sig" is an unadvertized extension for this test
# if run from nmake SIGINT is set to SIG_IGN
# this call sets it back to SIG_DFL
# semantics w.r.t. function scope must be worked out before
# making it public
trap + INT
set -o monitor
function gen
{
typeset o t x d
for x in - x z
do case $x in
[$1]) for t in - t
do case $t in
[$1]) for d in - d
do case $d in
[$1]) o="$o $x$t$d"
esac
done
esac
done
esac
done
echo '' $o
}
case $1 in
-v) v=v; shift ;;
-*v*) v=v ;;
*) v= ;;
esac
case $1 in
*' '*) o=$1; shift ;;
-*) o=$(gen $1); shift ;;
*) o=$(gen -txd) ;;
esac
case $# in
0) set ksh bash ksh88 pdksh ash zsh ;;
esac
for f in $o
do case $# in
1) ;;
*) echo ;;
esac
for sh
do if $sh -c 'exit 0' > /dev/null 2>&1
then case $# in
1) printf '%3s ' "$f" ;;
*) printf '%16s %3s ' "$sh" "$f" ;;
esac
$sh tst-1 $v$f $sh > tst.out &
wait
echo $(cat tst.out)
fi
done
done
case $# in
1) ;;
*) echo ;;
esac
!
cat > tst-1 <<'!'
exec 2>/dev/null
case $1 in
*v*) echo 1-main ;;
esac
{
sleep .2
case $1 in
*v*) echo "SIGINT" ;;
esac
kill -s INT 0
} &
case $1 in
*t*) trap '
echo 1-intr
trap - INT
# omitting the self kill exposes shells that deliver
# the SIGINT trap but exit 0 for -xt
# kill -s INT $$
' INT
;;
esac
case $1 in
*d*) tst-2 $1 $2; status=$? ;;
*) $2 -c "tst-2 $1 $2"; status=$? ;;
esac
printf '1-%04d\n' $status
sleep .2
!
cat > tst-2 <<'!'
case $1 in
*z*) trap '
echo 2-intr
exit 0
' INT
;;
*x*) trap '
echo 2-intr
exit
' INT
;;
*t*) trap '
echo 2-intr
trap - INT
kill -s INT $$
' INT
;;
esac
case $1 in
*v*) echo 2-main ;;
esac
case $1 in
*d*) tst-3 $1 $2; status=$? ;;
*) $2 -c "tst-3 $1 $2"; status=$? ;;
esac
printf '2-%04d\n' $status
!
cat > tst-3 <<'!'
case $1 in
*[xz]*) trap '
sleep .2
echo 3-intr
exit 0
' INT
;;
*) trap '
sleep .2
echo 3-intr
trap - INT
kill -s INT $$
' INT
;;
esac
case $1 in
*v*) echo 3-main ;;
esac
sleep .5
printf '3-%04d\n' $?
!
chmod +x tst tst-?
# end standalone test generation
export PATH=$PATH:
typeset -A expected
expected[---]="3-intr"
expected[--d]="3-intr"
expected[-t-]="3-intr 2-intr 1-intr 1-0258"
expected[-td]="3-intr 2-intr 1-intr 1-0258"
expected[x--]="3-intr 2-intr 1-0000"
expected[x-d]="3-intr 2-intr 1-0000"
expected[xt-]="3-intr 2-intr 1-intr 1-0000"
expected[xtd]="3-intr 2-intr 1-intr 1-0000"
expected[z--]="3-intr 2-intr 1-0000"
expected[z-d]="3-intr 2-intr 1-0000"
expected[zt-]="3-intr 2-intr 1-intr 1-0000"
expected[ztd]="3-intr 2-intr 1-intr 1-0000"
tst $SHELL > tst.got
while read ops out
do [[ $out == ${expected[$ops]} ]] || err_exit "interrupt $ops test failed -- expected '${expected[$ops]}', got '$out'"
done < tst.got
if [[ ${SIG[USR1]} ]]
then float s=$SECONDS
exp=SIGUSR1
got=$(LC_ALL=C $SHELL -c '
trap "print SIGUSR1 ; exit 0" USR1
(trap "" USR1 ; exec kill -USR1 $$ & sleep .5)
print done')
[[ $got == "$exp" ]] || err_exit 'subshell ignoring signal does not send signal to parent' \
"(expected '$exp', got '$got')"
(( (SECONDS-s) < .4 )) && err_exit 'parent does not wait for child to complete before handling signal'
((s = SECONDS))
exp=SIGUSR1
got=$(LC_ALL=C $SHELL -c '
trap "print SIGUSR1 ; exit 0" USR1
(trap "exit" USR1 ; exec kill -USR1 $$ & sleep .5)
print done')
[[ $got == "$exp" ]] || err_exit 'subshell catching signal does not send signal to parent' \
"(expected '$exp', got '$got')"
(( SECONDS-s < .4 )) && err_exit 'parent completes early'
fi
yes() for ((;;)); do print y; done
for exp in TERM VTALRM PIPE
do if [[ ${SIG[$exp]} ]]
then {
bindate=$(whence -p date) "$SHELL" <<- EOF
foo() { return 0; }
trap foo EXIT
{ sleep .2; kill -$exp \$\$; sleep .3; kill -0 \$\$ && kill -KILL \$\$; } &
yes |
while read yes
do ("\$bindate"; sleep .01)
done > /dev/null
EOF
} 2>> /dev/null
got=$(kill -l $?)
[[ $exp == $got ]] || err_exit "kill -$exp \$\$ failed, required termination by signal '$got'"
fi
done
SECONDS=0
$SHELL 2> /dev/null -c 'sleep .2 && kill $$ & trap "print done; exit 3" EXIT; (sleep .5); print finished' > $tmp/sig
e=$?
[[ $e == 3 ]] || err_exit "exit status failed -- expected 3, got $e"
x=$(<$tmp/sig)
[[ $x == done ]] || err_exit "output failed -- expected 'done', got '$x'"
(( SECONDS > .35 )) && err_exit "took $SECONDS seconds, expected around .2"
SECONDS=0
$SHELL 2> /dev/null -c 'sleep .2 && kill $$ & trap "print done; exit 3" EXIT; sleep .5; print finished' > $tmp/sig
e=$?
[[ $e == 3 ]] || err_exit "exit status failed -- expected 3, got $e"
x=$(<$tmp/sig)
[[ $x == done ]] || err_exit "output failed -- expected 'done', got '$x'"
(( SECONDS > .35 )) && err_exit "took $SECONDS seconds, expected around .2"
SECONDS=0
{ $SHELL 2> /dev/null -c 'sleep .2 && kill $$ & trap "print done; exit 3" EXIT; (sleep .5); print finished' > $tmp/sig ;} 2> /dev/null
e=$?
[[ $e == 3 ]] || err_exit "exit status failed -- expected 3, got $e"
x=$(<$tmp/sig)
[[ $x == done ]] || err_exit "output failed -- expected 'done', got '$x'"
(( SECONDS > .35 )) && err_exit "took $SECONDS seconds, expected around .2"
SECONDS=0
{ $SHELL 2> /dev/null -c 'sleep .2 && kill $$ & trap "print done; exit 3" EXIT; sleep .5; print finished' > $tmp/sig ;} 2> /dev/null
e=$?
[[ $e == 3 ]] || err_exit "exit status failed -- expected 3, got $e"
x=$(<$tmp/sig)
[[ $x == done ]] || err_exit "output failed -- expected 'done', got '$x'"
(( SECONDS > .35 )) && err_exit "took $SECONDS seconds, expected around .2"
SECONDS=0
x=$($SHELL 2> /dev/null -c 'sleep .2 && kill $$ & trap "print done; exit 3" EXIT; (sleep .5); print finished')
e=$?
[[ $e == 3 ]] || err_exit "exit status failed -- expected 3, got $e"
[[ $x == done ]] || err_exit "output failed -- expected 'done', got '$x'"
(( SECONDS > .35 )) && err_exit "took $SECONDS seconds, expected around .2"
SECONDS=0
x=$($SHELL 2> /dev/null -c 'sleep .2 && kill $$ & trap "print done; exit 3" EXIT; sleep .5; print finished')
e=$?
[[ $e == 3 ]] || err_exit "exit status failed -- expected 3, got $e"
[[ $x == done ]] || err_exit "output failed -- expected 'done', got '$x'"
(( SECONDS > .35 )) && err_exit "took $SECONDS seconds, expected around .2"
trap '' SIGBUS
[[ $($SHELL -c 'trap date SIGBUS; trap -p SIGBUS') ]] && err_exit 'SIGBUS should not have a trap'
trap -- - SIGBUS
{
x=$(
$SHELL <<- \++EOF
timeout()
{
trap 'trap - TERM; return' TERM
( sleep $1; kill -TERM $$ ) >/dev/null 2>&1 &
sleep .3
}
timeout .1
print ok
++EOF
)
} 2> /dev/null
[[ $x == ok ]] || err_exit 'return without arguments in trap not preserving exit status'
x=$(
$SHELL <<- \++EOF
set -o pipefail
foobar()
{
for ((i=0; i < 10000; i++))
do print abcdefghijklmnopqrstuvwxyz
done | head > /dev/null
}
foobar
print ok
++EOF
)
[[ $x == ok ]] || err_exit 'SIGPIPE exit status causes PIPE signal to be propogaged'
x=$(
$SHELL <<- \EOF
trap "print GNAW" URG
print 1
( sleep .1 ; kill -URG $$ ; sleep .1 ; print S1 ; )
print 2
EOF
)
[[ $x == $'1\nS1\nGNAW\n2' ]] || err_exit 'signal ignored in subshell not propagated to parent'
if [[ ${SIG[RTMIN]} ]]
then {
$SHELL <<- \EOF
trap : RTMIN
for ((i=0 ; i < 3 ; i++))
do sleep .1
kill -RTMIN $$ 2> /dev/null
done &
wait
EOF
} 2> /dev/null
[[ $? == 0 ]] && err_exit 'wait interrupted by caught signal should have non-zero exit status'
{
$SHELL <<- \EOF
for ((i=0 ; i < 3 ; i++))
do sleep .1
kill -RTMIN $$ 2> /dev/null
done &
wait
EOF
} 2> /dev/null
[[ $(kill -l $?) == RTMIN ]] || err_exit 'wait interrupted by signal not caught should exit with the value of that signal+256'
fi
# sh.1: "A trap condition that is not caught or ignored by the function causes
# the function to terminate and the condition to be passed on to the caller."
function b
{
sleep .3
endb=1
}
function a
{
trap 'endc=1' TERM
b
enda=1
}
{ sleep .1;kill -s TERM $$;}&
unset enda endb endc
a
[[ $endb ]] && err_exit 'TERM signal did not kill function b'
[[ $enda == 1 ]] || err_exit 'TERM signal killed function a'
[[ $endc == 1 ]] || err_exit 'TERM trap not triggered in function a'
# ======
# Exit status checks
# Verify that 'exit x' for x > 256 does not make the shell send a signal to itself
"$SHELL" -c 'exit $((256+9))'
let "$? == 256+9" && err_exit 'exit with status > 256 makes shell kill itself'
# Verify that the shell does not kill itself after detecting that a child process is killed by a signal,
# and that a signal still causes the exit status to be set to a value > 128
cat >"$tmp/sigtest.sh" <<\EOF
echo begin
"$1" -c 'kill -9 "$$"'
EOF
expect=$'^begin\n/.*/sigtest.sh: line 2: [1-9][0-9]*: Killed\n[1-9][0-9]{1,2}$'
actual=$(LANG=C "$SHELL" -c '"$1" "$2" "$1"; echo "$?"' x "$SHELL" "$tmp/sigtest.sh" 2>&1)
if ! [[ $actual =~ $expect ]]
then [[ $actual == *Killed*Killed* ]] && msg='ksh killed itself' || msg='unexpected output'
err_exit "$msg after child process signal (expected match to $(printf %q "$expect"); got $(printf %q "$actual"))"
fi
let "${actual##*$'\n'} > 128" || err_exit "child process signal did not cause exit status > 128"
# ======
# 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 \'%-\''
# ======
# 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`'
fi
# ======
# Due to an off-by-one error, the last signal in 'kill -l' output wasn't treated properly and could crash.
sig=$(kill -l | tail -n 1)
exp="OK: $sig"
got=$(export sig; "$SHELL" -c '
trap "print '\''OK: $sig'\''" "$sig"
(kill -s "$sig" "$$")
trap - "$sig"
' 2>&1)
((!(e = $?))) && [[ $got == "$exp" ]] || err_exit "failed to handle SIG$sig from subshell" \
"(got status $e$( ((e>128)) && print -n / && kill -l "$e"), $(printf %q "$got"))"
got=$(export sig; "$SHELL" -c '
function tryTrap
{
kill -s "$1" "$$"
}
trap "print '\''OK: $sig'\''" "$sig"
tryTrap "$sig"
trap - "$sig"
' 2>&1)
((!(e = $?))) && [[ $got == "$exp" ]] || err_exit "failed to handle SIG$sig from ksh function" \
"(got status $e$( ((e>128)) && print -n / && kill -l "$e"), $(printf %q "$got"))"
# ======
# ksh-style functions didn't handle signals other than SIGINT and SIGQUIT (rhbz#1454804)
exp="HUP INT PIPE QUIT TERM USR1 USR2"
got=$(export exp; "$SHELL" -c '
function tryTrap
{
kill -s "$1" "$$"
}
for sig in $exp # split
do trap "print -n '\''$sig '\''" "$sig"
tryTrap "$sig"
trap - "$sig"
done
' 2>&1)
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"))"
# ======
exit $((Errors<125?Errors:125))