tor-browser

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

browser_sw_lifetime_extension.js (15225B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /**
      5 * Verify that ServiceWorkers interacting with each other can only set/extend
      6 * the lifetime of other ServiceWorkers to match their own lifetime, while
      7 * other clients that correspond to an open tab can provide fresh lifetime
      8 * extensions.  The specific scenario we want to ensure is impossible is two
      9 * ServiceWorkers interacting to keep each other alive indefinitely without the
     10 * involvement of a live tab.
     11 *
     12 * ### Test Machinery
     13 *
     14 * #### Determining Lifetimes
     15 *
     16 * In order to determine the lifetime deadline of ServiceWorkers, we have
     17 * exposed `lifetimeDeadline` on nsIServiceWorkerInfo.  This is a value
     18 * maintained exclusively by the ServiceWorkerManager on the
     19 * ServiceWorkerPrivate instances corresponding to each ServiceWorker.  It's not
     20 * something the ServiceWorker workers know, so it's appropriate to implement
     21 * this as a browser test with most of the logic in the parent process.
     22 *
     23 * #### Communicating with ServiceWorkers
     24 *
     25 * We use BroadcastChannel to communicate from a page in the test origin that
     26 * does not match any ServiceWorker scopes with the ServiceWorkers under test.
     27 * BroadcastChannel explicitly will not do anything to extend the lifetime of
     28 * the ServiceWorkers and is much simpler for us to use than trying to transfer
     29 * MessagePorts around since that would involve ServiceWorker.postMessage()
     30 * which will extend the ServiceWorker lifetime if used from a window client.
     31 *
     32 * #### Making a Service Worker that can Keep Updating
     33 *
     34 * ServiceWorker update checks do a byte-wise comparison; if the underlying
     35 * script/imports have not changed, the update process will be aborted.  So we
     36 * use an .sjs script that generates a script payload that has a "version" that
     37 * updates every time the script is fetched.
     38 *
     39 * Note that one has to be careful with an .sjs like that because
     40 * non-subresource fetch events will automatically run a soft update check, and
     41 * functional events will run a soft update if the registration is stale.  We
     42 * never expect the registration to be stale in our tests because 24 hours won't
     43 * have passed, but page navigation is obviously a very common testing thing.
     44 * We ensure we don't perform any intercepted navigations.
     45 *
     46 * To minimize code duplication, we have that script look like:
     47 * ```
     48 * var version = ${COUNTER};
     49 * importScripts("sw_inter_sw_postmessage.js");
     50 * ```
     51 */
     52 
     53 /* import-globals-from browser_head.js */
     54 Services.scriptloader.loadSubScript(
     55  "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js",
     56  this
     57 );
     58 
     59 const TEST_ORIGIN = "https://test1.example.org";
     60 
     61 /**
     62 * Install equivalent ServiceWorkers on 2 scopes that will message each other
     63 * on request via BroadcastChannel message, verifying that the ServiceWorkers
     64 * cannot extend each other's deadlines beyond their own deadline.
     65 */
     66 async function test_post_message_between_service_workers() {
     67  info("## Installing the ServiceWorkers");
     68  const aSwDesc = {
     69    origin: TEST_ORIGIN,
     70    scope: "sw-a",
     71    script: "sw_inter_sw_postmessage.js?a",
     72  };
     73  const bSwDesc = {
     74    origin: TEST_ORIGIN,
     75    scope: "sw-b",
     76    script: "sw_inter_sw_postmessage.js?b",
     77  };
     78 
     79  // Wipe the origin for cleanup; this will remove the registrations too.
     80  registerCleanupFunction(async () => {
     81    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
     82  });
     83 
     84  const aReg = await install_sw(aSwDesc);
     85  const bReg = await install_sw(bSwDesc);
     86 
     87  info("## Terminating the ServiceWorkers");
     88  // We always want to wait for the workers to be fully terminated because they
     89  // listen for our BroadcastChannel messages and until ServiceWorkers are no
     90  // longer owned by the main thread, a race is possible if we don't wait.
     91  const aSWInfo = aReg.activeWorker;
     92  await aSWInfo.terminateWorker();
     93 
     94  const bSWInfo = bReg.activeWorker;
     95  await bSWInfo.terminateWorker();
     96 
     97  is(aSWInfo.lifetimeDeadline, 0, "SW A not running.");
     98  is(bSWInfo.lifetimeDeadline, 0, "SW B not running.");
     99  is(aSWInfo.launchCount, 1, "SW A did run once, though.");
    100  is(bSWInfo.launchCount, 1, "SW B did run once, though.");
    101 
    102  info("## Beginning PostMessage Checks");
    103  let testStart = ChromeUtils.now();
    104 
    105  const { closeHelperTab, postMessageScopeAndWaitFor, broadcastAndWaitFor } =
    106    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
    107  registerCleanupFunction(closeHelperTab);
    108 
    109  // - Have the helper page postMessage SW A to spawn it, waiting for SW A to report in.
    110  await postMessageScopeAndWaitFor(
    111    "sw-a",
    112    "Hello, the contents of this message don't matter!",
    113    "a:received-post-message-from:wc-helper"
    114  );
    115 
    116  let aLifetime = aSWInfo.lifetimeDeadline;
    117  Assert.greater(
    118    aLifetime,
    119    testStart,
    120    "SW A should be running with a deadline in the future."
    121  );
    122  is(bSWInfo.lifetimeDeadline, 0, "SW B not running.");
    123  is(aSWInfo.launchCount, 2, "SW A was launched by our postMessage.");
    124  is(bSWInfo.launchCount, 1, "SW B has not been re-launched yet.");
    125 
    126  // - Ask SW A to postMessage SW B, waiting for SW B to report in.
    127  await broadcastAndWaitFor(
    128    "a:post-message-to:reg-sw-b",
    129    "b:received-post-message-from:sw-a"
    130  );
    131 
    132  is(
    133    bSWInfo.lifetimeDeadline,
    134    aLifetime,
    135    "SW B has same deadline as SW A after cross-SW postMessage"
    136  );
    137 
    138  // - Ask SW B to postMessage SW A, waiting for SW A to report in.
    139  await broadcastAndWaitFor(
    140    "b:post-message-to:reg-sw-a",
    141    "a:received-post-message-from:sw-b"
    142  );
    143 
    144  is(
    145    bSWInfo.lifetimeDeadline,
    146    aLifetime,
    147    "SW A still has the same deadline after B's cross-SW postMessage"
    148  );
    149  is(bSWInfo.launchCount, 2, "SW B was re-launched.");
    150  is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged");
    151  is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
    152 
    153  // - Have the helper page postMessage SW B, waiting for B to report in.
    154  await postMessageScopeAndWaitFor(
    155    "sw-b",
    156    "Hello, the contents of this message don't matter!",
    157    "b:received-post-message-from:wc-helper"
    158  );
    159  let bLifetime = bSWInfo.lifetimeDeadline;
    160  Assert.greater(
    161    bLifetime,
    162    aLifetime,
    163    "SW B should have a deadline after A's after the page postMessage"
    164  );
    165  is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged");
    166  is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
    167 
    168  // - Have SW B postMessage SW A, waiting for SW A to report in.
    169  await broadcastAndWaitFor(
    170    "b:post-message-to:reg-sw-a",
    171    "a:received-post-message-from:sw-b"
    172  );
    173  is(
    174    aSWInfo.lifetimeDeadline,
    175    bLifetime,
    176    "SW A should have the same deadline as B after B's cross-SW postMessage"
    177  );
    178  is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
    179  is(bSWInfo.lifetimeDeadline, bLifetime, "SW B deadline unchanged");
    180  is(bSWInfo.launchCount, 2, "SW B launch count unchanged.");
    181 }
    182 add_task(test_post_message_between_service_workers);
    183 
    184 /**
    185 * Install a ServiceWorker that will update itself on request via
    186 * BroadcastChannel message and verify that the lifetimes of the new updated
    187 * ServiceWorker are the same as the requesting ServiceWorker.  We also want to
    188 * verify that a request to update from a page gets a fresh lifetime.
    189 */
    190 async function test_eternally_updating_service_worker() {
    191  info("## Installing the Eternally Updating ServiceWorker");
    192  const swDesc = {
    193    origin: TEST_ORIGIN,
    194    scope: "sw-u",
    195    script: "sw_always_updating_inter_sw_postmessage.sjs?u",
    196  };
    197 
    198  // Wipe the origin for cleanup; this will remove the registrations too.
    199  registerCleanupFunction(async () => {
    200    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
    201  });
    202 
    203  let testStart = ChromeUtils.now();
    204 
    205  const reg = await install_sw(swDesc);
    206  const firstInfo = reg.activeWorker;
    207  const firstLifetime = firstInfo.lifetimeDeadline;
    208 
    209  Assert.greater(
    210    firstLifetime,
    211    testStart,
    212    "The first generation should be running with a deadline in the future."
    213  );
    214 
    215  const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } =
    216    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
    217  registerCleanupFunction(closeHelperTab);
    218 
    219  info("## Beginning Self-Update Requests");
    220 
    221  // - Ask 1st gen SW to update the reg, 2nd gen SW should have same lifetime.
    222  await broadcastAndWaitFor("u#1:update-reg:sw-u", "u:version-activated:2");
    223 
    224  // We don't have to worry about async races here because these state changes
    225  // are authoritative on the parent process main thread, which is where we are
    226  // running; we can only have heard about the activation via BroadcastChannel
    227  // after the state has updated.
    228  const secondInfo = reg.activeWorker;
    229  const secondLifetime = secondInfo.lifetimeDeadline;
    230  is(firstLifetime, secondLifetime, "Version 2 has same lifetime as 1.");
    231 
    232  // - Ask 2nd gen SW to update the reg, 3rd gen SW should have same lifetime.
    233  await broadcastAndWaitFor("u#2:update-reg:sw-u", "u:version-activated:3");
    234 
    235  const thirdInfo = reg.activeWorker;
    236  const thirdLifetime = thirdInfo.lifetimeDeadline;
    237  is(firstLifetime, thirdLifetime, "Version 3 has same lifetime as 1 and 2.");
    238 
    239  // - Ask the helper page to update the reg, 4th gen SW should have fresh life.
    240  await updateScopeAndWaitFor("sw-u", "u:version-activated:4");
    241 
    242  const fourthInfo = reg.activeWorker;
    243  const fourthLifetime = fourthInfo.lifetimeDeadline;
    244  Assert.greater(
    245    fourthLifetime,
    246    firstLifetime,
    247    "Version 4 has a fresh lifetime."
    248  );
    249 
    250  // - Ask 4th gen SW to update the reg, 5th gen SW should have same lifetime.
    251  await broadcastAndWaitFor("u#4:update-reg:sw-u", "u:version-activated:5");
    252 
    253  const fifthInfo = reg.activeWorker;
    254  const fifthLifetime = fifthInfo.lifetimeDeadline;
    255  is(fourthLifetime, fifthLifetime, "Version 5 has same lifetime as 4.");
    256 }
    257 add_task(test_eternally_updating_service_worker);
    258 
    259 /**
    260 * Install a ServiceWorker that will create a new registration and verify that
    261 * the lifetime for the new ServiceWorker being installed for the new
    262 * registration is the same as the requesting ServiceWorker.
    263 */
    264 async function test_service_worker_creating_new_registrations() {
    265  info("## Installing the Bootstrap ServiceWorker");
    266  const cSwDesc = {
    267    origin: TEST_ORIGIN,
    268    scope: "sw-c",
    269    script: "sw_inter_sw_postmessage.js?c",
    270  };
    271 
    272  // Wipe the origin for cleanup; this will remove the registrations too.
    273  registerCleanupFunction(async () => {
    274    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
    275  });
    276 
    277  let testStart = ChromeUtils.now();
    278 
    279  const cReg = await install_sw(cSwDesc);
    280  const cSWInfo = cReg.activeWorker;
    281  const cLifetime = cSWInfo.lifetimeDeadline;
    282 
    283  Assert.greater(
    284    cLifetime,
    285    testStart,
    286    "The bootstrap registration worker should be running with a deadline in the future."
    287  );
    288 
    289  const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } =
    290    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
    291  registerCleanupFunction(closeHelperTab);
    292 
    293  info("## Beginning Propagating Registrations");
    294 
    295  // - Ask the SW to install a ServiceWorker at scope d
    296  await broadcastAndWaitFor("c:install-reg:d", "d:version-activated:0");
    297 
    298  const dSwDesc = {
    299    origin: TEST_ORIGIN,
    300    scope: "sw-d",
    301    script: "sw_inter_sw_postmessage.js?d",
    302  };
    303  const dReg = swm_lookup_reg(dSwDesc);
    304  ok(dReg, "found the new 'd' registration");
    305  const dSWInfo = dReg.activeWorker;
    306  ok(dSWInfo, "The 'd' registration has the expected active worker.");
    307  const dLifetime = dSWInfo.lifetimeDeadline;
    308  is(
    309    dLifetime,
    310    cLifetime,
    311    "The new worker has the same lifetime as the worker that triggered its installation."
    312  );
    313 }
    314 add_task(test_service_worker_creating_new_registrations);
    315 
    316 /**
    317 * In bug 1927247 a ServiceWorker respawned sufficiently soon after its
    318 * termination resulted in a defensive content-process crash when the new SW's
    319 * ClientSource was registered with the same Client Id while the old SW's
    320 * ClientSource still existed.  We synthetically induce this situation through
    321 * use of nsIServiceWorkerInfo::terminateWorker() immediately followed by use of
    322 * nsIServiceWorkerInfo::attachDebugger().
    323 */
    324 async function test_respawn_immediately_after_termination() {
    325  // Make WorkerTestUtils work in the SW.
    326  await SpecialPowers.pushPrefEnv({
    327    set: [["dom.workers.testing.enabled", true]],
    328  });
    329 
    330  // We need to ensure all ServiceWorkers are spawned consistently in the same
    331  // process because of the trick we do with workerrefs and using the observer
    332  // service, so force us to only use a single process if fission is not on.
    333  if (!Services.appinfo.fissionAutostart) {
    334    // Force use of only a single process
    335    await SpecialPowers.pushPrefEnv({
    336      set: [["dom.ipc.processCount", 1]],
    337    });
    338  }
    339 
    340  info("## Installing the ServiceWorker we will terminate and respawn");
    341  const tSwDesc = {
    342    origin: TEST_ORIGIN,
    343    scope: "sw-t",
    344    script: "sw_inter_sw_postmessage.js?t",
    345  };
    346 
    347  // Wipe the origin for cleanup; this will remove the registrations too.
    348  registerCleanupFunction(async () => {
    349    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
    350  });
    351 
    352  const tReg = await install_sw(tSwDesc);
    353  const tSWInfo = tReg.activeWorker;
    354 
    355  info("## Induce the SW to acquire a WorkerRef that prevents shutdown.");
    356 
    357  const { closeHelperTab, broadcastAndWaitFor, postMessageScopeAndWaitFor } =
    358    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
    359  registerCleanupFunction(closeHelperTab);
    360 
    361  // Tell the ServiceWorker to block on a monitor that will prevent the worker
    362  // from transitioning to the Canceling state and notifying its WorkerRefs,
    363  // thereby preventing the ClientManagerChild from beginning teardown of itself
    364  // and thereby the ClientSourceChild.  The monitor will be released when we
    365  // cause "serviceworker-t-release" to be notified on that process's main
    366  // thread.
    367  await broadcastAndWaitFor(
    368    "t:block:serviceworker-t-release",
    369    "t:blocking:serviceworker-t-release"
    370  );
    371 
    372  info("## Terminating and respawning the ServiceWorker via attachDebugger");
    373  // We must not await the termination if we want to create the lifetime overlap
    374  const terminationPromise = tSWInfo.terminateWorker();
    375  // Message the ServiceWorker to cause it to spawn, waiting for the newly
    376  // spawned ServiceWorker to indicate it is alive and running.
    377  await postMessageScopeAndWaitFor(
    378    "sw-t",
    379    "Hello, the contents of this message don't matter!",
    380    "t:received-post-message-from:wc-helper"
    381  );
    382 
    383  // Tell the successor to generate an observer notification that will release
    384  // the ThreadSafeWorkerRef.  Note that this does assume the ServiceWorker is
    385  // placed in the same process as its predecessor.  When isolation is enabled,
    386  // like on desktop, this will always be the same process because there will
    387  // only be the one possible process.  "browser" tests like this are only run
    388  // on desktop, never on Android!  But if we weren't isolating,
    389  await broadcastAndWaitFor(
    390    "t:notify-observer:serviceworker-t-release",
    391    "t:notified-observer:serviceworker-t-release"
    392  );
    393 
    394  info("## Awaiting the termination");
    395  await terminationPromise;
    396 }
    397 add_task(test_respawn_immediately_after_termination);