Isolation layers
No single kernel feature makes a sandbox. Faber stacks eleven of them around every task, and most of them cover a gap that another leaves: the read-only root stops writes, Landlock stops the writes a read-only mount does not cover, seccomp stops the system calls that could undo either. The sandbox page describes the result from inside a task; this page takes it apart.
The layers, outermost first
Section titled “The layers, outermost first”The numbers are the order in which they are applied. The first two are set up
once per request by the jailer; the rest are set up for every task, by its
supervisor or by the task process itself between fork and exec.
- Mount namespace and read-only root per request, by the jailer unshare(CLONE_NEWNS), pivot_root, root remounted ro,nosuid,nodev,noexec
- stops
- Seeing the image beyond its read-only toolchain paths. Creating or changing files anywhere but
/faber,/tmpand/dev/mqueue(EROFS). Setuid bits everywhere, and device files other than the five in/dev. - leaves open
- Reading the toolchains and the few files under
/etc. Tasks of one request share the workspace and/tmp.
- Network, UTS and IPC namespaces per request, by the jailer CLONE_NEWNET, CLONE_NEWUTS, CLONE_NEWIPC
- stops
- Every connection off the sandbox: the only interface is loopback. Renaming the host. Reaching the host's or other requests' System V IPC and message queues.
- leaves open
- Tasks of one request share these namespaces, so they can talk through Unix sockets and IPC.
- PID namespace per task, by the supervisor unshare(CLONE_NEWPID), an init as PID 1
- stops
- Seeing or signalling Faber, host processes and other tasks, including the parallel tasks of the same request. Reaching through
/proc/1/root. - leaves open
- The processes the task starts itself.
- Task cgroup per task, joined before anything else memory.max, memory.swap.max 0, pids.max, cpu.max
- stops
- Using more memory or processes than the limit, for the whole process tree. Swapping. More CPU per period than the quota.
- leaves open
- Disk I/O bandwidth: there is no
iocontroller (the task writes only to tmpfs, which is memory). Wall time is the supervisor's job.
- Its own /proc and /sys per task unshare(CLONE_NEWNS), proc with subset=pid, empty read-only tmpfs on /sys
- stops
- Reading host-wide kernel state:
/proc/meminfo,/proc/cmdline,/proc/sys, sysfs, the cgroup filesystem. - leaves open
- The per-process files of its own PID namespace. Tools that need host-wide files, such as
ps, do not work.
- Resource limits per task process setrlimit: CPU, FSIZE, NOFILE, STACK, CORE, MEMLOCK, MSGQUEUE, SIGPENDING, NICE, RTPRIO
- stops
- CPU time past the limit, files over 64 MiB, more than 256 descriptors, stacks over 8 MiB, core dumps, raising priority, real-time scheduling.
- leaves open
- Most are per process. Locked memory, message-queue bytes and pending signals are charged to the request's identity, so they hold for the request as a whole.
- User namespace per task, created by the request's identity CLONE_NEWUSER, uid_map and gid_map 65534 → leased ID, setgroups deny
- stops
- Being root anywhere. Inside the task is 65534; outside it is a host UID no other running request has. Per-user kernel limits are charged to that UID.
- leaves open
- Tasks of one request share the identity, and so its per-user limits.
- Capabilities per task ambient, bounding, effective, permitted, inheritable: all cleared
- stops
- Privileged operations, even those a user namespace would grant its owner: mounting,
chroot, setting the hostname, raw sockets, rewriting the ID maps. - leaves open
- What any unprivileged process may do.
- No new privileges per task PR_SET_NO_NEW_PRIVS
- stops
- Gaining privileges through
execof a setuid or file-capability binary. - leaves open
- Nothing on its own; it is what lets an unprivileged process install Landlock and seccomp.
- Landlock per task, Linux 5.19 and later landlock_restrict_self over every write right
- stops
- Creating, writing, removing, renaming, linking or truncating outside
/faber,/tmpand/dev/mqueue. Opening anything else for writing,/proc/self/memincluded. - leaves open
- Reading and executing. On kernels without Landlock ABI 2 it is skipped, and the service says so at startup.
- Seccomp per task, last before exec x32 guard, ENOSYS filter, denylist, allowlist
- stops
- Kernel interfaces a task has no use for: namespaces, mounts, modules, BPF,
ptrace, keyrings,perf. Undernativeprofiles also processes, threads and sockets. - leaves open
- Bugs in the system calls it allows.
Your command exec, with every layer above in place
- Faber and the kernel (trusted)
- Submitted code (untrusted)
The filesystem a task sees covers layers 1 and 5 path by path, cgroups and resource accounting layer 4, and seccomp profiles layer 11.
Entering the user namespace
Section titled “Entering the user namespace”Layer 7 needs two processes. A process cannot write its own UID map after dropping to an unprivileged ID, and Faber wants the namespace created by the request’s identity rather than by root, so that per-user kernel counters (pending signals, inotify instances, nested namespaces) are charged to the request. The task and its supervisor do it together over two pipes:
- Task process: setgroups([]); keep capabilities across the ID change
- Task process: setresgid, setresuid to the leased host ID
- Task process: unshare(CLONE_NEWUSER): the namespace is owned by the leased ID
- Task process to Supervisor: ready, with its PID
- Supervisor: Write deny to setgroups, then 65534 <leased ID> 1 to uid_map and gid_map
- Supervisor to Task process: mapped
- Task process: setgid(65534), setuid(65534), and check them
- Task process to Supervisor: done
- Supervisor: Read /proc/<pid>/status: all four Uid and Gid values must be the leased ID
- Task process: Clear capabilities, no new privileges, Landlock, seccomp, exec
- Faber and the kernel (trusted)
- Submitted code (untrusted)
- Starts or calls
The map has one line, so the task’s 65534 is the only ID that exists for it.
Calls such as setuid are allowed under compile_v2, but they can only keep
the ID it already has.
What tasks share
Section titled “What tasks share”- Faber and the kernel (trusted)
- Submitted code (untrusted)
Identities are leased per Faber process. Two Faber services on one kernel
hand out the same UIDs unless each has its own SANDBOX_IDENTITY_BASE range.
What no layer stops
Section titled “What no layer stops”- Kernel bugs. Every layer is enforced by the kernel the task talks to. A bug in a system call the profile allows can bypass all of them, and the Faber container is privileged, so that is a compromise of the host. See the isolation tier.
- Shared hardware. CPU caches, memory bandwidth and speculative execution are shared with everything else on the host.
- Observing the cache. All API key holders share one step cache; a hit tells a request that someone ran the same step before.
- Memory deduplication. If the host runs KSM, tasks can mark memory mergeable and time the merges. Keep KSM off on Faber hosts.
Where this lives in the code
Section titled “Where this lives in the code”crates/faber-runtime/src/container/core.rs: layers 1 and 2, and the masking for layer 5.crates/faber-runtime/src/runtime/core.rs:supervise_task(layer 3),execute_single_taskandchild_setup_security(the order of everything else),enter_user_namespaceandconfigure_child_user_namespace(layer 7), capabilities andno_new_privs.crates/faber-runtime/src/cgroup/task.rs: layer 4.crates/faber-runtime/src/runtime/rlimits.rs: layer 6.crates/faber-runtime/src/runtime/identity.rs: the leased identities.crates/faber-runtime/src/runtime/landlock.rs: layer 10.crates/faber-runtime/src/runtime/seccomp.rs: layer 11.