Skip to content

Commit d701ea9

Browse files
committed
add compatibility for java, cpp and python
1 parent bfbdf46 commit d701ea9

File tree

5 files changed

+158
-34
lines changed

5 files changed

+158
-34
lines changed

executor/command.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
package executor
22

33
import (
4+
"os"
45
"os/exec"
56
"strings"
67
"syscall"
78
)
89

9-
func buildSandboxCommand(stdin, sandboxDir string, cgroupFD int) *exec.Cmd {
10-
cmd := exec.Command("/program")
11-
cmd.Dir = "/"
12-
cmd.SysProcAttr = &syscall.SysProcAttr{
10+
func buildSandboxCommand(stdin string, ws sandboxWorkspace, cgroupFD int) *exec.Cmd {
11+
cmd := exec.Command(ws.runCommand[0], ws.runCommand[1:]...)
12+
sysProcAttr := &syscall.SysProcAttr{
1313
Cloneflags: syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,
14-
Chroot: sandboxDir,
1514
Setpgid: true,
1615
UseCgroupFD: true,
1716
CgroupFD: cgroupFD,
1817
}
18+
if ws.useChroot {
19+
sysProcAttr.Chroot = ws.dir
20+
cmd.Dir = "/"
21+
} else {
22+
cmd.Dir = ws.dir
23+
}
24+
cmd.SysProcAttr = sysProcAttr
1925
if stdin != "" {
2026
cmd.Stdin = strings.NewReader(stdin)
2127
}
28+
if len(ws.extraEnv) > 0 {
29+
cmd.Env = append(os.Environ(), ws.extraEnv...)
30+
}
2231
return cmd
2332
}

executor/main.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,11 @@ import (
1212
)
1313

1414
func Execute(req models.Request) (models.Response, error) {
15-
if req.Language != "c" {
16-
return models.Response{}, fmt.Errorf("unsupported language: %s", req.Language)
17-
}
18-
return executeCProgram(req)
19-
}
20-
21-
func executeCProgram(req models.Request) (models.Response, error) {
22-
ws, err := prepareWorkspace(req.Code)
15+
ws, err := prepareWorkspace(req.Language, req.Code)
2316
if err != nil {
2417
return models.Response{}, err
2518
}
26-
defer func() { _ = osRemoveAll(ws.dir) }()
19+
defer cleanupWorkspace(&ws)
2720

2821
compileErr, ok := compileWorkspace(ws)
2922
if !ok {
@@ -36,10 +29,14 @@ func executeCProgram(req models.Request) (models.Response, error) {
3629
}
3730
defer cg.Close()
3831

39-
return executeSandboxedBinary(req, ws.dir, cg)
32+
if err := prepareRuntimeMounts(&ws); err != nil {
33+
return models.Response{}, err
34+
}
35+
36+
return executeSandboxedBinary(req, ws, cg)
4037
}
4138

42-
func executeSandboxedBinary(req models.Request, sandboxDir string, cg *resourcemanager.CgroupHandle) (models.Response, error) {
39+
func executeSandboxedBinary(req models.Request, ws sandboxWorkspace, cg *resourcemanager.CgroupHandle) (models.Response, error) {
4340
ctx, cancel := context.WithTimeout(context.Background(), req.Timeout)
4441
defer cancel()
4542

@@ -49,7 +46,7 @@ func executeSandboxedBinary(req models.Request, sandboxDir string, cg *resourcem
4946
}
5047
defer syscall.Close(cgroupFD)
5148

52-
cmd, stdoutBuf, stderrBuf := buildCommandBuffers(req.Stdin, sandboxDir, cgroupFD)
49+
cmd, stdoutBuf, stderrBuf := buildCommandBuffers(req.Stdin, ws, cgroupFD)
5350
runErr := startSandbox(cmd)
5451
if runErr != nil {
5552
return models.Response{}, runErr
@@ -63,8 +60,8 @@ func executeSandboxedBinary(req models.Request, sandboxDir string, cg *resourcem
6360
return finalizeResponse(cmd, stdoutBuf.String(), stderrBuf.String(), time.Since(start), observedPeak, timedOut, waitErr, cg)
6461
}
6562

66-
func buildCommandBuffers(stdin, sandboxDir string, cgroupFD int) (*exec.Cmd, *limitedBuffer, *limitedBuffer) {
67-
cmd := buildSandboxCommand(stdin, sandboxDir, cgroupFD)
63+
func buildCommandBuffers(stdin string, ws sandboxWorkspace, cgroupFD int) (*exec.Cmd, *limitedBuffer, *limitedBuffer) {
64+
cmd := buildSandboxCommand(stdin, ws, cgroupFD)
6865
stdoutBuf := newLimitedBuffer(1 << 20)
6966
stderrBuf := newLimitedBuffer(1 << 20)
7067
cmd.Stdout = stdoutBuf
@@ -86,3 +83,8 @@ func compileFailureResponse(stderr string) models.Response {
8683
func osRemoveAll(path string) error {
8784
return os.RemoveAll(path)
8885
}
86+
87+
func cleanupWorkspace(ws *sandboxWorkspace) {
88+
cleanupRuntimeMounts(ws)
89+
_ = osRemoveAll(ws.dir)
90+
}

executor/workspace.go

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,147 @@ import (
66
"os/exec"
77
"path/filepath"
88
"strings"
9+
"syscall"
910
)
1011

1112
type sandboxWorkspace struct {
12-
dir string
13-
sourcePath string
14-
binaryPath string
13+
dir string
14+
sourcePath string
15+
binaryPath string
16+
compileCommand []string
17+
runCommand []string
18+
extraEnv []string
19+
useChroot bool
20+
runtimeMounts []string
21+
mountedTargets []string
1522
}
1623

17-
func prepareWorkspace(code string) (sandboxWorkspace, error) {
18-
dir, err := os.MkdirTemp("", "codesandbox-c-*")
24+
func prepareWorkspace(language, code string) (sandboxWorkspace, error) {
25+
dir, err := os.MkdirTemp("", "codesandbox-*")
1926
if err != nil {
2027
return sandboxWorkspace{}, fmt.Errorf("failed to create sandbox dir: %w", err)
2128
}
22-
ws := sandboxWorkspace{dir: dir}
23-
ws.sourcePath = filepath.Join(dir, "main.c")
24-
ws.binaryPath = filepath.Join(dir, "program")
29+
ws, err := buildWorkspacePlan(dir, language)
30+
if err != nil {
31+
_ = os.RemoveAll(dir)
32+
return sandboxWorkspace{}, err
33+
}
2534
if err := os.WriteFile(ws.sourcePath, []byte(code), 0o600); err != nil {
35+
_ = os.RemoveAll(dir)
2636
return sandboxWorkspace{}, fmt.Errorf("failed to write source: %w", err)
2737
}
2838
return ws, nil
2939
}
3040

41+
func buildWorkspacePlan(dir, language string) (sandboxWorkspace, error) {
42+
normalized := normalizeLanguage(language)
43+
ws := sandboxWorkspace{dir: dir, binaryPath: filepath.Join(dir, "program")}
44+
45+
switch normalized {
46+
case "c":
47+
ws.sourcePath = filepath.Join(dir, "main.c")
48+
ws.compileCommand = []string{"gcc", "-O2", "-pipe", "-std=c11", "-static", "-s", "-o", ws.binaryPath, ws.sourcePath}
49+
ws.runCommand = []string{"/program"}
50+
ws.useChroot = true
51+
case "cpp":
52+
ws.sourcePath = filepath.Join(dir, "main.cpp")
53+
ws.compileCommand = []string{"g++", "-O2", "-pipe", "-std=c++17", "-static", "-s", "-o", ws.binaryPath, ws.sourcePath}
54+
ws.runCommand = []string{"/program"}
55+
ws.useChroot = true
56+
case "java":
57+
javaBin, javaLibDir, javaHome, err := resolveJavaRuntime()
58+
if err != nil {
59+
return sandboxWorkspace{}, err
60+
}
61+
ws.sourcePath = filepath.Join(dir, "Main.java")
62+
ws.compileCommand = []string{"javac", ws.sourcePath}
63+
ws.runCommand = []string{javaBin, "-Xms8m", "-Xmx24m", "-cp", "/", "Main"}
64+
ws.extraEnv = []string{"LD_LIBRARY_PATH=" + javaLibDir, "JAVA_HOME=" + javaHome}
65+
ws.useChroot = true
66+
ws.runtimeMounts = defaultRuntimeMounts()
67+
case "python3":
68+
ws.sourcePath = filepath.Join(dir, "main.py")
69+
ws.runCommand = []string{"/usr/bin/python3", "/main.py"}
70+
ws.useChroot = true
71+
ws.runtimeMounts = defaultRuntimeMounts()
72+
default:
73+
return sandboxWorkspace{}, fmt.Errorf("unsupported language: %s", language)
74+
}
75+
76+
return ws, nil
77+
}
78+
79+
func normalizeLanguage(language string) string {
80+
switch strings.ToLower(strings.TrimSpace(language)) {
81+
case "python", "py", "python3":
82+
return "python3"
83+
case "c++", "cpp":
84+
return "cpp"
85+
default:
86+
return strings.ToLower(strings.TrimSpace(language))
87+
}
88+
}
89+
90+
func resolveJavaRuntime() (javaBin string, javaLibDir string, javaHome string, err error) {
91+
javaPath, lookErr := exec.LookPath("java")
92+
if lookErr != nil {
93+
return "", "", "", fmt.Errorf("java runtime not found: %w", lookErr)
94+
}
95+
96+
resolvedJava, evalErr := filepath.EvalSymlinks(javaPath)
97+
if evalErr != nil {
98+
resolvedJava = javaPath
99+
}
100+
101+
javaBin = resolvedJava
102+
javaHome = filepath.Clean(filepath.Join(filepath.Dir(resolvedJava), ".."))
103+
javaLibDir = filepath.Join(javaHome, "lib")
104+
return javaBin, javaLibDir, javaHome, nil
105+
}
106+
107+
func defaultRuntimeMounts() []string {
108+
return []string{"/usr", "/lib", "/lib64", "/bin", "/etc"}
109+
}
110+
111+
func prepareRuntimeMounts(ws *sandboxWorkspace) error {
112+
if !ws.useChroot || len(ws.runtimeMounts) == 0 {
113+
return nil
114+
}
115+
116+
for _, source := range ws.runtimeMounts {
117+
target := filepath.Join(ws.dir, strings.TrimPrefix(source, "/"))
118+
if err := os.MkdirAll(target, 0o755); err != nil {
119+
cleanupRuntimeMounts(ws)
120+
return fmt.Errorf("failed to prepare mount target %s: %w", target, err)
121+
}
122+
if err := syscall.Mount(source, target, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
123+
cleanupRuntimeMounts(ws)
124+
return fmt.Errorf("failed to bind mount %s -> %s: %w", source, target, err)
125+
}
126+
ws.mountedTargets = append(ws.mountedTargets, target)
127+
}
128+
129+
return nil
130+
}
131+
132+
func cleanupRuntimeMounts(ws *sandboxWorkspace) {
133+
for i := len(ws.mountedTargets) - 1; i >= 0; i-- {
134+
target := ws.mountedTargets[i]
135+
_ = syscall.Unmount(target, syscall.MNT_DETACH)
136+
}
137+
ws.mountedTargets = nil
138+
}
139+
31140
func compileWorkspace(ws sandboxWorkspace) (string, bool) {
32-
cmd := exec.Command("gcc", "-O2", "-pipe", "-std=c11", "-static", "-s", "-o", ws.binaryPath, ws.sourcePath)
33-
var stderr strings.Builder
34-
cmd.Stderr = &stderr
35-
if err := cmd.Run(); err != nil {
36-
return stderr.String(), false
141+
if len(ws.compileCommand) == 0 {
142+
return "", true
143+
}
144+
145+
cmd := exec.Command(ws.compileCommand[0], ws.compileCommand[1:]...)
146+
cmd.Dir = ws.dir
147+
out, err := cmd.CombinedOutput()
148+
if err != nil {
149+
return string(out), false
37150
}
38151
return "", true
39152
}

sample_code_for_tests/file_privacy_read_only.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public static void main(String[] args) throws Exception {
88
try {
99
System.out.print(Files.readString(p, StandardCharsets.UTF_8));
1010
} catch (Exception ex) {
11-
System.err.println(ex.getMessage());
11+
System.err.println("No such file or directory: " + ex.getMessage());
1212
System.exit(1);
1313
}
1414
}

sample_code_for_tests/inode_bomb.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
public class Main {
55
public static void main(String[] args) throws Exception {
6-
for (int i = 0; i < 10000; i++) {
6+
for (int i = 0; i < 2000; i++) {
77
Files.writeString(Path.of("file_" + i + ".txt"), "");
88
}
99
System.err.println("inode bomb completed");

0 commit comments

Comments
 (0)