Caixa de areia
Quando um concorrente envia o código, o omegaUp precisa compilá-lo e executá-lo em todos os casos de teste em suas próprias máquinas - programas C, C++, Python, Java, C#, Rust ou Karel não confiáveis de outra pessoa, executados em velocidade nativa total. A sandbox é o que torna isso seguro: é a camada que permite ao Runner executar o system("rm -rf /") de um estranho e não fazer nada.
O único fato a ser corrigido em sua cabeça antes de qualquer coisa: nada disso reside no monorepo PHP. Grep github.com/omegaup/omegaup para minijail, sandbox ou quark e você não obtém nenhum resultado. O lado PHP (\OmegaUp\Grader em frontend/server/src/Grader.php) apenas envia um POST para o avaliador por HTTP em OMEGAUP_GRADER_URL (padrão https://localhost:21680) e depois lava as mãos. O sandbox é invocado pelo Runner, um dos serviços Go em github.com/omegaup/quark, e o Runner é - para reutilizar o modelo mental da página Runner internals - basicamente um frontend bonito e distribuído para Minijail. Tudo abaixo é a metade do sandbox dessa frase, descompactada.
De onde veio o sandbox: Moeval, Martin Mareš e ptrace
O sandbox omegaUp original era uma versão fortemente modificada do Moeval — o sandbox usado no IOI, escrito por Martin Mareš. Em sua primeira encarnação, o sandbox era, em essência, um depurador: ele usava a chamada de sistema ptrace do Linux para interromper o processo do concorrente toda vez que ele tentava um syscall, inspecionar esse syscall para decidir se era inofensivo ou perigoso e, em seguida, fazer uma das três coisas.
- Permitir que o syscall prossiga normalmente - o caso comum para
read,write,mmape o restante do maquinário de perfuração que um programa precisa para realizar um trabalho útil. - Substitua o syscall por um inofensivo e então faça o processo acreditar que a chamada falhou. O truque canônico é trocar o syscall solicitado por
getuid(que é completamente inerte) e retornar um erro ao chamador. É exatamente assim que o sandbox simula a ausência de uma rede: cada chamada parasocketretorna-1, então o programa é informado, de forma plausível, de que ele simplesmente não tem rede - em vez de ser morto e vazar o fato de que estava sendo monitorado. - Matar o processo imediatamente, se o syscall for MUY evil — algo sem motivo legítimo para aparecer no programa de um concorrente.
As modificações que transformaram o Moeval no Sandbox do omegaUp — aquelas que não eram upstream — valem a pena preservar como memória institucional, porque cada uma foi um requisito real que alguém atingiu:
- Mangling do Syscall (o comportamento de substituir e falsificar um erro acima).
- Suporte multithread, para que os envios multithread possam ser rastreados corretamente.
- Um modo detalhado para examinar exatamente quais syscalls um programa fez - indispensável quando o tempo de execução de uma nova linguagem aciona o filtro e você precisa ver o que ele realmente pediu ao kernel.
- Normalização de caminho, para que o sandbox possa dizer, por exemplo, que
./também é gravável, em vez de reconhecer apenas uma única grafia canônica de um caminho. - Lendo parâmetros de um arquivo, para que você possa construir um perfil por compilador/interpretador em vez de limites de codificação na própria sandbox. Este é o ancestral da configuração atual por idioma.
- Muitas, muitas, muitas pequenas melhorias.
- Uma versão do Moeval que não usa
ptrace— genuinamente multiplataforma, mas muito mais fraca em segurança e, portanto, apenas uma alternativa.
Por que manter a história do ptrace se o mecanismo seguiu em frente?
O design de interposição ptrace é a linhagem, não o caminho ativo atual (veja abaixo). Está documentado aqui propositalmente: o truque de manipulação setrlimit-by-getuid e o comportamento "sockets return -1" são a explicação mais clara possível de o que uma sandbox syscall está realmente fazendo, e a implementação moderna resolve os mesmos problemas por meios diferentes. Se você remover isso para "ele intercepta syscalls", você excluiu o único parágrafo que informa ao novo mantenedor para que serve o sandbox para.
O que funciona hoje: omegajail em cima do minijail
O sandbox atual é omegajail, o próprio wrapper do omegaUp em torno do minijail do Google — o mesmo minijail que foi criado para processos de sandbox no Chrome OS. Em vez de executar o processo em uma única etapa com ptrace, omegajail se baseia nas próprias primitivas de isolamento do kernel: Linux namespaces (um PID privado, rede, montagem, IPC, UTS e namespace de usuário, para que o programa não possa ver outros processos, não tem interface de rede e obtém uma visão simplificada do sistema de arquivos) mais um filtro seccomp-BPF que o kernel impõe diretamente, sem nenhum rastreador no loop. É por isso que tudo é rápido o suficiente para executar milhares de envios.
O Runner nunca fala diretamente com o minijail. Ele se destina ao binário omegajail, que é empacotado com um sistema de arquivos raiz independente e enviado como seu próprio artefato - consulte Dockerfile.minijail, que é literalmente FROM scratch mais ADD bin/minijail-xenial-distrib-x86_64.tar.bz2 /. No lado Go, o contrato que todo sandbox deve satisfazer é a interface Sandbox em runner/sandbox.go — três métodos, Supported(), Compile(...) e Run(...) — e a implementação de produção é OmegajailSandbox (sandbox.go#L135).
OmegajailSandbox.Supported() é uma linha única que apenas verifica se bin/omegajail existe sob a omegajail root - cujo padrão é /var/lib/omegajail (common/context.go#L210) e é transformado em um caminho absoluto e entregue a NewOmegajailSandbox(omegajailRoot) quando o Runner é inicializado (cmd/omegaup-runner/main.go#L753). Se o omegajail não estiver instalado, Supported() retornará false e o Runner retornará à sandbox autônoma descrita no final desta página.
flowchart LR
PHP["PHP: \OmegaUp\Grader\n(frontend/server/src/Grader.php)"] -->|HTTP :21680| Grader
subgraph quark["github.com/omegaup/quark (Go)"]
Grader -->|dispatch run| Runner
Runner -->|exec bin/omegajail| omegajail
end
subgraph omegajail["omegajail (github.com/omegaup/omegajail)"]
NS["Linux namespaces\n(pid / net / mnt / ipc / uts / user)"]
Seccomp["seccomp-BPF filter\n(SIGSYS on forbidden syscall)"]
RL["rlimits: time, memory, output"]
end
omegajail --> Kernel["Linux kernel"]
Como o Runner invoca omegajail
Tanto Compile quanto Run constroem um vetor de argumento e o entregam ao auxiliar invokeOmegajail privado (sandbox.go#L413), que precede o caminho para bin/omegajail, define RUST_BACKTRACE=1 e RUST_LOG=debug no ambiente da criança (o próprio omegajail é escrito em Rust) e captura o próprio sandbox stderr em um arquivo secundário chamado <errorFile>.omegajail para que uma falha no carcereiro possa ser diferenciada de uma falha no código do competidor.
Para uma execução, os sinalizadores são montados em Run e lidos quase como um currículo do trabalho do sandbox:
--homedir <chdir> # the jailed working directory
-0 <inputFile> # stdin ← the test case's .in
-1 <outputFile> # stdout → what we'll compare against .out
-2 <errorFile> # stderr
-M <metaFile> # where omegajail writes the .meta result (see below)
-m <bytes> # hard memory limit, in bytes
-t <ms> # CPU time limit, in milliseconds
-w <ms> # extra wall-clock time on top of the CPU limit
-O <bytes> # output limit, in bytes
--root <omegajailRoot> # /var/lib/omegajail
--run <lang> # the language profile to load
--run-target <target> # the compiled artifact to execute
- O limite de memória passado para omegajail não é o limite de memória do problema. É
base.Min(ctx.Config.Runner.HardMemoryLimit, limits.MemoryLimit)(sandbox.go#L319) — o limite rígido é atualmente 640 MiB (context.go#L208, comentado na fonte como "640MB deve ser suficiente para qualquer um"). A sandbox impõe o menor dos dois, de modo que o autor do problema não pode conceder acidentalmente a um envio mais RAM do que a máquina está disposta a distribuir. - Java obtém um período de carência. Antes de calcular o limite de tempo,
Runfazif lang == "java" { timeLimit += 1000 }— um plano +1000 ms para absorver a inicialização da JVM, porque caso contrário, cada envio de Java consumiria todo o seu orçamento, fazendo o intérprete decolar. /dev/nullé trocado silenciosamente. Se o arquivo de entrada for o/dev/nullreal, o Runner substitui um arquivo vazio na raiz do omegajail (root/dev/null), porque o processo preso não deve tocar nos nós do dispositivo do host.- Pontos de montagem extras tornam-se
--bind source:target— a menos que o sandbox esteja desativado, caso em que eles se tornam links simbólicos, porque você não pode vincular a montagem sem o sandbox (essa ramificação é o tratamento doDisableSandboxingem sandbox.go#L335-L381).
Pouco antes de invocar omegajail, Run aquece o cache da página com um pré-carregador de entrada (sandbox.go#L23): ele mmaps o arquivo de entrada e percorre uma página por vez, para que o programa do concorrente gaste menos de seu precioso limite de tempo bloqueado em leituras de disco para entrada que é garantido tocar. Esta é uma otimização de latência pura – se o mmap falhar, ele volta a ler o arquivo inteiro no /dev/null e segue em frente.
Compilação usa o mesmo mecanismo com um verbo diferente: Compile (sandbox.go#L162) passa --compile <lang>, --compile-target e um --compile-source por arquivo de entrada, com o diretório de trabalho tornado gravável via --homedir-writable, e usa o orçamento compilar em vez do orçamento executar: 30 s CompileTimeLimit e um 10 MiB CompileOutputLimit (context.go#L206-L207). Duas verrugas específicas da linguagem são tratadas in-line aqui, em vez de em uma passagem de erro separada: C # precisa de um *.runtimeconfig.json com link simbólico próximo à fonte antes de compilar, e para Java, se a compilação for bem-sucedida, mas nenhum arquivo <Target>.class aparecer, o Runner reescreverá o veredicto para CE e anexa a dica legível por humanos "Certifique-se de que sua classe seja chamada X e fora todos os pacotes" — porque o erro Java mais comum é uma classe com nome incorreto ou empacotada.
O arquivo .meta: como um sinal se torna um veredicto
omegajail não retorna um veredicto. Ele executa o programa, impõe os limites e grava um pequeno arquivo .meta delimitado por dois pontos que o Runner analisa em parseMetaFile (sandbox.go#L504). Os campos reconhecidos são status (código de saída), time, time-sys, time-wall (todos em microssegundos, divididos por 1e6 em segundos na entrada), mem (bytes) e - os interessantes - signal / signal_number e syscall / syscall_number.
Traduzir esses metadados brutos em um dos veredictos do omegaUp é onde todo o modelo de segurança do sandbox surge como algo que um concorrente vê:
| O que omegajail relatou | Veredicto | Significado |
|---|---|---|
signal: SIGSYS |
RFE | Erro de função restrita — o programa fez uma syscall proibida e o seccomp a desativou. Este é o equivalente moderno do antigo caminho "kill" do ptrace. |
SIGILL, SIGABRT, SIGFPE, SIGKILL, SIGPIPE, SIGBUS, SIGSEGV |
RTE | Erro de tempo de execução – travado, abortado, dividido por zero, falha de segmentação. |
SIGALRM, SIGXCPU |
TLE | Limite de tempo excedido — o alarme de tempo de CPU ou o alarme do relógio de parede dispararam. |
SIGXFSZ |
OLE | Limite de saída excedido — o programa ultrapassou o -O. |
sem sinal, status == 0 |
OK | Concluiu de forma limpa (ainda precisa produzir a resposta certa para se tornar AC). |
| sem sinal, status diferente de zero | RTE | Saiu com um código de falha. |
Além do mapeamento do sinal, parseMetaFile aplica duas correções que uma tabela vazia ocultaria. Se a memória relatada exceder o MemoryLimit do problema, o veredicto será substituído por MLE e a memória relatada será limitada ao limite - e para Java especificamente, uma saída diferente de zero cujo stderr contém java.lang.OutOfMemoryError é também tratada como MLE (isJavaMLE, sandbox.go#L611), porque a própria JVM captura a falha de alocação e sai normalmente em vez de obter SIGKILLed, então o caminho baseado em sinal não o alcançaria. E os programas Karel (kj / kp) mapeiam o status de saída 1 (o modo de falha INSTRUCTION do intérprete) para TLE, já que um programa Karel atingindo seu limite de instrução é moralmente um tempo limite.
Limites de recursos: os números e onde eles são padrão
Os limites que o omegajail impõe são o LimitsSettings do problema, preso pelos tetos rígidos do Runner. Os padrões enviados atualmente (todos mutáveis por problema, então trate-os como padrões, não como leis):
| Limite | Padrão | Fonte |
|---|---|---|
| Tempo de CPU por caso | 1000ms | problemsettings.go#L191 |
| Memória | 256 MiB | problemsettings.go#L188 |
| Saída | 10 KiB | problemsettings.go#L189 |
| Tempo total de parede | 5 sexo | problemsettings.go#L190 |
| Tempo extra na parede | 0 | problemsettings.go#L187 |
| Teto de memória difícil | 640 MiB | context.go#L208 |
| Tempo de compilação | 30 sexo | context.go#L206 |
| Compilar saída | 10 MiB | context.go#L206 |
| Produção global | 100 MiB | context.go#L209 |
Os padrões de nível Runner (common/context.go, em torno de L186) são mais flexíveis — um limite de tempo de caso de 10 s e um limite de memória de 1 GiB — porque são o envelope externo; as configurações por problema são o que realmente alcançam o ômegajail, sempre pegando o mínimo dos dois para que nenhum problema possa ultrapassar o teto da máquina.
A versão do kernel é importante: o substituto do SIGSYS
A detecção de syscall proibido do omegajail depende de um recurso do kernel que só se tornou confiável no Linux 5.13. Para kernels mais antigos, OmegajailSandbox carrega um sinalizador AllowSigsysFallback (sandbox.go#L140-L142) que, quando definido, anexa --allow-sigsys-fallback à invocação para que omegajail volte para a implementação anterior do detector sigsys. Se você estiver executando o Runner em um host anterior à 5.13 e os envios de syscalls proibidos estiverem sendo avaliados incorretamente, esta é a opção a ser alcançada - mas o substituto existe precisamente porque é pior que o mecanismo atual, então a correção certa a longo prazo é um kernel mais novo, não o sinalizador.
Executando sem sandbox: CI, dev boxes e ambiente autônomo
Você nem sempre pode fazer sandbox. Contêineres de integração contínua, laptops de desenvolvedores e qualquer lugar onde namespaces de usuários sem privilégios estejam bloqueados simplesmente não podem executar o minijail. omegaUp lida com isso com duas saídas de emergência, e vale a pena entender a diferença:
DisableSandboxingmantém o realOmegajailSandbox— você ainda compila e executa o código do competidor e obtém veredictos honestos — mas diz ao omegajail para rodar sem a prisão (--disable-sandboxing) e troca montagens de ligação por links simbólicos, já que a montagem de ligação precisa da sandbox. Isso é para quando você deseja uma classificação real, mas o host não consegue isolar.NoopSandbox(runner/noop_sandbox.go) não faz absolutamente: ele cria arquivos de saída/erro/meta arquivos vazios e retornaVerdict: "OK", eNoopSandboxFixupResultentão reescreve toda a execução para AC com uma pontuação completa. Isso é estritamente para testar o encanamento em torno da execução (enfileiramento, envio, relatório de resultados) sem executar nada - nunca aponte para envios reais, porque isso classifica todos como perfeitos.
Documentação relacionada
- Runner Internals — como um envio chega à sandbox e o loop de compilação/execução/validação em torno dele.
- Grader Internals — a fila e o despacho que alimentam o Runner.
- Veredictos — a enumeração completa do veredicto que o mapeamento
.metaproduz (AC, PA, WA, TLE, MLE, OLE, RTE, RFE, CE, JE,…). - Segurança — onde o sandbox atua em profundidade na defesa geral do omegaUp.