DEV Community

SwiftQuest
SwiftQuest

Posted on

Running strangers' Swift code: one gVisor container per run

I'm building SwiftQuest, a course where you learn Swift by writing it and running it in the browser. Not a simulator, not a toy interpreter: the real Swift 6.4 compiler, on a Linux server, with its real error messages.

That means strangers type code into a box and I compile and run it on my machine. Here's how that works, and what it costs.

The rule: the API never runs learner code

The API server that handles accounts and lessons refuses to compile anything in hosted mode. Every run goes to a separate runner server (an arm64 Linux box) that has one job: take a run, execute it in a sandbox, send back the result.

The API doesn't send commands. It sends a signed job with the run's inputs only: the files, the mode (script, multi-file compile, or a Swift package with tests), the test fixtures. The job is signed with Ed25519 over the exact bytes, with an id, an expiry, and a digest of the plan the API expects. The runner re-plans the command line itself from those inputs and refuses anything that is:

  • unsigned, expired, or replayed,
  • planned differently from what the API signed,
  • outside a small allowlist of run plans.

So even a compromised API can't ask the runner to run curl or rm -rf. It can only ask "compile these files in one of these known ways".

The container

Each job gets its own throwaway container under gVisor (runsc), Google's user-space kernel. The flags are boring on purpose:

  • --network=none
  • read-only root filesystem
  • --cap-drop=ALL and no-new-privileges
  • runs as uid 1000, not root
  • caps on memory, CPU, process count, open files and core dumps
  • two small size-capped tmpfs mounts for the workspace and temp files
  • no host mounts, and only HOME and TMPDIR in the environment

When the program ends, the container is gone. The next run starts from a clean image with the pinned toolchain.

Testing the sandbox like an attacker

I didn't want to trust my own config, so there's a hostile attack suite: programs written to escape, read secrets, reach the network, or leave things behind for the next run. It runs against the real sandbox on the real runner, not against mocks.

On the current setup it's 18 out of 18: seven real lessons that must still work, and eleven attacks that must all fail. After each attack, a plain run has to pass and no job container may be left behind. A small program that reports facts about itself shows uid 1000, gVisor's kernel, a fresh workspace and no secrets in its environment.

What it costs

gVisor isn't free. Measured on the runner, runc vs gVisor:

  • a script: 0.18 s → 0.46 s
  • a compile: 0.24 s → 0.52 s
  • a cold Swift package build: about 10 s → 22 s

End to end, a learner sees about 1.3 s for a script and 1.5 s for a two-file compile. The first package build is around 20 s. That's fast enough that people hit Run without thinking about it, which matters more for learning than any feature.

Two things that turned out to matter for beginners

Show the compiler's words, not mine. Every error is shown word for word, and a plain-language explanation goes underneath, never instead. One gotcha: Swift's default diagnostic style draws notes inside an ASCII picture of the code, where a line-based parser can't find them, so notes and fix-its were silently dropped. Compiling with -diagnostic-style=llvm prints them as ordinary file:line:col: note: lines.

Test the lesson text like code. Lessons say things like "try changing this to let and see what happens". Each of those suggestions is now a test run against the pinned toolchain (289 of them). The first time I checked, 18 of 90 suggestions were simply wrong. Every starter, solution, demo and hint in the course also runs through the real sandbox: 1,789 checks, about 87 minutes.

Try it

SwiftQuest's beta is open now. All 15 islands and 40 quests are free for testers, there's no deadline, and if you work through it and answer a five-minute survey you keep Pro for life. I'd especially love people who have never written code, plus a few Swift devs to catch anything I teach wrong.

Apply here: https://swiftquest.dev/beta?ref=devto

Happy to answer anything about the sandbox in the comments.

Top comments (0)