AlgoJudge documentation0.1

The policy profile

What a Runner refuses to compile, why a refusal is a verdict rather than a crash, and why every rule in it is expected to be bypassable.

Before a submission is built, the Runner reads its source and checks it against a forbidden-identifier dictionary. The profile that ships is standard-io/default@1, compiled into the Runner and versioned with it.

It exists so that a problem states "the default rules" rather than restating sixty words, and so a problem written today still gets a reviewed set tomorrow.

A refusal is a verdict

A submission that matches a rule is never compiled and never run. The result is the verdict PolicyViolation with a score of zero, listing every rule that matched with the text and the line it matched on.

Three things follow, and each is deliberate:

  • It is not a crash and not an infrastructure failure. The evaluation completed; the answer is that the submission broke a rule of the activity.
  • The participant is told which rule, early, instead of finding out from a results table.
  • The submission stays rejudgeable. A manager who decides the rule should not have applied can widen the problem's configuration and rejudge.

Zero is the honest score here. A submission refused for infrastructure reasons carries no score at all, because a zero on a board reads as a wrong answer about a program that was never run.

The source has a ceiling of its own

A submission larger than 8 MiB is refused here, as a PolicyViolation verdict rather than an infrastructure failure — so it is scored zero, the participant is told, and the submission stays rejudgeable like any other refusal on this page.

The number is not a rule a manager sets. It is the outer wall on this Runner's own work, chiefly the scan below, and it is pinned to the ceiling the Server accepts an upload at. That pinning is the point: while the two disagreed, a submission the Server had accepted, stored and handed out was refused by the Runner citing a limit nobody had configured.

At most 100 violations are reported, with the remainder stated in the message. A megabyte of one denied identifier is a hundred and fifty thousand matches and seven megabytes of text, written into the result document and the log both.

What is checked, and how

The source is read as text, with comments and string literals stripped first, and matches are whole tokens — never substrings — and case-sensitive. Every match is reported rather than only the first. Only the submitted source is scanned; expanding the preprocessor and scanning that again is available per problem and is not the default, because system headers expand into everything.

C and C++

C and C++ share one rule set. <unistd.h> is <unistd.h> and fopen is fopen; the handful of C++-only names in the list are words a C program cannot contain, so they cost nothing.

Denied headers are matched as #include directives. The list covers the POSIX and Linux system headers — process control, file descriptors, sockets, memory mapping, ptrace, raw syscalls, capabilities — the Windows headers, and the C++ standard headers that reach outside the process: fstream, filesystem, thread, future, mutex, condition_variable, semaphore, execution, syncstream among them.

Denied identifiers, grouped, because <bits/stdc++.h> transitively provides much of the above whatever a submission includes:

RuleWhat it catchesDefault
Running another programsystem, popen, pclose, std::systemon
Opening a filefopen and its family, mkstemp, ifstream, ofstream, std::filesystemon
Changing the file systemremove, renameoff
Reading the environmentgetenv, setenv, putenv, environon
Loading external codedlopen, dlsym, dlcloseon
Calling the kernel directlysyscall, ptraceon
Inline assemblyasm, __asm, __asm__on
Creating threadspthread_create, thrd_create, std::thread, std::asyncon
Changing compiler optimisation#pragma GCC optimize, #pragma GCC target, #pragma ompon

"Changing the file system" is off by default, and the reason is worth carrying: remove collides with std::remove and std::remove_if, which are entirely legal and common. A rule that fires on correct standard-library use is a rule that teaches people to distrust the checker.

None of this reaches a checker or an interactor. The dictionary is read before a submission is compiled; a judging program comes from the problem author, not from a participant, and is contained by the sandbox rather than by a word list.

A list of names is explicitly never denied, recorded so nobody re-adds them from a legacy list. All of them operate on standard input, output and error, or name no file at all: printf, scanf, fprintffprintf(stderr, …) is explicitly permitted — fread, which is the fastest legal way to read input, getline, setvbuf, sync_with_stdio, signal, malloc, exit.

Python

Denied imports cover the filesystem, subprocesses, sockets and every network protocol module, ctypes and cffi, mmap, threading and multiprocessing, signal, resource, terminal control, the import machinery itself, platform, archive and compression modules, sqlite3, and the profilers.

Denied builtins: open, exec, eval, compile, __import__, breakpoint. Denied patterns: sys.modules, sys.settrace, sys.setprofile.

sys and gc stay permitted. sys.stdin and sys.setrecursionlimit are necessary, and gc.disable() is a legitimate PyPy speed idiom.

Languages this Runner does not run

The profile also carries entries for Rust and Java. They are carried, not enforced — this Runner has no toolchain for either, so nothing reaches them.

This is a policy control, not a security boundary

Every rule here is expected to be bypassable: token pasting, macro indirection, building a name at runtime with dlsym, inline assembly, raw syscalls, encoding tricks.

That is the project's own conclusion, and the consensus elsewhere — none of isolate, nsjail or sio2jail implements source filtering at all.

A submission that gets past the dictionary is still contained by the sandbox, which is what actually stops a hostile program. A document suggesting otherwise would be teaching a false sense of safety.

Two silent failures the design closes

Both were real, and both fail in the direction of reporting nothing:

  • A rule set found by the whole toolchain id alone finds nothing for cpp17-gcc, and no rules means no violations — indistinguishable from a clean submission. Rules are therefore looked up by id and then by family, and which checks run is decided by matching the family as a closed set, so a fourth family cannot be added without somebody deciding what its rules are.
  • An empty rule set for a language means the check is skipped, not that everything is denied. Worth knowing when reading a profile rather than inferring one.

Where the rule names come from

The shipped profile is crates/aj-standard-io/policy/standard-io-default.yml in AlgoJudge-Runner, and the names in the table above are the names it carries — they are what a participant reads beside their verdict, so they are prose rather than symbols.

On this page