# voj judges code on Linux and Windows without a Linux-only runtime

> Verwandlung Online Judge is a self-hostable competitive programming judge built on Spring Boot with the web app and the sandboxed judgers decoupled through an ActiveMQ queue. Its distinguishing claim is native sandboxing on both Linux and Windows via JNI, with a shared MySQL database and streamed results over Server-Sent Events.

**hzxie/voj** — A cross-platform online judge system based on Spring framework and ActiveMQ.

- Repository: https://github.com/hzxie/voj
- Website: https://voj.infinitescript.com
- Stars: 615 · Forks: 118
- Language: Java
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/hzxie-voj

## A judge you can host on a Windows box, not just Linux

Verwandlung Online Judge is a self-hostable, open-source online judge for competitive programming. It presents algorithmic problems, accepts submissions in multiple languages, then compiles, runs and evaluates them against predefined test cases, reporting correctness, run time and memory usage.

The feature that separates it from most self-hosted judges is described first in the highlights: cross-platform native judging, where submissions are judged natively on Linux and Windows through JNI rather than relying on a Linux-only runtime.

That matters because the usual answer to running submissions safely is a Linux container, which rules out hosting the judger on a Windows machine. This project takes the other route, writing a native sandbox for each platform and calling it through JNI from the Java judger.

The rest of the stack is conventional and well chosen. It is built on Spring Boot with the MyBatis persistence framework, and the requirements table asks for JDK 17 or newer with testing through JDK 25, MySQL 8.0 or newer or MariaDB 10.4 or newer, and ActiveMQ 6.0 or newer speaking Jakarta JMS. Contests and internationalization with English and Chinese are included out of the box.

The audience is an institution or community that wants to run its own judge. There is a public demo at verwandlung.org and a project page, and the project ships a Docker path so you can see the whole thing running before deciding anything.

## The web app and the judgers never talk directly

The architecture has two components, the web application and one or more judgers, communicating over ActiveMQ.

That decoupling is what makes horizontal scaling work. The documentation says the web app and the judgers are decoupled through an ActiveMQ queue, so you can run as many judgers in parallel as you need. A judger is a worker: it pulls a submission from the queue, sandboxes it, evaluates it, and puts the result back. Add another judger and throughput scales, provided the database and the queue keep up.

The other half of the decoupling is how results come back to the user. Feedback is real-time and streamed to the browser with Server-Sent Events, so results appear progressively as each test case finishes rather than appearing all at once when the whole run ends. For a judge that runs a dozen test cases against a submission, that changes the feel of the product entirely, since a slow test case no longer leaves the user staring at a spinner.

The repository layout reflects the split: a web/ directory for the Spring Boot application, a judger/ directory for the worker with its native library, plus docker/, docs/, scripts/, sql/ and a root pom.xml. The docker-compose file describes the same two-container topology as the plain docker run script, and the documentation is explicit that the two routes are equivalent, so you can pick whichever fits your deployment habits.

Persistence is handled by a named volume called voj-db, and the compose file is explicit that problems, users, submissions and problem test data all live in MariaDB, so pulling a newer image and recreating the containers does not lose data. On the very first start Docker seeds the empty volume from the image's bundled database.

## The Linux sandbox is seccomp, namespaces and dropped privileges

This is the part of the project that deserves a careful read, because it is where a judge either earns trust or loses it.

On Linux, natively, each submission runs in a forked child with setrlimit applying CPU time, output size and process-count caps. CPU time and peak memory are accounted for accurately using wait4 and getrusage. Each run gets a private network namespace, so a submission cannot reach the network at all, and a seccomp syscall filter blocks calls such as ptrace, mount and module loading. Privileges are then dropped to an unprivileged user, configured by system.username and defaulting to nobody.

That last point has an explicit failure mode. If the judger runs as root and cannot drop privileges, the submission is refused rather than run as root. The documentation is saying that refusing to judge is the correct outcome, not a bug to work around.

There is an alternative path. Setting judger.sandbox = isolate delegates to the external isolate tool instead, which is the standard sandbox used for competitive programming and confines each submission to its own mount namespace.

The honest limitation is stated as well. The native sandbox does not provide full filesystem isolation. Reference answers and configuration are protected by file permissions, with the work directory handed to the sandbox user and the checkpoint directory staying readable only by the judger, but a submission can still read world-readable host files. For complete filesystem isolation, the documentation says to run the judger as root with judger.sandbox = isolate.

## Windows gets a Job Object and a low-privilege account

The Windows path is the unusual one, and it works the same way conceptually.

Each submission runs under a separate low-privilege Windows account, launched with CreateProcessWithLogonW. The process is placed inside a Job Object, which is the Windows mechanism for constraining a process tree. The Job Object caps the process count, which is the anti fork-bomb measure, enforces memory and wall-clock limits, applies UI restrictions so the submission has no access to the interactive desktop or the clipboard, and kills the whole process tree when the run ends.

The UI restriction is worth pausing on. It means a submission cannot pop a window on the desktop of a machine that might be in use, and cannot read the clipboard, which on a shared or hosted Windows box is a genuine data leak vector. Killing the process tree matters too, since a submission that spawns a child which outlives the run would otherwise keep consuming the machine.

Judger OS is listed in the requirements table as Linux or Windows, and the JNI build needs a C++ toolchain. On Linux the build prerequisites are g++, make and libseccomp. The documentation also notes a JDK layout quirk: on some installations the JNI build needs jni_md.h and jawt_md.h copied from $JAVA_HOME/include/linux, or the equivalent win32 directory, up into $JAVA_HOME/include.

That is a real installation detail that will cost you an afternoon if you hit it, so it is worth knowing before you start.

## Running the stack with Docker in two equivalent ways

Docker is the recommended route, and the documentation gives two options that produce the same result:

```bash
git clone https://github.com/hzxie/voj.git
cd voj

# A) Docker Compose
docker compose up -d

# B) Plain `docker run`, no compose
scripts/run-docker.sh
```

Both bring up the voj.web and voj.judger containers and persist the database in a named volume, so the web UI is available at http://localhost:8080/voj and the data survives image updates. The script also generates strong random secrets and prints them once.

If you would rather run the prebuilt images from Docker Hub than build locally, the script supports that with the VOJ_PULL environment variable set to 1 in front of scripts/run-docker.sh, and docker compose up pulls by default.

Configuration is read from VOJ_* environment variables at runtime, so you can reconfigure the deployment or put it behind a reverse proxy without rebuilding the image. The compose file shows the shape of that: VOJ_HTTP_PORT with a default of 8080, VOJ_BASE_URL defaulting to http://localhost:8080/voj, VOJ_CONTEXT_PATH defaulting to /voj, and VOJ_JUDGER_API_TOKEN, which the compose file describes as the shared secret the judger presents to download test data and which must match on both sides.

The .env.example file is where you set those values. It also carries the SMTP block, with an empty host disabling email, so password-reset mails are off by default in a local trial.

## Building the two jars from source

The web application produces a self-contained executable JAR with an embedded Tomcat, which is a pleasant thing to have when you are deploying to a plain JVM host:

```bash
cd web
mvn package -DskipTests        # -> web/target/voj.web.jar
java -jar target/voj.web.jar
```

The judger is different. It builds a Spring Boot jar plus the JNI native library, so a C++ toolchain is required:

```bash
# Linux build prerequisites: g++, make and libseccomp
sudo apt-get install -y g++ make libseccomp-dev

cd judger
mvn package -DskipTests        # -> judger/target/voj.judger.jar
```

The convenience wrappers live in scripts/: build-jars.sh, build-docker.sh, run-web.sh and run-judger.sh. The run scripts are worth noting because they preflight MySQL and ActiveMQ and import sql/schema.sql, sql/seed.sql and sql/demo.sql on first launch, which is the step that turns an empty database into a working judge with sample problems.

Both components read a voj.properties file covering database, mail, message queue and sandbox settings, and the full property reference is in docs/configuration.md. The docker/README.md file holds the equivalent for the containerised deployment.

The top-level pom.xml is the parent POM for the multi-module build, and CI is configured on both CircleCI and AppVeyor, with a codecov badge for coverage. The Docker Hub images are published under the zjhzxhz/voj.web name.

## Multi-tenant hosts need their own unprivileged account

The security section ends with a piece of advice that is easy to skim and expensive to ignore. For shared, multi-tenant hosts, the documentation says to create a dedicated unprivileged account for system.username instead of reusing nobody.

The reasoning is straightforward. nobody is a shared account. If a submission can escape its working directory and read world-readable host files, and every sandboxed run on that machine executes as nobody, then anything one submission manages to write somewhere reachable becomes a resource for the next submission. A dedicated account per deployment, and ideally per tenant, means one escape does not become a foothold for everything else.

The full deployment security guidance lives in a separate document rather than in the readme.

Combining the pieces gives a defensible deployment shape. Set a long random VOJ_JUDGER_API_TOKEN so a judger cannot pull test data without the shared secret. Give the judger its own unprivileged account rather than nobody. And if the host is shared and the filesystem matters, run the judger as root with judger.sandbox = isolate so each submission gets its own mount namespace instead of relying on file permissions alone.

On upkeep, the project is young but moving. Releases are 1.0.1 from 2026-06-29, 1.0.0 from 2026-06-28 and 0.9.0 from 2026-06-25, and the last push was on 2026-10-13. The licence is GPL-3.0, which matters if you intend to modify and deploy it, and it is a 1.0.x line, so you should expect some API churn in the properties and environment variables while the layout settles.

## Conclusion

voj suits a school, a competitive programming club or a company that wants to run its own judge on hardware it controls, including Windows machines, and that wants results streamed to the browser as each test case finishes. It is the wrong choice if you need filesystem isolation out of the box, since the native sandbox leaves world-readable host files reachable and the documentation says to use the external isolate tool for that, and the wrong choice if you want a managed service. Before deploying anything public, set VOJ_JUDGER_API_TOKEN to a long random value, create a dedicated unprivileged account for system.username instead of reusing nobody, and decide whether to run the judger as root with judger.sandbox = isolate.

## FAQ

### How do I run the Verwandlung Online Judge with Docker?

Clone the repository, then either run docker compose up -d or scripts/run-docker.sh for a plain docker run setup. Both start the voj.web and voj.judger containers and keep the database in a named volume, with the web UI at http://localhost:8080/voj. Use VOJ_PULL=1 scripts/run-docker.sh to run the prebuilt images.

### How are untrusted submissions sandboxed on Linux and on Windows?

On Linux each submission runs in a forked child with setrlimit caps, a private network namespace, a seccomp syscall filter and privileges dropped to system.username, defaulting to nobody, and it is refused rather than run as root if privileges cannot be dropped. On Windows it runs under a separate low-privilege account inside a Job Object that caps the process count, enforces memory and wall-clock limits and kills the process tree at the end.

### Does voj provide full filesystem isolation for submissions?

Not with the native sandbox. The documentation states the native sandbox does not provide full filesystem isolation, so a submission can still read world-readable host files, with reference answers protected by file permissions. For complete isolation the documentation says to run the judger as root with judger.sandbox = isolate, which confines each submission to its own mount namespace.

## Sources

- [hzxie/voj on GitHub](https://github.com/hzxie/voj)
- [License: GPL-3.0](https://github.com/hzxie/voj/blob/master/LICENSE)
- [Project website](https://voj.infinitescript.com)
- [README](https://github.com/hzxie/voj/blob/master/README.md)
- [Releases](https://github.com/hzxie/voj/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hzxie-voj
