All projects

info.bogdan.ws

A judge for competitive programming that compiles and runs strangers' C++ on my own server. Most of the project is the story of making that not a terrible idea.

  • React
  • Go
  • Monaco Editor
info.bogdan.ws — screenshot
info.bogdan.ws

What it is

Problems sorted by difficulty, a Monaco editor in the browser, and two buttons: run against your own input, or submit against hidden tests. Submissions come back with a verdict per test (AC, WA, TLE, MLE, RE, CE) along with runtime and memory for each.

Around that core there's an admin area for authoring problems and test data, a dashboard tracking progress and activity, and a time-travel debugger that steps execution forward and backward with variable state shown per line.

The one problem that matters

Everything else on the site is ordinary web development. The real question is how you run code written by someone untrusted, on a machine you care about, when that code might try to read files, open sockets, fork until the box dies, or just spin forever. I wanted to be able to sleep while people submit.

Defense in layers

The web stack is Go and Fiber over PostgreSQL and Redis, with a React 19 SPA. All of it runs in Docker except the piece that actually runs user code. That executor is a separate Go service running natively under systemd, because isolate, the sandbox tool, needs full cgroup v2 delegation and Docker's cgroup namespace refuses to share. The backend reaches it over host.docker.internal.

isolate, the sandbox from the IOI, does the heavy lifting: separate PID, network, and mount namespaces per run, cgroup ceilings on CPU, memory, and process count, and a read-only filesystem. I wasn't comfortable stopping there, so on top sits a seccomp-BPF filter of my own. It's built in Go and applied by a tiny C launcher right before exec, and it kills fork, socket, ptrace, and friends at the syscall level. Compiler flags are whitelisted, and every single test case gets a fresh sandbox.

Challenges & tradeoffs

isolate versus Docker cost me weeks. Every workaround for running it inside a container broke some other invariant, and the eventual fix was to put the executor on bare metal. Two process managers, two log streams, a slightly uglier deploy, but the isolation actually holds.

Auth is stateless access tokens plus refresh tokens stored as jti.secret: the jti gives an indexed O(1) lookup, and the secret half is bcrypt-hashed, so a stolen database can't mint sessions. And one small touch that I'm proud of: ANSI colors survive from clang all the way to the browser, so compile errors look exactly like they do in a terminal.