Skip to content

Commit afc51b2

Browse files
koki-developclaude
andcommitted
feat: enforce 1 MiB output limit and kill sandbox process on excess
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0bcc7a5 commit afc51b2

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

e2e/tests/security.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,20 @@ tests:
134134
output:
135135
status: 400
136136
error: 'file name "a/b.js" contains invalid characters'
137+
138+
- name: "output limit exceeded"
139+
input:
140+
runtime: node
141+
files:
142+
- name: index.js
143+
content: |
144+
console.log("AAAAAAAAAA".repeat(10000000))
145+
output:
146+
status: 200
147+
run:
148+
stdout: ""
149+
stderr: ""
150+
output: ""
151+
exit_code: -1
152+
status: "OUTPUT_LIMIT_EXCEEDED"
153+
signal: null

internal/sandbox/sandbox.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,15 @@ const (
4848
type Status string
4949

5050
const (
51-
StatusOK Status = "OK"
52-
StatusTimeout Status = "TIMEOUT"
51+
StatusOK Status = "OK"
52+
StatusTimeout Status = "TIMEOUT"
53+
StatusOutputLimitExceeded Status = "OUTPUT_LIMIT_EXCEEDED"
5354
)
5455

56+
var errOutputLimitExceeded = errors.New("output limit exceeded")
57+
58+
const outputLimit = 1 << 20 // 1 MiB
59+
5560
type runtimeConfig struct {
5661
binaryPath string
5762
installDir string
@@ -190,18 +195,33 @@ func Run(ctx context.Context, rt Runtime, tmpDir, entryFile string) (Result, err
190195

191196
var stdoutBuf, stderrBuf, combined bytes.Buffer
192197

193-
if err := drainPipes(ctx, stdoutR, stderrR, &stdoutBuf, &stderrBuf, &combined); err != nil {
198+
drainErr := drainPipes(ctx, cmd.Process, stdoutR, stderrR, &stdoutBuf, &stderrBuf, &combined)
199+
if drainErr != nil && !errors.Is(drainErr, errOutputLimitExceeded) {
194200
_ = cmd.Wait()
195201
_ = stdoutR.Close()
196202
_ = stderrR.Close()
197203
_ = logR.Close()
198-
return Result{}, fmt.Errorf("sandbox execution failed: %w", err)
204+
return Result{}, fmt.Errorf("sandbox execution failed: %w", drainErr)
199205
}
206+
outputLimitHit := errors.Is(drainErr, errOutputLimitExceeded)
200207

201208
_ = stdoutR.Close()
202209
_ = stderrR.Close()
203210

204211
waitErr := cmd.Wait()
212+
213+
if outputLimitHit {
214+
_ = logR.Close()
215+
return Result{
216+
Stdout: "",
217+
Stderr: "",
218+
Output: "",
219+
ExitCode: -1,
220+
Status: StatusOutputLimitExceeded,
221+
Signal: nil,
222+
}, nil
223+
}
224+
205225
if ctx.Err() != nil {
206226
_ = logR.Close()
207227
return Result{}, ctx.Err()
@@ -250,7 +270,7 @@ func Run(ctx context.Context, rt Runtime, tmpDir, entryFile string) (Result, err
250270
// pipes are ready simultaneously, stdout is processed first. The poll
251271
// timeout is derived from ctx's deadline so that the execution timeout
252272
// and client disconnects are respected promptly.
253-
func drainPipes(ctx context.Context, stdoutR, stderrR *os.File, stdoutBuf, stderrBuf, combined *bytes.Buffer) error {
273+
func drainPipes(ctx context.Context, proc *os.Process, stdoutR, stderrR *os.File, stdoutBuf, stderrBuf, combined *bytes.Buffer) error {
254274
type pipe struct {
255275
file *os.File
256276
buf *bytes.Buffer
@@ -312,6 +332,10 @@ func drainPipes(ctx context.Context, stdoutR, stderrR *os.File, stdoutBuf, stder
312332
if nr > 0 {
313333
pipes[i].buf.Write(buf[:nr])
314334
combined.Write(buf[:nr])
335+
if combined.Len() > outputLimit {
336+
_ = proc.Kill()
337+
return errOutputLimitExceeded
338+
}
315339
}
316340
if readErr != nil {
317341
if readErr == io.EOF {

0 commit comments

Comments
 (0)