tor-browser

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

browser_content_sandbox_utils.js (13578B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 "use strict";
      4 
      5 const uuidGenerator = Services.uuid;
      6 
      7 /*
      8 * Utility functions for the browser content sandbox tests.
      9 */
     10 
     11 function sanityChecks() {
     12  // This test is only relevant in e10s
     13  if (!gMultiProcessBrowser) {
     14    ok(false, "e10s is enabled");
     15    info("e10s is not enabled, exiting");
     16    return;
     17  }
     18 
     19  let level = 0;
     20  let prefExists = true;
     21 
     22  // Read the security.sandbox.content.level pref.
     23  // eslint-disable-next-line mozilla/use-default-preference-values
     24  try {
     25    level = Services.prefs.getIntPref("security.sandbox.content.level");
     26  } catch (e) {
     27    prefExists = false;
     28  }
     29 
     30  ok(prefExists, "pref security.sandbox.content.level exists");
     31  if (!prefExists) {
     32    return;
     33  }
     34 
     35  info(`security.sandbox.content.level=${level}`);
     36  Assert.greater(level, 0, "content sandbox is enabled.");
     37 
     38  let isFileIOSandboxed = isContentFileIOSandboxed(level);
     39 
     40  // Content sandbox enabled, but level doesn't include file I/O sandboxing.
     41  ok(isFileIOSandboxed, "content file I/O sandboxing is enabled.");
     42  if (!isFileIOSandboxed) {
     43    info("content sandbox level too low for file I/O tests, exiting\n");
     44  }
     45 }
     46 
     47 function isXdgEnabled() {
     48  try {
     49    return Services.prefs.getBoolPref("widget.support-xdg-config");
     50  } catch (ex) {
     51    // if the pref is not there it means we dont have XDG support
     52    if (ex.name === "NS_ERROR_UNEXPECTED") {
     53      return false;
     54    }
     55    throw ex;
     56  }
     57 }
     58 
     59 // Creates file at |path| and returns a promise that resolves with an object
     60 // with .ok boolean to indicate true if the file was successfully created,
     61 // otherwise false. Include imports so this can be safely serialized and run
     62 // remotely by ContentTask.spawn.
     63 //
     64 // Report the exception's error code in .code as well.
     65 function createFile(path) {
     66  const { FileUtils } = ChromeUtils.importESModule(
     67    "resource://gre/modules/FileUtils.sys.mjs"
     68  );
     69 
     70  try {
     71    const fstream = Cc[
     72      "@mozilla.org/network/file-output-stream;1"
     73    ].createInstance(Ci.nsIFileOutputStream);
     74 
     75    fstream.init(
     76      new FileUtils.File(path),
     77      -1, // readonly mode
     78      -1, // default permissions
     79      0
     80    ); // behaviour flags
     81 
     82    const ostream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
     83      Ci.nsIBinaryOutputStream
     84    );
     85    ostream.setOutputStream(fstream);
     86 
     87    const data = "TEST FILE DUMMY DATA";
     88    ostream.writeBytes(data, data.length);
     89 
     90    ostream.close();
     91    fstream.close();
     92  } catch (e) {
     93    return { ok: false, code: e.result };
     94  }
     95 
     96  return { ok: true };
     97 }
     98 
     99 // Creates a symlink at |path| and returns a promise that resolves with an
    100 // object with .ok boolean to indicate true if the symlink was successfully
    101 // created, otherwise false. Include imports so this can be safely serialized
    102 // and run remotely by ContentTask.spawn.
    103 //
    104 // Report the exception's error code in .code as well.
    105 // Report errno in .code if syscall returns -1.
    106 function createSymlink(path) {
    107  const { ctypes } = ChromeUtils.importESModule(
    108    "resource://gre/modules/ctypes.sys.mjs"
    109  );
    110 
    111  try {
    112    // Trying to open "libc.so" on linux will fail with invalid elf header error
    113    // because it would be a linker script. Using libc.so.6 avoids that.
    114    const libc = ctypes.open(
    115      Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so.6"
    116    );
    117 
    118    const symlink = libc.declare(
    119      "symlink",
    120      ctypes.default_abi,
    121      ctypes.int, // return value
    122      ctypes.char.ptr, // target
    123      ctypes.char.ptr //linkpath
    124    );
    125 
    126    ctypes.errno = 0;
    127    const rv = symlink("/etc", path);
    128    const _errno = ctypes.errno;
    129    if (rv < 0) {
    130      return { ok: false, code: _errno };
    131    }
    132  } catch (e) {
    133    return { ok: false, code: e.result };
    134  }
    135 
    136  return { ok: true };
    137 }
    138 
    139 // Deletes file at |path| and returns a promise that resolves with an object
    140 // with .ok boolean to indicate true if the file was successfully deleted,
    141 // otherwise false. Include imports so this can be safely serialized and run
    142 // remotely by ContentTask.spawn.
    143 //
    144 // Report the exception's error code in .code as well.
    145 function deleteFile(path) {
    146  const { FileUtils } = ChromeUtils.importESModule(
    147    "resource://gre/modules/FileUtils.sys.mjs"
    148  );
    149 
    150  try {
    151    const file = new FileUtils.File(path);
    152    file.remove(false);
    153  } catch (e) {
    154    return { ok: false, code: e.result };
    155  }
    156 
    157  return { ok: true };
    158 }
    159 
    160 // Reads the directory at |path| and returns a promise that resolves when
    161 // iteration over the directory finishes or encounters an error. The promise
    162 // resolves with an object where .ok indicates success or failure and
    163 // .numEntries is the number of directory entries found.
    164 //
    165 // Report the exception's error code in .code as well.
    166 function readDir(path) {
    167  const { FileUtils } = ChromeUtils.importESModule(
    168    "resource://gre/modules/FileUtils.sys.mjs"
    169  );
    170 
    171  let numEntries = 0;
    172 
    173  try {
    174    const file = new FileUtils.File(path);
    175    const enumerator = file.directoryEntries;
    176 
    177    while (enumerator.hasMoreElements()) {
    178      void enumerator.nextFile;
    179      numEntries++;
    180    }
    181  } catch (e) {
    182    return { ok: false, numEntries, code: e.result };
    183  }
    184 
    185  return { ok: true, numEntries };
    186 }
    187 
    188 // Reads the file at |path| and returns a promise that resolves when
    189 // reading is completed. Returned object has boolean .ok to indicate
    190 // success or failure.
    191 //
    192 // Report the exception's error code in .code as well.
    193 function readFile(path) {
    194  const { FileUtils } = ChromeUtils.importESModule(
    195    "resource://gre/modules/FileUtils.sys.mjs"
    196  );
    197 
    198  try {
    199    const file = new FileUtils.File(path);
    200 
    201    const fstream = Cc[
    202      "@mozilla.org/network/file-input-stream;1"
    203    ].createInstance(Ci.nsIFileInputStream);
    204    fstream.init(file, -1, -1, 0);
    205 
    206    const istream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
    207      Ci.nsIBinaryInputStream
    208    );
    209    istream.setInputStream(fstream);
    210 
    211    const available = istream.available();
    212    void istream.readBytes(available);
    213  } catch (e) {
    214    return { ok: false, code: e.result };
    215  }
    216 
    217  return { ok: true };
    218 }
    219 
    220 // Does a stat of |path| and returns a promise that resolves if the
    221 // stat is successful. Returned object has boolean .ok to indicate
    222 // success or failure.
    223 //
    224 // Report the exception's error code in .code as well.
    225 function statPath(path) {
    226  const { FileUtils } = ChromeUtils.importESModule(
    227    "resource://gre/modules/FileUtils.sys.mjs"
    228  );
    229 
    230  try {
    231    const file = new FileUtils.File(path);
    232    void file.lastModifiedTime;
    233  } catch (e) {
    234    return { ok: false, code: e.result };
    235  }
    236 
    237  return { ok: true };
    238 }
    239 
    240 // Returns true if the current content sandbox level, passed in
    241 // the |level| argument, supports filesystem sandboxing.
    242 function isContentFileIOSandboxed(level) {
    243  let fileIOSandboxMinLevel = 0;
    244 
    245  // Set fileIOSandboxMinLevel to the lowest level that has
    246  // content filesystem sandboxing enabled. For now, this
    247  // varies across Windows, Mac, Linux, other.
    248  switch (Services.appinfo.OS) {
    249    case "WINNT":
    250      fileIOSandboxMinLevel = 1;
    251      break;
    252    case "Darwin":
    253      fileIOSandboxMinLevel = 1;
    254      break;
    255    case "Linux":
    256      fileIOSandboxMinLevel = 2;
    257      break;
    258    default:
    259      Assert.ok(false, "Unknown OS");
    260  }
    261 
    262  return level >= fileIOSandboxMinLevel;
    263 }
    264 
    265 // Returns the lowest sandbox level where blanket reading of the profile
    266 // directory from the content process should be blocked by the sandbox.
    267 function minProfileReadSandboxLevel() {
    268  switch (Services.appinfo.OS) {
    269    case "WINNT":
    270      return 3;
    271    case "Darwin":
    272      return 2;
    273    case "Linux":
    274      return 3;
    275    default:
    276      Assert.ok(false, "Unknown OS");
    277      return 0;
    278  }
    279 }
    280 
    281 // Returns the lowest sandbox level where blanket reading of the home
    282 // directory from the content process should be blocked by the sandbox.
    283 function minHomeReadSandboxLevel() {
    284  switch (Services.appinfo.OS) {
    285    case "WINNT":
    286      return 3;
    287    case "Darwin":
    288      return 3;
    289    case "Linux":
    290      return 3;
    291    default:
    292      Assert.ok(false, "Unknown OS");
    293      return 0;
    294  }
    295 }
    296 
    297 function isMac() {
    298  return Services.appinfo.OS == "Darwin";
    299 }
    300 function isWin() {
    301  return Services.appinfo.OS == "WINNT";
    302 }
    303 function isLinux() {
    304  return Services.appinfo.OS == "Linux";
    305 }
    306 
    307 function isNightly() {
    308  let version = SpecialPowers.Services.appinfo.version;
    309  return version.endsWith("a1");
    310 }
    311 
    312 function uuid() {
    313  return uuidGenerator.generateUUID().toString();
    314 }
    315 
    316 // Returns a file object for a new file in the home dir ($HOME/<UUID>).
    317 function fileInHomeDir() {
    318  // get home directory, make sure it exists
    319  let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
    320  Assert.ok(homeDir.exists(), "Home dir exists");
    321  Assert.ok(homeDir.isDirectory(), "Home dir is a directory");
    322 
    323  // build a file object for a new file named $HOME/<UUID>
    324  let homeFile = homeDir.clone();
    325  homeFile.appendRelativePath(uuid());
    326  Assert.ok(!homeFile.exists(), homeFile.path + " does not exist");
    327  return homeFile;
    328 }
    329 
    330 // Returns a file object for a new file in the content temp dir (.../<UUID>).
    331 function fileInTempDir() {
    332  let contentTempKey = "TmpD";
    333 
    334  // get the content temp dir, make sure it exists
    335  let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile);
    336  Assert.ok(ctmp.exists(), "Temp dir exists");
    337  Assert.ok(ctmp.isDirectory(), "Temp dir is a directory");
    338 
    339  // build a file object for a new file in content temp
    340  let tempFile = ctmp.clone();
    341  tempFile.appendRelativePath(uuid());
    342  Assert.ok(!tempFile.exists(), tempFile.path + " does not exist");
    343  return tempFile;
    344 }
    345 
    346 function GetProfileDir() {
    347  // get profile directory
    348  let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    349  return profileDir;
    350 }
    351 
    352 function GetHomeDir() {
    353  // get home directory
    354  let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
    355  return homeDir;
    356 }
    357 
    358 function GetHomeSubdir(subdir) {
    359  return GetSubdir(GetHomeDir(), subdir);
    360 }
    361 
    362 function GetHomeSubdirFile(subdir) {
    363  return GetSubdirFile(GetHomeSubdir(subdir));
    364 }
    365 
    366 function GetSubdir(dir, subdir) {
    367  let newSubdir = dir.clone();
    368  newSubdir.appendRelativePath(subdir);
    369  return newSubdir;
    370 }
    371 
    372 function GetSubdirFile(dir) {
    373  let newFile = dir.clone();
    374  newFile.appendRelativePath(uuid());
    375  return newFile;
    376 }
    377 
    378 function GetPerUserExtensionDir() {
    379  return Services.dirsvc.get("XREUSysExt", Ci.nsIFile);
    380 }
    381 
    382 // Returns a file object for the file or directory named |name| in the
    383 // profile directory.
    384 function GetProfileEntry(name) {
    385  let entry = GetProfileDir();
    386  entry.append(name);
    387  return entry;
    388 }
    389 
    390 function GetDir(path) {
    391  info(`GetDir(${path})`);
    392  let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
    393  dir.initWithPath(path);
    394  Assert.ok(dir.isDirectory(), `${path} is a directory`);
    395  return dir;
    396 }
    397 
    398 function GetDirFromEnvVariable(varName) {
    399  return GetDir(Services.env.get(varName));
    400 }
    401 
    402 function GetFile(path) {
    403  let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
    404  file.initWithPath(path);
    405  return file;
    406 }
    407 
    408 function GetBrowserType(type) {
    409  let browserType = undefined;
    410 
    411  if (!GetBrowserType[type]) {
    412    if (type === "web") {
    413      GetBrowserType[type] = gBrowser.selectedBrowser;
    414    } else {
    415      // open a tab in a `type` content process
    416      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
    417        preferredRemoteType: type,
    418        allowInheritPrincipal: true,
    419      });
    420      // get the browser for the `type` process tab
    421      GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab);
    422    }
    423  }
    424 
    425  browserType = GetBrowserType[type];
    426  Assert.strictEqual(
    427    browserType.remoteType,
    428    type,
    429    `GetBrowserType(${type}) returns a ${type} process`
    430  );
    431  return browserType;
    432 }
    433 
    434 function GetWebBrowser() {
    435  return GetBrowserType("web");
    436 }
    437 
    438 function isFileContentProcessEnabled() {
    439  // Ensure that the file content process is enabled.
    440  let fileContentProcessEnabled = Services.prefs.getBoolPref(
    441    "browser.tabs.remote.separateFileUriProcess"
    442  );
    443  ok(fileContentProcessEnabled, "separate file content process is enabled");
    444  return fileContentProcessEnabled;
    445 }
    446 
    447 function GetFileBrowser() {
    448  if (!isFileContentProcessEnabled()) {
    449    return undefined;
    450  }
    451  return GetBrowserType("file");
    452 }
    453 
    454 function GetSandboxLevel() {
    455  // Current level
    456  return Services.prefs.getIntPref("security.sandbox.content.level");
    457 }
    458 
    459 async function runTestsList(tests) {
    460  let level = GetSandboxLevel();
    461 
    462  // remove tests not enabled by the current sandbox level
    463  tests = tests.filter(test => test.minLevel <= level);
    464 
    465  for (let test of tests) {
    466    let okString = test.ok ? "allowed" : "blocked";
    467    let processType = test.browser.remoteType;
    468 
    469    // ensure the file/dir exists before we ask a content process to stat
    470    // it so we know a failure is not due to a nonexistent file/dir
    471    if (test.func === statPath) {
    472      ok(test.file.exists(), `${test.file.path} exists`);
    473    }
    474 
    475    let result = await ContentTask.spawn(
    476      test.browser,
    477      test.file.path,
    478      test.func
    479    );
    480 
    481    Assert.equal(
    482      result.ok,
    483      test.ok,
    484      `reading ${test.desc} from a ${processType} process ` +
    485        `is ${okString} (${test.file.path})`
    486    );
    487 
    488    // if the directory is not expected to be readable,
    489    // ensure the listing has zero entries
    490    if (test.func === readDir && !test.ok) {
    491      Assert.equal(
    492        result.numEntries,
    493        0,
    494        `directory list is empty (${test.file.path})`
    495      );
    496    }
    497 
    498    if (test.cleanup != undefined) {
    499      await test.cleanup(test.file.path);
    500    }
    501  }
    502 }