88
99import os
1010import shutil
11+ import subprocess
1112
1213import pytest
1314
1718 _is_under_any ,
1819 _resolve_writable_roots ,
1920 _validate_bind_prefix ,
21+ _verify_bwrap_capabilities ,
2022 build_hardened_command ,
2123 check_command_allowed ,
2224 default_hardened_config ,
@@ -32,6 +34,21 @@ def _config(**overrides) -> SandboxConfig:
3234 return SandboxConfig (** overrides )
3335
3436
37+ def _mock_bwrap_success (* args , ** kwargs ):
38+ """Monkeypatch target for subprocess.run that simulates bwrap success."""
39+ return subprocess .CompletedProcess (args = args [0 ], returncode = 0 , stdout = "" , stderr = "" )
40+
41+
42+ def _mock_bwrap_failure (* args , ** kwargs ):
43+ """Monkeypatch target for subprocess.run that simulates bwrap failure."""
44+ return subprocess .CompletedProcess (
45+ args = args [0 ],
46+ returncode = 1 ,
47+ stdout = "" ,
48+ stderr = "bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted" ,
49+ )
50+
51+
3552# ---------------------------------------------------------------------------
3653# Config validation
3754# ---------------------------------------------------------------------------
@@ -235,10 +252,108 @@ def _boom():
235252 verify_network_disabled ()
236253
237254
255+ # ---------------------------------------------------------------------------
256+ # bwrap capability smoke test
257+ # ---------------------------------------------------------------------------
258+
259+
260+ class TestVerifyBwrapCapabilities :
261+ def test_bwrap_smoke_success (self , monkeypatch ):
262+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
263+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _mock_bwrap_success )
264+ config = _config ()
265+ _verify_bwrap_capabilities (config )
266+
267+ def test_bwrap_smoke_failure_gives_clear_error (self , monkeypatch ):
268+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
269+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _mock_bwrap_failure )
270+ config = _config ()
271+ with pytest .raises (SandboxError , match = "runtime check failed" ):
272+ _verify_bwrap_capabilities (config )
273+
274+ def test_bwrap_smoke_failure_mentions_docs (self , monkeypatch ):
275+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
276+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _mock_bwrap_failure )
277+ config = _config ()
278+ with pytest .raises (SandboxError , match = "deployment-modes" ):
279+ _verify_bwrap_capabilities (config )
280+
281+ def test_bwrap_smoke_failure_includes_stderr (self , monkeypatch ):
282+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
283+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _mock_bwrap_failure )
284+ config = _config ()
285+ with pytest .raises (SandboxError , match = "RTM_NEWADDR" ):
286+ _verify_bwrap_capabilities (config )
287+
288+ def test_bwrap_smoke_empty_stderr_handled (self , monkeypatch ):
289+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
290+
291+ def _fail_no_stderr (* args , ** kwargs ):
292+ return subprocess .CompletedProcess (args = args [0 ], returncode = 1 , stdout = "" , stderr = "" )
293+
294+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _fail_no_stderr )
295+ config = _config ()
296+ with pytest .raises (SandboxError , match = "<no stderr>" ):
297+ _verify_bwrap_capabilities (config )
298+
299+ def test_bwrap_probe_uses_usr_bin_true_when_no_bin (self , monkeypatch ):
300+ """Config with /usr but not /bin should select /usr/bin/true."""
301+
302+ def _selective_exists (path ):
303+ if path in ("/usr" , "/usr/bin/true" ):
304+ return True
305+ if path == "/bin" :
306+ return False
307+ return True
308+
309+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , _selective_exists )
310+
311+ captured_argv = []
312+
313+ def _capture_run (argv , ** kwargs ):
314+ captured_argv .extend (argv )
315+ return subprocess .CompletedProcess (args = argv , returncode = 0 , stdout = "" , stderr = "" )
316+
317+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _capture_run )
318+
319+ config = _config (readonly_bind_paths = ("/usr" ,))
320+ _verify_bwrap_capabilities (config )
321+
322+ assert "/usr/bin/true" in captured_argv
323+ assert "/bin/true" not in captured_argv
324+
325+ def test_bwrap_probe_no_executable_found (self , monkeypatch ):
326+ """Config with no usable readonly paths should fail clearly."""
327+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : False )
328+
329+ config = _config (readonly_bind_paths = ())
330+ with pytest .raises (SandboxError , match = "cannot select a probe executable" ):
331+ _verify_bwrap_capabilities (config )
332+
333+ def test_bwrap_probe_timeout_handled (self , monkeypatch ):
334+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
335+
336+ def _timeout (* args , ** kwargs ):
337+ raise subprocess .TimeoutExpired (cmd = args [0 ], timeout = 5 )
338+
339+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _timeout )
340+
341+ config = _config ()
342+ with pytest .raises (SandboxError , match = "timed out" ):
343+ _verify_bwrap_capabilities (config )
344+
345+
346+ # ---------------------------------------------------------------------------
347+ # verify_hardened_runtime (full preflight)
348+ # ---------------------------------------------------------------------------
349+
350+
238351class TestVerifyHardenedRuntime :
239352 def test_full_preflight (self , monkeypatch ):
240353 monkeypatch .setattr ("csc_runner.sandbox.platform.system" , lambda : "Linux" )
241354 monkeypatch .setattr ("csc_runner.sandbox.shutil.which" , lambda name : f"/usr/bin/{ name } " )
355+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _mock_bwrap_success )
356+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
242357 monkeypatch .setattr (
243358 "csc_runner.sandbox.socket.if_nameindex" ,
244359 lambda : [(1 , "lo" )],
@@ -249,6 +364,8 @@ def test_full_preflight(self, monkeypatch):
249364 def test_network_check_skippable (self , monkeypatch ):
250365 monkeypatch .setattr ("csc_runner.sandbox.platform.system" , lambda : "Linux" )
251366 monkeypatch .setattr ("csc_runner.sandbox.shutil.which" , lambda name : f"/usr/bin/{ name } " )
367+ monkeypatch .setattr ("csc_runner.sandbox.subprocess.run" , _mock_bwrap_success )
368+ monkeypatch .setattr ("csc_runner.sandbox.os.path.exists" , lambda p : True )
252369 config = _config (require_network_disabled = False )
253370 verify_hardened_runtime (config )
254371
@@ -479,7 +596,7 @@ def test_relative_read_prefix_rejected(self, tmp_path):
479596 self ._build (tmp_path , read_bind_prefixes = ["relative/path" ])
480597
481598 def test_system_paths_readonly (self , tmp_path , monkeypatch ):
482- monkeypatch .setattr ("os.path.exists" , lambda p : True )
599+ monkeypatch .setattr ("csc_runner.sandbox. os.path.exists" , lambda p : True )
483600 cwd = str (tmp_path / "workspace" )
484601 os .makedirs (cwd , exist_ok = True )
485602 config = _config (readonly_bind_paths = ("/usr" , "/lib" ))
0 commit comments