From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../browser_navigation_fetch_fault_handling.js | 276 +++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js (limited to 'dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js') diff --git a/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js new file mode 100644 index 0000000000..a72fc68b69 --- /dev/null +++ b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js @@ -0,0 +1,276 @@ +/** + * This test file tests our automatic recovery and any related mitigating + * heuristics that occur during intercepted navigation fetch request. + * Specifically, we should be resetting interception so that we go to the + * network in these cases and then potentially taking actions like unregistering + * the ServiceWorker and/or clearing QuotaManager-managed storage for the + * origin. + * + * See specific test permutations for specific details inline in the test. + * + * NOTE THAT CURRENTLY THIS TEST IS DISCUSSING MITIGATIONS THAT ARE NOT YET + * IMPLEMENTED, JUST PLANNED. These will be iterated on and added to the rest + * of the stack of patches on Bug 1503072. + * + * ## Test Mechanics + * + * ### Fetch Fault Injection + * + * We expose: + * - On nsIServiceWorkerInfo, the per-ServiceWorker XPCOM interface: + * - A mechanism for creating synthetic faults by setting the + * `nsIServiceWorkerInfo::testingInjectCancellation` attribute to a failing + * nsresult. The fault is applied at the beginning of the steps to dispatch + * the fetch event on the global. + * - A count of the number of times we experienced these navigation faults + * that had to be reset as `nsIServiceWorkerInfo::navigationFaultCount`. + * (This would also include real faults, but we only expect to see synthetic + * faults in this test.) + * - On nsIServiceWorkerRegistrationInfo, the per-registration XPCOM interface: + * - A readonly attribute that indicates how many times an origin storage + * usage check has been initiated. + * + * We also use: + * - `nsIServiceWorkerManager::addListener(nsIServiceWorkerManagerListener)` + * allows our test to listen for the unregistration of registrations. This + * allows us to be notified when unregistering or origin-clearing actions have + * been taken as a mitigation. + * + * ### General Test Approach + * + * For each test we: + * - Ensure/confirm the testing origin has no QuotaManager storage in use. + * - Install the ServiceWorker. + * - If we are testing the situation where we want to simulate the origin being + * near its quota limit, we also generate Cache API and IDB storage usage + * sufficient to put our origin over the threshold. + * - We run a quota check on the origin after doing this in order to make sure + * that we did this correctly and that we properly constrained the limit for + * the origin. We fail the test for test implementation reasons if we + * didn't accomplish this. + * - Verify a fetch navigation to the SW works without any fault injection, + * producing a result produced by the ServiceWorker. + * - Begin fault permutations in a loop, where for each pass of the loop: + * - We trigger a navigation which will result in an intercepted fetch + * which will fault. We wait until the navigation completes. + * - We verify that we got the request from the network. + * - We verify that the ServiceWorker's navigationFaultCount increased. + * - If this the count at which we expect a mitigation to take place, we wait + * for the registration to become unregistered AND: + * - We check whether the storage for the origin was cleared or not, which + * indicates which mitigation of the following happened: + * - Unregister the registration directly. + * - Clear the origin's data which will also unregister the registration + * as a side effect. + * - We check whether the registration indicates an origin quota check + * happened or not. + * + * ### Disk Usage Limits + * + * In order to avoid gratuitous disk I/O and related overheads, we limit QM + * ("temporary") storage to 10 MiB which ends up limiting group usage to 10 MiB. + * This lets us set a threshold situation where we claim that a SW needs at + * least 4 MiB of storage for installation/operation, meaning that any usage + * beyond 6 MiB in the group will constitute a need to clear the group or + * origin. We fill with the storage with 8 MiB of artificial usage to this end, + * storing 4 MiB in Cache API and 4 MiB in IDB. + **/ + +// Because of the amount of I/O involved in this test, pernosco reproductions +// may experience timeouts without a timeout multiplier. +requestLongerTimeout(2); + +/* import-globals-from browser_head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", + this +); + +// The origin we run the tests on. +const TEST_ORIGIN = "https://test1.example.org"; +// An origin in the same group that impacts the usage of the TEST_ORIGIN. Used +// to verify heuristics related to group-clearing (where clearing the +// TEST_ORIGIN itself would not be sufficient for us to mitigate quota limits +// being reached.) +const SAME_GROUP_ORIGIN = "https://test2.example.org"; + +const TEST_SW_SETUP = { + origin: TEST_ORIGIN, + // Page with a body textContent of "NETWORK" and has utils.js loaded. + scope: "network_with_utils.html", + // SW that serves a body with a textContent of "SERVICEWORKER" and + // has utils.js loaded. + script: "sw_respondwith_serviceworker.js", +}; + +const TEST_STORAGE_SETUP = { + cacheBytes: 4 * 1024 * 1024, // 4 MiB + idbBytes: 4 * 1024 * 1024, // 4 MiB +}; + +const FAULTS_BEFORE_MITIGATION = 3; + +/** + * Core test iteration logic. + * + * Parameters: + * - name: Human readable name of the fault we're injecting. + * - useError: The nsresult failure code to inject into fetch. + * - errorPage: The "about" page that we expect errors to leave us on. + * - consumeQuotaOrigin: If truthy, the origin to place the storage usage in. + * If falsey, we won't fill storage. + */ +async function do_fault_injection_test({ + name, + useError, + errorPage, + consumeQuotaOrigin, +}) { + info( + `### testing: error: ${name} (${useError}) consumeQuotaOrigin: ${consumeQuotaOrigin}` + ); + + // ## Ensure/confirm the testing origins have no QuotaManager storage in use. + await clear_qm_origin_group_via_clearData(TEST_ORIGIN); + + // ## Install the ServiceWorker + const reg = await install_sw(TEST_SW_SETUP); + const sw = reg.activeWorker; + + // ## Generate quota usage if appropriate + if (consumeQuotaOrigin) { + await consume_storage(consumeQuotaOrigin, TEST_STORAGE_SETUP); + } + + // ## Verify normal navigation is served by the SW. + info(`## Checking normal operation.`); + { + const debugTag = `err=${name}&fault=0`; + const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); + is( + docInfo.body, + "SERVICEWORKER", + "navigation without injected fault originates from ServiceWorker" + ); + + is( + docInfo.controlled, + true, + "successfully intercepted navigation should be controlled" + ); + } + + // Make sure the test is listening on the ServiceWorker unregistration, since + // we expect it happens after navigation fault threshold reached. + const unregisteredPromise = waitForUnregister(reg.scope); + + // Make sure the test is listening on the finish of quota checking, since we + // expect it happens after navigation fault threshold reached. + const quotaUsageCheckFinishPromise = waitForQuotaUsageCheckFinish(reg.scope); + + // ## Inject faults in a loop until expected mitigation. + sw.testingInjectCancellation = useError; + for (let iFault = 0; iFault < FAULTS_BEFORE_MITIGATION; iFault++) { + info(`## Testing with injected fault number ${iFault + 1}`); + // We should never have triggered an origin quota usage check before the + // final fault injection. + is(reg.quotaUsageCheckCount, 0, "No quota usage check yet"); + + // Make sure our loads encode the specific + const debugTag = `err=${name}&fault=${iFault + 1}`; + + const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); + // We should always be receiving network fallback. + is( + docInfo.body, + "NETWORK", + "navigation with injected fault originates from network" + ); + + is(docInfo.controlled, false, "bypassed pages shouldn't be controlled"); + + // The fault count should have increased + is( + sw.navigationFaultCount, + iFault + 1, + "navigation fault increased (to expected value)" + ); + } + + await unregisteredPromise; + is(reg.unregistered, true, "registration should be unregistered"); + + //is(reg.quotaUsageCheckCount, 1, "Quota usage check must be started"); + await quotaUsageCheckFinishPromise; + + if (consumeQuotaOrigin) { + // Check that there is no longer any storage usaged by the origin in this + // case. + const originUsage = await get_qm_origin_usage(TEST_ORIGIN); + ok( + is_minimum_origin_usage(originUsage), + "origin usage should be mitigated" + ); + + if (consumeQuotaOrigin === SAME_GROUP_ORIGIN) { + const sameGroupUsage = await get_qm_origin_usage(SAME_GROUP_ORIGIN); + Assert.strictEqual( + sameGroupUsage, + 0, + "same group usage should be mitigated" + ); + } + } +} + +add_task(async function test_navigation_fetch_fault_handling() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.mitigations.bypass_on_fault", true], + ["dom.serviceWorkers.mitigations.group_usage_headroom_kb", 5 * 1024], + ["dom.quotaManager.testing", true], + // We want the temporary global limit to be 10 MiB (the pref is in KiB). + // This will result in the group limit also being 10 MiB because on small + // disks we provide a group limit value of min(10 MiB, global limit). + ["dom.quotaManager.temporaryStorage.fixedLimit", 10 * 1024], + ], + }); + + // Need to reset the storages to make dom.quotaManager.temporaryStorage.fixedLimit + // works. + await qm_reset_storage(); + + const quotaOriginVariations = [ + // Don't put us near the storage limit. + undefined, + // Put us near the storage limit in the SW origin itself. + TEST_ORIGIN, + // Put us near the storage limit in the SW origin's group but not the origin + // itself. + SAME_GROUP_ORIGIN, + ]; + + for (const consumeQuotaOrigin of quotaOriginVariations) { + await do_fault_injection_test({ + name: "NS_ERROR_DOM_ABORT_ERR", + useError: 0x80530014, // Not in `Cr`. + // Abort errors manifest as about:blank pages. + errorPage: "about:blank", + consumeQuotaOrigin, + }); + + await do_fault_injection_test({ + name: "NS_ERROR_INTERCEPTION_FAILED", + useError: 0x804b0064, // Not in `Cr`. + // Interception failures manifest as corrupt content pages. + errorPage: "about:neterror", + consumeQuotaOrigin, + }); + } + + // Cleanup: wipe the origin and group so all the ServiceWorkers go away. + await clear_qm_origin_group_via_clearData(TEST_ORIGIN); +}); -- cgit v1.2.3