1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* eslint "valid-jsdoc": [2, {requireReturn: false}] */
9 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
12 ChromeUtils.defineESModuleGetters(lazy, {
13 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
14 AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
15 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
16 jexlFilterFunc: "resource://services-settings/remote-settings.sys.mjs",
19 const CascadeFilter = Components.Constructor(
20 "@mozilla.org/cascade-filter;1",
25 // The whole ID should be surrounded by literal ().
26 // IDs may contain alphanumerics, _, -, {}, @ and a literal '.'
27 // They may also contain backslashes (needed to escape the {} and dot)
28 // We filter out backslash escape sequences (like `\w`) separately
29 // (see kEscapeSequences).
32 "\\\\" + // note: just a backslash, but between regex and string it needs escaping.
36 // Find regular expressions of the form:
37 // /^((id1)|(id2)|(id3)|...|(idN))$/
38 // The outer set of parens enclosing the entire list of IDs is optional.
39 const kIsMultipleIds = new RegExp(
40 // Start with literal sequence /^(
41 // (the `(` is optional)
43 // Then at least one ID in parens ().
45 // Followed by any number of IDs in () separated by pipes.
46 // Note: using a non-capturing group because we don't care about the value.
47 "(?:\\|" + kIdSubRegex + ")*" +
48 // Finally, we need to end with literal sequence )$/
49 // (the leading `)` is optional like at the start)
53 // Check for a backslash followed by anything other than a literal . or curlies
54 const kEscapeSequences = /\\[^.{}]/;
56 // Used to remove the following 3 things:
57 // leading literal /^(
60 // trailing literal )$/
61 // plus an optional ) before the )$/
62 const kRegExpRemovalRegExp = /^\/\^\(\(?|\\|\)\)?\$\/$/g;
64 // In order to block add-ons, their type should be part of this list. There
65 // may be additional requirements such as requiring the add-on to be signed.
66 // See the uses of kXPIAddonTypes before introducing new addon types or
67 // providers that differ from the existing types.
68 ChromeUtils.defineLazyGetter(lazy, "kXPIAddonTypes", () => {
69 // In practice, this result is equivalent to ALL_XPI_TYPES in XPIProvider.jsm.
70 // "plugin" (from GMPProvider.sys.mjs) is intentionally omitted, as we decided to
71 // not support blocklisting of GMP plugins in bug 1086668.
72 return lazy.AddonManagerPrivate.getAddonTypesByProvider("XPIProvider");
75 // For a given input string matcher, produce either a string to compare with,
76 // a regular expression, or a set of strings to compare with.
77 function processMatcher(str) {
78 if (!str.startsWith("/")) {
81 // Process regexes matching multiple IDs into a set.
82 if (kIsMultipleIds.test(str) && !kEscapeSequences.test(str)) {
83 // Remove the regexp gunk at the start and end of the string, as well
84 // as all backslashes, and split by )|( to leave the list of IDs.
85 return new Set(str.replace(kRegExpRemovalRegExp, "").split(")|("));
87 let lastSlash = str.lastIndexOf("/");
88 let pattern = str.slice(1, lastSlash);
89 let flags = str.slice(lastSlash + 1);
90 return new RegExp(pattern, flags);
93 // Returns true if the addonProps object passes the constraints set by matches.
94 // (For every non-null property in matches, the same key must exist in
95 // addonProps and its value must match)
96 function doesAddonEntryMatch(matches, addonProps) {
97 for (let [key, value] of Object.entries(matches)) {
98 if (value === null || value === undefined) {
101 if (addonProps[key]) {
102 // If this property matches (member of the set, matches regex, or
103 // an exact string match), continue to look at the other properties of
104 // the `matches` object.
105 // If not, return false immediately.
106 if (value.has && value.has(addonProps[key])) {
109 if (value.test && value.test(addonProps[key])) {
112 if (typeof value == "string" && value === addonProps[key]) {
116 // If we get here, the property doesn't match, so this entry doesn't match.
119 // If we get here, all the properties must have matched.
123 const TOOLKIT_ID = "toolkit@mozilla.org";
124 const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
125 const PREF_BLOCKLIST_ADDONITEM_URL = "extensions.blocklist.addonItemURL";
126 const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
127 const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
128 const PREF_BLOCKLIST_USE_MLBF = "extensions.blocklist.useMLBF";
129 const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
130 const DEFAULT_SEVERITY = 3;
131 const DEFAULT_LEVEL = 2;
132 const MAX_BLOCK_LEVEL = 3;
134 const BLOCKLIST_BUCKET = "blocklists";
136 const BlocklistTelemetry = {
138 // Used by BlocklistTelemetry.recordAddonBlockChangeTelemetry.
139 Services.telemetry.setEventRecordingEnabled("blocklist", true);
143 * Record the RemoteSettings Blocklist lastModified server time into the
144 * "blocklist.lastModified_rs keyed scalar (or "Missing Date" when unable
145 * to retrieve a valid timestamp).
147 * @param {string} blocklistType
148 * The blocklist type that has been updated ("addons" or "addons_mlbf");
149 * the "gfx" blocklist is not covered by this telemetry).
150 * @param {RemoteSettingsClient} remoteSettingsClient
151 * The RemoteSettings client to retrieve the lastModified timestamp from.
153 async recordRSBlocklistLastModified(blocklistType, remoteSettingsClient) {
154 // In some tests overrides ensureInitialized and remoteSettingsClient
155 // can be undefined, and in that case we don't want to record any
157 if (!remoteSettingsClient) {
161 let lastModified = await remoteSettingsClient.getLastModified();
162 if (blocklistType === "addons_mlbf") {
163 BlocklistTelemetry.recordTimeScalar(
164 "lastModified_rs_" + blocklistType,
167 BlocklistTelemetry.recordGleanDateTime(
168 Glean.blocklist.lastModifiedRsAddonsMblf,
175 * Record a timestamp in telemetry as a UTC string or "Missing Date" if the
176 * input is not a valid timestamp.
178 * @param {string} telemetryKey
179 * The part of after "blocklist.", as defined in Scalars.yaml.
180 * @param {number} time
181 * A timestamp to record. If invalid, "Missing Date" will be recorded.
183 recordTimeScalar(telemetryKey, time) {
185 // convert from timestamp in ms into UTC datetime string, so it is going
186 // to be record in the same format previously used by blocklist.lastModified_xml.
187 let dateString = new Date(time).toUTCString();
188 Services.telemetry.scalarSet("blocklist." + telemetryKey, dateString);
190 Services.telemetry.scalarSet("blocklist." + telemetryKey, "Missing Date");
195 * Records a glean datetime if time is > than 0, otherwise 0 is submitted.
197 * @param {nsIGleanDatetime} gleanTelemetry
198 * A glean telemetry datetime object.
199 * @param {number} time
200 * A timestamp to record.
202 recordGleanDateTime(gleanTelemetry, time) {
204 // Glean date times are provided in nanoseconds, `getTime()` yields
205 // milliseconds (after the Unix epoch).
206 gleanTelemetry.set(time * 1000);
208 gleanTelemetry.set(0);
213 * Record whether an add-on is blocked and the parameters that guided the
214 * decision to block or unblock the add-on.
216 * @param {AddonWrapper|object} addon
217 * The blocked or unblocked add-on. Not necessarily installed.
218 * Could be an object with the id, version and blocklistState
219 * properties when the AddonWrapper is not available (e.g. during
221 * @param {string} reason
222 * The reason for recording the event,
223 * "addon_install", "addon_update", "addon_update_check",
224 * "addon_db_modified", "blocklist_update".
226 recordAddonBlockChangeTelemetry(addon, reason) {
227 // Reduce the timer resolution for anonymity.
228 let hoursSinceInstall = -1;
229 if (reason === "blocklist_update" || reason === "addon_db_modified") {
230 hoursSinceInstall = Math.round(
231 (Date.now() - addon.installDate.getTime()) / 3600000
235 const value = addon.id;
237 blocklistState: `${addon.blocklistState}`,
238 addon_version: addon.version,
239 signed_date: `${addon.signedDate?.getTime() || 0}`,
240 hours_since: `${hoursSinceInstall}`,
242 ...ExtensionBlocklistMLBF.getBlocklistMetadataForTelemetry(),
244 Glean.blocklist.addonBlockChange.record({
247 blocklist_state: extra.blocklistState,
248 addon_version: extra.addon_version,
249 signed_date: extra.signed_date,
250 hours_since: extra.hours_since,
251 mlbf_last_time: extra.mlbf_last_time,
252 mlbf_generation: extra.mlbf_generation,
253 mlbf_source: extra.mlbf_source,
256 Services.telemetry.recordEvent(
268 * Checks whether this entry is valid for the current OS and ABI.
269 * If the entry has an "os" property then the current OS must appear in
270 * its comma separated list for it to be valid. Similarly for the
273 * @param {Object} item
274 * The blocklist item.
276 * Whether the entry matches the current OS.
280 let os = item.os.split(",");
281 if (!os.includes(lazy.gAppOS)) {
287 let xpcomabi = item.xpcomabi.split(",");
288 if (!xpcomabi.includes(lazy.gApp.XPCOMABI)) {
296 * Checks if a version is higher than or equal to the minVersion (if provided)
297 * and lower than or equal to the maxVersion (if provided).
298 * @param {string} version
299 * The version to test.
300 * @param {string?} minVersion
301 * The minimum version. If null it is assumed that version is always
303 * @param {string?} maxVersion
304 * The maximum version. If null it is assumed that version is always
307 * Whether the item matches the range.
309 versionInRange(version, minVersion, maxVersion) {
310 if (minVersion && Services.vc.compare(version, minVersion) < 0) {
313 if (maxVersion && Services.vc.compare(version, maxVersion) > 0) {
320 * Tests if this versionRange matches the item specified, and has a matching
321 * targetApplication id and version.
322 * @param {Object} versionRange
323 * The versionRange to check against
324 * @param {string} itemVersion
325 * The version of the actual addon/plugin to test for.
326 * @param {string} appVersion
327 * The version of the application to test for.
328 * @param {string} toolkitVersion
329 * The version of toolkit to check for.
331 * True if this version range covers the item and app/toolkit version given.
333 versionsMatch(versionRange, itemVersion, appVersion, toolkitVersion) {
334 // Some platforms have no version for plugins, these don't match if there
335 // was a min/maxVersion provided
336 if (!itemVersion && (versionRange.minVersion || versionRange.maxVersion)) {
340 // Check if the item version matches
342 !this.versionInRange(
344 versionRange.minVersion,
345 versionRange.maxVersion
351 // Check if the application or toolkit version matches
352 for (let tA of versionRange.targetApplication) {
354 tA.guid == lazy.gAppID &&
355 this.versionInRange(appVersion, tA.minVersion, tA.maxVersion)
360 tA.guid == TOOLKIT_ID &&
361 this.versionInRange(toolkitVersion, tA.minVersion, tA.maxVersion)
370 * Given a blocklist JS object entry, ensure it has a versionRange property, where
371 * each versionRange property has a valid severity property
372 * and at least 1 valid targetApplication.
373 * If it didn't have a valid targetApplication array before and/or it was empty,
374 * fill it with an entry with null min/maxVersion properties, which will match
377 * If there *are* targetApplications, if any of them don't have a guid property,
378 * assign them the current app's guid.
380 * @param {Object} entry
381 * blocklist entry object.
383 ensureVersionRangeIsSane(entry) {
384 if (!entry.versionRange.length) {
385 entry.versionRange.push({});
387 for (let vr of entry.versionRange) {
388 if (!vr.hasOwnProperty("severity")) {
389 vr.severity = DEFAULT_SEVERITY;
391 if (!Array.isArray(vr.targetApplication)) {
392 vr.targetApplication = [];
394 if (!vr.targetApplication.length) {
395 vr.targetApplication.push({ minVersion: null, maxVersion: null });
397 vr.targetApplication.forEach(tA => {
399 tA.guid = lazy.gAppID;
406 * Create a blocklist URL for the given blockID
407 * @param {String} id the blockID to use
408 * @returns {String} the blocklist URL.
410 _createBlocklistURL(id) {
411 let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
412 return url.replace(/%blockID%/g, id);
417 * This custom filter function is used to limit the entries returned
418 * by `RemoteSettings("...").get()` depending on the target app information
419 * defined on entries.
421 * Note that this is async because `jexlFilterFunc` is async.
423 * @param {Object} entry a Remote Settings record
424 * @param {Object} environment the JEXL environment object.
425 * @returns {Object} The entry if it matches, `null` otherwise.
427 async function targetAppFilter(entry, environment) {
428 // If the entry has a JEXL filter expression, it should prevail.
429 // The legacy target app mechanism will be kept in place for old entries.
430 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1463377
431 const { filter_expression } = entry;
432 if (filter_expression) {
433 return lazy.jexlFilterFunc(entry, environment);
436 // Keep entries without target information.
437 if (!("versionRange" in entry)) {
441 const { versionRange } = entry;
443 // Everywhere in this method, we avoid checking the minVersion, because
444 // we want to retain items whose minVersion is higher than the current
445 // app version, so that we have the items around for app updates.
447 // Gfx blocklist has a specific versionRange object, which is not a list.
448 if (!Array.isArray(versionRange)) {
449 const { maxVersion = "*" } = versionRange;
451 Services.vc.compare(lazy.gApp.version, maxVersion) <= 0;
452 return matchesRange ? entry : null;
455 // Iterate the targeted applications, at least one of them must match.
456 // If no target application, keep the entry.
457 if (!versionRange.length) {
460 for (const vr of versionRange) {
461 const { targetApplication = [] } = vr;
462 if (!targetApplication.length) {
465 for (const ta of targetApplication) {
470 const { maxVersion = "*" } = ta;
472 guid == lazy.gAppID &&
473 Services.vc.compare(lazy.gApp.version, maxVersion) <= 0
478 guid == "toolkit@mozilla.org" &&
479 Services.vc.compare(Services.appinfo.platformVersion, maxVersion) <= 0
490 * The Graphics blocklist implementation. The JSON objects for graphics blocks look
496 * "vendor": "0xabcd",
501 * "feature": " DIRECT2D ",
502 * "featureStatus": " BLOCKED_DRIVER_VERSION ",
503 * "driverVersion": " 8.52.322.2202 ",
504 * "driverVersionComparator": " LESS_THAN ",
505 * "versionRange": {"minVersion": "5.0", "maxVersion: "25.0"},
508 * The RemoteSetttings client takes care of filtering out versions that don't apply.
509 * The code here stores entries in memory and sends them to the gfx component in
510 * serialized text form, using ',', '\t' and '\n' as separators.
512 const GfxBlocklistRS = {
513 _ensureInitialized() {
514 if (this._initialized || !gBlocklistEnabled) {
517 this._initialized = true;
518 this._client = lazy.RemoteSettings("gfx", {
519 bucketName: BLOCKLIST_BUCKET,
520 filterFunc: targetAppFilter,
522 this.checkForEntries = this.checkForEntries.bind(this);
523 this._client.on("sync", this.checkForEntries);
528 this._client.off("sync", this.checkForEntries);
533 this._ensureInitialized();
534 return this._client.sync();
537 async checkForEntries() {
538 this._ensureInitialized();
539 if (!gBlocklistEnabled) {
540 return []; // return value expected by tests.
542 let entries = await this._client.get().catch(ex => Cu.reportError(ex));
543 // Handle error silently. This can happen if our request to fetch data is aborted,
544 // e.g. by application shutdown.
548 // Trim helper (spaces, tabs, no-break spaces..)
550 (s || "").replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, "");
552 entries = entries.map(entry => {
557 "driverVersionComparator",
565 for (let p of props) {
567 // Ignore falsy values or empty arrays.
568 if (!val || (Array.isArray(val) && !val.length)) {
571 if (typeof val == "string") {
573 } else if (p == "devices") {
574 let invalidDevices = [];
575 let validDevices = [];
576 // We serialize the array of devices as a comma-separated string, so
577 // we need to ensure that none of the entries contain commas, also in
580 v.includes(",") ? invalidDevices.push(v) : validDevices.push(v)
582 for (let dev of invalidDevices) {
584 `Block ${entry.blockID} contains unsupported device: ${dev}`
595 if (entry.versionRange) {
597 minVersion: trim(entry.versionRange.minVersion) || "0",
598 maxVersion: trim(entry.versionRange.maxVersion) || "*",
603 if (entries.length) {
608 "driverVersionComparator",
621 // Notify `GfxInfoBase`, by passing a string serialization.
623 for (let gfxEntry of entries) {
625 for (let key of sortedProps) {
627 let value = gfxEntry[key];
628 if (Array.isArray(value)) {
629 value = value.join(",");
630 } else if (value.maxVersion) {
631 // Both minVersion and maxVersion are always set on each entry.
632 value = value.minVersion + "," + value.maxVersion;
634 entryLines.push(key + ":" + value);
637 payload.push(entryLines.join("\t"));
639 Services.obs.notifyObservers(
641 "blocklist-data-gfxItems",
645 // The return value is only used by tests.
651 * The extensions blocklist implementation. The JSON objects for extension
652 * blocks look something like:
655 * "guid": "someguid@addons.mozilla.org",
656 * "prefs": ["i.am.a.pref.that.needs.resetting"],
657 * "schema": 1480349193877,
658 * "blockID": "i12345",
660 * "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1234567",
661 * "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.",
662 * "why": "This add-on is in violation of the <a href=\"https://developer.mozilla.org/en-US/Add-ons/Add-on_guidelines\">Add-on Guidelines</a>, using multiple add-on IDs and potentially doing other unwanted activities.",
663 * "name": "Some pretty name",
664 * "created": "2019-05-06T19:52:20Z"
672 * "targetApplication": []
675 * "id": "<unique guid>",
676 * "last_modified": 1480349215672,
679 * This is a legacy format, and implements deprecated operations (bug 1620580).
680 * ExtensionBlocklistMLBF supersedes this implementation.
682 const ExtensionBlocklistRS = {
683 async _ensureEntries() {
684 this.ensureInitialized();
685 if (!this._entries && gBlocklistEnabled) {
686 await this._updateEntries();
690 async _updateEntries() {
691 if (!gBlocklistEnabled) {
695 this._entries = await this._client.get().catch(ex => Cu.reportError(ex));
696 // Handle error silently. This can happen if our request to fetch data is aborted,
697 // e.g. by application shutdown.
698 if (!this._entries) {
702 this._entries.forEach(entry => {
705 entry.matches.id = processMatcher(entry.guid);
707 for (let key of EXTENSION_BLOCK_FILTERS) {
708 if (key == "id" || !entry[key]) {
711 entry.matches[key] = processMatcher(entry[key]);
713 Utils.ensureVersionRangeIsSane(entry);
716 BlocklistTelemetry.recordRSBlocklistLastModified("addons", this._client);
719 async _filterItem(entry, environment) {
720 if (!(await targetAppFilter(entry, environment))) {
723 if (!Utils.matchesOSABI(entry)) {
726 // Need something to filter on - at least a guid or name (either could be a regex):
727 if (!entry.guid && !entry.name) {
728 let blockID = entry.blockID || entry.id;
729 Cu.reportError(new Error(`Nothing to filter add-on item ${blockID} on`));
736 this.ensureInitialized();
737 return this._client.sync();
740 ensureInitialized() {
741 if (!gBlocklistEnabled || this._initialized) {
744 this._initialized = true;
745 this._client = lazy.RemoteSettings("addons", {
746 bucketName: BLOCKLIST_BUCKET,
747 filterFunc: this._filterItem,
749 this._onUpdate = this._onUpdate.bind(this);
750 this._client.on("sync", this._onUpdate);
755 this._client.off("sync", this._onUpdate);
756 this._didShutdown = true;
760 // Called when the blocklist implementation is changed via a pref.
762 if (this._didShutdown) {
763 this._client.on("sync", this._onUpdate);
764 this._didShutdown = false;
769 let oldEntries = this._entries || [];
770 await this.ensureInitialized();
771 await this._updateEntries();
773 let addons = await lazy.AddonManager.getAddonsByTypes(lazy.kXPIAddonTypes);
774 for (let addon of addons) {
775 let oldState = addon.blocklistState;
776 if (addon.updateBlocklistState) {
777 await addon.updateBlocklistState(false);
778 } else if (oldEntries) {
779 let oldEntry = this._getEntry(addon, oldEntries);
782 : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
784 oldState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
786 let state = addon.blocklistState;
789 "Blocklist state for " +
797 // We don't want to re-warn about add-ons
798 if (state == oldState) {
802 // Ensure that softDisabled is false if the add-on is not soft blocked
803 if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
804 await addon.setSoftDisabled(false);
807 // If an add-on has dropped from hard to soft blocked just mark it as
808 // soft disabled and don't warn about it.
810 state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
811 oldState == Ci.nsIBlocklistService.STATE_BLOCKED
813 await addon.setSoftDisabled(true);
817 state == Ci.nsIBlocklistService.STATE_BLOCKED ||
818 state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED
820 // Mark it as softblocked if necessary. Note that we avoid setting
821 // softDisabled at the same time as userDisabled to make it clear
822 // which was the original cause of the add-on becoming disabled in a
823 // way that the user can change.
825 state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
828 await addon.setSoftDisabled(true);
830 // It's a block. We must reset certain preferences.
831 let entry = this._getEntry(addon, this._entries);
832 if (entry.prefs && entry.prefs.length) {
833 for (let pref of entry.prefs) {
834 Services.prefs.clearUserPref(pref);
840 lazy.AddonManagerPrivate.updateAddonAppDisabledStates();
843 async getState(addon, appVersion, toolkitVersion) {
844 let entry = await this.getEntry(addon, appVersion, toolkitVersion);
845 return entry ? entry.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
848 async getEntry(addon, appVersion, toolkitVersion) {
849 await this._ensureEntries();
850 return this._getEntry(addon, this._entries, appVersion, toolkitVersion);
853 _getEntry(addon, addonEntries, appVersion, toolkitVersion) {
854 if (!gBlocklistEnabled || !addon) {
858 // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
859 if (!appVersion && !lazy.gApp.version) {
864 appVersion = lazy.gApp.version;
866 if (!toolkitVersion) {
867 toolkitVersion = lazy.gApp.platformVersion;
871 for (let key of EXTENSION_BLOCK_FILTERS) {
872 addonProps[key] = addon[key];
874 if (addonProps.creator) {
875 addonProps.creator = addonProps.creator.name;
878 for (let entry of addonEntries) {
879 // First check if it matches our properties. If not, just skip to the next item.
880 if (!doesAddonEntryMatch(entry.matches, addonProps)) {
883 // If those match, check the app or toolkit version works:
884 for (let versionRange of entry.versionRange) {
893 let blockID = entry.blockID || entry.id;
896 versionRange.severity >= gBlocklistLevel
897 ? Ci.nsIBlocklistService.STATE_BLOCKED
898 : Ci.nsIBlocklistService.STATE_SOFTBLOCKED,
899 url: Utils._createBlocklistURL(blockID),
900 prefs: entry.prefs || [],
910 * The extensions blocklist implementation, the third version.
912 * The current blocklist is represented by a multi-level bloom filter (MLBF)
913 * (aka "Cascade Bloom Filter") that works like a set, i.e. supports a has()
914 * operation, except it is probabilistic. The MLBF is 100% accurate for known
915 * entries and unreliable for unknown entries. When the backend generates the
916 * MLBF, all known add-ons are recorded, including their block state. Unknown
917 * add-ons are identified by their signature date being newer than the MLBF's
918 * generation time, and they are considered to not be blocked.
920 * Legacy blocklists used to distinguish between "soft block" and "hard block",
921 * but the current blocklist only supports one type of block ("hard block").
922 * After checking the blocklist states, any previous "soft blocked" addons will
923 * either be (hard) blocked or unblocked based on the blocklist.
925 * The MLBF is attached to a RemoteSettings record, as follows:
928 * "generation_time": 1585692000000,
929 * "attachment": { ... RemoteSettings attachment ... }
930 * "attachment_type": "bloomfilter-base",
933 * The collection can also contain stashes:
936 * "stash_time": 1585692000001,
938 * "blocked": [ "addonid:1.0", ... ],
939 * "unblocked": [ "addonid:1.0", ... ]
942 * Stashes can be used to update the blocklist without forcing the whole MLBF
943 * to be downloaded again. These stashes are applied on top of the base MLBF.
945 const ExtensionBlocklistMLBF = {
946 RS_ATTACHMENT_ID: "addons-mlbf.bin",
948 async _fetchMLBF(record) {
949 // |record| may be unset. In that case, the MLBF dump is used instead
950 // (provided that the client has been built with it included).
951 let hash = record?.attachment.hash;
952 if (this._mlbfData && hash && this._mlbfData.cascadeHash === hash) {
953 // MLBF not changed, save the efforts of downloading the data again.
955 // Although the MLBF has not changed, the time in the record has. This
956 // means that the MLBF is known to provide accurate results for add-ons
957 // that were signed after the previously known date (but before the newly
958 // given date). To ensure that add-ons in this time range are also blocked
959 // as expected, update the cached generationTime.
960 if (record.generation_time > this._mlbfData.generationTime) {
961 this._mlbfData.generationTime = record.generation_time;
963 return this._mlbfData;
967 record: actualRecord,
968 _source: rsAttachmentSource,
969 } = await this._client.attachments.download(record, {
970 attachmentId: this.RS_ATTACHMENT_ID,
971 fallbackToCache: true,
972 fallbackToDump: true,
975 cascadeHash: actualRecord.attachment.hash,
976 cascadeFilter: new CascadeFilter(new Uint8Array(buffer)),
977 // Note: generation_time is semantically distinct from last_modified.
978 // generation_time is compared with the signing date of the add-on, so it
979 // should be in sync with the signing service's clock.
980 // In contrast, last_modified does not have such strong requirements.
981 generationTime: actualRecord.generation_time,
982 // Used for telemetry.
987 async _updateMLBF(forceUpdate = false) {
988 // The update process consists of fetching the collection, followed by
989 // potentially multiple network requests. As long as the collection has not
990 // been changed, repeated update requests can be coalesced. But when the
991 // collection has been updated, all pending update requests should await the
992 // new update request instead of the previous one.
993 if (!forceUpdate && this._updatePromise) {
994 return this._updatePromise;
996 const isUpdateReplaced = () => this._updatePromise != updatePromise;
997 const updatePromise = (async () => {
998 if (!gBlocklistEnabled) {
999 this._mlbfData = null;
1000 this._stashes = null;
1003 let records = await this._client.get();
1004 if (isUpdateReplaced()) {
1008 let mlbfRecords = records
1009 .filter(r => r.attachment)
1010 // Newest attachments first.
1011 .sort((a, b) => b.generation_time - a.generation_time);
1012 const mlbfRecord = mlbfRecords.find(
1013 r => r.attachment_type == "bloomfilter-base"
1015 this._stashes = records
1016 .filter(({ stash }) => {
1018 // Exclude non-stashes, e.g. MLBF attachments.
1020 // Sanity check for type.
1021 Array.isArray(stash.blocked) &&
1022 Array.isArray(stash.unblocked)
1025 // Sort by stash time - newest first.
1026 .sort((a, b) => b.stash_time - a.stash_time)
1027 .map(({ stash, stash_time }) => ({
1028 blocked: new Set(stash.blocked),
1029 unblocked: new Set(stash.unblocked),
1033 let mlbf = await this._fetchMLBF(mlbfRecord);
1034 // When a MLBF dump is packaged with the browser, mlbf will always be
1035 // non-null at this point.
1036 if (isUpdateReplaced()) {
1039 this._mlbfData = mlbf;
1045 if (!isUpdateReplaced()) {
1046 this._updatePromise = null;
1047 this._recordPostUpdateTelemetry();
1049 return this._updatePromise;
1051 this._updatePromise = updatePromise;
1052 return updatePromise;
1055 // Update the telemetry of the blocklist. This is always called, even if
1056 // the update request failed (e.g. due to network errors or data corruption).
1057 _recordPostUpdateTelemetry() {
1058 BlocklistTelemetry.recordRSBlocklistLastModified(
1062 Glean.blocklist.mlbfSource.set(
1063 this._mlbfData?.rsAttachmentSource || "unknown"
1065 BlocklistTelemetry.recordTimeScalar(
1066 "mlbf_generation_time",
1067 this._mlbfData?.generationTime
1069 BlocklistTelemetry.recordGleanDateTime(
1070 Glean.blocklist.mlbfGenerationTime,
1071 this._mlbfData?.generationTime
1073 // stashes has conveniently already been sorted by stash_time, newest first.
1074 let stashes = this._stashes || [];
1075 BlocklistTelemetry.recordTimeScalar(
1076 "mlbf_stash_time_oldest",
1077 stashes[stashes.length - 1]?.stash_time
1079 BlocklistTelemetry.recordTimeScalar(
1080 "mlbf_stash_time_newest",
1081 stashes[0]?.stash_time
1083 BlocklistTelemetry.recordGleanDateTime(
1084 Glean.blocklist.mlbfStashTimeOldest,
1085 stashes[stashes.length - 1]?.stash_time
1088 BlocklistTelemetry.recordGleanDateTime(
1089 Glean.blocklist.mlbfStashTimeNewest,
1090 stashes[0]?.stash_time
1094 // Used by BlocklistTelemetry.recordAddonBlockChangeTelemetry.
1095 getBlocklistMetadataForTelemetry() {
1096 // Blocklist telemetry can only be reported when a blocklist decision
1097 // has been made. That implies that the blocklist has been loaded, so
1098 // ExtensionBlocklistMLBF should have been initialized.
1099 // (except when the blocklist is disabled, or blocklist v2 is used)
1100 const generationTime = this._mlbfData?.generationTime ?? 0;
1102 // Keys to include in the blocklist.addonBlockChange telemetry event.
1105 // stashes are sorted, newest first. Stashes are newer than the MLBF.
1106 `${this._stashes?.[0]?.stash_time ?? generationTime}`,
1107 mlbf_generation: `${generationTime}`,
1108 mlbf_source: this._mlbfData?.rsAttachmentSource ?? "unknown",
1112 ensureInitialized() {
1113 if (!gBlocklistEnabled || this._initialized) {
1116 this._initialized = true;
1117 this._client = lazy.RemoteSettings("addons-bloomfilters", {
1118 bucketName: BLOCKLIST_BUCKET,
1119 // Prevent the attachment for being pruned, since its ID does
1120 // not match any record.
1121 keepAttachmentsIds: [this.RS_ATTACHMENT_ID],
1123 this._onUpdate = this._onUpdate.bind(this);
1124 this._client.on("sync", this._onUpdate);
1129 this._client.off("sync", this._onUpdate);
1130 this._didShutdown = true;
1134 // Called when the blocklist implementation is changed via a pref.
1136 if (this._didShutdown) {
1137 this._client.on("sync", this._onUpdate);
1138 this._didShutdown = false;
1143 this.ensureInitialized();
1144 await this._updateMLBF(true);
1146 let addons = await lazy.AddonManager.getAddonsByTypes(lazy.kXPIAddonTypes);
1147 for (let addon of addons) {
1148 let oldState = addon.blocklistState;
1149 await addon.updateBlocklistState(false);
1150 let state = addon.blocklistState;
1153 "Blocklist state for " +
1161 // We don't want to re-warn about add-ons
1162 if (state == oldState) {
1166 // Ensure that softDisabled is false if the add-on is not soft blocked
1167 // (by a previous implementation of the blocklist).
1168 if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
1169 await addon.setSoftDisabled(false);
1172 BlocklistTelemetry.recordAddonBlockChangeTelemetry(
1178 lazy.AddonManagerPrivate.updateAddonAppDisabledStates();
1181 async getState(addon) {
1182 let state = await this.getEntry(addon);
1183 return state ? state.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
1186 async getEntry(addon) {
1187 if (!this._stashes) {
1188 this.ensureInitialized();
1189 await this._updateMLBF(false);
1190 } else if (this._updatePromise) {
1191 // _stashes has been initialized, but the initialization of _mlbfData is
1193 await this._updatePromise;
1196 let blockKey = addon.id + ":" + addon.version;
1198 // _stashes will be unset if !gBlocklistEnabled.
1199 if (this._stashes) {
1200 // Stashes are ordered by newest first.
1201 for (let stash of this._stashes) {
1202 // blocked and unblocked do not have overlapping entries.
1203 if (stash.blocked.has(blockKey)) {
1204 return this._createBlockEntry(addon);
1206 if (stash.unblocked.has(blockKey)) {
1212 // signedDate is a Date if the add-on is signed, null if not signed,
1213 // undefined if it's an addon update descriptor instead of an addon wrapper.
1214 let { signedDate } = addon;
1216 // The MLBF does not apply to unsigned add-ons.
1220 if (!this._mlbfData) {
1221 // This could happen in theory in any of the following cases:
1222 // - the blocklist is disabled.
1223 // - The RemoteSettings backend served a malformed MLBF.
1224 // - The RemoteSettings backend is unreachable, and this client was built
1225 // without including a dump of the MLBF.
1227 // ... in other words, this is unlikely to happen in practice.
1230 let { cascadeFilter, generationTime } = this._mlbfData;
1231 if (!cascadeFilter.has(blockKey)) {
1232 // Add-on not blocked or unknown.
1235 // Add-on blocked, or unknown add-on inadvertently labeled as blocked.
1237 let { signedState } = addon;
1239 signedState !== lazy.AddonManager.SIGNEDSTATE_PRELIMINARY &&
1240 signedState !== lazy.AddonManager.SIGNEDSTATE_SIGNED
1242 // The block decision can only be relied upon for known add-ons, i.e.
1243 // signed via AMO. Anything else is unknown and ignored:
1245 // - SIGNEDSTATE_SYSTEM and SIGNEDSTATE_PRIVILEGED are signed
1246 // independently of AMO.
1248 // - SIGNEDSTATE_NOT_REQUIRED already has an early return above due to
1249 // signedDate being unset for these kinds of add-ons.
1251 // - SIGNEDSTATE_BROKEN, SIGNEDSTATE_UNKNOWN and SIGNEDSTATE_MISSING
1252 // means that the signature cannot be relied upon. It is equivalent to
1253 // removing the signature from the XPI file, which already causes them
1254 // to be disabled on release builds (where MOZ_REQUIRE_SIGNING=true).
1258 if (signedDate.getTime() > generationTime) {
1259 // The bloom filter only reports 100% accurate results for known add-ons.
1260 // Since the add-on was unknown when the bloom filter was generated, the
1261 // block decision is incorrect and should be treated as unblocked.
1265 if (AppConstants.NIGHTLY_BUILD && addon.type === "locale") {
1266 // Only Mozilla can create langpacks with a valid signature.
1267 // Langpacks for Release, Beta and ESR are submitted to AMO.
1268 // DevEd does not support external langpacks (bug 1563923), only builtins.
1269 // (and built-in addons are not subjected to the blocklist).
1270 // Langpacks for Nightly are not known to AMO, so the MLBF cannot be used.
1274 return this._createBlockEntry(addon);
1277 _createBlockEntry(addon) {
1279 state: Ci.nsIBlocklistService.STATE_BLOCKED,
1280 url: this.createBlocklistURL(addon.id, addon.version),
1284 createBlocklistURL(id, version) {
1285 let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ADDONITEM_URL);
1286 return url.replace(/%addonID%/g, id).replace(/%addonVersion%/g, version);
1290 const EXTENSION_BLOCK_FILTERS = [
1298 var gLoggingEnabled = null;
1299 var gBlocklistEnabled = true;
1300 var gBlocklistLevel = DEFAULT_LEVEL;
1303 * @class nsIBlocklistPrompt
1305 * nsIBlocklistPrompt is used, if available, by the default implementation of
1306 * nsIBlocklistService to display a confirmation UI to the user before blocking
1307 * extensions/plugins.
1312 * Prompt the user about newly blocked addons. The prompt is then resposible
1313 * for soft-blocking any addons that need to be afterwards
1315 * @param {object[]} aAddons
1316 * An array of addons and plugins that are blocked. These are javascript
1317 * objects with properties:
1318 * name - the plugin or extension name,
1319 * version - the version of the extension or plugin,
1320 * icon - the plugin or extension icon,
1321 * disable - can be used by the nsIBlocklistPrompt to allows users to decide
1322 * whether a soft-blocked add-on should be disabled,
1323 * blocked - true if the item is hard-blocked, false otherwise,
1324 * item - the Addon object
1327 // It is not possible to use the one in Services since it will not successfully
1328 // QueryInterface nsIXULAppInfo in xpcshell tests due to other code calling
1329 // Services.appinfo before the nsIXULAppInfo is created by the tests.
1330 ChromeUtils.defineLazyGetter(lazy, "gApp", function () {
1331 // eslint-disable-next-line mozilla/use-services
1332 let appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
1334 appinfo.QueryInterface(Ci.nsIXULAppInfo);
1336 // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
1338 !(ex instanceof Components.Exception) ||
1339 ex.result != Cr.NS_NOINTERFACE
1347 ChromeUtils.defineLazyGetter(lazy, "gAppID", function () {
1348 return lazy.gApp.ID;
1350 ChromeUtils.defineLazyGetter(lazy, "gAppOS", function () {
1351 return lazy.gApp.OS;
1355 * Logs a string to the error console.
1356 * @param {string} string
1357 * The string to write to the error console..
1359 function LOG(string) {
1360 if (gLoggingEnabled) {
1361 dump("*** " + string + "\n");
1362 Services.console.logStringMessage(string);
1366 export let Blocklist = {
1368 Services.obs.addObserver(this, "xpcom-shutdown");
1369 gLoggingEnabled = Services.prefs.getBoolPref(
1370 PREF_EM_LOGGING_ENABLED,
1373 gBlocklistEnabled = Services.prefs.getBoolPref(
1374 PREF_BLOCKLIST_ENABLED,
1377 gBlocklistLevel = Math.min(
1378 Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
1381 this._chooseExtensionBlocklistImplementationFromPref();
1382 Services.prefs.addObserver("extensions.blocklist.", this);
1383 Services.prefs.addObserver(PREF_EM_LOGGING_ENABLED, this);
1384 BlocklistTelemetry.init();
1389 GfxBlocklistRS.shutdown();
1390 this.ExtensionBlocklist.shutdown();
1392 Services.obs.removeObserver(this, "xpcom-shutdown");
1393 Services.prefs.removeObserver("extensions.blocklist.", this);
1394 Services.prefs.removeObserver(PREF_EM_LOGGING_ENABLED, this);
1397 observe(subject, topic, prefName) {
1399 case "xpcom-shutdown":
1402 case "nsPref:changed":
1404 case PREF_EM_LOGGING_ENABLED:
1405 gLoggingEnabled = Services.prefs.getBoolPref(
1406 PREF_EM_LOGGING_ENABLED,
1410 case PREF_BLOCKLIST_ENABLED:
1411 gBlocklistEnabled = Services.prefs.getBoolPref(
1412 PREF_BLOCKLIST_ENABLED,
1415 this._blocklistUpdated();
1417 case PREF_BLOCKLIST_LEVEL:
1418 gBlocklistLevel = Math.min(
1419 Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
1422 this._blocklistUpdated();
1424 case PREF_BLOCKLIST_USE_MLBF:
1425 let oldImpl = this.ExtensionBlocklist;
1426 this._chooseExtensionBlocklistImplementationFromPref();
1427 // The implementation may be unchanged when the pref is ignored.
1428 if (oldImpl != this.ExtensionBlocklist && oldImpl._initialized) {
1430 this.ExtensionBlocklist.undoShutdown();
1431 this.ExtensionBlocklist._onUpdate();
1432 } // else neither has been initialized yet. Wait for it to happen.
1439 loadBlocklistAsync() {
1440 // Need to ensure we notify gfx of new stuff.
1441 // Geckoview calls this for each new tab (bug 1730026), so ensure we only
1442 // check for entries when first initialized.
1443 if (!GfxBlocklistRS._initialized) {
1444 GfxBlocklistRS.checkForEntries();
1446 this.ExtensionBlocklist.ensureInitialized();
1449 getAddonBlocklistState(addon, appVersion, toolkitVersion) {
1450 // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS.
1451 return this.ExtensionBlocklist.getState(addon, appVersion, toolkitVersion);
1454 getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
1455 // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS.
1456 return this.ExtensionBlocklist.getEntry(addon, appVersion, toolkitVersion);
1459 recordAddonBlockChangeTelemetry(addon, reason) {
1460 BlocklistTelemetry.recordAddonBlockChangeTelemetry(addon, reason);
1462 // TODO bug 1649906: Remove blocklist v2 (dead code).
1463 allowDeprecatedBlocklistV2: false,
1465 _chooseExtensionBlocklistImplementationFromPref() {
1467 this.allowDeprecatedBlocklistV2 &&
1468 !Services.prefs.getBoolPref(PREF_BLOCKLIST_USE_MLBF, false)
1470 this.ExtensionBlocklist = ExtensionBlocklistRS;
1472 this.ExtensionBlocklist = ExtensionBlocklistMLBF;
1476 _blocklistUpdated() {
1477 this.ExtensionBlocklist._onUpdate();
1483 // Allow tests to reach implementation objects.
1484 export const BlocklistPrivate = {
1486 ExtensionBlocklistMLBF,
1487 ExtensionBlocklistRS,