tor-browser

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

test_cookies_async_failure.js (16435B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 // Test the various ways opening a cookie database can fail in an asynchronous
      5 // (i.e. after synchronous initialization) manner, and that the database is
      6 // renamed and recreated under each circumstance. These circumstances are, in no
      7 // particular order:
      8 //
      9 // 1) A write operation failing after the database has been read in.
     10 // 2) Asynchronous read failure due to a corrupt database.
     11 // 3) Synchronous read failure due to a corrupt database, when reading:
     12 //    a) a single base domain;
     13 //    b) the entire database.
     14 // 4) Asynchronous read failure, followed by another failure during INSERT but
     15 //    before the database closes for rebuilding. (The additional error should be
     16 //    ignored.)
     17 // 5) Asynchronous read failure, followed by an INSERT failure during rebuild.
     18 //    This should result in an abort of the database rebuild; the partially-
     19 //    built database should be moved to 'cookies.sqlite.bak-rebuild'.
     20 
     21 "use strict";
     22 
     23 let profile;
     24 let cookie;
     25 
     26 add_task(async () => {
     27  // Set up a profile.
     28  profile = do_get_profile();
     29  Services.prefs.setBoolPref("dom.security.https_first", false);
     30 
     31  // Allow all cookies.
     32  Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
     33  Services.prefs.setBoolPref(
     34    "network.cookieJarSettings.unblocked_for_testing",
     35    true
     36  );
     37 
     38  // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
     39  Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
     40 
     41  // The server.
     42  const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"];
     43  for (let i = 0; i < 3000; ++i) {
     44    hosts.push(i + ".com");
     45  }
     46  CookieXPCShellUtils.createServer({ hosts });
     47 
     48  // Get the cookie file and the backup file.
     49  Assert.ok(!do_get_cookie_file(profile).exists());
     50  Assert.ok(!do_get_backup_file().exists());
     51 
     52  // Create a cookie object for testing.
     53  let now = Date.now() * 1000;
     54  let futureExpiry = Math.round(now / 1e3 + 1000000);
     55  cookie = new Cookie(
     56    "oh",
     57    "hai",
     58    "bar.com",
     59    "/",
     60    futureExpiry,
     61    now,
     62    now,
     63    false,
     64    false,
     65    false
     66  );
     67 
     68  await run_test_1();
     69  await run_test_2();
     70  await run_test_3();
     71  await run_test_4();
     72  await run_test_5();
     73  Services.prefs.clearUserPref("dom.security.https_first");
     74  Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
     75 });
     76 
     77 function do_get_backup_file() {
     78  let file = profile.clone();
     79  file.append("cookies.sqlite.bak");
     80  return file;
     81 }
     82 
     83 function do_get_rebuild_backup_file() {
     84  let file = profile.clone();
     85  file.append("cookies.sqlite.bak-rebuild");
     86  return file;
     87 }
     88 
     89 function do_corrupt_db(file) {
     90  // Sanity check: the database size should be larger than 320k, since we've
     91  // written about 460k of data. If it's not, let's make it obvious now.
     92  let size = file.fileSize;
     93  Assert.greater(size, 320e3);
     94 
     95  // Corrupt the database by writing bad data to the end of the file. We
     96  // assume that the important metadata -- table structure etc -- is stored
     97  // elsewhere, and that doing this will not cause synchronous failure when
     98  // initializing the database connection. This is totally empirical --
     99  // overwriting between 1k and 100k of live data seems to work. (Note that the
    100  // database file will be larger than the actual content requires, since the
    101  // cookie service uses a large growth increment. So we calculate the offset
    102  // based on the expected size of the content, not just the file size.)
    103  let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
    104    Ci.nsIFileOutputStream
    105  );
    106  ostream.init(file, 2, -1, 0);
    107  let sstream = ostream.QueryInterface(Ci.nsISeekableStream);
    108  let n = size - 320e3 + 20e3;
    109  sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n);
    110  for (let i = 0; i < n; ++i) {
    111    ostream.write("a", 1);
    112  }
    113  ostream.flush();
    114  ostream.close();
    115 
    116  Assert.equal(file.clone().fileSize, size);
    117  return size;
    118 }
    119 
    120 async function run_test_1() {
    121  // Load the profile and populate it.
    122  await CookieXPCShellUtils.setCookieToDocument(
    123    "http://foo.com/",
    124    "oh=hai; max-age=1000"
    125  );
    126 
    127  // Close the profile.
    128  await promise_close_profile();
    129 
    130  // Open a database connection now, before we load the profile and begin
    131  // asynchronous write operations.
    132  let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 15);
    133  Assert.equal(do_count_cookies_in_db(db.db), 1);
    134 
    135  // Load the profile, and wait for async read completion...
    136  await promise_load_profile();
    137 
    138  // Insert a row.
    139  db.insertCookie(cookie);
    140  db.close();
    141 
    142  // Attempt to insert a cookie with the same (name, host, path) triplet.
    143  const cv = Services.cookies.add(
    144    cookie.host,
    145    cookie.path,
    146    cookie.name,
    147    "hallo",
    148    cookie.isSecure,
    149    cookie.isHttpOnly,
    150    cookie.isSession,
    151    cookie.expiry,
    152    {},
    153    Ci.nsICookie.SAMESITE_UNSET,
    154    Ci.nsICookie.SCHEME_HTTPS
    155  );
    156  Assert.equal(cv.result, Ci.nsICookieValidation.eOK, "Valid cookie");
    157 
    158  // Check that the cookie service accepted the new cookie.
    159  Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
    160 
    161  let isRebuildingDone = false;
    162  let rebuildingObserve = function () {
    163    isRebuildingDone = true;
    164    Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
    165  };
    166  Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding");
    167 
    168  // Crash test: we're going to rebuild the cookie database. Close all the db
    169  // connections in the main thread and initialize a new database file in the
    170  // cookie thread. Trigger some access of cookies to ensure we won't crash in
    171  // the chaos status.
    172  for (let i = 0; i < 10; ++i) {
    173    Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
    174    await new Promise(resolve => executeSoon(resolve));
    175  }
    176 
    177  // Wait for the cookie service to rename the old database and rebuild if not yet.
    178  if (!isRebuildingDone) {
    179    Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
    180    await new _promise_observer("cookie-db-rebuilding");
    181  }
    182 
    183  await new Promise(resolve => executeSoon(resolve));
    184 
    185  // At this point, the cookies should still be in memory.
    186  Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1);
    187  Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
    188  Assert.equal(do_count_cookies(), 2);
    189 
    190  // Close the profile.
    191  await promise_close_profile();
    192 
    193  // Check that the original database was renamed, and that it contains the
    194  // original cookie.
    195  Assert.ok(do_get_backup_file().exists());
    196  let backupdb = Services.storage.openDatabase(do_get_backup_file());
    197  Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1);
    198  backupdb.close();
    199 
    200  // Load the profile, and check that it contains the new cookie.
    201  do_load_profile();
    202 
    203  Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1);
    204  let cookies = Services.cookies.getCookiesFromHost(cookie.host, {});
    205  Assert.equal(cookies.length, 1);
    206  let dbcookie = cookies[0];
    207  Assert.equal(dbcookie.value, "hallo");
    208 
    209  // Close the profile.
    210  await promise_close_profile();
    211 
    212  // Clean up.
    213  do_get_cookie_file(profile).remove(false);
    214  do_get_backup_file().remove(false);
    215  Assert.ok(!do_get_cookie_file(profile).exists());
    216  Assert.ok(!do_get_backup_file().exists());
    217 }
    218 
    219 async function run_test_2() {
    220  // Load the profile and populate it.
    221  do_load_profile();
    222 
    223  Services.cookies.runInTransaction(_ => {
    224    let uri = NetUtil.newURI("http://foo.com/");
    225    const channel = NetUtil.newChannel({
    226      uri,
    227      loadUsingSystemPrincipal: true,
    228      contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    229    });
    230 
    231    for (let i = 0; i < 3000; ++i) {
    232      uri = NetUtil.newURI("http://" + i + ".com/");
    233      Services.cookies.setCookieStringFromHttp(
    234        uri,
    235        "oh=hai; max-age=1000",
    236        channel
    237      );
    238    }
    239  });
    240 
    241  // Close the profile.
    242  await promise_close_profile();
    243 
    244  // Corrupt the database file.
    245  let size = do_corrupt_db(do_get_cookie_file(profile));
    246 
    247  // Load the profile.
    248  do_load_profile();
    249 
    250  // At this point, the database connection should be open. Ensure that it
    251  // succeeded.
    252  Assert.ok(!do_get_backup_file().exists());
    253 
    254  // Recreate a new database since it was corrupted
    255  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
    256  Assert.equal(do_count_cookies(), 0);
    257 
    258  // Close the profile.
    259  await promise_close_profile();
    260 
    261  // Check that the original database was renamed.
    262  Assert.ok(do_get_backup_file().exists());
    263  Assert.equal(do_get_backup_file().fileSize, size);
    264  let db = Services.storage.openDatabase(do_get_cookie_file(profile));
    265  db.close();
    266 
    267  do_load_profile();
    268  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
    269  Assert.equal(do_count_cookies(), 0);
    270 
    271  // Close the profile.
    272  await promise_close_profile();
    273 
    274  // Clean up.
    275  do_get_cookie_file(profile).remove(false);
    276  do_get_backup_file().remove(false);
    277  Assert.ok(!do_get_cookie_file(profile).exists());
    278  Assert.ok(!do_get_backup_file().exists());
    279 }
    280 
    281 async function run_test_3() {
    282  // Set the maximum cookies per base domain limit to a large value, so that
    283  // corrupting the database is easier.
    284  Services.prefs.setIntPref("network.cookie.maxPerHost", 3000);
    285 
    286  // Load the profile and populate it.
    287  do_load_profile();
    288  Services.cookies.runInTransaction(_ => {
    289    let uri = NetUtil.newURI("http://hither.com/");
    290    let channel = NetUtil.newChannel({
    291      uri,
    292      loadUsingSystemPrincipal: true,
    293      contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    294    });
    295    for (let i = 0; i < 10; ++i) {
    296      Services.cookies.setCookieStringFromHttp(
    297        uri,
    298        "oh" + i + "=hai; max-age=1000",
    299        channel
    300      );
    301    }
    302    uri = NetUtil.newURI("http://haithur.com/");
    303    channel = NetUtil.newChannel({
    304      uri,
    305      loadUsingSystemPrincipal: true,
    306      contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    307    });
    308    for (let i = 10; i < 3000; ++i) {
    309      Services.cookies.setCookieStringFromHttp(
    310        uri,
    311        "oh" + i + "=hai; max-age=1000",
    312        channel
    313      );
    314    }
    315  });
    316 
    317  // Close the profile.
    318  await promise_close_profile();
    319 
    320  // Corrupt the database file.
    321  let size = do_corrupt_db(do_get_cookie_file(profile));
    322 
    323  // Load the profile.
    324  do_load_profile();
    325 
    326  // At this point, the database connection should be open. Ensure that it
    327  // succeeded.
    328  Assert.ok(!do_get_backup_file().exists());
    329 
    330  // Recreate a new database since it was corrupted
    331  Assert.equal(Services.cookies.countCookiesFromHost("hither.com"), 0);
    332  Assert.equal(Services.cookies.countCookiesFromHost("haithur.com"), 0);
    333 
    334  // Close the profile.
    335  await promise_close_profile();
    336 
    337  let db = Services.storage.openDatabase(do_get_cookie_file(profile));
    338  Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0);
    339  Assert.equal(do_count_cookies_in_db(db), 0);
    340  db.close();
    341 
    342  // Check that the original database was renamed.
    343  Assert.ok(do_get_backup_file().exists());
    344  Assert.equal(do_get_backup_file().fileSize, size);
    345 
    346  // Rename it back, and try loading the entire database synchronously.
    347  do_get_backup_file().moveTo(null, "cookies.sqlite");
    348  do_load_profile();
    349 
    350  // At this point, the database connection should be open. Ensure that it
    351  // succeeded.
    352  Assert.ok(!do_get_backup_file().exists());
    353 
    354  // Synchronously read in everything.
    355  Assert.equal(do_count_cookies(), 0);
    356 
    357  // Close the profile.
    358  await promise_close_profile();
    359 
    360  db = Services.storage.openDatabase(do_get_cookie_file(profile));
    361  Assert.equal(do_count_cookies_in_db(db), 0);
    362  db.close();
    363 
    364  // Check that the original database was renamed.
    365  Assert.ok(do_get_backup_file().exists());
    366  Assert.equal(do_get_backup_file().fileSize, size);
    367 
    368  // Clean up.
    369  do_get_cookie_file(profile).remove(false);
    370  do_get_backup_file().remove(false);
    371  Assert.ok(!do_get_cookie_file(profile).exists());
    372  Assert.ok(!do_get_backup_file().exists());
    373 }
    374 
    375 async function run_test_4() {
    376  // Load the profile and populate it.
    377  do_load_profile();
    378  Services.cookies.runInTransaction(_ => {
    379    let uri = NetUtil.newURI("http://foo.com/");
    380    let channel = NetUtil.newChannel({
    381      uri,
    382      loadUsingSystemPrincipal: true,
    383      contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    384    });
    385    for (let i = 0; i < 3000; ++i) {
    386      uri = NetUtil.newURI("http://" + i + ".com/");
    387      Services.cookies.setCookieStringFromHttp(
    388        uri,
    389        "oh=hai; max-age=1000",
    390        channel
    391      );
    392    }
    393  });
    394 
    395  // Close the profile.
    396  await promise_close_profile();
    397 
    398  // Corrupt the database file.
    399  let size = do_corrupt_db(do_get_cookie_file(profile));
    400 
    401  // Load the profile.
    402  do_load_profile();
    403 
    404  // At this point, the database connection should be open. Ensure that it
    405  // succeeded.
    406  Assert.ok(!do_get_backup_file().exists());
    407 
    408  // Recreate a new database since it was corrupted
    409  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
    410 
    411  // Queue up an INSERT for the same base domain. This should also go into
    412  // memory and be written out during database rebuild.
    413  await CookieXPCShellUtils.setCookieToDocument(
    414    "http://0.com/",
    415    "oh2=hai; max-age=1000"
    416  );
    417 
    418  // At this point, the cookies should still be in memory.
    419  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1);
    420  Assert.equal(do_count_cookies(), 1);
    421 
    422  // Close the profile.
    423  await promise_close_profile();
    424 
    425  // Check that the original database was renamed.
    426  Assert.ok(do_get_backup_file().exists());
    427  Assert.equal(do_get_backup_file().fileSize, size);
    428 
    429  // Load the profile, and check that it contains the new cookie.
    430  do_load_profile();
    431  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1);
    432  Assert.equal(do_count_cookies(), 1);
    433 
    434  // Close the profile.
    435  await promise_close_profile();
    436 
    437  // Clean up.
    438  do_get_cookie_file(profile).remove(false);
    439  do_get_backup_file().remove(false);
    440  Assert.ok(!do_get_cookie_file(profile).exists());
    441  Assert.ok(!do_get_backup_file().exists());
    442 }
    443 
    444 async function run_test_5() {
    445  // Load the profile and populate it.
    446  do_load_profile();
    447  Services.cookies.runInTransaction(_ => {
    448    let uri = NetUtil.newURI("http://bar.com/");
    449    const channel = NetUtil.newChannel({
    450      uri,
    451      loadUsingSystemPrincipal: true,
    452      contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    453    });
    454    Services.cookies.setCookieStringFromHttp(
    455      uri,
    456      "oh=hai; path=/; max-age=1000",
    457      channel
    458    );
    459    for (let i = 0; i < 3000; ++i) {
    460      uri = NetUtil.newURI("http://" + i + ".com/");
    461      Services.cookies.setCookieStringFromHttp(
    462        uri,
    463        "oh=hai; max-age=1000",
    464        channel
    465      );
    466    }
    467  });
    468 
    469  // Close the profile.
    470  await promise_close_profile();
    471 
    472  // Corrupt the database file.
    473  let size = do_corrupt_db(do_get_cookie_file(profile));
    474 
    475  // Load the profile.
    476  do_load_profile();
    477 
    478  // At this point, the database connection should be open. Ensure that it
    479  // succeeded.
    480  Assert.ok(!do_get_backup_file().exists());
    481 
    482  // Recreate a new database since it was corrupted
    483  Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
    484  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
    485  Assert.equal(do_count_cookies(), 0);
    486  Assert.ok(do_get_backup_file().exists());
    487  Assert.equal(do_get_backup_file().fileSize, size);
    488  Assert.ok(!do_get_rebuild_backup_file().exists());
    489 
    490  // Open a database connection, and write a row that will trigger a constraint
    491  // violation.
    492  let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 15);
    493  db.insertCookie(cookie);
    494  Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1);
    495  Assert.equal(do_count_cookies_in_db(db.db), 1);
    496  db.close();
    497 
    498  // Check that the original backup and the database itself are gone.
    499  Assert.ok(do_get_backup_file().exists());
    500  Assert.equal(do_get_backup_file().fileSize, size);
    501 
    502  Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
    503  Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
    504  Assert.equal(do_count_cookies(), 0);
    505 
    506  // Close the profile. We do not need to wait for completion, because the
    507  // database has already been closed. Ensure the cookie file is unlocked.
    508  await promise_close_profile();
    509 
    510  // Clean up.
    511  do_get_cookie_file(profile).remove(false);
    512  do_get_backup_file().remove(false);
    513  Assert.ok(!do_get_cookie_file(profile).exists());
    514  Assert.ok(!do_get_backup_file().exists());
    515 }