Manifest V3: How Google is killing ad-blocking on Chromium

Manifest V3: How Google is killing ad-blocking on Chromium

Chromium has long been the most popular browser. Not only is it what powers Google Chrome, by far the most widespread web browser, but also most others: if you use Edge, Brave, Opera, Vivaldi, Samsung Browser and several others, you are indeed using Chromium.

Today, Chromium-based browsers make up most of the market share. But how has it become so popular?

Some browser history

Back when Google released Google Chrome in 2008, everyone was using Firefox. It was Mozilla's open-source successor to Netscape, the browser that had not much long prior saved the web from the infamous monopoly of Microsoft's Internet Explorer 6, thus gaining a lot of market share for bringing a long-awaited breath of fresh air.

At first, as one would expect, there was nothing majorly wrong with the browser. It was based on open-source code, it was using modern, high-performance components like the v8 JavaScript interpreter, and it quickly gained a lot of popularity because it was fast. Just a few years after release, there seemed to be no reason to use anything else: the browser worked very well, it had support for extensions, it worked swimmingly on smartphones and it provided very handy sync capabilities that allowed users to freely switch between several devices without losing their open tabs, bookmarks or passwords. Firefox for Android had been around for a little longer, but it had never worked really well; hence, a lot of people decided to trust Google in the name of convenience. After all, this was around 2012, the enshittification of the Internet wasn't really a thing yet, and we still trusted Google on some level. I, too, switched to Chrome back then - it was just the logical choice - but, as I was setting up my new browser with the same ad-blocking and privacy extensions that I had on Firefox, I couldn't help but have a lingering thought in my mind:

If Google makes money by profiling my data and serving me ads, why do they allow me to just install AdBlock and use it?

I remember mentioning this to my other techie friends, but they didn't see anything off: "AdBlock users are a niche - there are so few of us who are using AdBlock that Google won't bother." And, at the time, it was true. 10 years ago.

2014 is when things started getting ugly. Google took away the possibility to install extensions out of the Chrome web store, citing security concerns. Nobody really cared, though: ad-blockers were still available on the Chrome Web Store, so nobody's workflow was disrupted.

In 2018, Google anticipated they were working on Manifest v.3, a new security-focused API for extensions. It already faced a lot of criticism at the time but, as usual, people didn't really care: the deployment wasn't imminent, and ad-blockers kept working. That was until, as of very recent, Google finally decided to start moving to Manifest v.3 for real, after all.

What is Manifest v.3?

Manifest v.3 is the newest platform that Chromium extensions will have to use. The name "Manifest" comes from the manifest.json file that must exist in the root directory of every browser extension and contains information about how the extension is structured and how it behaves (Doc link). This update is particularly interesting, because it completely overhauls the security model of browser extensions, implementing more default-denial policies and a stronger permission system as well, to the point of limiting them enough to make it very hard for an ad-blocker to effectively do its job.

In their documentation, Google claims:

Manifest V3 aims to be the first step in our platform vision to improve the privacy, security, and performance of extensions. Along with the platform changes, we are working to give users more understanding and control over what extensions are capable of. The changes will take several years to complete.

But how exactly does this new platform work? Let's dive in.

From permanent backgound pages to ephemeral service workers

Traditionally, browser extensions have always used a background page that ran on the same thread as the extension and could be used to run arbitrary scripts on the same thread. In order to ease the resource consumption caused by this solution, Google created an object called the service worker.

In practice, a Manifest V3 extension will not load a background page that will stay alive for the entire lifetime of the browser session, but it will run a script called service_worker.js on demand. This webpage is not resident in memory: in fact, it gets started when the extension is needed, and stopped when the script has been idling for over 30 seconds. This has major implications in how the code needs to be maintained:

  • First off, for example, any listeners need to be registered immediately at the top of the script since they can no longer be a part of a promise or a callback. That is because, since the script gets started and stopped all the time, it is not guaranteed they will be able to be registered at all otherwise
  • Secondly, it is no longer acceptable to store data in global variables - global data now needs to be stored and accessed through the browser's Storage API, since the web page will get routinely destroyed and restarted.
  • Other changes include the switch from timers to alarms for situations where waiting for a certain amount of time is needed.

Is there any way to get around this limitation and prevent our service worker from getting closed automatically, disrupting our workflow? Well, the answer is "sort of", and this is exactly where the waters get muddy, and political changes start to stack on top of technical changes.

A script is allowed to ask to not be killed for a limited amount of time, as long as that request is motivated by a long-running operation that must not be interrupted, such as a fetch() request that is taking longer than expected since the web server is being slow in providing the requested resource, or an intensive computation that blocks the execution for more than 30 seconds. Here's the code snippet that can be used to achieve this:

async function waitUntil(promise) = {
	const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
	try {
		await promise;
	} finally {
		clearInterval(keepAlive);  
	}
}

waitUntil(someExpensiveCalculation());org.freedesktop.Platform.GL.default

Where's the kicker? Google themselves said, on their documentation, that enterprise and education consumers are allowed to access a special API to keep the service worker active indefinitely, similar to a Manifest v.2 extension; but that this feature will not be allowed in general. In fact, the documentation reads:

In rare cases, it is necessary to extend the lifetime indefinitely. We have identified enterprise and education as the biggest use cases, and we specifically allow this there, but we do not support this in general. In these exceptional circumstances, keeping a service worker alive can be achieved by periodically calling a trivial extension API. It is important to note that this recommendation only applies to extensions running on managed devices for enterprise or education use cases. It is not allowed in other cases and the Chrome extension team reserves the right to take action against those extensions in the future.

If this rubs you the wrong way, you are not alone: this is not a technical requirement, but it is very much a political change where Google reserves to themselves the rights to decide who is allowed to poke holes at Manifest V3's restriction model, in a completely arbitrary manner.

There is another architectural change that comes with Manifest V3: the introduction of offscreen documents, and the move from single-threaded sequential operation to multi-threaded operation with message passing and multiple child scripts running in parallel to each other and to the service worker.

DOM operations need to run in offscreen documents

Service workers do not have DOM access, but some extensions still need access to some sort of DOM to be useful. In Manifest V3, this is accomplished through offscreen documents and their companion Offscreen API Google Documentation. In practice, extensions will be able to ship with special documents that are supposed to run "off-screen" (so, not on a visible tab or window). They do not share APIs with extension contexts - the only API they support is the chrome.runtime API Docs, an event handler that is used for bidirectional message passing between the service worker and the offscreen document. These documents can still be used as full pages that the extension can interact with - but they cannot be displayed to the user graphically.

The Offscreen API needs to be called from the service worker script, and it returns an object that refers to the offscreen document and has the same callable methods as the chrome.runtime API.

chrome.offscreen.createDocument({
	url: chrome.runtime.getURL('offscreen.html'),
	reasons: ['CLIPBOARD'],
	justification: 'testing the offscreen API',
});

Remember how we talked about that the various documents and scripts that extensions need have been moved off the main thread? This change does have benefits in performance, but it also means that they need to use Message Passing APIs to communicate with the rest of the extension. Javascript does not natively support multi-threading, so it is necessary to resort to hacks like this in order to get multiple threads to work together with a caller execution stream. This API works exactly as you would expect: a common channel between the service worker and the document gets established first, then both the service worker and the document listen for messages from the other end and respond to them using that channel. This API can be used both for short, one-time messages (for example: the document must notify the service worker that some kind of processed content is ready for rendering, and should be rendered on the webpage that is visible from the user), and one for long-lived connections that allows for more messaged to be sent for more complex use cases. messages are JSON documents to serialize and de-serialize. Message passing is asynchronous and non-blocking, though, which makes this solution rather optimized.

Listeners must be registered immediately

While in Manifest V2 the background page is started once and never reinitialized, in Manifest V3 the service worker gets called only when it's needed. This means that any listeners that are registered inside of a promise or callback are not guaranteed to work - and they need to be moved to the top level of the script to work.

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
	chrome.action.setBadgeText({ text: badgeText }); 
});

Keeping a service worker alive indefinitely is only allowed in select use cases, like enterprise and education applications. This is the part that is most worrying so far: heavy limitations have been posed on how an extension can work in the name of performance, but some parties can escape a great deal of what makes this change so painful for ad-blockers, and Google is the only arbiter of who can and cannot get around one of the biggest limitations of Manifest V3, especially since it has not been possible to install extensions from external sources for years.

No more loading arbitrary code

One of the most fundamental changes to the new extension interface is the new security model. In the name of security, extensions now have to follow strict limits and have to run much of their code in a sandbox.

One of the crucial changes that extensions face in Manifest V3 is that they are no longer allowed to download and execute arbitrary code from external sources, but all of the JavaScript code they run needs to be bundled in the extension package, which needs to be distributed through the Chrome Web Store. This has advantages and disadvantages:

  • Through the course of history, it has happened several times that browser extensions got hacked or bought out, and, as they became compromised, they began pushing and running malicious code to people's browsers without any sort of review. With this change, Google will be able to review all code that users run, which allows Google to stop malicious code from being automatically downloaded to users' computers.
  • Extensions that needed to deliver hot-patches or just remotely download code for benign purposes can no longer do that, and this can be a major limitation. Many libraries assume remotely-hosted code, and Google makes no exceptions for those: not even for its own first-party Firebase library!

There are just a few exceptions to this rule: firstly, you can still inject remotely hosted CSS stylesheets into a web page using the InsertCSS() method; secondly, you can use the inspectWindow.eval() method inside chrome.devtools to allow executing JavaScript in the context of the inspected page; and lastly, extensions that act as debuggers are allowed to run JavaScript on a debug target. User script extensions like Tampermonkey and Violentmonkey will also get a pass for downloading remote user scripts.

From WebRequest to Declarative Web Request API

The change that impacts ad-blockers the most is how modification of web request works. Namely, the Web Request API was deprecated in favour of the Web Request API. But what does this mean for ad-blockers and privacy extensions?

Web Request API

In Manifest V2, extensions could access the Web Request API. The Web Request API is used to "observe and analyze traffic and to intercept, block or modify requests in-flight" (docs). This is an extremely powerful feature that allows to not only analyze traffic between the browser and a website, but it can also be used to block requests interesting certain domains. In its API, Google uses a code example about blocking traffic to a evil.com domain

chrome.webRequest.onBeforeRequest.addListener(
	function(details) { return {cancel: true}; },
	{urls: ["*://www.evil.com/*"]},
	["blocking"]
);

(This is what the architecture of an extension using the Web Requests API looks like:)

While this approach is very powerful and it gives benign extensions a lot of flexibility, Google claims there are two issues with it:

  1. It poses a security risk, because it has often been abused by bad actors. There have been, for example, instances of certain rogue extensions using this API to inject their own ads in web pages, or inject malicious code that tracks users and snoops on even their interaction with web pages. In the blog article where this change was proposed, Google claims that, "since January 2018, 42% of (reported) malicious extensions have used the Web Requests API".
  2. There are performance issues with this approach. The main one comes from the blocking nature of this API: in fact, several of its events are blocking - meaning, they pause execution of the caller thread until they are done with whatever job they were working on - and each of these events can be bound to an unlimited number of event workers. Finally, no locking mechanism has been implemented to prevent a Web Request event to perform a blocking operation to a page that is already been blocked by another extension, which can cause execution to hang, if not deadlock completely. In addition to that, using the Web Requests API requires a persistent process to run for the duration of the entire session, and it relies on several computationally expensive operations, like serialization of the request data, the IPC messaging required to send that data from the API over to extensions, and dealing with how the extension responds to receiving that data.

The solution Google proposes in this new version of Web Extensions is to use the Declarative Net Requests API.

To be completely honest, this new API absolutely solves the two outlined issues: it can no longer be used to inject malicious code into web pages for one, and its architecture is much more performance-friendly, meaning that a poorly programmed extension using this new API will not be able to completely halt your browser. The issue is that, to achieve this, a lot of functionality was axed, and this impacts even benign extensions that needed Web Requests to work. One of the affected extensions if uBlock Origin, the most reputable ad-blocker available. When we consider a case like uBlock Origin's, neither the problems outlined by Google are a real issue: the extension is free and open source software, developed completely in the open and subject to constant audits; and not only is its use of the API pretty efficient and lightweight, but it oftentimes leads to lower overall resource usage, since the overhead of using the Web Requests API is far, far less than the overhead of running heavy tracking scripts and displaying the invasive apps that a lot of modern web pages load.

But why cannot uBlock Origin just adapt to the new API? Well, it's pretty simple. If you remember what we said earlier about the move to Service Workers, you will have realized that a common theme in Manifest V3 is switching from persistent processes to event-driven approaches: it comes at no surprise that the Declarative Net Requests API follows the same philosophy.

As opposed to having the long-running resident process of old, an extension using the new API needs to register certain rules to instruct the browser on how to react to certain scenarios that are outlined in said rules. This shifts the processing duties of those web requests from the WebRequests process spawned by the MV2 extension over to the browser itself. Mostly, we are going from the extension itself being the central part of the web request handling process, to the browser taking its role, and receiving some guidelines from it on how to handle certain situations. This, of course, removes a lot of freedom from the extension, as the extent of what the extension is allowed to ask the browser to do is very limited.

Of course, the flip side of the coin is that this absolutely solves the problems that Google saw with the previous approach: a malicious extension will no longer be allowed to view any sensitive data, as its handling is delegated to the browser; there is no more long-running process to keep in memory, and the cost of IPC and serialization is seriously cut down.

Most privacy-focused extension developers, such as uBlock Origin and Ghostery, are expressing criticism on this new approach, since it is simply not possible to block ads and arbitrary code to the extent than it was using the old Web Requests API. The only vendor that seemed to be enthusiastically on board with this change is Adblock Plus, an ad-blocking extension that has not been recommended for a long time, since they have a financial relationship with Google, and they allow a limited set of Google-controlled ads to slip through the cracks.

Is this the end of ad-blocking? Well, if you use Chromium-based browser, yes, kinda. However, not all hope is completely lost.

uBlock Origin Lite

In an attempt to retain basic ad-blocking functionality even after the deprecation of the Web Requests API, the uBlock Origin team has released uBlock Origin Lite, a re-implementation of their content blocker that is fully permission-less and Manifest V3-compliant.

This new implementation of the extension comes with everything you'd expect from what we just said: non-persistent service workers, no persistent Web Request processes, lower resource usage. The kicker is that, on top of not being as powerful and thorough as the regular uBlock Origin, it has to comply with the new default-denial permission system, so, you will have to grant the extension the permission to operate more advanced content blocking on a per-site basis.

We can see, upon installing the extension, that there are three different filtering modes: Basic, Optimal, and Complete.org.freedesktop.Platform.GL.default

From the extension settings, an user can request to change the global blocking policy. When this is done, the extension will ask the user to deny or grant the new set of permissions it's asking for, through Chromium's permission manager.

It is also possible to grant per-domain permissions: if the global policy is left on "Basic", it is still possible to grant the required permission for Optimal or Complete blocking for a specific domain.

While this is a nice workaround, it absolutely cannot compete with the real deal, and the flexibility it grants. This cannot be considered a real solution. Enter Firefox.

It's time to switch to Firefox.

Fortunately, Chromium is not the only browser vendor that discusses on the next step for WebExtensions. Mozilla's Gecko-based Firefox browser, in fact, will take a more measured approach: while it will ultimately have to migrate to Manifest V3 as well in order to retain support for modern extensions, Mozilla have said that they will not deprecate the Web Requests API, and that their implementation of Manifest V3 will differ from Google's in key areas in order to allow uBlock Origin and other privacy extensions to retain full functionality on Firefox. Moreover, Mozilla does not seem particularly keen on dropping Manifest V2 support anytime soon, which means that uBlock Origin and other privacy-focused extensions will keep working on Firefox without changes for the foreseeable future.

For those of you who left Firefox due to bad experiences with it, fear not: using Firefox is actually a pretty good experience nowadays. It's a lot more lightweight than Chromium, performance is absolutely good enough, website incompatibilities are rare and Linux support is top-notch, with Firefox having much better touchscreen and touchpad scroll physics on Linux compared to Chromium, as well as having a more stable Wayland mode that is active by default, which also allows to use smooth touchpad gestures that follow your finger from the beginning to the end, like butter-smooth 1:1 zoom and pan and two-finger scrolling to quickly go back and forth in your browser history for the current tab. On top of that, Firefox is extensively customizable, allowing you to mostly work around any major quirks you might have with it: there is an entire community dedicated to customizing Firefox through very pretty themes that give the entire UI a full make-over, and, for everything else, you can use a user.js file - whether yours or one maintained by the community, like Arkenfox and Betterfox - to customize every single tiny thing you could possibly imagine, or tune Firefox to suit your specific needs. Let's also not forget that, if ad-blocking is important to you, even on Manifest V2, the developers claim that uBlock Origin works better and more reliably on Firefox and, on top of that, it is also available on the Android version of Firefox, so you don't have to pick between having your passwords and tabs synced across devices or ad-blocking on your phone, as you have to do with Chrome. If you use Linux, it's likely Firefox already comes pre-installed by your distro vendor. Otherwise, you can very easily install it from the repos and be good to go. Migrating to it is not hard either: inside the settings, you will find an "Import Browser Data" option that can be used to seamlessly import most data from your old Chromium-based browser over to Firefox, including the extensions you have installed!

This move might seem daunting, but moving over to Gecko browsers, as for the current state of affairs, looks like the only realistic way out of this unfortunate situation.