Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Verifies that flipping the at-rest disk cache encryption pref
# (browser.cache.disk.encryption.enabled) purges the disk cache on the next
# startup. The encryption state the cache was written with is stored in the
# index header (mIsEncrypted); when it no longer matches the pref at startup the
# whole cache is purged, so the cache never holds a mix of encrypted and
# plaintext entries.
import time
from pathlib import Path
from marionette_harness import MarionetteTestCase
DATA = "cache-encryption-flip-test-payload-0123456789"
# Writes a disk cache entry for the given url and resolves once it is stored.
WRITE_SCRIPT = """
const [url, data, resolve] = arguments;
const uri = Services.io.newURI(url);
const storage = Services.cache2.diskCacheStorage(Services.loadContextInfo.default);
storage.asyncOpenURI(uri, "", Ci.nsICacheStorage.OPEN_TRUNCATE, {
QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
onCacheEntryCheck() {
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
},
onCacheEntryAvailable(entry, isNew, status) {
try {
entry.setMetaDataElement("test", "1");
entry.metaDataReady();
const os = entry.openOutputStream(0, -1);
os.write(data, data.length);
os.close();
resolve(true);
} catch (e) {
resolve("error: " + e);
}
},
});
"""
# Resolves true iff a disk cache entry for the given url exists.
EXISTS_SCRIPT = """
const [url, resolve] = arguments;
const uri = Services.io.newURI(url);
const storage = Services.cache2.diskCacheStorage(Services.loadContextInfo.default);
storage.asyncOpenURI(uri, "", Ci.nsICacheStorage.OPEN_READONLY, {
QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
onCacheEntryCheck() {
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
},
onCacheEntryAvailable(entry, isNew, status) {
resolve(!!entry && Components.isSuccessCode(status));
},
});
"""
class CacheEncryptionFlipPurgeTestCase(MarionetteTestCase):
def setUp(self):
super().setUp()
self.marionette.enforce_gecko_prefs({
"browser.cache.disk.enable": True,
# Start unencrypted.
"browser.cache.disk.encryption.enabled": False,
# Force the index to be written back to disk eagerly so it
# exists (carrying the encryption flag) before we restart: start the
# index build immediately and write it after a single change.
"browser.cache.disk.index.update_start_delay_ms": 0,
"browser.cache.disk.index.min_unwritten_changes": 1,
"browser.cache.disk.index.min_dump_interval_ms": 0,
# Keep anything else from clearing the cache on shutdown.
"privacy.sanitize.sanitizeOnShutdown": False,
"privacy.clearOnShutdown.cache": False,
})
self.index_path = Path(self.marionette.profile_path).joinpath("cache2", "index")
self.marionette.set_context("chrome")
def tearDown(self):
self.marionette.restart(in_app=False, clean=True)
super().tearDown()
def write_entry(self, url):
result = self.marionette.execute_async_script(
WRITE_SCRIPT, script_args=(url, DATA)
)
self.assertEqual(result, True, "writing the cache entry should succeed")
def entry_exists(self, url):
return self.marionette.execute_async_script(EXISTS_SCRIPT, script_args=(url,))
def populate_and_persist_index(self):
# The index is written back to disk only from the READY state (after the
# initial build finishes), and only as a side effect of an entry
# operation. The write thresholds are lowered to 1 change / 0ms in
# setUp, so keep writing entries until the index file appears on disk.
self.write_entry(URL)
deadline = time.monotonic() + 30
i = 0
while not self.index_path.exists() and time.monotonic() < deadline:
self.write_entry(f"{URL}-{i}")
i += 1
time.sleep(0.2)
self.assertTrue(
self.index_path.exists(),
"the disk cache index file must be written to disk",
)
def test_purge_on_encryption_flip(self):
# 1. Populate the (unencrypted) cache and persist the index.
self.populate_and_persist_index()
self.assertTrue(
self.entry_exists(URL), "entry should exist after being written"
)
# 2. Control: restart without changing the pref. The index's stored
# encryption flag still matches the pref, so the entry must survive.
self.marionette.restart(in_app=True, clean=False)
self.marionette.set_context("chrome")
self.assertTrue(
self.entry_exists(URL),
"entry must survive a restart when the encryption pref is unchanged",
)
# 3. Flip encryption on and restart. The index was written with the flag
# off, so at startup it no longer matches the pref and the whole cache
# must be purged.
self.marionette.enforce_gecko_prefs({
"browser.cache.disk.encryption.enabled": True
})
self.marionette.restart(in_app=True, clean=False)
self.marionette.set_context("chrome")
self.assertFalse(
self.entry_exists(URL),
"entry must be gone after flipping encryption purges the cache",
)