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 }