Skip to content

Commit 39b5c92

Browse files
committed
Fix resume() for COW clones: send SIGCONT instead of no-op
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent ff3dc8d commit 39b5c92

File tree

2 files changed

+77
-7
lines changed

2 files changed

+77
-7
lines changed

src/sandlock/sandbox.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,18 +284,19 @@ def pause(self) -> None:
284284
os.killpg(p, signal.SIGSTOP)
285285

286286
def resume(self) -> None:
287-
"""Resume the sandbox by sending SIGCONT to the process group.
287+
"""Resume the sandbox by sending SIGCONT.
288288
289-
For COW clones, this is a no-op — clones start running
290-
immediately after creation.
289+
Uses ``kill`` for COW clones (single process) and ``killpg``
290+
for regular sandboxes (entire process group).
291291
"""
292-
if self._clone_pid is not None:
293-
return # Clone is already running
294292
p = self.pid
295-
if p is None:
293+
if p is None or not self.alive:
296294
raise SandboxError("No running process to resume")
297295
try:
298-
os.killpg(p, signal.SIGCONT)
296+
if self._clone_pid is not None:
297+
os.kill(p, signal.SIGCONT)
298+
else:
299+
os.killpg(p, signal.SIGCONT)
299300
except ProcessLookupError:
300301
raise SandboxError("Process no longer exists")
301302

tests/test_clone.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,74 @@ def work():
184184
os.unlink(path)
185185

186186

187+
class TestClonePauseResume(unittest.TestCase):
188+
"""Verify that pause()/resume() works on COW clones."""
189+
190+
def test_pause_resume_clone(self):
191+
"""A paused clone resumes and completes after SIGCONT."""
192+
import sys
193+
import tempfile
194+
import time
195+
196+
marker = tempfile.mktemp(prefix="sandlock_test_pause_")
197+
198+
def init():
199+
pass
200+
201+
def work():
202+
# Write marker after a brief moment to prove we ran
203+
with open(marker, "w") as f:
204+
f.write("done")
205+
206+
policy = Policy(
207+
fs_writable=["/tmp"],
208+
fs_readable=[sys.prefix, "/usr", "/lib", "/etc", "/proc", "/dev"],
209+
)
210+
211+
with Sandbox(policy, init, work) as sb:
212+
clones = sb.fork(1)
213+
clone = clones[0]
214+
# Give the clone a moment to start
215+
time.sleep(0.1)
216+
clone.pause()
217+
self.assertTrue(clone.is_paused)
218+
# Resume and wait for completion
219+
clone.resume()
220+
clone.wait(timeout=10)
221+
222+
self.assertTrue(os.path.exists(marker))
223+
self.assertEqual(open(marker).read(), "done")
224+
os.unlink(marker)
225+
226+
def test_resume_without_pause_is_harmless(self):
227+
"""Calling resume() on a running clone should not error."""
228+
import sys
229+
import tempfile
230+
231+
marker = tempfile.mktemp(prefix="sandlock_test_resume_noop_")
232+
233+
def init():
234+
pass
235+
236+
def work():
237+
with open(marker, "w") as f:
238+
f.write("ok")
239+
240+
policy = Policy(
241+
fs_writable=["/tmp"],
242+
fs_readable=[sys.prefix, "/usr", "/lib", "/etc", "/proc", "/dev"],
243+
)
244+
245+
with Sandbox(policy, init, work) as sb:
246+
clones = sb.fork(1)
247+
clone = clones[0]
248+
# resume a non-paused clone — should be harmless
249+
clone.resume()
250+
clone.wait(timeout=10)
251+
252+
self.assertTrue(os.path.exists(marker))
253+
os.unlink(marker)
254+
255+
187256
if __name__ == "__main__":
188257
unittest.main()

0 commit comments

Comments
 (0)