tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });