browser_navigation_fetch_fault_handling.js (10708B)
1 /** 2 * This test file tests our automatic recovery and any related mitigating 3 * heuristics that occur during intercepted navigation fetch request. 4 * Specifically, we should be resetting interception so that we go to the 5 * network in these cases and then potentially taking actions like unregistering 6 * the ServiceWorker and/or clearing QuotaManager-managed storage for the 7 * origin. 8 * 9 * See specific test permutations for specific details inline in the test. 10 * 11 * NOTE THAT CURRENTLY THIS TEST IS DISCUSSING MITIGATIONS THAT ARE NOT YET 12 * IMPLEMENTED, JUST PLANNED. These will be iterated on and added to the rest 13 * of the stack of patches on Bug 1503072. 14 * 15 * ## Test Mechanics 16 * 17 * ### Fetch Fault Injection 18 * 19 * We expose: 20 * - On nsIServiceWorkerInfo, the per-ServiceWorker XPCOM interface: 21 * - A mechanism for creating synthetic faults by setting the 22 * `nsIServiceWorkerInfo::testingInjectCancellation` attribute to a failing 23 * nsresult. The fault is applied at the beginning of the steps to dispatch 24 * the fetch event on the global. 25 * - A count of the number of times we experienced these navigation faults 26 * that had to be reset as `nsIServiceWorkerInfo::navigationFaultCount`. 27 * (This would also include real faults, but we only expect to see synthetic 28 * faults in this test.) 29 * - On nsIServiceWorkerRegistrationInfo, the per-registration XPCOM interface: 30 * - A readonly attribute that indicates how many times an origin storage 31 * usage check has been initiated. 32 * 33 * We also use: 34 * - `nsIServiceWorkerManager::addListener(nsIServiceWorkerManagerListener)` 35 * allows our test to listen for the unregistration of registrations. This 36 * allows us to be notified when unregistering or origin-clearing actions have 37 * been taken as a mitigation. 38 * 39 * ### General Test Approach 40 * 41 * For each test we: 42 * - Ensure/confirm the testing origin has no QuotaManager storage in use. 43 * - Install the ServiceWorker. 44 * - If we are testing the situation where we want to simulate the origin being 45 * near its quota limit, we also generate Cache API and IDB storage usage 46 * sufficient to put our origin over the threshold. 47 * - We run a quota check on the origin after doing this in order to make sure 48 * that we did this correctly and that we properly constrained the limit for 49 * the origin. We fail the test for test implementation reasons if we 50 * didn't accomplish this. 51 * - Verify a fetch navigation to the SW works without any fault injection, 52 * producing a result produced by the ServiceWorker. 53 * - Begin fault permutations in a loop, where for each pass of the loop: 54 * - We trigger a navigation which will result in an intercepted fetch 55 * which will fault. We wait until the navigation completes. 56 * - We verify that we got the request from the network. 57 * - We verify that the ServiceWorker's navigationFaultCount increased. 58 * - If this the count at which we expect a mitigation to take place, we wait 59 * for the registration to become unregistered AND: 60 * - We check whether the storage for the origin was cleared or not, which 61 * indicates which mitigation of the following happened: 62 * - Unregister the registration directly. 63 * - Clear the origin's data which will also unregister the registration 64 * as a side effect. 65 * - We check whether the registration indicates an origin quota check 66 * happened or not. 67 * 68 * ### Disk Usage Limits 69 * 70 * In order to avoid gratuitous disk I/O and related overheads, we limit QM 71 * ("temporary") storage to 10 MiB which ends up limiting group usage to 10 MiB. 72 * This lets us set a threshold situation where we claim that a SW needs at 73 * least 4 MiB of storage for installation/operation, meaning that any usage 74 * beyond 6 MiB in the group will constitute a need to clear the group or 75 * origin. We fill with the storage with 8 MiB of artificial usage to this end, 76 * storing 4 MiB in Cache API and 4 MiB in IDB. 77 */ 78 79 // Because of the amount of I/O involved in this test, pernosco reproductions 80 // may experience timeouts without a timeout multiplier. 81 requestLongerTimeout(2); 82 83 /* import-globals-from browser_head.js */ 84 Services.scriptloader.loadSubScript( 85 "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", 86 this 87 ); 88 89 // The origin we run the tests on. 90 const TEST_ORIGIN = "https://test1.example.org"; 91 // An origin in the same group that impacts the usage of the TEST_ORIGIN. Used 92 // to verify heuristics related to group-clearing (where clearing the 93 // TEST_ORIGIN itself would not be sufficient for us to mitigate quota limits 94 // being reached.) 95 const SAME_GROUP_ORIGIN = "https://test2.example.org"; 96 97 const TEST_SW_SETUP = { 98 origin: TEST_ORIGIN, 99 // Page with a body textContent of "NETWORK" and has utils.js loaded. 100 scope: "network_with_utils.html", 101 // SW that serves a body with a textContent of "SERVICEWORKER" and 102 // has utils.js loaded. 103 script: "sw_respondwith_serviceworker.js", 104 }; 105 106 const TEST_STORAGE_SETUP = { 107 cacheBytes: 4 * 1024 * 1024, // 4 MiB 108 idbBytes: 4 * 1024 * 1024, // 4 MiB 109 }; 110 111 const FAULTS_BEFORE_MITIGATION = 3; 112 113 /** 114 * Core test iteration logic. 115 * 116 * Parameters: 117 * - name: Human readable name of the fault we're injecting. 118 * - useError: The nsresult failure code to inject into fetch. 119 * - errorPage: The "about" page that we expect errors to leave us on. 120 * - consumeQuotaOrigin: If truthy, the origin to place the storage usage in. 121 * If falsey, we won't fill storage. 122 */ 123 async function do_fault_injection_test({ 124 name, 125 useError, 126 errorPage, 127 consumeQuotaOrigin, 128 }) { 129 info( 130 `### testing: error: ${name} (${useError}) consumeQuotaOrigin: ${consumeQuotaOrigin}` 131 ); 132 133 // ## Ensure/confirm the testing origins have no QuotaManager storage in use. 134 await clear_qm_origin_group_via_clearData(TEST_ORIGIN); 135 136 // ## Install the ServiceWorker 137 const reg = await install_sw(TEST_SW_SETUP); 138 const sw = reg.activeWorker; 139 140 // ## Generate quota usage if appropriate 141 if (consumeQuotaOrigin) { 142 await consume_storage(consumeQuotaOrigin, TEST_STORAGE_SETUP); 143 } 144 145 // ## Verify normal navigation is served by the SW. 146 info(`## Checking normal operation.`); 147 { 148 const debugTag = `err=${name}&fault=0`; 149 const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); 150 is( 151 docInfo.body, 152 "SERVICEWORKER", 153 "navigation without injected fault originates from ServiceWorker" 154 ); 155 156 is( 157 docInfo.controlled, 158 true, 159 "successfully intercepted navigation should be controlled" 160 ); 161 } 162 163 // Make sure the test is listening on the ServiceWorker unregistration, since 164 // we expect it happens after navigation fault threshold reached. 165 const unregisteredPromise = waitForUnregister(reg.scope); 166 167 // Make sure the test is listening on the finish of quota checking, since we 168 // expect it happens after navigation fault threshold reached. 169 const quotaUsageCheckFinishPromise = waitForQuotaUsageCheckFinish(reg.scope); 170 171 // ## Inject faults in a loop until expected mitigation. 172 sw.testingInjectCancellation = useError; 173 for (let iFault = 0; iFault < FAULTS_BEFORE_MITIGATION; iFault++) { 174 info(`## Testing with injected fault number ${iFault + 1}`); 175 // We should never have triggered an origin quota usage check before the 176 // final fault injection. 177 is(reg.quotaUsageCheckCount, 0, "No quota usage check yet"); 178 179 // Make sure our loads encode the specific 180 const debugTag = `err=${name}&fault=${iFault + 1}`; 181 182 const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); 183 // We should always be receiving network fallback. 184 is( 185 docInfo.body, 186 "NETWORK", 187 "navigation with injected fault originates from network" 188 ); 189 190 is(docInfo.controlled, false, "bypassed pages shouldn't be controlled"); 191 192 // The fault count should have increased 193 is( 194 sw.navigationFaultCount, 195 iFault + 1, 196 "navigation fault increased (to expected value)" 197 ); 198 } 199 200 await unregisteredPromise; 201 is(reg.unregistered, true, "registration should be unregistered"); 202 203 //is(reg.quotaUsageCheckCount, 1, "Quota usage check must be started"); 204 await quotaUsageCheckFinishPromise; 205 206 if (consumeQuotaOrigin) { 207 // Check that there is no longer any storage usaged by the origin in this 208 // case. 209 const originUsage = await get_qm_origin_usage(TEST_ORIGIN); 210 ok( 211 is_minimum_origin_usage(originUsage), 212 "origin usage should be mitigated" 213 ); 214 215 if (consumeQuotaOrigin === SAME_GROUP_ORIGIN) { 216 const sameGroupUsage = await get_qm_origin_usage(SAME_GROUP_ORIGIN); 217 Assert.strictEqual( 218 sameGroupUsage, 219 0, 220 "same group usage should be mitigated" 221 ); 222 } 223 } 224 } 225 226 add_task(async function test_navigation_fetch_fault_handling() { 227 await SpecialPowers.pushPrefEnv({ 228 set: [ 229 ["dom.serviceWorkers.enabled", true], 230 ["dom.serviceWorkers.exemptFromPerDomainMax", true], 231 ["dom.serviceWorkers.testing.enabled", true], 232 ["dom.serviceWorkers.mitigations.bypass_on_fault", true], 233 ["dom.serviceWorkers.mitigations.group_usage_headroom_kb", 5 * 1024], 234 ["dom.quotaManager.testing", true], 235 // We want the temporary global limit to be 10 MiB (the pref is in KiB). 236 // This will result in the group limit also being 10 MiB because on small 237 // disks we provide a group limit value of min(10 MiB, global limit). 238 ["dom.quotaManager.temporaryStorage.fixedLimit", 10 * 1024], 239 ], 240 }); 241 242 // Need to reset the storages to make dom.quotaManager.temporaryStorage.fixedLimit 243 // works. 244 await qm_reset_storage(); 245 246 const quotaOriginVariations = [ 247 // Don't put us near the storage limit. 248 undefined, 249 // Put us near the storage limit in the SW origin itself. 250 TEST_ORIGIN, 251 // Put us near the storage limit in the SW origin's group but not the origin 252 // itself. 253 SAME_GROUP_ORIGIN, 254 ]; 255 256 for (const consumeQuotaOrigin of quotaOriginVariations) { 257 await do_fault_injection_test({ 258 name: "NS_ERROR_DOM_ABORT_ERR", 259 useError: 0x80530014, // Not in `Cr`. 260 // Abort errors manifest as about:blank pages. 261 errorPage: "about:blank", 262 consumeQuotaOrigin, 263 }); 264 265 await do_fault_injection_test({ 266 name: "NS_ERROR_INTERCEPTION_FAILED", 267 useError: 0x804b0064, // Not in `Cr`. 268 // Interception failures manifest as corrupt content pages. 269 errorPage: "about:neterror", 270 consumeQuotaOrigin, 271 }); 272 } 273 274 // Cleanup: wipe the origin and group so all the ServiceWorkers go away. 275 await clear_qm_origin_group_via_clearData(TEST_ORIGIN); 276 });