Skip to content

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.

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.

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).

  1. The manifest is read. install.rdf is parsed with the RDFManifestConverter helper, and the properties Firefox still knows are copied onto an AddonInternal: id, version, name, description, creator, locale strings, icons, targetApplications, dependencies. install.rdf properties that no longer exist are ignored.
  2. Type mapping. The old numeric type values are translated to modern names (2 → extension, 64 → dictionary).
  3. bootstrap.js is executed. In a Cu.Sandbox with 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 with ChromeUtils.compileScript rather than a plain loadSubScript, working around bug 1974213 (loading file: and jar: URIs). It waits for the script to finish before continuing.
  4. startup() / shutdown() are called. Before startup(), the add-on's chrome.manifest is registered temporarily as chrome:// chrome registrations, and removed again on shutdown. That is how a legacy add-on's own UI files become reachable.
  5. Re-registration is cleaned up. A killed session can leave a stale chrome.manifest* in browser-extension-data/<id>/; the leftovers are swept at startup, and fresh registrations are re-derived each session.

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 to false, so the add-on subsystem does not skip non-WebExtension items.
  • XPIExports.verifyBundleSignedState → for a non-WebExtension extension type (and ids containing _N_SIGN_), the signature check returns "not required". Without this, every legacy XPI reads as unsigned and is refused.
  • about:addons options handling → an addon-card click 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.xhtml loads, so the update UI is available even when the utils' other entry points have not run yet.

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.