Repository navigation
fix(judge): remove safeexec and the rest of the pre-sandbox model - #96
Merged
Merged
Conversation
Part of #86. The cgroup half stays open; see the issue for why. safeexec is a setuid-root helper from the chroot era. Since #77 it is dead in production and broken outside it: safeexec inside bwrap -> 137 "ERROR Operation not permitted" bwrap without safeexec -> 0 "8" safeexec without bwrap -> 16 "unable to exec python3" The first line is by design: bubblewrap mounts everything nosuid and sets no_new_privs, so the escalation cannot happen -- which is why #77 made the two mutually exclusive, and why safeexec has not run in a sandboxed deployment since. The third is the one that matters here: it is broken on its own too, and has been since before the sandbox existed. Any Docker deployment pointing AUTOJUDGE_SAFEEXEC_PATH at the real binary (which .env.docker did) had judging broken by it. So it provided nothing, and a setuid-root binary that provides nothing is attack surface for free. Gone: tools/safeexec.c, the gcc + chmod 4555 step in Dockerfile.judge, wrapWithSafeExec(), the config key and the env var. That also takes with it the five semgrep strncpy findings triaged in #90. The same commit clears the rest of the model bwrap replaced, all of it config nobody reads: - autojudge.jail_path (/bocajail) -- assigned in the constructor and referenced nowhere else, plus the mkdir and chown in Dockerfile.judge - autojudge.judge_user / judge_group -- no reader in app/ at all; they were safeexec's -U/-G, which this code never passed Verified in an image built from the changed Dockerfile.judge, --privileged, --fail-on-skipped: 861 tests, 4271 assertions, all fourteen languages still compiling and judging AC inside the sandbox. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY
3 tasks
matbrgz
added a commit
that referenced
this pull request
Sep 14, 2026
… exists Following the README's Auto-Judge Setup today does not produce a working judge, and one step of it produces a liability. It said to `cd tools` and compile safeexec as setuid root -- the directory does not exist, safeexec was removed in #96, and the current sandbox cannot use a setuid binary anyway: bwrap mounts nosuid and sets no_new_privs, which is what broke judging in #77. Anyone who followed it has a setuid-root binary on their judge for no benefit at all. It said to debootstrap a chroot into /bocajail and set AUTOJUDGE_JAIL_PATH. That variable is read by nothing; grep finds no "jail" anywhere in config/. The isolation has been bubblewrap since #49. And it never mentioned installing bubblewrap, which the judge actually requires. Rewritten against what the code does now, with the stale instructions called out explicitly rather than silently dropped -- someone who already built that chroot needs to be told it is dead, not left to wonder. Every command was run in the judge image before being written down, which caught the one I would otherwise have shipped: the bwrap smoke test I first wrote fails, because `echo` lives in /bin and /bin was not bound, so an operator checking their sandbox would conclude it was broken. It now uses --ro-bind-try for the directories whose layout varies by distribution and runs a binary that exists, and it is verified both privileged and unprivileged. Also adds the judgehost section, which did not exist: issuing a credential, running the agent, what the agent refuses to do and why, and the requirement for identical hardware with the reason it is a requirement. Two things operators could only have learned from the source: more than one local worker is safe and helps (about 3.6x at eight on a 10-CPU box), and on SQLite it helps not at all, because SQLite serialises the writes judging does several of per run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY
matbrgz
added a commit
that referenced
this pull request
Sep 14, 2026
… exists (#131) Following the README's Auto-Judge Setup today does not produce a working judge, and one step of it produces a liability. It said to `cd tools` and compile safeexec as setuid root -- the directory does not exist, safeexec was removed in #96, and the current sandbox cannot use a setuid binary anyway: bwrap mounts nosuid and sets no_new_privs, which is what broke judging in #77. Anyone who followed it has a setuid-root binary on their judge for no benefit at all. It said to debootstrap a chroot into /bocajail and set AUTOJUDGE_JAIL_PATH. That variable is read by nothing; grep finds no "jail" anywhere in config/. The isolation has been bubblewrap since #49. And it never mentioned installing bubblewrap, which the judge actually requires. Rewritten against what the code does now, with the stale instructions called out explicitly rather than silently dropped -- someone who already built that chroot needs to be told it is dead, not left to wonder. Every command was run in the judge image before being written down, which caught the one I would otherwise have shipped: the bwrap smoke test I first wrote fails, because `echo` lives in /bin and /bin was not bound, so an operator checking their sandbox would conclude it was broken. It now uses --ro-bind-try for the directories whose layout varies by distribution and runs a binary that exists, and it is verified both privileged and unprivileged. Also adds the judgehost section, which did not exist: issuing a credential, running the agent, what the agent refuses to do and why, and the requirement for identical hardware with the reason it is a requirement. Two things operators could only have learned from the source: more than one local worker is safe and helps (about 3.6x at eight on a 10-CPU box), and on SQLite it helps not at all, because SQLite serialises the writes judging does several of per run. Claude-Session: https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parte da #86. A metade de cgroups continua aberta — ver o comentário que deixei lá com as medições.
Por que remover
O safeexec é um helper setuid-root da era do chroot. Desde a #77 ele está morto em produção e quebrado fora dela:
A primeira linha é por design: o bubblewrap monta tudo
nosuide setano_new_privs, então a escalada não acontece — foi por isso que a #77 tornou os dois mutuamente exclusivos, e por isso ele não roda numa implantação com sandbox desde então.A terceira linha é a que importa aqui: ele está quebrado sozinho também, e estava antes do sandbox existir. Qualquer implantação Docker apontando
AUTOJUDGE_SAFEEXEC_PATHpara o binário real — como o.env.dockerfazia — tinha o julgamento quebrado por causa dele.Ou seja: não entregava nada. E um binário setuid-root que não entrega nada é superfície de ataque de graça.
Vão junto as cinco ocorrências de
strncpyque o semgrep apontou e que eu triei na #90 — elas somem com o arquivo.O resto do modelo pré-sandbox
Mesmo commit limpa o que o bwrap substituiu, tudo config que ninguém lê:
autojudge.jail_path(/bocajail) — atribuído no construtor e referenciado em lugar nenhum, mais omkdire ochownnoDockerfile.judgeautojudge.judge_user/judge_group— sem nenhum leitor emapp/; eram o-U/-Gdo safeexec, que este código nunca passouVerificação
Imagem reconstruída do
Dockerfile.judgealterado,--privileged, com--fail-on-skipped: 861 testes, 4271 asserções, as catorze linguagens ainda compilando e julgando AC dentro do sandbox.🤖 Generated with Claude Code
https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY