Brutalist build environments and sandboxing
August 6, 2025
broot
is a small utility I wrote specialized to build sandboxing on linux.
A certain amount can be done with containers already, but I wanted something simpler and more specialized, with the aim of being able to check out a codebase, drop network access (because blindly pulling dependencies from the internet is dumb), and then build anything within that codebase (most of my personal code is in a monorepo).
Lets run it to see what options it presents.
$ broot
usage: broot: brutalist build sandbox
Required flags
--buildroot - path to folder containing the root filesystem
--src - path to folder containing source code to mount
--out - path to folder for writing output
--src-bind - path inside the build environment to bind source directory
--out-bind - path inside the build environment to bind output directory
-- COMMAND - the command to run inside the sandbox
Other flags
--tmpfs-bind - path inside the build environment to bind a tmpfs
--env - specify an environment variable with form VAR=VALUE
--help - display this message
Subcommands
check - check the binary has the needed capabilities to execute
help - display this message
The intention is that this be used inside scripts / Makefiles, and not necessarily be driven by a human. The options are minimal and specialized specifically to build sandboxing.
- A folder (the root filesystem used for the build) is bound as root, and made read-only.
- The input source directory is mounted readonly at a provided path.
- The output directory is mounted at a provided path.
- A temporary filesystem can be optionally bound (our root filesystem is readonly, and, for example,
gcc
need some scratch space). - No network access is provided (there is a loopback interface, but nothing else).
If binfmt
support with the appropriate architecture emulation is installed, the root filesystem can be for a different architecture, and everything still works (I’m hoping to use this to help test my compiler).
The intention here is that this be driven by a higher level script/tool.
broot
itself needs CAP_SYS_ADMIN
to do the syscalls (which it drops before running the internal command), so keeping its surface minimal is generally a good idea.
It’s around 600 lines of code, the bulk of which is command line parsing and alternatives for libc
procedures, so can be easily audited, and linked statically to make it easy to distribute.
An example workflow
My first test case has been compiling a small game project.
The root filesystem was constructed using debootstrap
, and chroot
-ing in to install dependencies, much like here.
This filesystem was then tarballed and gzipped, and put in the games data folder - with binary blob support in your version control system (e.g. git-lfs, which I’m no lover of), this can be checked in.
The build script checks for an extracted filesystem, and if it doesn’t exist, extracts the tarball, before calling itself (with slightly different parameters) via broot
.
Subsequent builds only incur a small overhead (order of milliseconds) from running via broot
.
This means a build workflow of a new project can just be git clone
followed by running the build script - no other access required!
This isn’t entirely robust, since changes to the buildroot tarball won’t be detected and update the unpacked filesystem, but it’s not difficult to imagine how this can be fixed, and the overall process improved.
Closing thoughts
This approach is very minimal, but rather than requiring specialized image formats, can just make use of a directory on the filesystem. This makes iteration on the filesystem easy. It also lets you manage these filesystems in any way you choose. Archiving tarballs either within the version control system, or on a simple NFS drive / server somewhere is a tried-and-true way of managing things, and likely to survive for some years. One can avoid the largely unnecesary churn of container image formats, needing to run container registries, or depending on external images of unknown life expectancy.
Ultimately, it’s still convenient to have all the dependencies you need installed on your system for day-to-day work, even though you can perfectly well launch an iteractive shell using broot
.
Still, having these build environments makes it easier to get up and running quickly, work cross architecure, and of course run builds and tests and be certain all dependencies are captured.
Source code
An initial snapshot of the sourcecode can be found here.