Skip to content

Commit

Permalink
Bug 1620621 - Unit tests for MLBF-based blocklist r=Gijs
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob--W committed Apr 29, 2020
1 parent 045492d commit e8e5d91
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,9 @@ module.exports = {
"toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js",
"toolkit/mozapps/extensions/test/xpcshell/head_addons.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_clients.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_regexp_split.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_targetapp_filter.js",
"toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_telemetry.js",
Expand Down
1 change: 1 addition & 0 deletions toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ var AddonTestUtils = {
);
const blocklistMapping = {
extensions: bsPass.ExtensionBlocklistRS,
extensionsMLBF: bsPass.ExtensionBlocklistMLBF,
plugins: bsPass.PluginBlocklistRS,
};

Expand Down
Binary file not shown.
12 changes: 12 additions & 0 deletions toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
// Appease eslint.
/* import-globals-from ../head_addons.js */

const MLBF_RECORD = {
id: "A blocklist entry that refers to a MLBF file",
last_modified: 1,
attachment: {
size: 32,
hash: "6af648a5d6ce6dbee99b0aab1780d24d204977a6606ad670d5372ef22fac1052",
filename: "does-not-matter.bin",
},
attachment_type: "bloomfilter-full",
generation_time: 1577833200000,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true);

const { ExtensionBlocklistMLBF } = ChromeUtils.import(
"resource://gre/modules/Blocklist.jsm",
null
);

createAppInfo("[email protected]", "XPCShell", "1", "1");
AddonTestUtils.useRealCertChecks = true;

// A real, signed XPI for use in the test.
const SIGNED_ADDON_XPI_FILE = do_get_file("../data/webext-implicit-id.xpi");
const SIGNED_ADDON_ID = "[email protected]";
const SIGNED_ADDON_VERSION = "1.0";
const SIGNED_ADDON_KEY = `${SIGNED_ADDON_ID}:${SIGNED_ADDON_VERSION}`;
const SIGNED_ADDON_SIGN_TIME = 1459980789000; // notBefore of certificate.

function mockMLBF({ blocked = [], notblocked = [], generationTime }) {
// Mock _fetchMLBF to be able to have a deterministic cascade filter.
ExtensionBlocklistMLBF._fetchMLBF = async () => {
return {
cascadeFilter: {
has(blockKey) {
if (blocked.includes(blockKey)) {
return true;
}
if (notblocked.includes(blockKey)) {
return false;
}
throw new Error(`Block entry must explicitly be listed: ${blockKey}`);
},
},
generationTime,
};
};
}

add_task(async function setup() {
await promiseStartupManager();
mockMLBF({});
await AddonTestUtils.loadBlocklistRawData({
extensionsMLBF: [MLBF_RECORD],
});
});

// Checks: Initially unblocked, then blocked, then unblocked again.
add_task(async function signed_xpi_initially_unblocked() {
mockMLBF({
blocked: [],
notblocked: [SIGNED_ADDON_KEY],
generationTime: SIGNED_ADDON_SIGN_TIME + 1,
});
await ExtensionBlocklistMLBF._onUpdate();

await promiseInstallFile(SIGNED_ADDON_XPI_FILE);

let addon = await promiseAddonByID(SIGNED_ADDON_ID);
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);

mockMLBF({
blocked: [SIGNED_ADDON_KEY],
notblocked: [],
generationTime: SIGNED_ADDON_SIGN_TIME + 1,
});
await ExtensionBlocklistMLBF._onUpdate();
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED);
Assert.deepEqual(
await Blocklist.getAddonBlocklistEntry(addon),
{
state: Ci.nsIBlocklistService.STATE_BLOCKED,
url:
"https://addons.mozilla.org/en-US/xpcshell/blocked-addon/[email protected]/1.0/",
},
"Blocked addon should have blocked entry"
);

mockMLBF({
blocked: [SIGNED_ADDON_KEY],
notblocked: [],
// MLBF generationTime is older, so "blocked" entry should not apply.
generationTime: SIGNED_ADDON_SIGN_TIME - 1,
});
await ExtensionBlocklistMLBF._onUpdate();
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);

await addon.uninstall();
});

// Checks: Initially blocked on install, then unblocked.
add_task(async function signed_xpi_blocked_on_install() {
mockMLBF({
blocked: [SIGNED_ADDON_KEY],
notblocked: [],
generationTime: SIGNED_ADDON_SIGN_TIME + 1,
});
await ExtensionBlocklistMLBF._onUpdate();

await promiseInstallFile(SIGNED_ADDON_XPI_FILE);
let addon = await promiseAddonByID(SIGNED_ADDON_ID);
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED);
Assert.ok(addon.appDisabled, "Blocked add-on is disabled on install");

mockMLBF({
blocked: [],
notblocked: [SIGNED_ADDON_KEY],
generationTime: SIGNED_ADDON_SIGN_TIME - 1,
});
await ExtensionBlocklistMLBF._onUpdate();
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
Assert.ok(!addon.appDisabled, "Re-enabled after unblock");

await addon.uninstall();
});

// An unsigned add-on cannot be blocked.
add_task(async function unsigned_not_blocked() {
const UNSIGNED_ADDON_ID = "[email protected]";
const UNSIGNED_ADDON_VERSION = "1.0";
const UNSIGNED_ADDON_KEY = `${UNSIGNED_ADDON_ID}:${UNSIGNED_ADDON_VERSION}`;
mockMLBF({
blocked: [UNSIGNED_ADDON_KEY],
notblocked: [],
generationTime: SIGNED_ADDON_SIGN_TIME + 1,
});
await ExtensionBlocklistMLBF._onUpdate();

let unsignedAddonFile = createTempWebExtensionFile({
manifest: {
version: UNSIGNED_ADDON_VERSION,
applications: { gecko: { id: UNSIGNED_ADDON_ID } },
},
});

// Unsigned add-ons can generally only be loaded as a temporary install.
let [addon] = await Promise.all([
AddonManager.installTemporaryAddon(unsignedAddonFile),
promiseWebExtensionStartup(UNSIGNED_ADDON_ID),
]);
Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
Assert.equal(
await Blocklist.getAddonBlocklistState(addon),
Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
"Unsigned temporary add-on is not blocked"
);
await addon.uninstall();
});

// To make sure that unsigned_not_blocked did not trivially pass, we also check
// that add-ons can actually be blocked when installed as a temporary add-on.
add_task(async function signed_temporary() {
mockMLBF({
blocked: [SIGNED_ADDON_KEY],
notblocked: [],
generationTime: SIGNED_ADDON_SIGN_TIME + 1,
});
await ExtensionBlocklistMLBF._onUpdate();

await Assert.rejects(
AddonManager.installTemporaryAddon(SIGNED_ADDON_XPI_FILE),
/Add-on [email protected] is not compatible with application version/,
"Blocklisted add-on cannot be installed"
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
* @fileOverview Tests the MLBF and RemoteSettings synchronization logic.
*/

Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true);

const { Downloader } = ChromeUtils.import(
"resource://services-settings/Attachments.jsm"
);

const { ExtensionBlocklistMLBF } = ChromeUtils.import(
"resource://gre/modules/Blocklist.jsm",
null
);

// This test needs to interact with the RemoteSettings client.
ExtensionBlocklistMLBF.ensureInitialized();

add_task(async function fetch_invalid_mlbf_record() {
let invalidRecord = {
attachment: { size: 1, hash: "definitely not valid" },
generation_time: 1,
};

let resultPromise = ExtensionBlocklistMLBF._fetchMLBF(invalidRecord);

// TODO bug ...: When the MLBF is packaged with the application, this
// assertion should be updated to pass.
await Assert.rejects(resultPromise, /NetworkError/, "record not found");

// Forget about the packaged attachment.
Downloader._RESOURCE_BASE_URL = "invalid://bogus";
await Assert.rejects(
ExtensionBlocklistMLBF._fetchMLBF(invalidRecord),
/NetworkError/,
"record not found when there is no packaged MLBF"
);
});

// Other tests can mock _testMLBF, so let's verify that it works as expected.
add_task(async function fetch_valid_mlbf() {
const url = Services.io.newFileURI(
do_get_file("../data/mlbf-blocked1-unblocked2.bin")
).spec;
Cu.importGlobalProperties(["fetch"]);
const blob = await (await fetch(url)).blob();

await ExtensionBlocklistMLBF._client.db.saveAttachment(
ExtensionBlocklistMLBF.RS_ATTACHMENT_ID,
{ record: JSON.parse(JSON.stringify(MLBF_RECORD)), blob }
);

const result = await ExtensionBlocklistMLBF._fetchMLBF(MLBF_RECORD);
Assert.equal(result.cascadeHash, MLBF_RECORD.attachment.hash, "hash OK");
Assert.equal(result.generationTime, MLBF_RECORD.generation_time, "time OK");
Assert.ok(result.cascadeFilter.has("@blocked:1"), "item blocked");
Assert.ok(!result.cascadeFilter.has("@unblocked:2"), "item not blocked");

const result2 = await ExtensionBlocklistMLBF._fetchMLBF({
attachment: { size: 1, hash: "invalid" },
generation_time: Date.now(),
});
Assert.equal(
result2.cascadeHash,
MLBF_RECORD.attachment.hash,
"The cached MLBF should be used when the attachment is invalid"
);

// The attachment is kept in the database for use by the next test task.
});

// Test that results of the public API are consistent with the MLBF file.
add_task(async function public_api_uses_mlbf() {
createAppInfo("[email protected]", "XPCShell", "1", "1");
await promiseStartupManager();

const blockedAddon = {
id: "@blocked",
version: "1",
signedState: 2, // = AddonManager.SIGNEDSTATE_SIGNED.
signedDate: 0, // a date in the past, before MLBF's generationTime.
};
const nonBlockedAddon = {
id: "@unblocked",
version: "2",
signedState: 2, // = AddonManager.SIGNEDSTATE_SIGNED.
signedDate: 0, // a date in the past, before MLBF's generationTime.
};

await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [MLBF_RECORD] });

Assert.deepEqual(
await Blocklist.getAddonBlocklistEntry(blockedAddon),
{
state: Ci.nsIBlocklistService.STATE_BLOCKED,
url:
"https://addons.mozilla.org/en-US/xpcshell/blocked-addon/@blocked/1/",
},
"Blocked addon should have blocked entry"
);

Assert.deepEqual(
await Blocklist.getAddonBlocklistEntry(nonBlockedAddon),
null,
"Non-blocked addon should not be blocked"
);

Assert.equal(
await Blocklist.getAddonBlocklistState(blockedAddon),
Ci.nsIBlocklistService.STATE_BLOCKED,
"Blocked entry should have blocked state"
);

Assert.equal(
await Blocklist.getAddonBlocklistState(nonBlockedAddon),
Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
"Non-blocked entry should have unblocked state"
);

// Note: Blocklist collection and attachment carries over to the next test.
});

// Checks the remaining cases of database corruption that haven't been handled
// before.
add_task(async function handle_database_corruption() {
const blockedAddon = {
id: "@blocked",
version: "1",
signedState: 2, // = AddonManager.SIGNEDSTATE_SIGNED.
signedDate: 0, // a date in the past, before MLBF's generationTime.
};
async function checkBlocklistWorks() {
Assert.equal(
await Blocklist.getAddonBlocklistState(blockedAddon),
Ci.nsIBlocklistService.STATE_BLOCKED,
"Add-on should be blocked by the blocklist"
);
}

// In the fetch_invalid_mlbf_record we checked that a cached / packaged MLBF
// attachment is used as a fallback when the record is invalid. Here we also
// check that there is a fallback when there is no record at all.

await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
// When the collection is empty, the last known MLBF should be used anyway.
await checkBlocklistWorks();

// Now we also remove the cached file...
await ExtensionBlocklistMLBF._client.db.saveAttachment(
ExtensionBlocklistMLBF.RS_ATTACHMENT_ID,
null
);
// Deleting the file shouldn't cause issues because the MLBF is loaded once
// and then kept in memory.
await checkBlocklistWorks();

// Force an update while we don't have any blocklist data nor cache.
await ExtensionBlocklistMLBF._onUpdate();
// As a fallback, continue to use the in-memory version of the blocklist.
await checkBlocklistWorks();

// Memory gone, e.g. after a browser restart.
delete ExtensionBlocklistMLBF._mlbfData;
Assert.equal(
await Blocklist.getAddonBlocklistState(blockedAddon),
Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
"Blocklist can't work if all blocklist data is gone"
);
});
Loading

0 comments on commit e8e5d91

Please sign in to comment.