Repository navigation
fix: harden submission containers and stop grading crashes on bad monitor_out - #82
Open
felipelageduarte wants to merge 1 commit into
Open
felipelageduarte wants to merge 1 commit into
felipelageduarte wants to merge 1 commit into
Conversation
…itor_out Two P0 findings from a security audit of the sandbox pipeline (compiler-monitor-oportunidades.md), both verified via unit tests added in this commit and a before/after run against the unmodified code (see PR description for the full before/after evidence, including why a full docker end-to-end run could not be reproduced on this contributor's machine). - `container.py`'s `run()` started every submission's container with full network access and the default Docker capability set. No part of the compile/test pipeline needs network (everything happens locally against the mounted workspace), so this is pure unused attack surface: exfiltrating test cases, reaching other services on the bridge network, or using the container as an egress point. Added `network_mode="none"`, `cap_drop=["ALL"]`, and `security_opt=["no-new-privileges"]`. These are additive constraints only - they do not touch the image, volumes, or command, so they have no effect on well-behaved submissions. Per-language memory/CPU/pids ceilings (the same audit's S1/E4) are intentionally left out of this change: Java's JVM and MPI-based jobs need very different budgets than a plain C binary, and there is no per-language config field for that yet. Picking one hardcoded number risks silently breaking a whole class of submissions untested, so that is tracked as follow-up work instead of guessed here. - `grading.process_test_results` read `<test_id>.monitor_out` with no error handling. If the file was missing (monitor never ran) or malformed (e.g. the monitor binary itself crashed before writing anything - see the paired compiler-monitor fix), this raised `FileNotFoundError`/`KeyError` out of `process_test_results_batch`, aborting grading for every other test case in the same commit. It now catches both, logs which test case and file were unreadable, and reports that single test case as incorrect instead of taking the whole batch down with it. The well-formed path is untouched. Co-Authored-By: Claude Fable 5 <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.
Contexto
Parte da mesma auditoria de segurança referenciada no PR companheiro runcodes-icmc/compiler-monitor#14 (documento completo:
compiler-monitor-oportunidades.md, anexado naquele PR). Este PR cobre os itens S2/S10 (parcial de S1) e B6 do roadmap P0.O que mudou
S2 / S10 (parcial de S1) — Container do aluno sem hardening de rede/capabilities (
container.py)client.containers.run(...)não passava nenhum parâmetro de contenção. O código do aluno (compilação + execução de testes) rodava com rede completa e o conjunto default de capabilities do Docker, sem nenhuma necessidade legítima de rede (compilação e execução de teste são 100% locais, contra o workspace montado). Isso abria para: exfiltração de casos de teste para fora da rede, ataque a outros serviços na bridge do Docker, uso do container como ponto de saída.Adicionado:
Escopo deliberadamente reduzido: limites de memória/CPU/pids por linguagem (S1/E4 completos no doc) foram propositalmente não incluídos aqui. Java (JVM) e jobs baseados em MPI precisam de orçamentos de memória/processos muito diferentes de um binário C simples, e hoje não existe campo de configuração por linguagem para isso. Chutar um número fixo arrisca quebrar silenciosamente uma classe inteira de submissões sem eu poder testar todas as imagens de linguagem — isso fica de follow-up (ver roadmap P1/P2 no doc).
B6 —
grading.pyabortava o lote inteiro de testes nummonitor_outruimprocess_test_resultsabria e parseava<test_id>.monitor_outsem nenhum tratamento de erro. Se o arquivo não existisse (monitor nunca rodou) ou estivesse vazio/malformado (ex.: o próprio monitor crashou antes de escrever algo — exatamente o cenário do bug B4 corrigido no PR companheiro), a exceção (FileNotFoundError/KeyError) subia porprocess_test_results_batche abortava a avaliação de todos os outros casos de teste daquele commit, não só do caso afetado.Agora a falha de leitura é capturada, logada (com o id do commit/teste e o motivo), e aquele caso de teste específico é reportado como incorreto — sem afetar os demais.
Evidência: testes antes e depois
Não há CI de testes configurado neste repositório hoje (
.github/workflows/só builda imagem e faz scan de segurança) — os testes existem mas não rodam automaticamente. Rodei manualmente compython -m unittest.Suíte completa (sem depender de Docker) antes vs. depois, usando
git stashpara isolar o código antigo:Baseline (
git stashremovendo as duas correções, mantendo os testes novos):Depois (
git stash pop, código com as correções):(34 testes já existentes no repo + 5 novos, incluindo o controle
test_well_formed_monitor_out_still_works, que prova que o caminho normal/legítimo continua produzindo exatamente o mesmo resultado de antes.)Lint/type-check:
ruff checklimpo;basedpyrightcom 0 erros (restam só warnings deAnyinerentes ao uso deMagicMocknos testes, sem impacto em runtime).Sobre não ter rodado o teste de integração Docker completo (
test_process_commit_hello*)Tentei validar
S2também via o teste de integração real (rcc.tests.engine.test_process_commit_hello, que builda e roda containers de linguagem reais). Isso não foi possível de forma confiável nesta máquina de desenvolvimento (Apple Silicon): o bináriogccestaticamente linkado, ao rodar dentro do container via emulação Rosetta sobre um volume virtiofs, falha comrosetta error: mmap_anonymous_rw mmap failed— e isso acontece mesmo com o código original, sem nenhuma das mudanças deste PR, confirmando que é uma limitação do ambiente de emulação local (Docker Desktop + Rosetta + Apple Silicon), não algo causado por esta mudança.Para compensar, validei
S2/S10com um teste unitário que mocka o client do Docker e verifica quecontainers.run()é chamado com exatamente os argumentos anteriores (imagem,volumes,detach,remove) mais os três novos parâmetros de hardening — ou seja, a mudança é comprovadamente aditiva e não toca em nada que determina o que roda dentro do container (imagem, volumes, comando). Isso foi confirmado também viagit stash: o mesmo teste falha comKeyError: 'network_mode'contra o código original.Recomendação: antes do merge, rodar
test_process_commit_hello*(ou um smoke test equivalente) num runner amd64 real (CI do GitHub Actions, por exemplo) para confirmar o comportamento ponta-a-ponta com containers de linguagem reais — algo que não pude fazer com confiança nesta máquina.Arquivos
src/rcc/engine/container.py— hardening docontainers.run().src/rcc/engine/grading.py— tratamento de erro emprocess_test_results.src/rcc/tests/test_container_hardening.py— novo, cobre S2/S10.src/rcc/tests/test_grading.py— novo, cobre B6 (+ caso de controle do caminho normal).