Skip to content

Commit 39592b5

Browse files
committed
added tests for java and cpp
1 parent f19c906 commit 39592b5

26 files changed

Lines changed: 652 additions & 34 deletions

.github/workflows/test.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,72 @@ jobs:
236236
env:
237237
ENABLE_QUEUE: "false"
238238
run: sudo -E go test -v -run 'TestContainerizationAPISecurityIntegration/privileged reboot syscall is denied' ./...
239+
240+
test-python3-suite:
241+
name: "Language Gap: Python3 Security/Resilience Suite (Expected Fail)"
242+
runs-on: ubuntu-latest
243+
244+
steps:
245+
- name: Checkout
246+
uses: actions/checkout@v4
247+
248+
- name: Setup Go
249+
uses: actions/setup-go@v5
250+
with:
251+
go-version-file: go.mod
252+
253+
- name: Install native dependencies
254+
run: |
255+
sudo apt-get update
256+
sudo apt-get install -y gcc libc6-dev python3
257+
258+
- name: Run Python3 mirrored suite
259+
env:
260+
ENABLE_QUEUE: "false"
261+
run: sudo -E go test -v -run '^TestContainerizationAPISecurityIntegrationPython3$' ./...
262+
263+
test-cpp-suite:
264+
name: "Language Gap: C++ Security/Resilience Suite (Expected Fail)"
265+
runs-on: ubuntu-latest
266+
267+
steps:
268+
- name: Checkout
269+
uses: actions/checkout@v4
270+
271+
- name: Setup Go
272+
uses: actions/setup-go@v5
273+
with:
274+
go-version-file: go.mod
275+
276+
- name: Install native dependencies
277+
run: |
278+
sudo apt-get update
279+
sudo apt-get install -y gcc g++ libc6-dev
280+
281+
- name: Run C++ mirrored suite
282+
env:
283+
ENABLE_QUEUE: "false"
284+
run: sudo -E go test -v -run '^TestContainerizationAPISecurityIntegrationCpp$' ./...
285+
286+
test-java-suite:
287+
name: "Language Gap: Java Security/Resilience Suite (Expected Fail)"
288+
runs-on: ubuntu-latest
289+
290+
steps:
291+
- name: Checkout
292+
uses: actions/checkout@v4
293+
294+
- name: Setup Go
295+
uses: actions/setup-go@v5
296+
with:
297+
go-version-file: go.mod
298+
299+
- name: Install native dependencies
300+
run: |
301+
sudo apt-get update
302+
sudo apt-get install -y gcc libc6-dev openjdk-21-jdk
303+
304+
- name: Run Java mirrored suite
305+
env:
306+
ENABLE_QUEUE: "false"
307+
run: sudo -E go test -v -run '^TestContainerizationAPISecurityIntegrationJava$' ./...

integration_test.go

Lines changed: 230 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,6 @@ func TestContainerizationAPISecurityIntegrationPython3(t *testing.T) {
111111

112112
h := integrationHarness{baseURL: testServer.URL}
113113
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-
}
117114

118115
cases := []struct {
119116
name string
@@ -136,6 +133,77 @@ func TestContainerizationAPISecurityIntegrationPython3(t *testing.T) {
136133
}
137134
}
138135

136+
func TestContainerizationAPISecurityIntegrationCpp(t *testing.T) {
137+
if config.Config.Globals.ENABLE_QUEUE {
138+
t.Fatal("ENABLE_QUEUE must be false for integration tests")
139+
}
140+
141+
h := integrationHarness{baseURL: testServer.URL}
142+
h.apiPort = mustExtractPort(t, h.baseURL)
143+
runLanguageMirrorSuite(t, h, "cpp", "cpp")
144+
}
145+
146+
func TestContainerizationAPISecurityIntegrationJava(t *testing.T) {
147+
if config.Config.Globals.ENABLE_QUEUE {
148+
t.Fatal("ENABLE_QUEUE must be false for integration tests")
149+
}
150+
151+
h := integrationHarness{baseURL: testServer.URL}
152+
h.apiPort = mustExtractPort(t, h.baseURL)
153+
runLanguageMirrorSuite(t, h, "java", "java")
154+
}
155+
156+
func runLanguageMirrorSuite(t *testing.T, h integrationHarness, language, ext string) {
157+
t.Helper()
158+
159+
cases := []struct {
160+
name string
161+
run func(*testing.T, integrationHarness)
162+
}{
163+
{name: "file privacy across request IDs", run: func(t *testing.T, h integrationHarness) {
164+
testFilesystemIsolationForLanguage(t, h, language, "file_privacy_write_read."+ext, "file_privacy_read_only."+ext)
165+
}},
166+
{name: "disk spammer is terminated and data is reclaimed", run: func(t *testing.T, h integrationHarness) {
167+
testDiskCleanupForLanguage(t, h, language, "disk_spammer."+ext)
168+
}},
169+
{name: "fork bomb does not poison subsequent requests", run: func(t *testing.T, h integrationHarness) {
170+
testForkBombContainmentForLanguage(t, h, language, "fork_bomb."+ext, "hello_world."+ext)
171+
}},
172+
{name: "network namespace blocks localhost bridge", run: func(t *testing.T, h integrationHarness) {
173+
testNetworkIsolationForLanguage(t, h, language, "network_localhost_bridge."+ext)
174+
}},
175+
{name: "memory hard limit triggers oom kill", run: func(t *testing.T, h integrationHarness) {
176+
testMemoryHardLimitForLanguage(t, h, language, "memory_bomb."+ext)
177+
}},
178+
{name: "io flood is bounded and returns before timeout", run: func(t *testing.T, h integrationHarness) {
179+
testIOFloodResilienceForLanguage(t, h, language, "io_spam."+ext)
180+
}},
181+
{name: "signal trap cannot survive forced timeout", run: func(t *testing.T, h integrationHarness) {
182+
testSignalTrapTimeoutForLanguage(t, h, language, "signal_trap."+ext)
183+
}},
184+
{name: "orphan grandchild is reaped after request exits", run: func(t *testing.T, h integrationHarness) {
185+
name := "orphanmakergc"
186+
if language == "python3" {
187+
name = "orphanpygc"
188+
}
189+
if language == "java" {
190+
name = "orphanjavagc"
191+
}
192+
testOrphanReapingForLanguage(t, h, language, "orphan_maker."+ext, name)
193+
}},
194+
{name: "inode bomb does not poison host temp filesystem", run: func(t *testing.T, h integrationHarness) {
195+
testInodeExhaustionForLanguage(t, h, language, "inode_bomb."+ext)
196+
}},
197+
{name: "privileged reboot syscall is denied", run: func(t *testing.T, h integrationHarness) {
198+
testPrivilegedSyscallDeniedForLanguage(t, h, language, "try_reboot."+ext)
199+
}},
200+
}
201+
202+
for _, tc := range cases {
203+
t.Run(tc.name, func(t *testing.T) { tc.run(t, h) })
204+
}
205+
}
206+
139207
func testFilesystemIsolation(t *testing.T, h integrationHarness) {
140208
writeCode := mustLoadSampleCode(t, "file_privacy_write_read.c", nil)
141209
writeResp := callSimpleExecute(t, h.baseURL, buildCRequest(writeCode, 4, 32768))
@@ -447,6 +515,155 @@ func testPrivilegedSyscallDeniedPython3(t *testing.T, h integrationHarness) {
447515
}
448516
}
449517

518+
func testFilesystemIsolationForLanguage(t *testing.T, h integrationHarness, language, writeFile, readFile string) {
519+
writeCode := mustLoadSampleCode(t, writeFile, nil)
520+
writeResp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, writeCode, 4, 32768))
521+
if !strings.Contains(writeResp.Output, "SecretData123") {
522+
t.Fatalf("step A did not return secret in stdout; stdout=%q stderr=%q", writeResp.Output, writeResp.Error)
523+
}
524+
525+
readCode := mustLoadSampleCode(t, readFile, nil)
526+
readResp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, readCode, 4, 32768))
527+
if !strings.Contains(strings.ToLower(readResp.Error), "no such file or directory") {
528+
t.Fatalf("step B unexpectedly accessed file from another sandbox; stdout=%q stderr=%q", readResp.Output, readResp.Error)
529+
}
530+
}
531+
532+
func testDiskCleanupForLanguage(t *testing.T, h integrationHarness, language, payload string) {
533+
beforeFree := mustFreeBytes(t, os.TempDir())
534+
beforeCount := countSandboxTempDirs(t)
535+
536+
code := mustLoadSampleCode(t, payload, nil)
537+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 2, 32768))
538+
stderrLower := strings.ToLower(resp.Error)
539+
if !containsAny(stderrLower, []string{"execution timed out", "memory limit exceeded"}) {
540+
t.Fatalf("disk spammer was not terminated as expected; stdout=%q stderr=%q", resp.Output, resp.Error)
541+
}
542+
543+
time.Sleep(400 * time.Millisecond)
544+
assertDiskReclaimed(t, beforeFree, mustFreeBytes(t, os.TempDir()))
545+
assertNoSandboxLeak(t, beforeCount, countSandboxTempDirs(t))
546+
}
547+
548+
func testForkBombContainmentForLanguage(t *testing.T, h integrationHarness, language, bombFile, helloFile string) {
549+
bombCode := mustLoadSampleCode(t, bombFile, nil)
550+
_ = callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, bombCode, 2, 32768))
551+
552+
helloCode := mustLoadSampleCode(t, helloFile, nil)
553+
helloResp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, helloCode, 4, 32768))
554+
if !strings.Contains(helloResp.Output, "Hello World") {
555+
t.Fatalf("follow-up request failed after fork bomb; stdout=%q stderr=%q", helloResp.Output, helloResp.Error)
556+
}
557+
if strings.Contains(strings.ToLower(helloResp.Error), "resource temporarily unavailable") {
558+
t.Fatalf("follow-up request indicates host PID exhaustion; stderr=%q", helloResp.Error)
559+
}
560+
}
561+
562+
func testNetworkIsolationForLanguage(t *testing.T, h integrationHarness, language, payload string) {
563+
replacements := map[string]string{"__API_PORT__": strconv.Itoa(h.apiPort)}
564+
code := mustLoadSampleCode(t, payload, replacements)
565+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 3, 32768))
566+
567+
combined := strings.ToLower(resp.Output + "\n" + resp.Error)
568+
if strings.Contains(combined, "connected") {
569+
t.Fatalf("localhost bridge unexpectedly succeeded; stdout=%q stderr=%q", resp.Output, resp.Error)
570+
}
571+
if !containsAny(combined, []string{"connection refused", "network is unreachable", "no route to host"}) {
572+
t.Fatalf("network isolation did not produce expected connect error; stdout=%q stderr=%q", resp.Output, resp.Error)
573+
}
574+
}
575+
576+
func testMemoryHardLimitForLanguage(t *testing.T, h integrationHarness, language, payload string) {
577+
code := mustLoadSampleCode(t, payload, nil)
578+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 3, 16384))
579+
if !containsAny(strings.ToLower(resp.Error), []string{"memory limit exceeded", "killed", "execution timed out"}) {
580+
t.Fatalf("memory bomb did not terminate as expected; stdout=%q stderr=%q", resp.Output, resp.Error)
581+
}
582+
583+
execDur, err := time.ParseDuration(resp.CPUTime)
584+
if err != nil {
585+
t.Fatalf("failed to parse cpu_time %q: %v", resp.CPUTime, err)
586+
}
587+
if execDur > 500*time.Millisecond {
588+
t.Fatalf("memory enforcement was too slow: cpu_time=%s stderr=%q", resp.CPUTime, resp.Error)
589+
}
590+
}
591+
592+
func testIOFloodResilienceForLanguage(t *testing.T, h integrationHarness, language, payload string) {
593+
code := mustLoadSampleCode(t, payload, nil)
594+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 1, 32768))
595+
596+
stderrLower := strings.ToLower(resp.Error)
597+
if !containsAny(stderrLower, []string{"execution timed out", "killed", "memory limit exceeded"}) {
598+
t.Fatalf("io flood did not terminate with expected error; stdout=%q stderr=%q", resp.Output, resp.Error)
599+
}
600+
601+
if strings.TrimSpace(resp.Error) == "" {
602+
t.Fatalf("io flood returned empty stderr; expected bounded but non-empty error output")
603+
}
604+
605+
const maxErrorBytes = (1 << 20) + 4096
606+
if len(resp.Error) > maxErrorBytes {
607+
t.Fatalf("stderr exceeded resilience cap: got=%d bytes cap=%d", len(resp.Error), maxErrorBytes)
608+
}
609+
610+
assertDurationNotExcessive(t, resp.CPUTime, 3*time.Second, "io flood request")
611+
}
612+
613+
func testSignalTrapTimeoutForLanguage(t *testing.T, h integrationHarness, language, payload string) {
614+
code := mustLoadSampleCode(t, payload, nil)
615+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 1, 32768))
616+
617+
if !strings.Contains(strings.ToLower(resp.Error), "execution timed out") {
618+
t.Fatalf("signal trap did not timeout as expected; stdout=%q stderr=%q", resp.Output, resp.Error)
619+
}
620+
621+
assertDurationWindow(t, resp.CPUTime, 900*time.Millisecond, 2500*time.Millisecond, "signal trap timeout")
622+
}
623+
624+
func testOrphanReapingForLanguage(t *testing.T, h integrationHarness, language, payload, orphanName string) {
625+
code := mustLoadSampleCode(t, payload, nil)
626+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 2, 32768))
627+
assertDurationNotExcessive(t, resp.CPUTime, 3*time.Second, "orphan maker request")
628+
629+
time.Sleep(400 * time.Millisecond)
630+
if cnt := countProcessesByComm(t, orphanName); cnt > 0 {
631+
t.Fatalf("orphan grandchild leaked to host after request completion: count=%d stderr=%q", cnt, resp.Error)
632+
}
633+
}
634+
635+
func testInodeExhaustionForLanguage(t *testing.T, h integrationHarness, language, payload string) {
636+
beforeFree := mustFreeBytes(t, os.TempDir())
637+
beforeCount := countSandboxTempDirs(t)
638+
639+
code := mustLoadSampleCode(t, payload, nil)
640+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 4, 32768))
641+
642+
combinedLower := strings.ToLower(resp.Output + "\n" + resp.Error)
643+
if !containsAny(combinedLower, []string{"inode bomb completed", "no space left", "disk quota exceeded"}) {
644+
t.Fatalf("inode exhaustion test produced unexpected result; stdout=%q stderr=%q", resp.Output, resp.Error)
645+
}
646+
647+
time.Sleep(400 * time.Millisecond)
648+
assertDiskReclaimed(t, beforeFree, mustFreeBytes(t, os.TempDir()))
649+
assertNoSandboxLeak(t, beforeCount, countSandboxTempDirs(t))
650+
mustCreateAndDeleteTempFile(t)
651+
}
652+
653+
func testPrivilegedSyscallDeniedForLanguage(t *testing.T, h integrationHarness, language, payload string) {
654+
code := mustLoadSampleCode(t, payload, nil)
655+
resp := callSimpleExecute(t, h.baseURL, buildLanguageRequest(language, code, 3, 32768))
656+
assertDurationNotExcessive(t, resp.CPUTime, 3*time.Second, "privileged reboot syscall")
657+
658+
combinedLower := strings.ToLower(resp.Output + "\n" + resp.Error)
659+
if strings.Contains(combinedLower, "reboot succeeded unexpectedly") {
660+
t.Fatalf("privileged reboot syscall unexpectedly succeeded; stdout=%q stderr=%q", resp.Output, resp.Error)
661+
}
662+
if !containsAny(combinedLower, []string{"operation not permitted", "permission denied", "bad system call", "killed", "hangup"}) {
663+
t.Fatalf("expected privileged syscall denial signal was not observed; stdout=%q stderr=%q", resp.Output, resp.Error)
664+
}
665+
}
666+
450667
func mustLoadSampleCode(t *testing.T, fileName string, replacements map[string]string) string {
451668
t.Helper()
452669
path := filepath.Join(sampleCodeDir, fileName)
@@ -463,44 +680,23 @@ func mustLoadSampleCode(t *testing.T, fileName string, replacements map[string]s
463680
}
464681

465682
func buildCRequest(code string, timeoutSec, maxMemoryKB uint) simpleExecuteRequest {
466-
return simpleExecuteRequest{Language: "c", Code: code, Timeout: timeoutSec, MaxMemory: maxMemoryKB}
683+
return buildLanguageRequest("c", code, timeoutSec, maxMemoryKB)
467684
}
468685

469686
func buildPython3Request(code string, timeoutSec, maxMemoryKB uint) simpleExecuteRequest {
470-
return simpleExecuteRequest{Language: "python3", Code: code, Timeout: timeoutSec, MaxMemory: maxMemoryKB}
687+
return buildLanguageRequest("python3", code, timeoutSec, maxMemoryKB)
471688
}
472689

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-
}
690+
func buildCppRequest(code string, timeoutSec, maxMemoryKB uint) simpleExecuteRequest {
691+
return buildLanguageRequest("cpp", code, timeoutSec, maxMemoryKB)
692+
}
497693

498-
var parsed simpleExecuteResponse
499-
if err := json.Unmarshal(rawBody, &parsed); err != nil {
500-
return false
501-
}
694+
func buildJavaRequest(code string, timeoutSec, maxMemoryKB uint) simpleExecuteRequest {
695+
return buildLanguageRequest("java", code, timeoutSec, maxMemoryKB)
696+
}
502697

503-
return strings.Contains(parsed.Output, "py-ok")
698+
func buildLanguageRequest(language, code string, timeoutSec, maxMemoryKB uint) simpleExecuteRequest {
699+
return simpleExecuteRequest{Language: language, Code: code, Timeout: timeoutSec, MaxMemory: maxMemoryKB}
504700
}
505701

506702
func callSimpleExecute(t *testing.T, baseURL string, req simpleExecuteRequest) simpleExecuteResponse {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#include <stdio.h>
2+
#include <string.h>
3+
4+
int main() {
5+
static char buf[1024 * 1024];
6+
memset(buf, 'A', sizeof(buf));
7+
8+
FILE *f = fopen("/disk_spam.bin", "w");
9+
if (!f) {
10+
perror("fopen");
11+
return 1;
12+
}
13+
14+
while (1) {
15+
if (fwrite(buf, 1, sizeof(buf), f) != sizeof(buf)) {
16+
perror("fwrite");
17+
fflush(f);
18+
return 1;
19+
}
20+
fflush(f);
21+
}
22+
}

0 commit comments

Comments
 (0)