You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CRITICAL: judged submissions run with no filesystem/namespace isolation from the host #49
AutoJudgeService (app/Services/AutoJudgeService.php) compiles and runs untrusted, arbitrary user-submitted source code by shelling out directly on the host via Laravel's Process facade. The only isolation is an optional custom C sandboxer (tools/safeexec.c, wired in via wrapWithSafeExec()) that enforces CPU time and memory via rlimit -- it does not provide filesystem or namespace isolation. If safeexec isn't present (file_exists($this->safeExecPath) is false, which is the actual state in the dev image today), submissions run completely unconfined: full host filesystem visibility, no memory cap beyond the OS's own limits, same user as the PHP-FPM/queue worker process.
Why this is critical
This is a real-world contest judge that will run untrusted code from anonymous or semi-anonymous participants. A submission (in any of the ~15 languages activated in #39, including native binaries via C/C++/Rust/Go/Pascal) can currently read/write anywhere the www user can, exhaust host memory, or otherwise escape the intended sandbox.
Inspiration
cd-moj/cdmoj's judge runs every submission inside bubblewrap (bwrap, the same user-namespace sandboxing technology Flatpak uses) against a dedicated, immutable per-language compiler rootfs -- not the host filesystem at all -- with memory hard-capped via systemd-run --user --scope -p MemoryMax= (real cgroup v2 enforcement, not rlimit sampling) and a dynamic wall-clock anti-wedge timeout that SIGKILLs the whole process group.
Suggested scope
Require bubblewrap (or an equivalent: Docker itself is already used elsewhere in this stack, so a per-submission ephemeral container is also viable, though heavier-weight than bwrap) in the judge image (Dockerfile.dev/Dockerfile/Dockerfile.judge).
Wrap AutoJudgeService::compile()/executeProgram()'s shelled-out commands in bwrap with a read-only bind of the language toolchains and the run's own working directory only -- no host filesystem access.
Replace/augment safeexec's rlimit-based memory limit with systemd-run --user --scope -p MemoryMax= (or cgroup v2 directly) for real enforcement, and make this mandatory rather than "optional if the binary happens to exist."
This should be treated as the top-priority follow-up from this comparison -- it's a security gap in a system that runs arbitrary code by design, not a feature nice-to-have.
Vou registrar a dependência de isolamento obrigatório nas specs do frontend de submissão/Treino Livre. Esta issue é de runtime/backend: não vou implementar sandbox nem alterar o juiz. A entrega será um contrato de disponibilidade que impeça a UI de anunciar envio habilitado quando o backend não puder executar com isolamento, além dos critérios de validação do runtime.
Registro do escopo antes de escrever a spec, conforme solicitado pelo mantenedor. O PR de interfaces e contratos será vinculado aqui.
Entrega desta issue: análise e spec para backend/infraestrutura, incluindo impactos e dependências das interfaces. Não houve alteração de runtime, provisionamento ou deploy.
Validação da branch: 604 testes PHP (3248 assertions), 26 testes de frontend e build aprovados. Fluxos Vue foram testados com contratos de teste; integração funcional exige endpoints reais. Revisão visual de biblioteca/enunciado feita no navegador, incluindo 375 px; telas administrativas tiveram revisão de código/componentes, com conferência visual limitada por bloqueio de outra extensão no Chrome.
As responsabilidades e critérios de aceite estão na spec. Este PR não encerra o trabalho pendente do backend.
Fechada pela #77 (+ correções na própria branch antes do merge).
O que entrou
Compilação e execução de código submetido rodam dentro de um sandbox bubblewrap, e o serviço falha fechado em vez de rodar submissão desconfinada no host.
O sandbox é uma allowlist (autojudge.sandbox_paths), não uma visão read-only do host: a raiz da aplicação nunca é montada, então .env, a árvore de código, os diretórios dos outros runs e os testes ocultos dos outros problemas simplesmente não existem lá dentro. O que cada passo legitimamente precisa de fora — o arquivo de entrada daquele test case, o compile/run script do problema, {judge_runtime} — é montado read-only, um caminho por vez.
Rede desligada, /tmp privado, ulimit -t/-f/-u, 152/153 mapeados para TLE e RE, --new-session contra injeção TIOCSTI. Todo caminho que executa entrada não confiável está coberto, incluindo o compile script customizado do problema e o compare script.
Falha de startup do bwrap vira SandboxUnavailableException → CS, então problema de infraestrutura nunca é cobrado do competidor como erro de compilação ou execução.
Encontrado durante a revisão
A versão inicial da PR tinha --ro-bind / /, que confina escrita mas deixa leitura aberta. A cadeia de exfiltração era completa: um #include "…/.env" em C faz o gcc citar o arquivo no diagnóstico → auto_judge_stderr → submission-show.blade.php renderiza para o próprio time. DB_PASSWORD e APP_KEY visíveis para qualquer competidor.
E o safeexec dentro do bwrap matava toda submissão com TLE espúrio (setuid-root contra nosuid+no_new_privs). Os dois agora são mutuamente exclusivos.
Verificação
tests/Integration/JudgeSandboxConfinementTest.php dirige um bwrap real só com builtins de shell — não precisa da imagem de linguagens — e afere os critérios de aceite um a um: não lê o sentinela nem o .env fora da tentativa, não escreve fora do runDir, /tmp privado, só loopback, PIDs do host invisíveis, rlimits aplicados.
O CI (#80/#81/#84) roda a suíte inteira dentro da imagem real do juiz, --privileged, com --fail-on-skipped — que é o que impede um sandbox quebrado de aparecer como build verde. 820 testes, nada pulado.
Memória residente ainda depende só do flag {memory} por linguagem, não de cgroups. Uma submissão em C/C++/Rust/Go/Pascal não tem teto de memória real.
O gate de API que o spec pede (can_submit=false / 503 quando o executor não está saudável) existe desde a feat(practice): Treino Livre phase 1 backend (issue #43) #82, via JudgeExecutorHealth, para o Treino Livre. Vale estender o mesmo gate ao envio de competição.
Activity
matbrgz commentedon Sep 11, 2026
Vou registrar a dependência de isolamento obrigatório nas specs do frontend de submissão/Treino Livre. Esta issue é de runtime/backend: não vou implementar sandbox nem alterar o juiz. A entrega será um contrato de disponibilidade que impeça a UI de anunciar envio habilitado quando o backend não puder executar com isolamento, além dos critérios de validação do runtime.
Registro do escopo antes de escrever a spec, conforme solicitado pelo mantenedor. O PR de interfaces e contratos será vinculado aqui.
matbrgz commentedon Sep 11, 2026
Entrega publicada no PR #70: #70
Spec desta issue · Índice dos contratos
Entrega desta issue: análise e spec para backend/infraestrutura, incluindo impactos e dependências das interfaces. Não houve alteração de runtime, provisionamento ou deploy.
Validação da branch: 604 testes PHP (3248 assertions), 26 testes de frontend e build aprovados. Fluxos Vue foram testados com contratos de teste; integração funcional exige endpoints reais. Revisão visual de biblioteca/enunciado feita no navegador, incluindo 375 px; telas administrativas tiveram revisão de código/componentes, com conferência visual limitada por bloqueio de outra extensão no Chrome.
As responsabilidades e critérios de aceite estão na spec. Este PR não encerra o trabalho pendente do backend.
fix(judge): close the read side of the bwrap sandbox and cover every …
fix(judge): make judging actually work under the sandbox
feat: implement bwrap sandbox isolation for autojudge (issue #49) (#77)
feat(practice): Treino Livre phase 1 backend (issue #43) (#82)
matbrgz commentedon Sep 13, 2026
Fechada pela #77 (+ correções na própria branch antes do merge).
O que entrou
Compilação e execução de código submetido rodam dentro de um sandbox bubblewrap, e o serviço falha fechado em vez de rodar submissão desconfinada no host.
O sandbox é uma allowlist (
autojudge.sandbox_paths), não uma visão read-only do host: a raiz da aplicação nunca é montada, então.env, a árvore de código, os diretórios dos outros runs e os testes ocultos dos outros problemas simplesmente não existem lá dentro. O que cada passo legitimamente precisa de fora — o arquivo de entrada daquele test case, o compile/run script do problema,{judge_runtime}— é montado read-only, um caminho por vez.Rede desligada,
/tmpprivado,ulimit -t/-f/-u, 152/153 mapeados para TLE e RE,--new-sessioncontra injeção TIOCSTI. Todo caminho que executa entrada não confiável está coberto, incluindo o compile script customizado do problema e o compare script.Falha de startup do bwrap vira
SandboxUnavailableException→ CS, então problema de infraestrutura nunca é cobrado do competidor como erro de compilação ou execução.Encontrado durante a revisão
A versão inicial da PR tinha
--ro-bind / /, que confina escrita mas deixa leitura aberta. A cadeia de exfiltração era completa: um#include "…/.env"em C faz o gcc citar o arquivo no diagnóstico →auto_judge_stderr→submission-show.blade.phprenderiza para o próprio time.DB_PASSWORDeAPP_KEYvisíveis para qualquer competidor.E o
safeexecdentro do bwrap matava toda submissão com TLE espúrio (setuid-root contranosuid+no_new_privs). Os dois agora são mutuamente exclusivos.Verificação
tests/Integration/JudgeSandboxConfinementTest.phpdirige um bwrap real só com builtins de shell — não precisa da imagem de linguagens — e afere os critérios de aceite um a um: não lê o sentinela nem o.envfora da tentativa, não escreve fora do runDir,/tmpprivado, só loopback, PIDs do host invisíveis, rlimits aplicados.O CI (#80/#81/#84) roda a suíte inteira dentro da imagem real do juiz,
--privileged, com--fail-on-skipped— que é o que impede um sandbox quebrado de aparecer como build verde. 820 testes, nada pulado.Continua em aberto → #86
{memory}por linguagem, não de cgroups. Uma submissão em C/C++/Rust/Go/Pascal não tem teto de memória real.can_submit=false/ 503 quando o executor não está saudável) existe desde a feat(practice): Treino Livre phase 1 backend (issue #43) #82, viaJudgeExecutorHealth, para o Treino Livre. Vale estender o mesmo gate ao envio de competição.docs: the judging setup instructions described a judge that no longer…
docs: the judging setup instructions described a judge that no longer…
feat(backend): CRUD de linguagens pela interface (issue #140) (#158)
Autoteste do sandbox na maquina de julgamento (#194) (#210)