Skip to content

How Faber works

Faber takes a list of commands, runs them in a sandbox it builds for that one request, and answers with what each command printed and how it ended. This page follows a request through Faber in plain terms. Each section ends with links to the pages of the Under the hood section, which have the details.

  • Faber and the kernel (trusted)
  • Submitted code (untrusted)
  • Outside Faber
  • Starts or calls
  • Carries data
One request, one sandbox. The sandbox is gone before the response is sent.
  1. Your service sends the steps. A request is JSON: a list of steps, each a command with its arguments, input and files. Faber checks the API key and refuses requests over its configured limits before anything runs.
  2. Faber reserves room for it. Each instance runs a fixed number of tasks at once, and when there is no room it answers 503 straight away, with a Retry-After header saying when to try again. It never queues work.
  3. Faber builds a sandbox for this request: an empty working directory called the workspace (/faber), the compilers and interpreters installed in its image (readable, not writable), and no network.
  4. The steps run in order. A step is one command, or several that run at the same time (a parallel step). Every step sees the files the earlier ones left in the workspace.
  5. Faber answers with one result per command: its output, exit code, how much memory and CPU it used, and an outcome that says how it ended. The sandbox and everything in it is deleted first.
  • Submitted code (untrusted)
A typical judge request: compile once, then test in parallel. The workspace is shared by every step of the request and by nothing else.

The request lifecycle follows the same request through every process and check, and the architecture page shows which process does what.

Faber uses features of the Linux kernel (the core of the operating system, which every program on the machine shares) to give each sandbox a private view of the machine:

  • A task sees the workspace, a scratch /tmp and the image’s toolchains, which it can read but not change. Faber’s files, the host’s files and other requests’ workspaces are not there.
  • It has no network connection at all.
  • It sees only the processes it started itself. It cannot see or signal Faber, other tasks, or anything else on the machine.
  • It has no administrator rights, inside the sandbox or outside. On the host, each running request has a user ID of its own, so two requests cannot touch each other’s processes.
  • What it may ask of the kernel is filtered. Programs work by making system calls, requests to the kernel such as “open this file” or “start a process”. A call that could reach beyond the sandbox (loading a kernel module, mounting a filesystem, reading another process’s memory) kills the program that made it.
  • Faber and the kernel (trusted)
  • Submitted code (untrusted)
  • Outside Faber
Every box is a boundary the kernel enforces. Everything shares the one kernel at the outside, which is why Faber's isolation is that of a hardened container, not of a virtual machine.

One thing is shared by everything: the kernel. A bug in the Linux kernel that a task can reach is a way out, and the Faber container holds the rights to build sandboxes, so getting out of a sandbox means getting into the host. Give Faber a machine of its own, and a virtual machine per trust domain for anonymous public code. The isolation tier explains where that line is.

Isolation layers lists each mechanism with what it stops and what it does not, and the filesystem a task sees maps every path.

Each task runs with a budget. The operator sets the numbers; a request cannot raise them.

LimitDefaultWhen a task reaches it
Memory256 MiBIt is killed: out_of_memory
Processes and threads64It cannot start more: pids_limit
CPU speedhalf a coreIt is slowed down, not stopped
CPU time5 sIt is killed by a signal: signaled
Running time5 sIt and everything it started are killed: timed_out
Output1 MiB per streamIt is killed, and the output cut: output_limit
Whole request30 sSteps that cannot start in time: not_started

A task that hits a limit is a normal result. The HTTP status is still 200, and the task’s outcome says what happened, so a judge can turn it straight into a verdict.

Faber's demo app showing a JavaScript program stopped at its 10-second wall-clock limit. The verdict reads Timed out; the syntax check step was replayed from the step cache; wall time, CPU time, peak memory and process count are shown against their limits.
The demo app in the repository’s demo/ directory turns each outcome into a verdict. Here an endless loop has reached the wall-clock limit its operator configured.

Cgroups and resource accounting shows how the kernel enforces each budget, and outcomes and failure handling how Faber decides which outcome to report.

Judges compile the same submission again and again: once per batch of tests, once per retry. With the step cache on, Faber can skip a step it has already run. You mark the steps that always give the same result for the same input (typically the compile step) with "cache": true.

  • Faber and the kernel (trusted)
  • Submitted code (untrusted)
  • Starts or calls
  • Carries data
The cache stores a step's result together with the workspace it left behind, so later steps find the compiled program where they expect it.

An entry is found only when everything about the step is the same (command, arguments, input files), every step before it is the same, and the server runs with the same configuration and image. A failed compile is never stored, so a fixed submission is always compiled again. Several Faber instances can share one cache through Redis.

Step cache internals covers how entries are named, stored, checked and shared between machines.

You can call Faber with any HTTP client: the API is JSON, and its OpenAPI document describes every operation. The SDKs for TypeScript, Rust, Go, Python and PHP save the boilerplate: they build requests step by step, retry 503 and 429 answers after the server’s Retry-After, and give you typed results that tell a command that ran apart from one that never started.

Keep the SDK on your servers. The API key lets its holder run code and read every uploaded file, so it does not belong in a browser.

One Faber container serves a fixed number of tasks at once (10 by default) and turns the rest away with 503. To serve more, run several instances behind a load balancer, each on its own machine, and point them at one Redis so that a step cached by one is replayed by all. Scaling and deployment topology shows what is shared between instances and what is not.