diff options
author | Pedro Alves <palves@redhat.com> | 2015-07-31 20:06:24 +0100 |
---|---|---|
committer | Pedro Alves <palves@redhat.com> | 2015-07-31 20:06:24 +0100 |
commit | 2c8c5d375e91824387eeacd1d710e714f1534d36 (patch) | |
tree | c68c7a4f0c1bf22a52792ce400a3ab5d7a91faa8 /gdb/testsuite/lib/gdb.exp | |
parent | b1c59ddc809bc4ad2c082b5cae02a18c68746257 (diff) | |
download | gdb-2c8c5d375e91824387eeacd1d710e714f1534d36.zip gdb-2c8c5d375e91824387eeacd1d710e714f1534d36.tar.gz gdb-2c8c5d375e91824387eeacd1d710e714f1534d36.tar.bz2 |
testsuite: tcl exec& -> 'kill -9 $pid' is racy (attach-many-short-lived-thread.exp races and others)
The buildbots show that attach-many-short-lived-thread.exp is racy.
But after staring at debug logs and playing with SystemTap scripts for
a (long) while, I figured out that neither GDB, nor the kernel nor the
test's program itself are at fault.
The problem is simply that the testsuite machinery is currently
subject to PID-reuse races. The attach-many-short-lived-threads.c
test program just happens to be much more susceptible to trigger this
race because threads and processes share the same number space on
Linux, and the test spawns many many short lived threads in
succession, thus enlarging the race window a lot.
Part of the problem is that several tests spawn processes with "exec&"
(in order to test the "attach" command) , and then at the end of the
test, to make sure things are cleaned up, issue a 'remote_spawn "kill
-p $testpid"'. Since with tcl's "exec&", tcl itself is responsible
for reaping the process's exit status, when we go kill the process,
testpid may have already exited _and_ its status may have (and often
has) been reaped already. Thus it can happen that another process
meanwhile reuses $testpid, and that "kill" command kills the wrong
process... Frequently, that happens to be
attach-many-short-lived-thread, but this explains other test's races
as well.
In the attach-many-short-lived-threads test, it sometimes manifests
like this:
(gdb) file /home/pedro/gdb/mygit/build/gdb/testsuite/gdb.threads/attach-many-short-lived-threads
Reading symbols from /home/pedro/gdb/mygit/build/gdb/testsuite/gdb.threads/attach-many-short-lived-threads...done.
(gdb) Loaded /home/pedro/gdb/mygit/build/gdb/testsuite/gdb.threads/attach-many-short-lived-threads into /home/pedro/gdb/mygit/build/gdb/testsuite/../../gdb/gdb
attach 5940
Attaching to program: /home/pedro/gdb/mygit/build/gdb/testsuite/gdb.threads/attach-many-short-lived-threads, process 5940
warning: process 5940 is a zombie - the process has already terminated
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ptrace: Operation not permitted.
(gdb) PASS: gdb.threads/attach-many-short-lived-threads.exp: iter 1: attach
info threads
No threads.
(gdb) PASS: gdb.threads/attach-many-short-lived-threads.exp: iter 1: no new threads
set breakpoint always-inserted on
(gdb) PASS: gdb.threads/attach-many-short-lived-threads.exp: iter 1: set breakpoint always-inserted on
Other times the process dies while the test is ongoing (the process is
ptrace-stopped):
(gdb) print again = 1
Cannot access memory at address 0x6020cc
(gdb) FAIL: gdb.threads/attach-many-short-lived-threads.exp: iter 2: reset timer in the inferior
(Recall that on Linux, SIGKILL is not interceptable)
And other times it dies just while we're detaching:
$4 = 319
(gdb) PASS: gdb.threads/attach-many-short-lived-threads.exp: iter 2: print seconds_left
detach
Can't detach Thread 0x7fb13b7de700 (LWP 1842): No such process
(gdb) FAIL: gdb.threads/attach-many-short-lived-threads.exp: iter 2: detach
GDB mishandles the latter (it should ignore ESRCH while detaching just
like when continuing), but that's another story.
The fix here is to change spawn_wait_for_attach to use Expect's
'spawn' command instead of Tcl's 'exec&' to spawn programs, because
with spawn we control when to wait for/reap the process. That allows
killing the process by PID without being subject to pid-reuse races,
because even if the process is already dead, the kernel won't reuse
the process's PID until the zombie is reaped.
The other part of the problem lies in DejaGnu itself, unfortunately.
I have occasionally seen tests (attach-many-short-lived-threads
included, but not only that one) die with a random inexplicable
SIGTERM too, and that too is caused by the same reason, except that in
that case, the rogue SIGTERM is sent from this bit in DejaGnu's remote.exp:
exec sh -c "exec > /dev/null 2>&1 && (kill -2 $pgid || kill -2 $pid) && sleep 5 && (kill $pgid || kill $pid) && sleep 5 && (kill -9 $pgid || kill -9 $pid) &"
...
catch "wait -i $shell_id"
Even if the program exits promptly, that whole cascade of kills
carries on in the background, thus potentially killing the poor
process that manages to reuse $pid...
I sent a fix for that to the DejaGnu list:
http://lists.gnu.org/archive/html/dejagnu/2015-07/msg00000.html
With both patches in place, I haven't seen
attach-many-short-lived-threads.exp fail again.
Tested on x86_64 Fedora 20, native, gdbserver and extended-gdbserver.
gdb/testsuite/ChangeLog:
2015-07-31 Pedro Alves <palves@redhat.com>
* gdb.base/attach-pie-misread.exp: Rename $res to $test_spawn_id.
Use spawn_id_get_pid. Wait for spawn id after eof. Use
kill_wait_spawned_process instead of explicit "kill -9".
* gdb.base/attach-pie-noexec.exp: Adjust to spawn_wait_for_attach
returning a spawn id instead of a pid. Use spawn_id_get_pid and
kill_wait_spawned_process.
* gdb.base/attach-twice.exp: Likewise.
* gdb.base/attach.exp: Likewise.
(do_command_attach_tests): Use gdb_spawn_with_cmdline_opts and
gdb_test_multiple.
* gdb.base/solib-overlap.exp: Adjust to spawn_wait_for_attach
returning a spawn id instead of a pid. Use spawn_id_get_pid and
kill_wait_spawned_process.
* gdb.base/valgrind-infcall.exp: Likewise.
* gdb.multi/multi-attach.exp: Likewise.
* gdb.python/py-prompt.exp: Likewise.
* gdb.python/py-sync-interp.exp: Likewise.
* gdb.server/ext-attach.exp: Likewise.
* gdb.threads/attach-into-signal.exp (corefunc): Use
spawn_wait_for_attach, spawn_id_get_pid and
kill_wait_spawned_process.
* gdb.threads/attach-many-short-lived-threads.exp: Adjust to
spawn_wait_for_attach returning a spawn id instead of a pid. Use
spawn_id_get_pid and kill_wait_spawned_process.
* gdb.threads/attach-stopped.exp (corefunc): Use
spawn_wait_for_attach, spawn_id_get_pid and
kill_wait_spawned_process.
* gdb.base/break-interp.exp: Rename $res to $test_spawn_id.
Use spawn_id_get_pid. Wait for spawn id after eof. Use
kill_wait_spawned_process instead of explicit "kill -9".
* lib/gdb.exp (can_spawn_for_attach): Adjust comment.
(kill_wait_spawned_process, spawn_id_get_pid): New procedures.
(spawn_wait_for_attach): Use spawn instead of exec to spawn
processes. Don't map cygwin/windows pids here. Now returns a
spawn id list.
Diffstat (limited to 'gdb/testsuite/lib/gdb.exp')
-rw-r--r-- | gdb/testsuite/lib/gdb.exp | 66 |
1 files changed, 50 insertions, 16 deletions
diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp index e3faf18..986d920 100644 --- a/gdb/testsuite/lib/gdb.exp +++ b/gdb/testsuite/lib/gdb.exp @@ -3706,7 +3706,9 @@ proc gdb_exit { } { # it. proc can_spawn_for_attach { } { - # We use TCL's exec to get the inferior's pid. + # We use exp_pid to get the inferior's pid, assuming that gives + # back the pid of the program. On remote boards, that would give + # us instead the PID of e.g., the ssh client, etc. if [is_remote target] then { return 0 } @@ -3722,12 +3724,50 @@ proc can_spawn_for_attach { } { return 1 } +# Kill a progress previously started with spawn_wait_for_attach, and +# reap its wait status. PROC_SPAWN_ID is the spawn id associated with +# the process. + +proc kill_wait_spawned_process { proc_spawn_id } { + set pid [exp_pid -i $proc_spawn_id] + + verbose -log "killing ${pid}" + remote_exec build "kill -9 ${pid}" + + verbose -log "closing ${proc_spawn_id}" + catch "close -i $proc_spawn_id" + verbose -log "waiting for ${proc_spawn_id}" + + # If somehow GDB ends up still attached to the process here, a + # blocking wait hangs until gdb is killed (or until gdb / the + # ptracer reaps the exit status too, but that won't happen because + # something went wrong.) Passing -nowait makes expect tell Tcl to + # wait for the PID in the background. That's fine because we + # don't care about the exit status. */ + wait -nowait -i $proc_spawn_id +} + +# Returns the process id corresponding to the given spawn id. + +proc spawn_id_get_pid { spawn_id } { + set testpid [exp_pid -i $spawn_id] + + if { [istarget "*-*-cygwin*"] } { + # testpid is the Cygwin PID, GDB uses the Windows PID, which + # might be different due to the way fork/exec works. + set testpid [ exec ps -e | gawk "{ if (\$1 == $testpid) print \$4; }" ] + } + + return $testpid +} + # Start a set of programs running and then wait for a bit, to be sure -# that they can be attached to. Return a list of the processes' PIDs. -# It's a test error to call this when [can_spawn_for_attach] is false. +# that they can be attached to. Return a list of processes spawn IDs, +# one element for each process spawned. It's a test error to call +# this when [can_spawn_for_attach] is false. proc spawn_wait_for_attach { executable_list } { - set pid_list {} + set spawn_id_list {} if ![can_spawn_for_attach] { # The caller should have checked can_spawn_for_attach itself @@ -3736,22 +3776,16 @@ proc spawn_wait_for_attach { executable_list } { } foreach {executable} $executable_list { - lappend pid_list [eval exec $executable &] + # Note we use Expect's spawn, not Tcl's exec, because with + # spawn we control when to wait for/reap the process. That + # allows killing the process by PID without being subject to + # pid-reuse races. + lappend spawn_id_list [remote_spawn target $executable] } sleep 2 - if { [istarget "*-*-cygwin*"] } { - for {set i 0} {$i < [llength $pid_list]} {incr i} { - # testpid is the Cygwin PID, GDB uses the Windows PID, - # which might be different due to the way fork/exec works. - set testpid [lindex $pid_list $i] - set testpid [ exec ps -e | gawk "{ if (\$1 == $testpid) print \$4; }" ] - set pid_list [lreplace $pid_list $i $i $testpid] - } - } - - return $pid_list + return $spawn_id_list } # |