Skip to content

Commit f7324fa

Browse files
shailend-ggvisor-bot
authored andcommitted
signals: Fix races that prevent a SIGCONT from aborting a SIGSTOP
The target tg's leader's goroutine can set `groupStopDequeued` with the signals lock held and then give up the lock to the SIGCONT-sending goroutine, which in turn will return early from endGroupStopLocked() because a stop has not begun yet. So we fix this bug by having endGroupStopLocked() always clear `groupStopDequeued`. In other words, this fixes a race between: - deliverSingal() -> initiateGroupStop() and - applySignalSideEffectsLocked() -> endGroupStopLocked() Now there's a second bug. It is a race between: - runInterrupt.execute() - applySignalSideEffectsLocked() -> endGroupStopLocked() It occurs this way: in runInterrupt(), between participateGroupStopLocked() and beginInternalStopLocked(), the signal mutex is dropped, allowing a SIGCONT issuing thread to run its endGroupStopLocked() in the interim, which as it turns out fails to abort the stop, for runInterrupt.execute() will not consult `groupStopPending` again when it resumes. We fix this by not dropping the signals mutex in runInterrupt(). PiperOrigin-RevId: 885288544
1 parent 8baf482 commit f7324fa

File tree

3 files changed

+75
-10
lines changed

3 files changed

+75
-10
lines changed

pkg/sentry/kernel/task_signals.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,9 @@ func (t *Task) initiateGroupStop(info *linux.SignalInfo) {
812812
func (tg *ThreadGroup) endGroupStopLocked(broadcast bool) {
813813
// Discard all previously-queued stop signals.
814814
linux.ForEachSignal(StopSignals, tg.discardSpecificLocked)
815+
// Unsetting groupStopDequeued will cause racing calls to initiateGroupStop
816+
// to recognize that the group stop has been cancelled.
817+
tg.groupStopDequeued = false
815818

816819
if tg.groupStopPendingCount == 0 && !tg.groupStopComplete {
817820
return
@@ -850,9 +853,6 @@ func (tg *ThreadGroup) endGroupStopLocked(broadcast bool) {
850853
tg.groupContInterrupted = !tg.groupStopComplete
851854
tg.groupContWaitable = true
852855
}
853-
// Unsetting groupStopDequeued will cause racing calls to initiateGroupStop
854-
// to recognize that the group stop has been cancelled.
855-
tg.groupStopDequeued = false
856856
tg.groupStopSignal = 0
857857
tg.groupStopPendingCount = 0
858858
tg.groupStopComplete = false
@@ -986,14 +986,14 @@ func (*runInterrupt) execute(t *Task) taskRunState {
986986
}
987987
t.trapStopPending = false
988988
t.trapNotifyPending = false
989-
// Drop the signal mutex so we can take the TaskSet mutex.
990-
t.tg.signalHandlers.mu.Unlock()
991989

992-
t.tg.pidns.owner.mu.RLock()
993-
if t.tg.leader.parent == nil {
994-
notifyParent = false
995-
}
996990
if tracer := t.Tracer(); tracer != nil {
991+
// Drop the signal mutex so we can take the TaskSet mutex.
992+
t.tg.signalHandlers.mu.Unlock()
993+
t.tg.pidns.owner.mu.RLock()
994+
if t.tg.leader.parent == nil {
995+
notifyParent = false
996+
}
997997
if t.ptraceSeized {
998998
if sig == 0 {
999999
sig = linux.SIGTRAP
@@ -1026,11 +1026,15 @@ func (*runInterrupt) execute(t *Task) taskRunState {
10261026
}
10271027
}
10281028
} else {
1029-
t.tg.signalHandlers.mu.Lock()
10301029
if !t.killedLocked() {
10311030
t.beginInternalStopLocked((*groupStop)(nil))
10321031
}
1032+
// Drop the signal mutex so we can take the TaskSet mutex.
10331033
t.tg.signalHandlers.mu.Unlock()
1034+
t.tg.pidns.owner.mu.RLock()
1035+
if t.tg.leader.parent == nil {
1036+
notifyParent = false
1037+
}
10341038
}
10351039
if notifyParent {
10361040
t.tg.leader.parent.signalStop(t.tg.leader, linux.CLD_STOPPED, int32(sig))

test/syscalls/linux/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2628,6 +2628,7 @@ cc_binary(
26282628
deps = select_gtest() + [
26292629
"//test/util:multiprocess_util",
26302630
"//test/util:posix_error",
2631+
"//test/util:save_util",
26312632
"//test/util:test_util",
26322633
"//test/util:thread_util",
26332634
"@com_google_absl//absl/flags:flag",

test/syscalls/linux/sigstop.cc

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
#include <sys/select.h>
1818
#include <time.h>
1919

20+
#include <array>
21+
#include <atomic>
22+
#include <memory>
23+
24+
#include "gmock/gmock.h"
2025
#include "gtest/gtest.h"
2126
#include "absl/flags/flag.h"
2227
#include "absl/time/clock.h"
2328
#include "absl/time/time.h"
2429
#include "test/util/multiprocess_util.h"
2530
#include "test/util/posix_error.h"
31+
#include "test/util/save_util.h"
2632
#include "test/util/test_util.h"
2733
#include "test/util/thread_util.h"
2834

@@ -130,6 +136,60 @@ TEST(SigstopTest, WaitidCorrectness) {
130136
EXPECT_EQ(info.si_code, CLD_KILLED);
131137
}
132138

139+
TEST(SigstopTest, SIGCONTAbortsSIGSTOP) {
140+
DisableSave ds; // Forks a lot across many threads.
141+
constexpr size_t kIterCount = 10;
142+
std::array<std::unique_ptr<ScopedThread>, 100> threads;
143+
std::atomic<bool> failed{false};
144+
145+
// The race takes a while to manifest, hence we use many threads.
146+
for (size_t i = 0; i < threads.size(); ++i) {
147+
threads[i] = std::make_unique<ScopedThread>([&failed] {
148+
for (size_t j = 0; j < kIterCount; ++j) {
149+
if (failed.load(std::memory_order_relaxed)) {
150+
return;
151+
}
152+
153+
int pid = fork();
154+
ASSERT_GE(pid, 0);
155+
if (pid == 0) {
156+
// Child just sits and waits for signals.
157+
// pause() is async-signal-safe and blocks until any signal arrives.
158+
while (true) pause();
159+
}
160+
161+
ASSERT_THAT(kill(pid, SIGSTOP), SyscallSucceeds());
162+
ASSERT_THAT(kill(pid, SIGCONT), SyscallSucceeds());
163+
164+
// Fire a SIGTERM:
165+
// - If the child is STOPPED, SIGTERM is queued.
166+
// - If the child is RUNNING, it dies.
167+
// Either way the waitpid below should not hang.
168+
ASSERT_THAT(kill(pid, SIGTERM), SyscallSucceeds());
169+
170+
int status;
171+
EXPECT_THAT(RetryEINTR(waitpid)(pid, &status, WSTOPPED),
172+
SyscallSucceeds());
173+
if (WIFSTOPPED(status)) {
174+
failed.store(true, std::memory_order_relaxed);
175+
// Clean up: kill and reap the stuck child.
176+
kill(pid, SIGKILL);
177+
RetryEINTR(waitpid)(pid, &status, 0);
178+
break; // Test failed, don't continue this thread.
179+
} else {
180+
EXPECT_TRUE(WIFSIGNALED(status));
181+
EXPECT_EQ(WTERMSIG(status), SIGTERM);
182+
}
183+
}
184+
});
185+
}
186+
187+
for (const auto& t : threads) {
188+
t->Join();
189+
}
190+
EXPECT_FALSE(failed.load(std::memory_order_relaxed));
191+
}
192+
133193
// Like base:SleepFor, but tries to avoid counting time spent stopped due to a
134194
// stop signal toward the sleep.
135195
//

0 commit comments

Comments
 (0)