BootstrapLoader.js
A legacy add-on is an XPI whose metadata lives in an RDF file called install.rdf, with its
code in bootstrap.js. Firefox 57 removed the component that loaded these; BootstrapLoader.js
puts it back. Why Firefox removed that capability, and why these scripts restore it anyway, is in
Why we need it.
In plain language
Section titled “In plain language”Old-style extensions (like Tab Mix Plus) speak a language Firefox no longer understands out of
the box. This file is the translator: it teaches Firefox to recognize, start, and stop those
extensions again. There is nothing to configure. If the file is installed, legacy add-ons
simply appear in about:addons like any other extension. (Waterfox users can skip this page:
that browser ships its own translator, and ours politely steps aside.) How the translation
works, for the curious, is below.
The one-line idea
Section titled “The one-line idea”Firefox did not delete the add-on machinery. It deleted the registration of external loaders.
Gecko's AddonManager still supports them through a single, public entry point:
AddonManager.addExternalExtensionLoader({ name: "bootstrap", manifestFile: "install.rdf", async loadManifest(pkg) { /* build an AddonInternal from install.rdf */ }, loadScope(addon) { /* run bootstrap.js in a sandbox */ },});BootstrapLoader.js is a loader with the install.rdf manifest type. Everything it does hangs off
that registration.
It is loaded from chrome://userchromejs/content/BootstrapLoader.js (the path the utils'
chrome.manifest registers in config.js).
What happens to a legacy add-on
Section titled “What happens to a legacy add-on”- The manifest is read.
install.rdfis parsed with theRDFManifestConverterhelper, and the properties Firefox still knows are copied onto anAddonInternal: id, version, name, description, creator, locale strings, icons,targetApplications, dependencies.install.rdfproperties that no longer exist are ignored. - Type mapping. The old numeric
typevalues are translated to modern names (2→extension,64→dictionary). bootstrap.jsis executed. In aCu.Sandboxwith the system principal, the same trust level as the browser's own code, which is inherent to how legacy add-ons work. The sandbox is created withChromeUtils.compileScriptrather than a plainloadSubScript, working around bug 1974213 (loadingfile:andjar:URIs). It waits for the script to finish before continuing.startup()/shutdown()are called. Beforestartup(), the add-on'schrome.manifestis registered temporarily aschrome://chrome registrations, and removed again on shutdown. That is how a legacy add-on's own UI files become reachable.- Re-registration is cleaned up. A killed session can leave a stale
chrome.manifest*inbrowser-extension-data/<id>/; the leftovers are swept at startup, and fresh registrations are re-derived each session.
The patches that keep old add-ons alive
Section titled “The patches that keep old add-ons alive”Restoring the loader is not enough. The surrounding browser code has moved on. The loader re-implements or overrides the pieces an old add-on expects:
XPIDatabase.isDisabledLegacy→ forced tofalse, so the add-on subsystem does not skip non-WebExtension items.XPIExports.verifyBundleSignedState→ for a non-WebExtensionextensiontype (and ids containing_N_SIGN_), the signature check returns "not required". Without this, every legacy XPI reads as unsigned and is refused.about:addonsoptions handling → anaddon-cardclick on Preferences now focuses an already-open options window instead of always opening a new one, and the Preferences button is shown for legacy options types. This restores the pre-2017 behaviour old add-ons were built against.- The updater hook → the same file initializes the in-browser updater when
browser.xhtmlloads, so the update UI is available even when the utils' other entry points have not run yet.
The double-load guard
Section titled “The double-load guard”Waterfox ships its own Bootstrap Loader. Two loaders registering under the same bootstrap key
would both patch the same prototypes, so this copy stays inert where one already exists:
function bootstrapLoaderBundled() { if (/waterfox/i.test(Services.appinfo.name)) { return true; } try { const registry = AddonManagerPrivate?.externalExtensionLoaders; if (!registry) return false; if (registry.get("bootstrap")) return true; for (const loader of registry.values()) { if (loader && loader.manifestFile === "install.rdf") return true; } } catch { /* older Gecko / exotic build: brand check only */ } return false;}Two signals: the brand name (deterministic: AutoConfig runs before the AddonManager registry exists, so the registry check only protects later evaluations), and the loader registry itself (name-independent, so a re-evaluation of the file in any browser is a no-op).
config.js applies the same check before loading the file, so on Waterfox neither copy's patches run.