Skip to content

How a build is contained

A build compiles code somebody else wrote, so it is arbitrary code execution by design. The only question is what it can reach.

A build compiles a repository somebody else wrote. That is arbitrary code execution by design, and the only interesting question is what the code can reach.

The container#

The build container has never had much.

Denied to the build
network--network none

Dependencies have to be baked into the image or committed to the repository.

capabilitiesall dropped

Plus no-new-privileges.

rootunprivileged user
a writable rootfsread-only, tmpfs for /tmp and $HOME
swapmemory-with-swap pinned to memory

So the ceiling is a real ceiling.

unbounded processespid ceiling
unbounded timewall-clock timeout

The runtime underneath it#

Container flags alone are not the boundary. Builds run on a rootless container daemon owned by an unprivileged user, separate from anything that serves the platform, and that daemon uses gVisor as its runtime, so a build sits behind a userspace kernel rather than the host's syscall table.

One build runs at a time, so a build cannot crowd out another, and the resource ceiling applied to the build subsystem is the ceiling one build gets.

Git, carefully#

Two things about the clone are deliberate.

  1. A protocol allow-list on every invocation

    Not just on the clone. A submodule configuration file is attacker-controlled and is read afterwards, so restricting only the first command would leave the second one open.

  2. Symlinks disabled in the checkout

    The checkout contains no links at all. Independently, the artifact collector refuses anything that is not a regular file inside the tree, because a symlink there would otherwise be followed and published.

Two independent controls for one risk is the point: either alone would be enough, and neither is relied on.

What a malicious build could still do#

Being honest about the boundary:

Burn the timeout

Up to 30 minutes of CPU, once, in the queue. Only one build runs at a time.

Fill its own tmpfs

Which is capped and thrown away with the container.

Publish something unpleasant

At its own address, under its own account, exactly as if it had been uploaded. Nothing about the sandbox makes published code safe to run.

Read its own repository

Which is public.

It cannot reach the network, other builds, the database, the object store, the host, or any credential.

The build environment

What is inside the container, and the limits your build runs under.

Read on
CEagle documentation