← Back to all posts
Published on · by Renaud Deraison

The free space was not empty

On September 24 Cloudflare and Oren Yomtov of Accomplish published a cross-tenant flaw in Cloudflare Containers and Sandboxes. Write 4 KiB into a fresh sandbox, read the raw disk, and the other 60 KiB of the block came back holding the previous tenant's .env files, credential files and Chromium profiles. Nothing escaped anything: every live boundary held, and the leak was in what happened to a block after its owner was done with it. Cloudflare sells Sandboxes to run code generated by language models. Bromure Agentic Coding runs that code from an image file on your own Mac, and the free blocks it draws from are yours.

A sandbox has two jobs. Contain what runs inside it, and forget what ran inside it before. The second one is a configuration option.

You send the agent's test suite to a cloud sandbox, because running unreviewed code on your laptop stopped feeling clever some time last year. The sandbox comes up in about a second with a full Linux filesystem. The agent writes a small file. Underneath, the platform hands it a 64 kilobyte block of disk, and 60 of those kilobytes still hold somebody else's .env.

You did not have to break out of anything to get there.

Write four kilobytes, read sixty

On September 24 Cloudflare published a joint write-up with the researchers of a cross-tenant flaw in Cloudflare Containers. Oren Yomtov of Accomplish reported it on September 4 through HackerOne and published his own account the same week. The Hacker News covered it the next day.

Containers give each customer a disk out of a shared pool, using Linux device-mapper thin provisioning, with a block size of 64 KiB. The pool carried the skip_block_zeroing option, which does what it says: dm-thin did not clear a newly allocated block before handing it to a container. When a container was deleted its blocks went back to the pool, contents intact, and the pool spanned customer accounts.

From there the technique is four steps and no exploit:

  1. Find 64 KiB-aligned regions that the container's own ext4 filesystem thinks are free.
  2. Write a single 4 KiB block into each one. That is enough to make dm-thin allocate a physical 64 KiB block from the shared pool.
  3. Read the raw device rather than the filesystem.
  4. Keep the other 60 KiB.

Cloudflare's own sentence for the primitive is the whole bug: "a full-block write replaced its previous contents, but a smaller write changed only the written portion."

The recovered blocks held usable structure. Cloudflare reports "directory structures, database pages, and structurally complete SQLite databases", and counts 2,700 distinct foreign directory inodes across six production placements, with residual material on 18 of 24 placements. The Hacker News reports data recovered on 20 of 22 underlying machines across four continents. Yomtov lists what he read off those blocks: "directory listings, SQLite databases, Chromium profiles, .env files, and credential files."

Cloudflare states two limits that bound the incident. An attacker "could not select a particular victim or access an actively attached disk", so you got whoever had been on that machine rather than a target you chose. And Cloudflare built detection signatures from the researchers' proof of concept, ran them against historical disk-activity telemetry, and found "no evidence that this specific attack vector was exploited by anyone else."

Cloudflare moved fast. Reported 15:26 UTC on September 4; production misconfiguration confirmed at 18:45; runtime fix merged at 21:27; pool configuration merged at 22:03; rollout started at 23:15. All of that inside one day. The rollout finished on September 7, the researchers confirmed their proof of concept was dead on September 14, and Cloudflare cleared the last pre-mitigation disk snapshots on September 19. Customers had no action to take, and no way to check from inside a sandbox whether their own blocks had arrived clean.

One pool of 64 KiB blocks, two customers, no zeroingContainer, customer A.envcredentialssqlite pages, chromium profiledeleted — blocks returned to the poolSandbox, customer Bwrite 4 KiB into free spaceread /dev/vdc, not the fskeeps the other 60 KiBa Workers Paid account, nothing moreShared dm-thin pool — 64 KiB blocks, spanning customer accountsskip_block_zeroing — a released block is reallocated with its contents intactThe block customer B was handed4 KiB written60 KiB of whoever was here beforethe filesystem calls this free space; the raw device disagrees
A 4 KiB write, a 64 KiB block. Cloudflare Containers allocate disk from a shared device-mapper thin pool with a 64 KiB block size, and the pool was configured with skip_block_zeroing, so a block released by one customer went back to the pool with its contents intact. Writing 4 KiB into a region the container's own ext4 filesystem believed was free triggered allocation of one of those blocks; reading the raw device instead of the filesystem returned the other 60 KiB. The researchers counted 2,700 distinct foreign directory inodes across six production placements, and what they read included .env files, credential files, SQLite databases and Chromium profiles.

Every live boundary held

The title on Yomtov's write-up, "Escaping the Cloudflare Sandbox", undersells how quiet the bug is.

He found no memory-safety bug, no privilege escalation and no container breakout. Every boundary around a running container held as designed for the duration of the incident. Customer B could not read customer A's live container, and Cloudflare notes that a sandbox could not reach an actively attached disk either.

The leak sat in the handover. Cloudflare released a block from one tenant and issued it to the next, and the thing standing between those two customers was a boolean in a pool configuration.

Researchers spend their attention on what the code inside a sandbox can reach right now, and much less on what the last occupant left behind. That second question has a boring answer most of the time: the platform zeroes the block. The answer holds until an engineer turns the zeroing off for throughput.

Accomplish counts this as its sixth published sandbox escape since July 2026. The previous five: SharedRoot in Claude Cowork, Beltdown in Claude Code, Beltdown2 in Cursor's CLI, one in Docker's VMM, and one in OpenAI's Codex sandbox. Five of those six run code an agent wrote. Yomtov's own conclusion runs one line: "isolation can fail in many ways."

This is where agent code was supposed to go

Cloudflare Sandboxes run on Containers, so they inherited the bug. Sandboxes are also the product Cloudflare sells for the thing this blog keeps writing about. From Cloudflare's own documentation, the SDK exists to "run untrusted code securely in isolated environments", it is for "Execute code generated by Large Language Models safely and reliably", and it suits "AI agents, code assistants, and autonomous systems that need to run untrusted code." Each sandbox runs "in its own isolated container with a full Linux environment, providing strong security boundaries while maintaining performance."

Every clause of that is a reasonable thing to want, and it describes the Bromure workspace VM about as well as it describes a Cloudflare Sandbox. The difference is who else is on the machine.

Moving agent execution off your laptop relocates a trust decision rather than removing it. You now trust a pool allocator you cannot see, in a placement you did not choose, beside tenants nobody will name for you. That can be the right trade. Make it on purpose, and ask the residue question first: where do my free blocks come from, and what was in them.

The disk is a file on your Mac

Bromure Agentic Coding answers that question by not having a pool.

Each workspace maps to exactly one VM. Two workspaces never share a VM, and one workspace never has two. The VM boots from two virtual disks that are ordinary files in that workspace's own directory on your Mac. disk.img is the system disk, created on first launch as an APFS copy-on-write clone of the local Ubuntu base image: instant, and free until the guest writes. home.img is a private sparse ext4 image holding /home/ubuntu, attached as a second virtio-blk device.

The four-step technique has nowhere to land here. An agent in the guest can read the raw block device instead of the filesystem, since it is root in there, and that is fine. It reaches one image file belonging to one workspace. The blocks that file grows into come from your Mac's own APFS volume, and the previous occupant of a free block on your Mac was you.

One object is shared between workspaces, and "nothing is shared" would be the wrong claim, so name it: every workspace disk is cloned from base.img. Ubuntu 24.04, 24 GB logical and 6 to 8 GB physical, fetched against a signed image catalog carrying an ed25519 signature made with the same key that signs app updates, covering the image identity, its SHA-256 and every postinstall command, with rollback protection and refusal of unsigned catalogs. It is byte-identical on every install and contains nothing of yours. That is the right kind of thing to share: a public artifact whose job is to be identical everywhere.

A multi-tenant placementcustomer Acustomer B…and the restone pool of 64 KiB blocksa released block is reissued to whoever is nextclean on arrival: a setting, not a guaranteewhat was in the blocks.env, credential filessqlite databases, chromium profiles2,700 foreign directory inodes across six placements;residue on 18 of 24. No customer action was possible.Bromure Agentic Coding, on your Macbase.imgUbuntu 24.04, signed catalog, identical everywhere, yours in none of itworkspace onedisk.img — CoW clonehome.img — private ext4workspace twodisk.img — CoW clonehome.img — private ext4what is in those blocksghp_… and sk-ant-api03-brm-… — placeholdersFree blocks come from your volume. The real secrets sit inthe host proxy and are swapped in on the way out.
Two answers to the same question. On a multi-tenant placement, a sandbox's disk is a lease on blocks from a pool shared with customers you are not told about, and whether those blocks arrive clean is a property of the platform's configuration. In Bromure Agentic Coding the workspace's system disk is an APFS copy-on-write clone of a signed, byte-identical public base image, its home is a private sparse ext4 image, both are files in that workspace's own directory, and the free blocks they grow into come from your own volume. The second difference is what gets written into those blocks: the recovered artifacts in the Cloudflare case were .env and credential files, and inside a Bromure guest those hold structure-preserving fakes while the real values stay in the host proxy on the Mac.

And the residue would be worthless

The block layout is half the answer. The other half is what a coding agent writes to disk, which is what made this bug worth reporting: .env files, credential files, Chromium profiles.

Inside a Bromure workspace those files exist, and the tools that read them work as usual, but the values in them are fake. No file, environment variable or process inside the VM ever holds a real API key, OAuth token, AWS secret or SSH private key. The guest gets structure-preserving placeholders instead: sk-ant-api03-brm-…, a ghp_-shaped GitHub token, brm_…, deterministic per install, so client-side validators are satisfied and no tool ever thinks a key rotated between sessions. The real values live in the host proxy's in-memory swap map on the Mac, each keyed to the destination host it belongs to, and get substituted on the way out with scoping that is exact-or-subdomain, never substring. AWS requests are re-signed host-side: the guest signs with a fake secret, the host strips that signature and re-signs. SSH is signature-only over a vsock bridge, and private key bytes never cross into the guest at all.

Bromure handles the Chromium profile separately. The browser pane the agent drives runs in its own ephemeral Alpine VM, a copy-on-write clone in the temporary directory that the app deletes when you tear the pane down. Keeping logins is opt-in, and it puts the profile on an encrypted per-workspace disk.

If some future flaw exposed a Bromure guest's blocks, the reader would come away with a directory tree, source code you already have, and a credential file that cannot be spent anywhere.

Disposability as a verb

Workspaces are persistent on purpose. Coding needs durable state: cloned repos, package caches, toolchains, shell history. So Bromure controls what the VM is allowed to hold rather than destroying it nightly, and erasing a disk is something you ask for. The app is loud about each way of asking:

Reset to base

Deletes disk.img so the next launch re-clones a fresh copy of the base image. The dialog names what goes: everything you apt installed, and your edits to /etc, /var, /usr, /opt. Home survives.

Erase home, delete workspace

Erase home drops /home/ubuntu and the generated keypair. Deleting the workspace removes the whole profile directory, both images, the encrypted secrets file, every checkpoint and the host-side SSH material, then releases its reserved MAC address.

Throwaways and checkpoints

bromure-cli vm run --rm gives you a docker-style workspace deleted when the VM stops. Every successful boot leaves a copy-on-write checkpoint of disk and home, taken only after a boot that produced a working terminal, so each one is proven bootable.

The forced wipe

If the proxy catches a placeholder credential heading somewhere it does not belong, Bromure marks the workspace compromised, and it will not boot again until you wipe its disk and home. You keep your settings, tokens and SSH keys, and the wipe never touches shared host folders.

That set of verbs is the difference between erasure you asked for and erasure you assumed.

Ask what the last tenant left

Cloudflare found the misconfiguration, fixed it in nine hours, cleared the pools and the snapshots, built detection signatures to check the past, and published the block size and the flag name. Read that as a vendor handling a bad day well.

"Sandbox" names a hope about a boundary, and this boundary turned out to be a resource lifecycle rather than a wall. The industry's answer to agents running unreviewed code has been to move the execution somewhere else and trust that the somewhere else arrives clean. It does, most of the time. You cannot check it from inside the sandbox, this week or any week.

Run the code on infrastructure you can see and you get to answer the residue question yourself: one image file per workspace, free blocks off your own volume, one shared object and it is a signed public base image, and nothing on that disk worth reading if it ever did leak. Erasing it is a menu item.