On June 3, 2026, a self-propagating npm worm researchers at StepSecurity named "Phantom Gyp" spread through 57 packages and 286+ malicious versions in under two hours, hiding its payload inside a 157-byte binding.gyp file that ran arbitrary code the moment npm install touched it — before any lifecycle script, before any --ignore-scripts flag could stop it. If your CI system ever runs npm install on a tenant's behalf, that payload just executed inside your build pod, on your kernel.
That's not a hypothetical. It's this year's concrete answer to a question every multi-tenant build system has to face eventually: a build step isn't like a web request, it's a program a stranger wrote, running with enough privilege to compile native code, and your platform invited it in. So here's the real question worth answering with numbers instead of vibes — does isolating that build inside its own microVM, with its own kernel, actually make sense for a self-hosted PaaS's build layer, and at what granularity?
The short verdict, before the case for it: isolating per build is real, cheap, and already running in production elsewhere. Isolating per build step — which is how this idea usually gets pitched — is not something anyone has actually built, and the reason why is the more interesting finding here.
What a Shared Kernel Actually Leaves Open
A container is a process on the host kernel with some namespaces and cgroups drawn around it. That boundary has held up well against ordinary workloads, but a build pod isn't an ordinary workload — it's designed to execute whatever the tenant's repository tells it to, including native compilation, install scripts, and arbitrary Dockerfile RUN steps. Two real CVEs show exactly what that buys an attacker:
- CVE-2024-21626, "Leaky Vessels." A bug in
runc(versions1.0.0-rc93through1.1.11) leaked an internal file descriptor — typically fd 7 — pointing at the host's working directory before the container'spivot_rootfinished isolating it. Settingprocess.cwdto/proc/self/fd/7broke out to the host. Both Palo Alto Networks and Snyk called out the exact vector that matters here: a maliciousDockerfiletriggering the escape duringdocker build, not just at container runtime. That is precisely the shared-kernel build-pod attack surface a git-push PaaS accepts on every tenant's behalf. - CVE-2022-0847, "Dirty Pipe." A Linux kernel page-cache bug (5.8 through 5.16.10) let an unprivileged process overwrite data in read-only files. This one isn't theoretical for multi-tenant platforms — Replit published its own account of using Dirty Pipe to escape its multi-tenant code-execution containers, demonstrating in production exactly the blast radius a shared kernel creates between tenants who were never supposed to touch each other's processes.
Neither of those needs a build system at all to be dangerous — Leaky Vessels needs a container build, and Dirty Pipe needs any unprivileged process on a vulnerable kernel. A build pod that runs a tenant's own install scripts and Dockerfiles is a standing invitation for both.
What MicroVM Isolation Buys, With Real Numbers
Firecracker — the KVM-based virtual machine monitor AWS built for Lambda and open-sourced in 2018 — gives each workload a dedicated guest kernel instead of a shared one. A kernel-level escape like Leaky Vessels or Dirty Pipe simply has nowhere to go: there's no host kernel to reach from inside the guest, only a hypervisor boundary that neither CVE touches. That's the entire security case in one sentence — it doesn't patch the bug, it makes the bug irrelevant to every other tenant.
The cost side is where people usually assume microVMs lose to containers, and the numbers are better than that assumption. Firecracker's own specification puts a cold boot — from the InstanceStart API call to the guest's /sbin/init — at 125ms or less, with 5MiB or less of VMM memory overhead per microVM on a tuned 1 vCPU / 128MiB guest, and up to 150 microVM boots per second on a single host. Those are AWS's own published ceiling numbers, not marketing rounding — they're backed by the team's NSDI 2020 paper.
That's the idealized per-boot cost. The number that matters more is what a real CI system does with it in production. BuildBuddy — a remote-execution platform for Bazel builds — runs every remote build inside its own Firecracker VM, and published exactly what that bought them in a January 2026 engineering post: a repo whose fully-cached Bazel build took 6 minutes, with 3.5 of those minutes spent just initializing a fresh GitHub Actions worker, now finishes with a median CI run of 30 seconds — about a 12x improvement — and BuildBuddy's BazelCon talk on the same technique cites speedups of up to 8x more broadly. The trick isn't a faster boot. It's skipping the boot: BuildBuddy snapshots a warm VM — memory image, Bazel's in-process analysis cache, and all — and restores it from that snapshot using userfaultfd for lazy memory paging and FUSE for copy-on-write disk, resuming a build's own already-warm VM in place of cold-starting one. The isolation boundary is real and per-build; the latency cost that boundary would otherwise impose gets engineered away by not re-paying it every time.
What Per-Step Isolation Would Actually Cost
Here's where the idea usually pitched — one microVM per pipeline step, not per build — runs into the math those numbers were hiding. A modern CI pipeline for even a modest app easily runs a dozen distinct steps: install dependencies, run a linter, compile, run three test suites, build an image, push it, deploy. Dagger's own execution model, for context, compiles a pipeline into a DAG and runs it container-per-operation by design — so "one microVM per step" isn't a small tweak to that model, it's asking every node in the DAG to pay a VM boot instead of a container start.
Even at Firecracker's best-case 125ms, twelve steps means roughly 1.5 seconds of pure boot overhead added back into a pipeline whose entire point was to be fast — and that's the optimistic case, before accounting for the fact that each step typically needs a different container image and command, not a resumable copy of the last step's state. BuildBuddy's actual trick — restoring a warm snapshot instead of cold-booting — works because a build's environment stays the same shape across a retry or a similar job. It stops working the moment you're isolating a lint step, then a compile step, then a test step, each wanting different tooling: there's no reusable warm state to clone, only a fresh cold boot every single node. You'd be paying the worst part of the Firecracker cost model (repeated cold boots) while giving up the part that made BuildBuddy's numbers real (snapshot reuse) — on top of the node-density loss of packing VM-sized workloads instead of container-sized ones onto the same fleet, and the plain integration cost that raw Firecracker has no OCI or CRI shim of its own; it needs Kata Containers' VMM abstraction, or bespoke orchestration like BuildBuddy built, to talk to anything that expects a container runtime.
Why Nobody Has Actually Shipped Per-Step
This isn't a hypothetical downside — it's the reason every real attempt at combining Dagger with Firecracker has stopped at the whole engine, never the individual step. A 2023 proof of concept by Felipe Cruz ran the entire Dagger engine — BuildKit and containerd together — inside a single Firecracker microVM per tenant or pipeline, explicitly as a multi-tenancy boundary, not a per-step one. Dagger's own GitHub issue #2057 floated the same shape two years earlier: one buildkitd instance per Firecracker VM. More recent work (issue #9319, PR #9309, both from January 2025) explored running the whole Dagger Engine under Kata Containers via a Kubernetes RuntimeClass — again, the entire engine in one VM-isolated pod, not one VM per DAG node.
Every real precedent lands on the same design for the same mechanical reason: Dagger's container-per-operation model already pays a container-start cost per step. Wrapping the whole pipeline in one VM adds exactly one boot to that — the cost the numbers above show is genuinely cheap and genuinely solvable with snapshotting. Wrapping each operation in its own VM would multiply that one-time cost by the DAG's width instead, with no equivalent trick to make it disappear. Nobody built it not because it's hard to imagine, but because the people who tried it landed on the coarser boundary and stopped.
The Actual Recommendation for a Build Layer
For a self-hosted PaaS's own build layer — the thing turning a tenant's git push into a running container — the shape that survives this math is the one the real precedents already validated: one microVM per build, not per step. Concretely, that means fronting the platform's existing container-based build pods on a Cluster API-managed fleet with firecracker-containerd or a Kata RuntimeClass, so a build pod's RuntimeClassName routes it into a VM-isolated sandbox instead of a shared-kernel container — while the steps inside that one build still run as Dagger-style container operations against the VM's own kernel, isolated from every other tenant's build but not from each other.
That draws the tenant boundary exactly where Leaky Vessels and Dirty Pipe would otherwise cross it — between one tenant's npm install (Phantom Gyp payload included) and the next tenant's build, never inside a single tenant's own pipeline where the isolation buys nothing but latency. It's the boundary BuildBuddy already proved can be fast, not the one nobody's gotten to work.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a build layer that's yours to isolate however your threat model actually requires. Star the repo on GitHub or deploy your first app today.



