Skip to content

fix(judge): remove safeexec and the rest of the pre-sandbox model - #96

Merged
matbrgz merged 1 commit into
masterfrom
fix/86-remove-safeexec
Sep 13, 2026
Merged

matbrgz merged 1 commit into
masterfrom
fix/86-remove-safeexec

Conversation

@matbrgz

@matbrgz matbrgz commented Sep 13, 2026

Copy link
Copy Markdown
Member

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:

safeexec DENTRO do bwrap  -> 137  "ERROR Operation not permitted"
bwrap SEM safeexec        ->   0  "8"
safeexec SEM bwrap        ->  16  "unable to exec python3"

A primeira linha é por design: o bubblewrap monta tudo nosuid e seta no_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_PATH para o binário real — como o .env.docker fazia — 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 strncpy que 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 o mkdir e o chown no Dockerfile.judge
  • autojudge.judge_user / judge_group — sem nenhum leitor em app/; eram o -U/-G do safeexec, que este código nunca passou

Verificação

Imagem reconstruída do Dockerfile.judge alterado, --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

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
@matbrgz
matbrgz merged commit 6ef3527 into master Sep 13, 2026
3 checks passed
@matbrgz
matbrgz deleted the fix/86-remove-safeexec branch September 13, 2026 19:17
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant