tor-browser

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

test_temporaryStorageEviction.js (11503B)


      1 /**
      2 * Any copyright is dedicated to the Public Domain.
      3 * http://creativecommons.org/publicdomain/zero/1.0/
      4 */
      5 
      6 const { PrefUtils } = ChromeUtils.importESModule(
      7  "resource://testing-common/dom/quota/test/modules/PrefUtils.sys.mjs"
      8 );
      9 const { PrincipalUtils } = ChromeUtils.importESModule(
     10  "resource://testing-common/dom/quota/test/modules/PrincipalUtils.sys.mjs"
     11 );
     12 const { QuotaUtils } = ChromeUtils.importESModule(
     13  "resource://testing-common/dom/quota/test/modules/QuotaUtils.sys.mjs"
     14 );
     15 const { SimpleDBUtils } = ChromeUtils.importESModule(
     16  "resource://testing-common/dom/simpledb/test/modules/SimpleDBUtils.sys.mjs"
     17 );
     18 
     19 // This value is used to set dom.quotaManager.temporaryStorage.fixedLimit
     20 // for this test, and must match the needs of the writes we plan to do.
     21 // The storage size must be a multiple of (number of origins - 1) to ensure
     22 // `dataSize` is a whole number. This is enforced below with a check to
     23 // guarantee predictable writes.
     24 const storageSizeKB = 32;
     25 
     26 /**
     27 * This test simulates origin usage across five related stages, exercising:
     28 * - Access time tracking on first and last access to each origin
     29 * - Eviction logic based on activity and last access time
     30 * - Usage reflection via QuotaManager's reporting APIs
     31 *
     32 * The test is data-driven: it defines a list of origins, each with a flag
     33 * array representing whether data on disk is expected to exist after each
     34 * stage. These flags are used to verify origin usage deterministically at each
     35 * point.
     36 *
     37 * The total storage size is evenly divided among all origins except the last
     38 * one. This ensures predictable write outcomes:
     39 * - All but the last origin succeed in writing initially
     40 * - The last origin exceeds quota and triggers eviction conditions
     41 *
     42 * Each stage simulates realistic temporary storage (AKA best-effort) behavior:
     43 * Stage 1 - Initializes all origin directories in reverse to test access time
     44 *           updates
     45 * Stage 2 - Opens connections and fills storage, leaving no room for the last
     46 *           origin
     47 * Stage 3 - Closes most connections to allow eviction of inactive origins
     48 * Stage 4 - Shrinks temporary storage by 50%, triggering additional evictions
     49 * Stage 5 - Writes again to the last origin to validate ongoing eviction
     50 *           behavior
     51 *
     52 * This test ensures correctness and robustness of temporary storage handling,
     53 * especially around eviction and access time policies.
     54 */
     55 async function testTemporaryStorageEviction() {
     56  const storageSize = storageSizeKB * 1024;
     57 
     58  // flags: [stage1, stage2, stage3, stage4, stage5]
     59  // 1 = data on disk should exist, 0 = data on disk should not exist
     60 
     61  /* prettier-ignore */
     62  const originInfos = [
     63    { url: "https://www.alpha.com",   flags: [0, 1, 1, 1, 1] },
     64    { url: "https://www.beta.com",    flags: [0, 1, 0, 0, 0] },
     65    { url: "https://www.gamma.com",   flags: [0, 1, 1, 0, 0] },
     66    { url: "https://www.delta.com",   flags: [0, 1, 1, 0, 0] },
     67    { url: "https://www.epsilon.com", flags: [0, 1, 1, 0, 0] },
     68    { url: "https://www2.alpha.com",  flags: [0, 1, 1, 0, 0] },
     69    { url: "https://www2.beta.com",   flags: [0, 1, 1, 0, 0] },
     70    { url: "https://www2.gamma.com",  flags: [0, 1, 1, 0, 0] },
     71    { url: "https://www2.delta.com",  flags: [0, 1, 1, 0, 0] },
     72    { url: "https://www2.epsilon.com",flags: [0, 1, 1, 0, 0] },
     73    { url: "https://www3.alpha.com",  flags: [0, 1, 1, 0, 0] },
     74    { url: "https://www3.beta.com",   flags: [0, 1, 1, 0, 0] },
     75    { url: "https://www3.gamma.com",  flags: [0, 1, 1, 0, 0] },
     76    { url: "https://www3.delta.com",  flags: [0, 1, 1, 0, 0] },
     77    { url: "https://www3.epsilon.com",flags: [0, 1, 1, 0, 0] },
     78    { url: "https://www.alpha.org",   flags: [0, 1, 1, 0, 0] },
     79    { url: "https://www.beta.org",    flags: [0, 1, 1, 0, 0] },
     80    { url: "https://www.gamma.org",   flags: [0, 1, 1, 0, 0] },
     81    { url: "https://www.delta.org",   flags: [0, 1, 1, 1, 0] },
     82    { url: "https://www.epsilon.org", flags: [0, 1, 1, 1, 1] },
     83    { url: "https://www.zeta.org",    flags: [0, 1, 1, 1, 1] },
     84    { url: "https://www.eta.org",     flags: [0, 1, 1, 1, 1] },
     85    { url: "https://www.theta.org",   flags: [0, 1, 1, 1, 1] },
     86    { url: "https://www.iota.org",    flags: [0, 1, 1, 1, 1] },
     87    { url: "https://www.kappa.org",   flags: [0, 1, 1, 1, 1] },
     88    { url: "https://www.lambda.org",  flags: [0, 1, 1, 1, 1] },
     89    { url: "https://www.mu.org",      flags: [0, 1, 1, 1, 1] },
     90    { url: "https://www.nu.org",      flags: [0, 1, 1, 1, 1] },
     91    { url: "https://www.xi.org",      flags: [0, 1, 1, 1, 1] },
     92    { url: "https://www.omicron.org", flags: [0, 1, 1, 1, 1] },
     93    { url: "https://www.pi.org",      flags: [0, 1, 1, 1, 1] },
     94    { url: "https://www.rho.org",     flags: [0, 1, 1, 1, 1] },
     95    { url: "https://www.omega.org",   flags: [0, 0, 1, 1, 1] },
     96  ];
     97  Assert.equal(
     98    storageSize % (originInfos.length - 1),
     99    0,
    100    "Correct storage size"
    101  );
    102 
    103  const name = "test_temporaryStorageEviction";
    104 
    105  const dataSize = storageSize / (originInfos.length - 1);
    106  const dataBuffer = new ArrayBuffer(dataSize);
    107 
    108  async function checkUsage(stage) {
    109    for (const originInfo of originInfos) {
    110      const url = originInfo.url;
    111 
    112      info(`Checking usage for ${url}`);
    113 
    114      const principal = PrincipalUtils.createPrincipal(url);
    115 
    116      const request = Services.qms.getUsageForPrincipal(principal, {});
    117      const usageResult = await QuotaUtils.requestFinished(request);
    118 
    119      if (originInfo.flags[stage - 1]) {
    120        Assert.greater(usageResult.usage, 0, "Correct usage");
    121      } else {
    122        Assert.equal(usageResult.usage, 0, "Correct usage");
    123      }
    124    }
    125  }
    126 
    127  async function createAndOpenConnection(url) {
    128    const principal = PrincipalUtils.createPrincipal(url);
    129 
    130    const connection = SimpleDBUtils.createConnection(principal);
    131 
    132    const openRequest = connection.open(name);
    133    await SimpleDBUtils.requestFinished(openRequest);
    134 
    135    return connection;
    136  }
    137 
    138  info(
    139    "Stage 1: Reverse creation of origins to test first/last access time updates"
    140  );
    141 
    142  // Initializes storage and temporary storage and creates all origin
    143  // directories with metadata, in reverse order. This ensures that the
    144  // "first access" and "last access" logic for updating origin access time is
    145  // properly exercised in other stages.
    146 
    147  info("Initializing storage");
    148 
    149  {
    150    const request = Services.qms.init();
    151    await QuotaUtils.requestFinished(request);
    152  }
    153 
    154  info("Initializing temporary storage");
    155 
    156  {
    157    const request = Services.qms.initTemporaryStorage();
    158    await QuotaUtils.requestFinished(request);
    159  }
    160 
    161  info("Initializing temporary origins");
    162 
    163  for (const originInfo of originInfos.toReversed()) {
    164    const principal = PrincipalUtils.createPrincipal(originInfo.url);
    165 
    166    const request = Services.qms.initializeTemporaryOrigin(
    167      "default",
    168      principal,
    169      /* aCreateIfNonExistent */ true
    170    );
    171    await QuotaUtils.requestFinished(request);
    172 
    173    // Wait 40ms to ensure the next origin gets a different access time. Some
    174    // systems have low timer resolution, so this adds a safe buffer.
    175    await new Promise(function (resolve) {
    176      do_timeout(40, resolve);
    177    });
    178  }
    179 
    180  info("Checking usage");
    181 
    182  await checkUsage(/* stage */ 1);
    183 
    184  info(
    185    "Stage 2: All origins active; eviction not possible, last write should fail"
    186  );
    187 
    188  // Opens connections for all origins and writes data to each except the last
    189  // one. Since all origins remain active (open connections), none can be
    190  // evicted, even if storage runs out. This tests that eviction logic respects
    191  // activity status.
    192 
    193  const connections = await (async function () {
    194    let connections = [];
    195    // Stage 1
    196    for (const originInfo of originInfos) {
    197      const connection = await createAndOpenConnection(originInfo.url);
    198 
    199      connections.push(connection);
    200    }
    201 
    202    return connections;
    203  })();
    204 
    205  // Write to all but the last origin.
    206  for (const connection of connections.slice(0, -1)) {
    207    const writeRequest = connection.write(dataBuffer);
    208    await SimpleDBUtils.requestFinished(writeRequest);
    209  }
    210 
    211  // Try to write to the last origin.
    212  {
    213    const writeRequest = connections.at(-1).write(dataBuffer);
    214    try {
    215      await SimpleDBUtils.requestFinished(writeRequest);
    216      Assert.ok(false, "Should have thrown");
    217    } catch (e) {
    218      Assert.ok(true, "Should have thrown");
    219      Assert.strictEqual(
    220        e.resultCode,
    221        NS_ERROR_FILE_NO_DEVICE_SPACE,
    222        "Threw right result code"
    223      );
    224    }
    225  }
    226 
    227  await checkUsage(/* stage */ 2);
    228 
    229  info("Stage 3: Inactive origins can be evicted; last origin writes again");
    230 
    231  // Closes all connections except the first and last origin. This leaves most
    232  // origins inactive, making them eligible for eviction. The last origin
    233  // writes data again, which should now succeed because there is at least one
    234  // inactive origin that can be evicted to make space.
    235 
    236  // Close all connections except the first and the last
    237  for (const connection of connections.slice(1, -1)) {
    238    const closeRequest = connection.close();
    239    await SimpleDBUtils.requestFinished(closeRequest);
    240 
    241    // Wait 40ms to ensure the next origin gets a different access time. Some
    242    // systems have low timer resolution, so this adds a safe buffer.
    243    await new Promise(function (resolve) {
    244      do_timeout(40, resolve);
    245    });
    246  }
    247 
    248  // Write to the last origin.
    249  {
    250    const writeRequest = connections.at(-1).write(dataBuffer);
    251    await SimpleDBUtils.requestFinished(writeRequest);
    252  }
    253 
    254  await checkUsage(/* stage */ 3);
    255 
    256  info("Stage 4: Shrink quota by 50%; evict origins by last access time");
    257 
    258  // Shrinks the temporary storage quota by 50%. This triggers eviction of
    259  // approximately half of the origins based on their last access time. It
    260  // tests that quota reduction correctly respects access time ordering when
    261  // deciding which origins to evict.
    262 
    263  info("Shutting down storage");
    264 
    265  {
    266    const request = Services.qms.reset();
    267    await QuotaUtils.requestFinished(request);
    268  }
    269 
    270  info("Setting preferences");
    271 
    272  {
    273    const prefs = [
    274      ["dom.quotaManager.temporaryStorage.fixedLimit", storageSizeKB / 2],
    275    ];
    276 
    277    PrefUtils.setPrefs(prefs);
    278  }
    279 
    280  info("Initializing storage");
    281 
    282  {
    283    const request = Services.qms.init();
    284    await QuotaUtils.requestFinished(request);
    285  }
    286 
    287  info("Initializing temporary storage");
    288 
    289  {
    290    const request = Services.qms.initTemporaryStorage();
    291    await QuotaUtils.requestFinished(request);
    292  }
    293 
    294  await checkUsage(/* stage */ 4);
    295 
    296  info("Stage 5: Last origin writes more; one more origin should be evicted");
    297 
    298  // The last origin writes additional data, which should exceed the current
    299  // quota again. This triggers eviction of one more inactive origin,
    300  // validating that eviction continues to respect quota limits and frees up
    301  // space as needed.
    302 
    303  {
    304    const connection = await createAndOpenConnection(originInfos.at(-1).url);
    305 
    306    const seekRequest = connection.seek(dataSize);
    307    await SimpleDBUtils.requestFinished(seekRequest);
    308 
    309    const writeRequest = connection.write(dataBuffer);
    310    await SimpleDBUtils.requestFinished(writeRequest);
    311  }
    312 
    313  await checkUsage(/* stage */ 5);
    314 }
    315 
    316 async function testSteps() {
    317  add_task(
    318    {
    319      pref_set: [
    320        ["dom.quotaManager.loadQuotaFromCache", false],
    321        ["dom.quotaManager.temporaryStorage.fixedLimit", storageSizeKB],
    322        ["dom.quotaManager.temporaryStorage.updateOriginAccessTime", true],
    323      ],
    324    },
    325    testTemporaryStorageEviction
    326  );
    327 }