This document describes the mitigations applied to limit the damage an adversarial submission can do. File and line references point to the implementation in this repository.
Risk: A client sends a multi-megabyte body to exhaust memory or cause a slow read.
Mitigation: http.MaxBytesReader wraps the request body with a 256 KiB
hard cap before any JSON decoding begins.
internal/handler/run.go:60 — r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
The limit constant is defined at internal/handler/run.go:15.
Risk: A 256 KiB source string passes the HTTP cap but takes excessive time or memory during compilation.
Mitigation: After JSON decoding and language lookup, the source length is checked against a 64 KiB cap. This sits below the HTTP cap so the two limits are distinguishable.
internal/handler/run.go:99 — source length check; run.go:16 — the constant.
Risk: A caller passes compiler or interpreter flags that write to
arbitrary paths (-o /etc/passwd), load plugins (-fplugin=evil.so), or
change security-relevant settings.
Mitigation: Each language in config/languages.yaml declares a
flag_allowlist. Every caller-supplied flag is matched against the list
using path.Match glob patterns. A flag that does not match any pattern
is rejected with 400 disallowed_flag. A missing or empty allow-list
denies all caller flags.
internal/sandbox/flags.go:17 — CheckFlagAllowlist
internal/handler/run.go:105 — enforcement in the handler
Risk: A caller sets filename to ../../etc/passwd or similar to
write the source file outside the working directory.
Mitigation: ValidateFilename rejects any name that is empty, longer
than 255 bytes, starts with a dot, contains a path separator, or differs
from its own filepath.Base.
internal/sandbox/sandbox.go:76 — ValidateFilename
internal/handler/run.go:115 — enforcement in the handler
Risk: If working directory names are sequential or time-based, an attacker could predict the next name and plant files, or two concurrent requests could collide.
Mitigation: Each working directory is named
goboxd-<pid>-<atomicCounter>-<random> where the random suffix comes from
os.MkdirTemp. The pid and an atomic counter make the names unique across
processes and concurrent goroutines without relying on randomness alone.
internal/sandbox/sandbox.go:21 — CreateWorkDir
Risk: A program that prints without limit drives the server's heap into OOM.
Mitigation: limitWriter wraps bytes.Buffer and silently discards
writes beyond 1 MiB per stream. Both stdout and stderr are capped
independently. Truncated output gets a [truncated] suffix.
internal/sandbox/nsjail.go:92 — stdout and stderr writers
Risk: If the server process is killed mid-request, the deferred
sandbox.Cleanup never runs, leaving working directories on disk
indefinitely.
Mitigation: At startup, SweepOrphans scans for goboxd-* directories
older than 10 minutes and removes them. This is a backstop for the defer
that covers normal operation.
internal/sandbox/sandbox.go:30 — SweepOrphans
cmd/goboxd/main.go:29 — called at startup
Risk: A submission runs forever or allocates unbounded memory.
Mitigation: nsjail is invoked with --time_limit (wall-clock seconds)
and --rlimit_as (address-space MiB) on every run. These come from the
language config's timeout_seconds and the per-request memory_mb
override (default 256 MiB). nsjail enforces these limits in the kernel via
rlimits and kills the process when they are exceeded.
internal/sandbox/nsjail.go:52-53 — --rlimit_as and --time_limit in args
Risk: A flood of concurrent requests causes unbounded goroutine and nsjail process growth, exhausting CPU and memory.
Mitigation: A buffered-channel semaphore limits active sandbox
executions to GOBOXD_MAX_CONCURRENT (default runtime.NumCPU()). Excess
requests wait up to GOBOXD_QUEUE_TIMEOUT_S (default 8 s) for a slot to
open. On timeout the handler returns HTTP 429 server_busy rather than
accumulating goroutines. The semaphore slot is released immediately after
runner.Run returns — before the JSON response write — so a slow-reading
client cannot pin a sandbox execution slot.
internal/handler/limiter.go — defaultLimiter, queueTimeout
internal/handler/run.go:123 — TryAcquire before runner.Run
Risk: A sandboxed process calls a privileged or dangerous kernel
interface (ptrace, bpf, mount, kexec_load, etc.) to escape the
sandbox or corrupt the host.
Mitigation: nsjail is compiled with Kafel support and launched with
--seccomp_policy_file /etc/goboxd/seccomp.policy. The policy uses
DEFAULT ALLOW with KILL_PROCESS on 27 syscalls covering:
- Ptrace / process introspection:
ptrace,process_vm_readv,process_vm_writev - Kernel module loading / kexec:
init_module,finit_module,delete_module,kexec_load,reboot - Clock manipulation:
settimeofday,adjtimex,clock_adjtime - Namespace / device access:
mknodat,chroot,pivot_root,unshare,setns,userfaultfd,name_to_handle_at,open_by_handle_at - Audit / capability / keyring:
acct,bpf,syslog,add_key,request_key,keyctl,fanotify_init,capset,mount
DEFAULT ALLOW preserves full Python, Java, Ruby, and Zig stdlib access.
The policy is loaded conditionally via os.Stat — absent in the dev/tools
image, the --seccomp_policy_file flag is simply not passed.
scripts/nsjail-seccomp.policy — Kafel policy
internal/sandbox/nsjail.go — conditional os.Stat load in BuildNsjailArgs
Risk: A sandbox process creates unlimited child processes (fork bomb), writes a core dump exhausting disk, consumes excessive stack, or monopolises a CPU core unfairly.
Mitigation: Four additional rlimits are set on every nsjail invocation:
| Flag | Value | Protection |
|---|---|---|
--rlimit_nproc |
128 | Fork-bomb cap; above JVM thread count (~40), far below OS capacity |
--rlimit_core |
0 | No core dumps — prevents disk exhaustion in /tmp |
--rlimit_stack |
8 MiB | Matches the standard Linux default; prevents stack overflow cascades |
--max_cpus |
1 | One CPU per jail — fairness and reproducible wall-clock timing |
internal/sandbox/nsjail.go:BuildNsjailArgs — all four flags added unconditionally