forked from mirrors/gecko-dev
Bug 1896628 - [DNR] Enforce a limit to the maximum number of static rules disabled individually. r=robwu
Differential Revision: https://phabricator.services.mozilla.com/D211727
This commit is contained in:
parent
e3ce8f90ca
commit
cca0097e2b
6 changed files with 299 additions and 11 deletions
|
|
@ -34,6 +34,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||
20
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"MAX_NUMBER_OF_DISABLED_STATIC_RULES",
|
||||
"extensions.dnr.max_number_of_disabled_static_rules",
|
||||
5000
|
||||
);
|
||||
|
||||
/**
|
||||
* NOTE: this limit may be increased in the future, see
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1894119
|
||||
|
|
@ -86,6 +93,14 @@ export const ExtensionDNRLimits = {
|
|||
return lazy.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS;
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum number of static rules that can be disabled on each static
|
||||
* ruleset.
|
||||
*/
|
||||
get MAX_NUMBER_OF_DISABLED_STATIC_RULES() {
|
||||
return lazy.MAX_NUMBER_OF_DISABLED_STATIC_RULES;
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum number of dynamic rules an extension can add.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs"
|
|||
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
|
||||
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
import { ExtensionDNRLimits } from "./ExtensionDNRLimits.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ class StoreData {
|
|||
// Changelog:
|
||||
// - 1: Initial DNR store schema:
|
||||
// Initial implementation officially release in Firefox 113.
|
||||
// Support for disableStaticRules added in Firefox 128 (Bug 1810762).
|
||||
// Support for disableStaticRuleIds added in Firefox 128 (Bug 1810762).
|
||||
static VERSION = 1;
|
||||
|
||||
static getLastUpdateTagPref(extensionUUID) {
|
||||
|
|
@ -168,6 +169,7 @@ class StoreData {
|
|||
// The lastUpdateTag gets set (and updated) by calls to updateRulesets.
|
||||
this.lastUpdateTag = undefined;
|
||||
this.#initialLastUdateTag = lastUpdateTag;
|
||||
|
||||
this.#updateRulesets({
|
||||
staticRulesets: staticRulesets ?? new Map(),
|
||||
disabledStaticRuleIds: disabledStaticRuleIds ?? {},
|
||||
|
|
@ -1269,6 +1271,29 @@ class RulesetsStore {
|
|||
result = null;
|
||||
}
|
||||
|
||||
// If the number of disabled rules exceeds the limit when loaded from the store
|
||||
// (e.g. if the limit has been customized through prefs, and so not expected to
|
||||
// be a common case), then we drop the entire list of disabled rules.
|
||||
if (result?.disabledStaticRuleIds) {
|
||||
for (const [rulesetId, disabledRuleIds] of Object.entries(
|
||||
result.disabledStaticRuleIds
|
||||
)) {
|
||||
if (
|
||||
Array.isArray(disabledRuleIds) &&
|
||||
disabledRuleIds.length <=
|
||||
ExtensionDNRLimits.MAX_NUMBER_OF_DISABLED_STATIC_RULES
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Cu.reportError(
|
||||
`Discard "${extension.id}" static ruleset "${rulesetId}" disabled rules` +
|
||||
` for exceeding the MAX_NUMBER_OF_DISABLED_STATIC_RULES (${ExtensionDNRLimits.MAX_NUMBER_OF_DISABLED_STATIC_RULES})`
|
||||
);
|
||||
result.disabledStaticRuleIds[rulesetId] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Use defaults and extension manifest if no data stored was found
|
||||
// (or it got reset due to an unsupported profile downgrade being detected).
|
||||
if (!result) {
|
||||
|
|
@ -1715,6 +1740,15 @@ class RulesetsStore {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
disabledRuleIdsSet.size >
|
||||
ExtensionDNRLimits.MAX_NUMBER_OF_DISABLED_STATIC_RULES
|
||||
) {
|
||||
throw new ExtensionError(
|
||||
`Number of individually disabled static rules exceeds MAX_NUMBER_OF_DISABLED_STATIC_RULES limit`
|
||||
);
|
||||
}
|
||||
|
||||
// Chrome doesn't seem to validate if the rule id actually exists in the ruleset,
|
||||
// and so set the resulting updated array of disabled rule ids right away.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ this.declarativeNetRequest = class extends ExtensionAPI {
|
|||
get MAX_NUMBER_OF_ENABLED_STATIC_RULESETS() {
|
||||
return ExtensionDNRLimits.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS;
|
||||
},
|
||||
get MAX_NUMBER_OF_DISABLED_STATIC_RULES() {
|
||||
return ExtensionDNRLimits.MAX_NUMBER_OF_DISABLED_STATIC_RULES;
|
||||
},
|
||||
get MAX_NUMBER_OF_DYNAMIC_RULES() {
|
||||
return ExtensionDNRLimits.MAX_NUMBER_OF_DYNAMIC_RULES;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -851,6 +851,10 @@
|
|||
"type": "number",
|
||||
"description": "The maximum number of static Rulesets an extension can specify as part of the rule_resources manifest key."
|
||||
},
|
||||
"MAX_NUMBER_OF_DISABLED_STATIC_RULES": {
|
||||
"type": "number",
|
||||
"description": "The maximum number of static rules that can be disabled on each static ruleset."
|
||||
},
|
||||
"MAX_NUMBER_OF_ENABLED_STATIC_RULESETS": {
|
||||
"type": "number",
|
||||
"description": "The maximum number of static Rulesets an extension can enable at any one time."
|
||||
|
|
|
|||
|
|
@ -358,6 +358,7 @@ add_task(async function test_dnr_limits_namespace_properties() {
|
|||
GUARANTEED_MINIMUM_STATIC_RULES,
|
||||
MAX_NUMBER_OF_STATIC_RULESETS,
|
||||
MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
|
||||
MAX_NUMBER_OF_DISABLED_STATIC_RULES,
|
||||
MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES,
|
||||
MAX_NUMBER_OF_DYNAMIC_RULES,
|
||||
MAX_NUMBER_OF_SESSION_RULES,
|
||||
|
|
@ -367,6 +368,7 @@ add_task(async function test_dnr_limits_namespace_properties() {
|
|||
GUARANTEED_MINIMUM_STATIC_RULES,
|
||||
MAX_NUMBER_OF_STATIC_RULESETS,
|
||||
MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
|
||||
MAX_NUMBER_OF_DISABLED_STATIC_RULES,
|
||||
MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES,
|
||||
MAX_NUMBER_OF_DYNAMIC_RULES,
|
||||
MAX_NUMBER_OF_SESSION_RULES,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,21 @@ Services.scriptloader.loadSubScript(
|
|||
this
|
||||
);
|
||||
|
||||
async function dropDNRStartupCache(dnrStore, extension) {
|
||||
// Drop the DNRStore cache file to ensure we will have to load
|
||||
// back the DNRStore data from the JSONFile.
|
||||
const { cacheFile } = dnrStore.getFilePaths(extension.uuid);
|
||||
ok(
|
||||
await IOUtils.exists(cacheFile),
|
||||
`Expect a DNRStore cache file found at ${cacheFile}`
|
||||
);
|
||||
await IOUtils.remove(cacheFile);
|
||||
ok(
|
||||
!(await IOUtils.exists(cacheFile)),
|
||||
`Expect a DNRStore cache file ${cacheFile} to be removed`
|
||||
);
|
||||
}
|
||||
|
||||
add_setup(async () => {
|
||||
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
|
||||
Services.prefs.setBoolPref("extensions.dnr.enabled", true);
|
||||
|
|
@ -240,16 +255,7 @@ add_task(async function test_update_individual_static_rules() {
|
|||
|
||||
// Drop the DNRStore cache file to ensure we will have to load
|
||||
// back the DNRStore data from the JSONFile.
|
||||
const { cacheFile } = dnrStore.getFilePaths(extension.uuid);
|
||||
ok(
|
||||
await IOUtils.exists(cacheFile),
|
||||
`Expect a DNRStore cache file found at ${cacheFile}`
|
||||
);
|
||||
await IOUtils.remove(cacheFile);
|
||||
ok(
|
||||
!(await IOUtils.exists(cacheFile)),
|
||||
`Expect a DNRStore cache file ${cacheFile} to be removed`
|
||||
);
|
||||
await dropDNRStartupCache(dnrStore, extension);
|
||||
|
||||
// Recreate a new DNR store to clear the data still cached in memory.
|
||||
ExtensionDNRStore._recreateStoreForTesting();
|
||||
|
|
@ -333,3 +339,227 @@ add_task(async function test_update_individual_static_rules() {
|
|||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.dnr.max_number_of_disabled_static_rules", 5]],
|
||||
},
|
||||
async function test_max_disabled_static_rules_limit() {
|
||||
const { MAX_NUMBER_OF_DISABLED_STATIC_RULES } = ExtensionDNRLimits;
|
||||
|
||||
const dnrRuleCommon = {
|
||||
action: { type: "block" },
|
||||
condition: {
|
||||
resourceTypes: ["xmlhttprequest"],
|
||||
requestDomains: ["example.com"],
|
||||
},
|
||||
};
|
||||
const ruleset1 = [];
|
||||
const ruleset2 = [];
|
||||
|
||||
for (let i = 0; i < MAX_NUMBER_OF_DISABLED_STATIC_RULES + 1; i++) {
|
||||
const id = i + 1;
|
||||
ruleset1.push(getDNRRule({ ...dnrRuleCommon, id }));
|
||||
ruleset2.push(getDNRRule({ ...dnrRuleCommon, id }));
|
||||
}
|
||||
|
||||
const rule_resources = [
|
||||
{
|
||||
id: "ruleset1",
|
||||
enabled: false,
|
||||
path: "ruleset1.json",
|
||||
},
|
||||
{
|
||||
id: "ruleset2",
|
||||
enabled: true,
|
||||
path: "ruleset2.json",
|
||||
},
|
||||
];
|
||||
|
||||
const files = {
|
||||
"ruleset1.json": JSON.stringify(ruleset1),
|
||||
"ruleset2.json": JSON.stringify(ruleset2),
|
||||
};
|
||||
|
||||
const extension = ExtensionTestUtils.loadExtension(
|
||||
getDNRExtension({
|
||||
id: "max-disabled-static-rules-limit@xpcshell",
|
||||
rule_resources,
|
||||
files,
|
||||
})
|
||||
);
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("bgpage:ready");
|
||||
|
||||
// Sanity check.
|
||||
await assertDNRGetEnabledRulesets(extension, ["ruleset2"]);
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset1" }, []);
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset2" }, []);
|
||||
|
||||
// Disable a number of rules below the limit.
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset1",
|
||||
disableRuleIds: [1],
|
||||
});
|
||||
await extension.awaitMessage("updateStaticRules:done");
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset1" }, [
|
||||
1,
|
||||
]);
|
||||
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset2",
|
||||
disableRuleIds: [2],
|
||||
});
|
||||
await extension.awaitMessage("updateStaticRules:done");
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset2" }, [
|
||||
2,
|
||||
]);
|
||||
|
||||
// Verify limit applied on both enabled and disabled rules.
|
||||
const rejectedWithErrorMessage =
|
||||
"Number of individually disabled static rules exceeds MAX_NUMBER_OF_DISABLED_STATIC_RULES limit";
|
||||
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset1",
|
||||
disableRuleIds: ruleset1.map(rule => rule.id),
|
||||
});
|
||||
Assert.deepEqual(
|
||||
await extension.awaitMessage("updateStaticRules:done"),
|
||||
[{ rejectedWithErrorMessage }],
|
||||
"Got the expected error rejected by updateStaticRules exceeding limit on disabled ruleset"
|
||||
);
|
||||
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset2",
|
||||
disableRuleIds: ruleset1.map(rule => rule.id),
|
||||
});
|
||||
Assert.deepEqual(
|
||||
await extension.awaitMessage("updateStaticRules:done"),
|
||||
[{ rejectedWithErrorMessage }],
|
||||
"Got the expected error rejected by updateStaticRules exceeding limit on enabled ruleset"
|
||||
);
|
||||
|
||||
// Expect the disabled rules to stay unchanged.
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset1" }, [
|
||||
1,
|
||||
]);
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset2" }, [
|
||||
2,
|
||||
]);
|
||||
|
||||
info(
|
||||
"Verify custom limit enforced when loading the DNR store data again (startup cache)"
|
||||
);
|
||||
// Increase the number of disabled rules without exceeding the current limit.
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset1",
|
||||
disableRuleIds: [1, 2, 3, 4],
|
||||
});
|
||||
await extension.awaitMessage("updateStaticRules:done");
|
||||
// Sanity check
|
||||
await assertDNRGetDisabledRuleIds(
|
||||
extension,
|
||||
{ rulesetId: "ruleset1" },
|
||||
[1, 2, 3, 4]
|
||||
);
|
||||
|
||||
// Make sure the DNR data is stored on disk.
|
||||
let dnrStore = ExtensionDNRStore._getStoreForTesting();
|
||||
await dnrStore.waitSaveCacheDataForTesting();
|
||||
|
||||
await AddonTestUtils.promiseShutdownManager();
|
||||
|
||||
// This pref value will be cleared when the tasks exits and the value set before
|
||||
// the prefs_set associated to this test task is restored.
|
||||
Services.prefs.setIntPref(
|
||||
"extensions.dnr.max_number_of_disabled_static_rules",
|
||||
2
|
||||
);
|
||||
|
||||
// Recreate a new DNR store to clear the data still cached in memory.
|
||||
ExtensionDNRStore._recreateStoreForTesting();
|
||||
|
||||
await AddonTestUtils.promiseStartupManager();
|
||||
|
||||
await extension.awaitStartup();
|
||||
await extension.awaitMessage("bgpage:ready");
|
||||
|
||||
// Expect ruleset1 disabled rules to be empty because it was exceeding the limit.
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset1" }, []);
|
||||
|
||||
// Expect ruleset2 disabled rules to have not been emptied.
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset2" }, [
|
||||
2,
|
||||
]);
|
||||
|
||||
// Try again after dropping the startup cache file (to make sure the limit
|
||||
// is enforced also when the data is loaded from the JSON file).
|
||||
info(
|
||||
"Verify custom limit enforced when loading the DNR store data again (JSON store)"
|
||||
);
|
||||
// Raise the limit again to prepare the initial state for testing it again.
|
||||
Services.prefs.setIntPref(
|
||||
"extensions.dnr.max_number_of_disabled_static_rules",
|
||||
5
|
||||
);
|
||||
|
||||
// Increase the number of disabled rules without exceeding the current limit.
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset2",
|
||||
disableRuleIds: [2, 3, 4, 5],
|
||||
});
|
||||
await extension.awaitMessage("updateStaticRules:done");
|
||||
// Add back disabled rules for ruleset1 to verify they don't get discarded.
|
||||
extension.sendMessage("updateStaticRules", {
|
||||
rulesetId: "ruleset1",
|
||||
disableRuleIds: [3],
|
||||
});
|
||||
await extension.awaitMessage("updateStaticRules:done");
|
||||
|
||||
// Sanity check
|
||||
await assertDNRGetDisabledRuleIds(
|
||||
extension,
|
||||
{ rulesetId: "ruleset2" },
|
||||
[2, 3, 4, 5]
|
||||
);
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset1" }, [
|
||||
3,
|
||||
]);
|
||||
|
||||
// Make sure the DNR data is stored on disk.
|
||||
dnrStore = ExtensionDNRStore._getStoreForTesting();
|
||||
await dnrStore.waitSaveCacheDataForTesting();
|
||||
|
||||
await AddonTestUtils.promiseShutdownManager();
|
||||
|
||||
// Drop the DNRStore cache file to ensure we will have to load
|
||||
// back the DNRStore data from the JSONFile.
|
||||
await dropDNRStartupCache(dnrStore, extension);
|
||||
|
||||
// Lower the limit again and verify it is enforced on the data
|
||||
// loaded back from the JSON file.
|
||||
Services.prefs.setIntPref(
|
||||
"extensions.dnr.max_number_of_disabled_static_rules",
|
||||
2
|
||||
);
|
||||
|
||||
// Recreate a new DNR store to clear the data still cached in memory.
|
||||
ExtensionDNRStore._recreateStoreForTesting();
|
||||
|
||||
await AddonTestUtils.promiseStartupManager();
|
||||
|
||||
await extension.awaitStartup();
|
||||
await extension.awaitMessage("bgpage:ready");
|
||||
|
||||
// Expect ruleset2 disabled rules to be empty because it was exceeding the limit.
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset2" }, []);
|
||||
|
||||
// Expect ruleset1 disabled rules to have not been emptied.
|
||||
await assertDNRGetDisabledRuleIds(extension, { rulesetId: "ruleset1" }, [
|
||||
3,
|
||||
]);
|
||||
|
||||
await extension.unload();
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue