@@ -104,6 +104,38 @@ func TestContainerizationAPISecurityIntegration(t *testing.T) {
104104 }
105105}
106106
107+ func TestContainerizationAPISecurityIntegrationPython3 (t * testing.T ) {
108+ if config .Config .Globals .ENABLE_QUEUE {
109+ t .Fatal ("ENABLE_QUEUE must be false for integration tests" )
110+ }
111+
112+ h := integrationHarness {baseURL : testServer .URL }
113+ h .apiPort = mustExtractPort (t , h .baseURL )
114+ if ! python3SupportAvailable (t , h .baseURL ) {
115+ t .Skip ("python3 execution is not supported by the API yet" )
116+ }
117+
118+ cases := []struct {
119+ name string
120+ run func (* testing.T , integrationHarness )
121+ }{
122+ {name : "file privacy across request IDs" , run : testFilesystemIsolationPython3 },
123+ {name : "disk spammer is terminated and data is reclaimed" , run : testDiskCleanupPython3 },
124+ {name : "fork bomb does not poison subsequent requests" , run : testForkBombContainmentPython3 },
125+ {name : "network namespace blocks localhost bridge" , run : testNetworkIsolationPython3 },
126+ {name : "memory hard limit triggers oom kill" , run : testMemoryHardLimitPython3 },
127+ {name : "io flood is bounded and returns before timeout" , run : testIOFloodResiliencePython3 },
128+ {name : "signal trap cannot survive forced timeout" , run : testSignalTrapTimeoutPython3 },
129+ {name : "orphan grandchild is reaped after request exits" , run : testOrphanReapingPython3 },
130+ {name : "inode bomb does not poison host temp filesystem" , run : testInodeExhaustionPython3 },
131+ {name : "privileged reboot syscall is denied" , run : testPrivilegedSyscallDeniedPython3 },
132+ }
133+
134+ for _ , tc := range cases {
135+ t .Run (tc .name , func (t * testing.T ) { tc .run (t , h ) })
136+ }
137+ }
138+
107139func testFilesystemIsolation (t * testing.T , h integrationHarness ) {
108140 writeCode := mustLoadSampleCode (t , "file_privacy_write_read.c" , nil )
109141 writeResp := callSimpleExecute (t , h .baseURL , buildCRequest (writeCode , 4 , 32768 ))
@@ -266,6 +298,155 @@ func testPrivilegedSyscallDenied(t *testing.T, h integrationHarness) {
266298 }
267299}
268300
301+ func testFilesystemIsolationPython3 (t * testing.T , h integrationHarness ) {
302+ writeCode := mustLoadSampleCode (t , "file_privacy_write_read.py" , nil )
303+ writeResp := callSimpleExecute (t , h .baseURL , buildPython3Request (writeCode , 4 , 32768 ))
304+ if ! strings .Contains (writeResp .Output , "SecretData123" ) {
305+ t .Fatalf ("step A did not return secret in stdout; stdout=%q stderr=%q" , writeResp .Output , writeResp .Error )
306+ }
307+
308+ readCode := mustLoadSampleCode (t , "file_privacy_read_only.py" , nil )
309+ readResp := callSimpleExecute (t , h .baseURL , buildPython3Request (readCode , 4 , 32768 ))
310+ if ! strings .Contains (strings .ToLower (readResp .Error ), "no such file or directory" ) {
311+ t .Fatalf ("step B unexpectedly accessed file from another sandbox; stdout=%q stderr=%q" , readResp .Output , readResp .Error )
312+ }
313+ }
314+
315+ func testDiskCleanupPython3 (t * testing.T , h integrationHarness ) {
316+ beforeFree := mustFreeBytes (t , os .TempDir ())
317+ beforeCount := countSandboxTempDirs (t )
318+
319+ code := mustLoadSampleCode (t , "disk_spammer.py" , nil )
320+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 2 , 32768 ))
321+ stderrLower := strings .ToLower (resp .Error )
322+ if ! containsAny (stderrLower , []string {"execution timed out" , "memory limit exceeded" }) {
323+ t .Fatalf ("disk spammer was not terminated as expected; stdout=%q stderr=%q" , resp .Output , resp .Error )
324+ }
325+
326+ time .Sleep (400 * time .Millisecond )
327+ assertDiskReclaimed (t , beforeFree , mustFreeBytes (t , os .TempDir ()))
328+ assertNoSandboxLeak (t , beforeCount , countSandboxTempDirs (t ))
329+ }
330+
331+ func testForkBombContainmentPython3 (t * testing.T , h integrationHarness ) {
332+ bombCode := mustLoadSampleCode (t , "fork_bomb.py" , nil )
333+ _ = callSimpleExecute (t , h .baseURL , buildPython3Request (bombCode , 2 , 32768 ))
334+
335+ helloCode := mustLoadSampleCode (t , "hello_world.py" , nil )
336+ helloResp := callSimpleExecute (t , h .baseURL , buildPython3Request (helloCode , 4 , 32768 ))
337+ if ! strings .Contains (helloResp .Output , "Hello World" ) {
338+ t .Fatalf ("follow-up request failed after fork bomb; stdout=%q stderr=%q" , helloResp .Output , helloResp .Error )
339+ }
340+ if strings .Contains (strings .ToLower (helloResp .Error ), "resource temporarily unavailable" ) {
341+ t .Fatalf ("follow-up request indicates host PID exhaustion; stderr=%q" , helloResp .Error )
342+ }
343+ }
344+
345+ func testNetworkIsolationPython3 (t * testing.T , h integrationHarness ) {
346+ replacements := map [string ]string {"__API_PORT__" : strconv .Itoa (h .apiPort )}
347+ code := mustLoadSampleCode (t , "network_localhost_bridge.py" , replacements )
348+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 3 , 32768 ))
349+
350+ combined := strings .ToLower (resp .Output + "\n " + resp .Error )
351+ if strings .Contains (combined , "connected" ) {
352+ t .Fatalf ("localhost bridge unexpectedly succeeded; stdout=%q stderr=%q" , resp .Output , resp .Error )
353+ }
354+ if ! containsAny (combined , []string {"connection refused" , "network is unreachable" , "no route to host" }) {
355+ t .Fatalf ("network isolation did not produce expected connect error; stdout=%q stderr=%q" , resp .Output , resp .Error )
356+ }
357+ }
358+
359+ func testMemoryHardLimitPython3 (t * testing.T , h integrationHarness ) {
360+ code := mustLoadSampleCode (t , "memory_bomb.py" , nil )
361+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 3 , 16384 ))
362+ if ! containsAny (strings .ToLower (resp .Error ), []string {"memory limit exceeded" , "killed" , "execution timed out" }) {
363+ t .Fatalf ("memory bomb did not terminate as expected; stdout=%q stderr=%q" , resp .Output , resp .Error )
364+ }
365+
366+ execDur , err := time .ParseDuration (resp .CPUTime )
367+ if err != nil {
368+ t .Fatalf ("failed to parse cpu_time %q: %v" , resp .CPUTime , err )
369+ }
370+ if execDur > 500 * time .Millisecond {
371+ t .Fatalf ("memory enforcement was too slow: cpu_time=%s stderr=%q" , resp .CPUTime , resp .Error )
372+ }
373+ }
374+
375+ func testIOFloodResiliencePython3 (t * testing.T , h integrationHarness ) {
376+ code := mustLoadSampleCode (t , "io_spam.py" , nil )
377+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 1 , 32768 ))
378+
379+ stderrLower := strings .ToLower (resp .Error )
380+ if ! containsAny (stderrLower , []string {"execution timed out" , "killed" , "memory limit exceeded" }) {
381+ t .Fatalf ("io flood did not terminate with expected error; stdout=%q stderr=%q" , resp .Output , resp .Error )
382+ }
383+
384+ if strings .TrimSpace (resp .Error ) == "" {
385+ t .Fatalf ("io flood returned empty stderr; expected bounded but non-empty error output" )
386+ }
387+
388+ const maxErrorBytes = (1 << 20 ) + 4096
389+ if len (resp .Error ) > maxErrorBytes {
390+ t .Fatalf ("stderr exceeded resilience cap: got=%d bytes cap=%d" , len (resp .Error ), maxErrorBytes )
391+ }
392+
393+ assertDurationNotExcessive (t , resp .CPUTime , 3 * time .Second , "io flood request" )
394+ }
395+
396+ func testSignalTrapTimeoutPython3 (t * testing.T , h integrationHarness ) {
397+ code := mustLoadSampleCode (t , "signal_trap.py" , nil )
398+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 1 , 32768 ))
399+
400+ if ! strings .Contains (strings .ToLower (resp .Error ), "execution timed out" ) {
401+ t .Fatalf ("signal trap did not timeout as expected; stdout=%q stderr=%q" , resp .Output , resp .Error )
402+ }
403+
404+ assertDurationWindow (t , resp .CPUTime , 900 * time .Millisecond , 2500 * time .Millisecond , "signal trap timeout" )
405+ }
406+
407+ func testOrphanReapingPython3 (t * testing.T , h integrationHarness ) {
408+ code := mustLoadSampleCode (t , "orphan_maker.py" , nil )
409+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 2 , 32768 ))
410+ assertDurationNotExcessive (t , resp .CPUTime , 3 * time .Second , "orphan maker request" )
411+
412+ time .Sleep (400 * time .Millisecond )
413+ if cnt := countProcessesByComm (t , "orphanpygc" ); cnt > 0 {
414+ t .Fatalf ("orphan grandchild leaked to host after request completion: count=%d stderr=%q" , cnt , resp .Error )
415+ }
416+ }
417+
418+ func testInodeExhaustionPython3 (t * testing.T , h integrationHarness ) {
419+ beforeFree := mustFreeBytes (t , os .TempDir ())
420+ beforeCount := countSandboxTempDirs (t )
421+
422+ code := mustLoadSampleCode (t , "inode_bomb.py" , nil )
423+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 4 , 32768 ))
424+
425+ combinedLower := strings .ToLower (resp .Output + "\n " + resp .Error )
426+ if ! containsAny (combinedLower , []string {"inode bomb completed" , "no space left" , "disk quota exceeded" }) {
427+ t .Fatalf ("inode exhaustion test produced unexpected result; stdout=%q stderr=%q" , resp .Output , resp .Error )
428+ }
429+
430+ time .Sleep (400 * time .Millisecond )
431+ assertDiskReclaimed (t , beforeFree , mustFreeBytes (t , os .TempDir ()))
432+ assertNoSandboxLeak (t , beforeCount , countSandboxTempDirs (t ))
433+ mustCreateAndDeleteTempFile (t )
434+ }
435+
436+ func testPrivilegedSyscallDeniedPython3 (t * testing.T , h integrationHarness ) {
437+ code := mustLoadSampleCode (t , "try_reboot.py" , nil )
438+ resp := callSimpleExecute (t , h .baseURL , buildPython3Request (code , 3 , 32768 ))
439+ assertDurationNotExcessive (t , resp .CPUTime , 3 * time .Second , "privileged reboot syscall" )
440+
441+ combinedLower := strings .ToLower (resp .Output + "\n " + resp .Error )
442+ if strings .Contains (combinedLower , "reboot succeeded unexpectedly" ) {
443+ t .Fatalf ("privileged reboot syscall unexpectedly succeeded; stdout=%q stderr=%q" , resp .Output , resp .Error )
444+ }
445+ if ! containsAny (combinedLower , []string {"operation not permitted" , "permission denied" , "bad system call" , "killed" , "hangup" }) {
446+ t .Fatalf ("expected privileged syscall denial signal was not observed; stdout=%q stderr=%q" , resp .Output , resp .Error )
447+ }
448+ }
449+
269450func mustLoadSampleCode (t * testing.T , fileName string , replacements map [string ]string ) string {
270451 t .Helper ()
271452 path := filepath .Join (sampleCodeDir , fileName )
@@ -285,6 +466,43 @@ func buildCRequest(code string, timeoutSec, maxMemoryKB uint) simpleExecuteReque
285466 return simpleExecuteRequest {Language : "c" , Code : code , Timeout : timeoutSec , MaxMemory : maxMemoryKB }
286467}
287468
469+ func buildPython3Request (code string , timeoutSec , maxMemoryKB uint ) simpleExecuteRequest {
470+ return simpleExecuteRequest {Language : "python3" , Code : code , Timeout : timeoutSec , MaxMemory : maxMemoryKB }
471+ }
472+
473+ func python3SupportAvailable (t * testing.T , baseURL string ) bool {
474+ t .Helper ()
475+ req := buildPython3Request ("print('py-ok')" , 1 , 32768 )
476+ body , err := json .Marshal (req )
477+ if err != nil {
478+ return false
479+ }
480+
481+ httpReq , err := http .NewRequest (http .MethodPost , baseURL + "/simple-execute" , bytes .NewReader (body ))
482+ if err != nil {
483+ return false
484+ }
485+ httpReq .Header .Set ("Content-Type" , "application/json" )
486+
487+ httpResp , err := httpClient .Do (httpReq )
488+ if err != nil {
489+ return false
490+ }
491+ defer httpResp .Body .Close ()
492+
493+ rawBody , err := io .ReadAll (httpResp .Body )
494+ if err != nil || httpResp .StatusCode != http .StatusOK {
495+ return false
496+ }
497+
498+ var parsed simpleExecuteResponse
499+ if err := json .Unmarshal (rawBody , & parsed ); err != nil {
500+ return false
501+ }
502+
503+ return strings .Contains (parsed .Output , "py-ok" )
504+ }
505+
288506func callSimpleExecute (t * testing.T , baseURL string , req simpleExecuteRequest ) simpleExecuteResponse {
289507 t .Helper ()
290508 body , err := json .Marshal (req )
0 commit comments