Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
// Test the various ways opening a cookie database can fail in an asynchronous
// (i.e. after synchronous initialization) manner, and that the database is
// renamed and recreated under each circumstance. These circumstances are, in no
// particular order:
//
// 1) A write operation failing after the database has been read in.
// 2) Asynchronous read failure due to a corrupt database.
// 3) Synchronous read failure due to a corrupt database, when reading:
// a) a single base domain;
// b) the entire database.
// 4) Asynchronous read failure, followed by another failure during INSERT but
// before the database closes for rebuilding. (The additional error should be
// ignored.)
// 5) Asynchronous read failure, followed by an INSERT failure during rebuild.
// This should result in an abort of the database rebuild; the partially-
// built database should be moved to 'cookies.sqlite.bak-rebuild'.
"use strict";
let profile;
let cookie;
add_task(async () => {
// Set up a profile.
profile = do_get_profile();
Services.prefs.setBoolPref("dom.security.https_first", false);
// Allow all cookies.
Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
Services.prefs.setBoolPref(
"network.cookieJarSettings.unblocked_for_testing",
true
);
// Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
// The server.
const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"];
for (let i = 0; i < 3000; ++i) {
hosts.push(i + ".com");
}
CookieXPCShellUtils.createServer({ hosts });
// Get the cookie file and the backup file.
Assert.ok(!do_get_cookie_file(profile).exists());
Assert.ok(!do_get_backup_file().exists());
// Create a cookie object for testing.
let now = Date.now() * 1000;
let futureExpiry = Math.round(now / 1e6 + 1000);
cookie = new Cookie(
"oh",
"hai",
"bar.com",
"/",
futureExpiry,
now,
now,
false,
false,
false
);
await run_test_1();
await run_test_2();
await run_test_3();
await run_test_4();
await run_test_5();
Services.prefs.clearUserPref("dom.security.https_first");
Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
});
function do_get_backup_file() {
let file = profile.clone();
file.append("cookies.sqlite.bak");
return file;
}
function do_get_rebuild_backup_file() {
let file = profile.clone();
file.append("cookies.sqlite.bak-rebuild");
return file;
}
function do_corrupt_db(file) {
// Sanity check: the database size should be larger than 320k, since we've
// written about 460k of data. If it's not, let's make it obvious now.
let size = file.fileSize;
Assert.ok(size > 320e3);
// Corrupt the database by writing bad data to the end of the file. We
// assume that the important metadata -- table structure etc -- is stored
// elsewhere, and that doing this will not cause synchronous failure when
// initializing the database connection. This is totally empirical --
// overwriting between 1k and 100k of live data seems to work. (Note that the
// database file will be larger than the actual content requires, since the
// cookie service uses a large growth increment. So we calculate the offset
// based on the expected size of the content, not just the file size.)
let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
Ci.nsIFileOutputStream
);
ostream.init(file, 2, -1, 0);
let sstream = ostream.QueryInterface(Ci.nsISeekableStream);
let n = size - 320e3 + 20e3;
sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n);
for (let i = 0; i < n; ++i) {
ostream.write("a", 1);
}
ostream.flush();
ostream.close();
Assert.equal(file.clone().fileSize, size);
return size;
}
async function run_test_1() {
// Load the profile and populate it.
await CookieXPCShellUtils.setCookieToDocument(
"oh=hai; max-age=1000"
);
// Close the profile.
await promise_close_profile();
// Open a database connection now, before we load the profile and begin
// asynchronous write operations.
let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
Assert.equal(do_count_cookies_in_db(db.db), 1);
// Load the profile, and wait for async read completion...
await promise_load_profile();
// Insert a row.
db.insertCookie(cookie);
db.close();
// Attempt to insert a cookie with the same (name, host, path) triplet.
Services.cookies.add(
cookie.host,
cookie.path,
cookie.name,
"hallo",
cookie.isSecure,
cookie.isHttpOnly,
cookie.isSession,
cookie.expiry,
{},
Ci.nsICookie.SAMESITE_NONE,
Ci.nsICookie.SCHEME_HTTPS
);
// Check that the cookie service accepted the new cookie.
Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
let isRebuildingDone = false;
let rebuildingObserve = function () {
isRebuildingDone = true;
Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
};
Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding");
// Crash test: we're going to rebuild the cookie database. Close all the db
// connections in the main thread and initialize a new database file in the
// cookie thread. Trigger some access of cookies to ensure we won't crash in
// the chaos status.
for (let i = 0; i < 10; ++i) {
Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
await new Promise(resolve => executeSoon(resolve));
}
// Wait for the cookie service to rename the old database and rebuild if not yet.
if (!isRebuildingDone) {
Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
await new _promise_observer("cookie-db-rebuilding");
}
await new Promise(resolve => executeSoon(resolve));
// At this point, the cookies should still be in memory.
Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1);
Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
Assert.equal(do_count_cookies(), 2);
// Close the profile.
await promise_close_profile();
// Check that the original database was renamed, and that it contains the
// original cookie.
Assert.ok(do_get_backup_file().exists());
let backupdb = Services.storage.openDatabase(do_get_backup_file());
Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1);
backupdb.close();
// Load the profile, and check that it contains the new cookie.
do_load_profile();
Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1);
let cookies = Services.cookies.getCookiesFromHost(cookie.host, {});
Assert.equal(cookies.length, 1);
let dbcookie = cookies[0];
Assert.equal(dbcookie.value, "hallo");
// Close the profile.
await promise_close_profile();
// Clean up.
do_get_cookie_file(profile).remove(false);
do_get_backup_file().remove(false);
Assert.ok(!do_get_cookie_file(profile).exists());
Assert.ok(!do_get_backup_file().exists());
}
async function run_test_2() {
// Load the profile and populate it.
do_load_profile();
Services.cookies.runInTransaction(_ => {
let uri = NetUtil.newURI("http://foo.com/");
const channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
for (let i = 0; i < 3000; ++i) {
uri = NetUtil.newURI("http://" + i + ".com/");
Services.cookies.setCookieStringFromHttp(
uri,
"oh=hai; max-age=1000",
channel
);
}
});
// Close the profile.
await promise_close_profile();
// Corrupt the database file.
let size = do_corrupt_db(do_get_cookie_file(profile));
// Load the profile.
do_load_profile();
// At this point, the database connection should be open. Ensure that it
// succeeded.
Assert.ok(!do_get_backup_file().exists());
// Recreate a new database since it was corrupted
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
Assert.equal(do_count_cookies(), 0);
// Close the profile.
await promise_close_profile();
// Check that the original database was renamed.
Assert.ok(do_get_backup_file().exists());
Assert.equal(do_get_backup_file().fileSize, size);
let db = Services.storage.openDatabase(do_get_cookie_file(profile));
db.close();
do_load_profile();
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
Assert.equal(do_count_cookies(), 0);
// Close the profile.
await promise_close_profile();
// Clean up.
do_get_cookie_file(profile).remove(false);
do_get_backup_file().remove(false);
Assert.ok(!do_get_cookie_file(profile).exists());
Assert.ok(!do_get_backup_file().exists());
}
async function run_test_3() {
// Set the maximum cookies per base domain limit to a large value, so that
// corrupting the database is easier.
Services.prefs.setIntPref("network.cookie.maxPerHost", 3000);
// Load the profile and populate it.
do_load_profile();
Services.cookies.runInTransaction(_ => {
let uri = NetUtil.newURI("http://hither.com/");
let channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
for (let i = 0; i < 10; ++i) {
Services.cookies.setCookieStringFromHttp(
uri,
"oh" + i + "=hai; max-age=1000",
channel
);
}
uri = NetUtil.newURI("http://haithur.com/");
channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
for (let i = 10; i < 3000; ++i) {
Services.cookies.setCookieStringFromHttp(
uri,
"oh" + i + "=hai; max-age=1000",
channel
);
}
});
// Close the profile.
await promise_close_profile();
// Corrupt the database file.
let size = do_corrupt_db(do_get_cookie_file(profile));
// Load the profile.
do_load_profile();
// At this point, the database connection should be open. Ensure that it
// succeeded.
Assert.ok(!do_get_backup_file().exists());
// Recreate a new database since it was corrupted
Assert.equal(Services.cookies.countCookiesFromHost("hither.com"), 0);
Assert.equal(Services.cookies.countCookiesFromHost("haithur.com"), 0);
// Close the profile.
await promise_close_profile();
let db = Services.storage.openDatabase(do_get_cookie_file(profile));
Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0);
Assert.equal(do_count_cookies_in_db(db), 0);
db.close();
// Check that the original database was renamed.
Assert.ok(do_get_backup_file().exists());
Assert.equal(do_get_backup_file().fileSize, size);
// Rename it back, and try loading the entire database synchronously.
do_get_backup_file().moveTo(null, "cookies.sqlite");
do_load_profile();
// At this point, the database connection should be open. Ensure that it
// succeeded.
Assert.ok(!do_get_backup_file().exists());
// Synchronously read in everything.
Assert.equal(do_count_cookies(), 0);
// Close the profile.
await promise_close_profile();
db = Services.storage.openDatabase(do_get_cookie_file(profile));
Assert.equal(do_count_cookies_in_db(db), 0);
db.close();
// Check that the original database was renamed.
Assert.ok(do_get_backup_file().exists());
Assert.equal(do_get_backup_file().fileSize, size);
// Clean up.
do_get_cookie_file(profile).remove(false);
do_get_backup_file().remove(false);
Assert.ok(!do_get_cookie_file(profile).exists());
Assert.ok(!do_get_backup_file().exists());
}
async function run_test_4() {
// Load the profile and populate it.
do_load_profile();
Services.cookies.runInTransaction(_ => {
let uri = NetUtil.newURI("http://foo.com/");
let channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
for (let i = 0; i < 3000; ++i) {
uri = NetUtil.newURI("http://" + i + ".com/");
Services.cookies.setCookieStringFromHttp(
uri,
"oh=hai; max-age=1000",
channel
);
}
});
// Close the profile.
await promise_close_profile();
// Corrupt the database file.
let size = do_corrupt_db(do_get_cookie_file(profile));
// Load the profile.
do_load_profile();
// At this point, the database connection should be open. Ensure that it
// succeeded.
Assert.ok(!do_get_backup_file().exists());
// Recreate a new database since it was corrupted
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
// Queue up an INSERT for the same base domain. This should also go into
// memory and be written out during database rebuild.
await CookieXPCShellUtils.setCookieToDocument(
"oh2=hai; max-age=1000"
);
// At this point, the cookies should still be in memory.
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1);
Assert.equal(do_count_cookies(), 1);
// Close the profile.
await promise_close_profile();
// Check that the original database was renamed.
Assert.ok(do_get_backup_file().exists());
Assert.equal(do_get_backup_file().fileSize, size);
// Load the profile, and check that it contains the new cookie.
do_load_profile();
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1);
Assert.equal(do_count_cookies(), 1);
// Close the profile.
await promise_close_profile();
// Clean up.
do_get_cookie_file(profile).remove(false);
do_get_backup_file().remove(false);
Assert.ok(!do_get_cookie_file(profile).exists());
Assert.ok(!do_get_backup_file().exists());
}
async function run_test_5() {
// Load the profile and populate it.
do_load_profile();
Services.cookies.runInTransaction(_ => {
let uri = NetUtil.newURI("http://bar.com/");
const channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
Services.cookies.setCookieStringFromHttp(
uri,
"oh=hai; path=/; max-age=1000",
channel
);
for (let i = 0; i < 3000; ++i) {
uri = NetUtil.newURI("http://" + i + ".com/");
Services.cookies.setCookieStringFromHttp(
uri,
"oh=hai; max-age=1000",
channel
);
}
});
// Close the profile.
await promise_close_profile();
// Corrupt the database file.
let size = do_corrupt_db(do_get_cookie_file(profile));
// Load the profile.
do_load_profile();
// At this point, the database connection should be open. Ensure that it
// succeeded.
Assert.ok(!do_get_backup_file().exists());
// Recreate a new database since it was corrupted
Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
Assert.equal(do_count_cookies(), 0);
Assert.ok(do_get_backup_file().exists());
Assert.equal(do_get_backup_file().fileSize, size);
Assert.ok(!do_get_rebuild_backup_file().exists());
// Open a database connection, and write a row that will trigger a constraint
// violation.
let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
db.insertCookie(cookie);
Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1);
Assert.equal(do_count_cookies_in_db(db.db), 1);
db.close();
// Check that the original backup and the database itself are gone.
Assert.ok(do_get_backup_file().exists());
Assert.equal(do_get_backup_file().fileSize, size);
Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
Assert.equal(do_count_cookies(), 0);
// Close the profile. We do not need to wait for completion, because the
// database has already been closed. Ensure the cookie file is unlocked.
await promise_close_profile();
// Clean up.
do_get_cookie_file(profile).remove(false);
do_get_backup_file().remove(false);
Assert.ok(!do_get_cookie_file(profile).exists());
Assert.ok(!do_get_backup_file().exists());
}