Back to all posts
Published on · by Renaud Deraison

The service worker that will not die, and the VM that does not care

Google accidentally republished a four-year-old Chromium bug last week — a service worker that keeps running JavaScript after the browser closes, on every major Chromium browser, still unpatched. The proof-of-concept is now in the wild. The interesting question is not how it works. It is what "persistence" means on a browser whose entire underlying machine ceases to exist when you close the tab.

Persistence is not a property of code. It is a property of the place the code is allowed to live. A service worker that "never dies" has a problem when the machine it was installed on is destroyed every time you close the window.

On May 20, 2026, the issue tracker for Chromium quietly removed access restrictions on a bug that had been sitting in restricted status since 2022. Within hours, the bug — together with a working proof-of-concept — was being mirrored, archived, and written up around the web. Google moved to close the issue again. It was too late; the page was already in the Wayback Machine, and BleepingComputer's writeup went live the next day.

The bug is real, the proof-of-concept works, and four years after its initial report it is still not patched in Chrome Dev 150 or Edge 148. It affects every Chromium-derived browser: Chrome, Edge, Brave, Opera, Vivaldi, Arc.

What it lets a page do is, as a sentence, mundane: register a service worker — a small piece of JavaScript that the browser agrees to keep running in the background — that does not stop running when the page closes, when the tab closes, when the browser closes, when the machine restarts. Researcher Lyra Rebane, who originally filed the bug in December 2022, summarized the worst case to BleepingComputer: on Microsoft Edge the download menu does not even appear, so the result is "completely silent JS RCE that keeps running even after you close the browser."

The technical detail of why the bug exists matters less than the class of attack it enables. It is a persistence primitive. A page you visited once, possibly months ago, possibly an ad you do not remember seeing, is allowed to keep executing code on your computer indefinitely. Botnet enrollment, ad fraud, cryptojacking, DDoS participation, slow data exfiltration, opportunistic credential harvesting whenever you log into the same site again — all of those become trivial once you have a foothold that survives the browser quitting.

It is a good bug, in the sense that it is illuminating. It is a bad bug, in the sense that it will get used.

What a service worker actually is

The web platform has, for about a decade, allowed pages to install small JavaScript programs called service workers. They run in the background, separate from any tab, and exist so that web apps can do reasonable things — cache assets so that a site loads when you are offline, deliver push notifications, sync data when the network comes back.

A service worker is registered against an origin (say, the combination of https, the hostname example.com, and the default port). Once registered, the browser stores it on disk and brings it back the next time you visit anything on that origin — or, in the case of background features like push messages, periodically wakes it up on its own. From the user's point of view, the worker is invisible. From the operating system's point of view, the worker is part of the browser, running with whatever permissions the browser process has.

1. Page registers workernavigator.serviceWorker.register('/sw.js')2. Browser persists itwritten to the on-disk profile,keyed by origin,scoped to a URL path,survives restarts.3. Browser wakes iton revisit, on push,on background syncTHE BROWSER PROFILE ON DISKcookies · localStorage · IndexedDB · service-worker registrations— the place where "persistence" actually lives —
The normal life of a service worker. A page registers it, the browser writes it to disk against the page's origin, and from then on the browser is responsible for waking the worker back up — when the user revisits the origin, when a push arrives, when a background sync fires. The worker's address is your local disk; the worker's lifetime is the lifetime of the browser profile that hosts it.

The bug Rebane reported is, broadly, that a hostile page can abuse a background download API to register a worker that the browser then refuses to terminate. Other Chromium service-worker bugs of the same family have shipped before; this is at least the second in the last twelve months. The pattern — the page plants something, the browser stores it, the browser brings it back later on its own — is the part worth holding onto. It is also the pattern that runs into a wall the moment the browser stops owning a piece of long-lived disk.

What persistence requires

Persistence — in the security sense, not the web-platform sense — is the move an attacker makes immediately after initial access. A page or a binary lands on your machine and, before doing anything else interesting, arranges for some part of itself to survive the obvious cleanup steps: closing the tab, quitting the browser, rebooting, putting the laptop to sleep on the train home. On a desktop OS, persistence has a vocabulary of its own — launch agents on macOS, scheduled tasks on Windows, systemd user units on Linux, browser extensions, autostart entries.

A service worker is, in effect, an in-browser launch agent. It is registered, stored, and brought back to life on a schedule the browser controls. It is one of the more elegant persistence primitives the web platform offers, which is part of why it keeps attracting bugs.

Crucially, every form of persistence in that list has the same requirement, stated in three words: something must persist. A launch agent persists because there is a filesystem on which the agent's .plist file is written, and the agent is read back from that filesystem at every boot. A service worker persists because there is a browser profile on which the worker's registration is written, and the registration is read back whenever the browser starts. Take away the storage layer and the "persistence" goes with it.

That sentence — take away the storage layer and the persistence goes with it — is the part of the architecture Bromure was written around.

How Bromure runs a browser

A Bromure session is not a process. It is a virtual machine. When you open a browser window, the host launches a small disposable Linux guest on Apple Silicon's hypervisor. The browser process inside that guest is what your tabs run in. The guest has its own filesystem, its own memory, its own kernel. None of those things are the host's.

There are two pieces of that architecture that matter for the service-worker bug in particular.

First, the on-disk profile that a service worker registration would normally live in is inside the guest's filesystem, not your Mac's filesystem. When the guest goes away, that filesystem goes with it. Second, the guest itself has a lifetime that the user sets — by default, a guest is tied to a profile (work, personal, banking, an LLM-coding sandbox) and is wiped when the user chooses, but for ephemeral tabs the guest is destroyed when the window is closed. Even in profile mode, the guest is one disk-volume snapshot the user can roll back at any time.

Traditional browserYOUR USER · YOUR DISKTab visits hostile pageregisters service workerWorker written to profile~/Library/Application Support/...on your real diskYou close the tab…the worker stays on disk…Browser revives it lateron next launch, on a push, on a sync —the JS keeps running. For months.BromureDISPOSABLE GUEST VMGUEST FILESYSTEMTab visits hostile pageregisters service workerWorker written to guest diskinside the VM — not on the hostnever touches your Mac's filesystemYou close the tabVM is destroyedthe guest's disk goes with it.nothing to revive.
The same hostile service worker registered against two browsers. In a traditional browser, the registration is written into a profile on the user's host disk; closing the browser does not remove it, and the next launch brings the worker back. In Bromure, the registration is written into a guest VM's filesystem; closing the tab destroys the VM, and the registration goes with it.

In the disposable case, the "never dies" worker has nowhere to not-die from. The on-disk row it would have been re-read from at the next browser launch was inside a virtual machine that no longer exists. The JavaScript that "kept running after the browser closed" only kept running because there was something for it to keep running on. When the browser closing also closes the underlying machine, the worker stops at the same time as the browser — exactly the behavior the original spec assumed.

In the profile case — say, you keep a long-lived "social media" profile that is meant to remember you between sessions — the worker survives the way it always has, scoped to that one profile's VM. It still cannot see your Documents, your keychain, your other profiles, or the rest of your computer. And if you ever suspect that profile has picked up something it should not, you can roll the whole VM back to its last clean snapshot, which takes about as long as relaunching a browser.

The general point

It would be easy to read the above as "Bromure happens to defeat this specific service-worker bug." That is true, but it is not the interesting claim. The interesting claim is more general, and this incident is one small piece of evidence for it.

Browser zero-days will keep landing. This is at least the second Chromium service-worker bug in the last twelve months. The big ones from 2024 and 2025 were V8 type confusions and Mojo sandbox escapes, paid for at six-figure prices, used by commercial spyware vendors and state-aligned groups. There were renderer compromises before service workers existed; there will be renderer compromises long after this particular bug is patched. The economics of finding new ones — especially with Mythos-class AI auditors now in the loop on both sides of the disclosure window — are moving against the defender, not towards them.

The relevant question, when a zero-day fires, is not was the browser flawless. It was not. The relevant question is what did the exploit actually reach.

Wall = in-process sandboxRenderer (V8, Blink, codecs…)SANDBOX (Mojo IPC, seccomp)same address space genealogy as the bugBrowser process (broker)YOUR HOSTDocuments · Keychain · SSH keys · ~/.awsiCloud · webcam · microphone · local network— what a sandbox escape reaches —Wall = separate VMRenderer (V8, Blink, codecs…)Browser process (broker) — inside guestHYPERVISOR BOUNDARYseparate kernel, separate address space,no shared bytes with the bugYOUR HOSTDocuments, Keychain, SSH keys, ~/.aws —none of these are visible to the guest.— what a sandbox escape reaches: more guest —
Two ways an attacker can extend their reach after a browser bug fires. On a traditional browser, the in-process sandbox is the wall between renderer code and the host; when somebody finds a memory bug in V8 or in a parser, that wall is what cracks. On Bromure, the wall is a separate VM that does not share an address space with the browser, does not parse web bytes, and does not break when a renderer does.

A Chromium sandbox escape — the canonical "browser zero-day" you have read about every few months for the last decade — is a chain that takes a memory bug in some piece of the renderer (V8, Skia, WebP, Dawn, libxml2) and uses it to convince the browser's broker process to do something the renderer was not supposed to be able to do. Those bugs are paid for at six figures because they cross the wall the Chromium engineers spend most of their time maintaining: the in-process sandbox.

That wall is a remarkable piece of engineering. It is also, genealogically, made of the same kind of stuff as the bug that crossed it — C++, shared address spaces, IPC primitives like Mojo. When a new memory bug shows up in V8 or in one of the hundred parsers bundled with Chromium, it is the kind of thing that can, given enough work, lever the sandbox open.

A VM is a different kind of wall. The browser running inside it does not have a virtual memory address that maps to the host kernel; the host kernel does not parse the page the browser is parsing; the virtualization stack is a tiny surface compared to a browser, and what it parses (vCPU register states, paravirt hypercalls, virtio buffer descriptors) is not adversarial bytes arranged by a web page. Bugs at that boundary do exist. They are in a different league of difficulty, are several orders of magnitude rarer, and when they appear they are usually announced by the hypervisor vendor with a CVE.

What this changes for the user

In the specific case of the service-worker bug exposed last week, a Bromure user does not need to wait for a patch. The bug exists in their browser too — Bromure ships Chromium upstream — but the behaviour the bug enables is the behaviour Bromure's architecture already disables. A worker that "lives forever" lives only as long as the guest VM it was registered in, which is at most as long as the profile you keep, and at least as long as the tab is open.

In the more general case of "a browser zero-day will land sometime in the next month," a Bromure user gets a different shape of outcome. The exploit fires, in the worst case, against the contents of the guest: the browser session, the cookies for the sites currently logged in inside that profile, anything the user has typed into that browser. That is a real loss and we are not trying to hide it. What is not lost is the host: no SSH keys, no ~/.aws, no keychain, no Documents, no source tree, no LLM-agent workspace.

That trade — the browser session can be compromised, the host cannot, and the session itself is short — is the trade that keeps making sense as the rate of new browser bugs goes up.

What we are not claiming

Two caveats, stated openly:

Disposable VMs do not patch Chromium

The service-worker bug is a Chromium bug. The right people to fix it are the Chromium engineers, and they should. Bromure's architecture changes the cost of the bug not being fixed yet; it is not a substitute for fixing it. We file upstream when we find things, and we read the same advisories everyone else does.

A VM escape is a real category

A bug in the hypervisor itself — Apple's Virtualization framework, or one of the smaller surfaces around it — would, in principle, let an attacker reach the host. Those bugs exist. They are also several orders of magnitude rarer than browser memory bugs, and the surface is dramatically smaller. Bromure makes the host's exposure to a renderer compromise contingent on that class of bug, instead of on the class that ships every few weeks.

The headline you will read next month

There will be another Chromium service-worker bug. There will be another V8 type confusion. There will be another libwebp heap overflow. There will be another zero-day that an extremely well-resourced adversary has been using against extremely specific people for extremely long, that becomes public on a Tuesday morning with an emergency patch advisory.

The useful question, on the morning of any of those, is not whether you have already restarted your browser. The useful question is what was actually inside the box the bug landed in. For most users, today, that box is the same box that holds their files and their keychain and their saved logins. For a Bromure user, the box is a guest virtual machine whose worst-case loss is the window they were about to close anyway.

Persistence requires something to persist on. Make that something disposable, and a whole class of attack stops being a class of attack.