tor-browser

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

browser_content_sandbox_syscalls.js (11210B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 /* import-globals-from browser_content_sandbox_utils.js */
      4 "use strict";
      5 
      6 Services.scriptloader.loadSubScript(
      7  "chrome://mochitests/content/browser/" +
      8    "security/sandbox/test/browser_content_sandbox_utils.js",
      9  this
     10 );
     11 
     12 const lazy = {};
     13 
     14 /* getLibcConstants is only present on *nix */
     15 ChromeUtils.defineLazyGetter(lazy, "LIBC", () =>
     16  ChromeUtils.getLibcConstants()
     17 );
     18 
     19 /*
     20 * This test is for executing system calls in content processes to validate
     21 * that calls that are meant to be blocked by content sandboxing are blocked.
     22 * We use the term system calls loosely so that any OS API call such as
     23 * fopen could be included.
     24 */
     25 
     26 // Calls the native execv library function. Include imports so this can be
     27 // safely serialized and run remotely by ContentTask.spawn.
     28 function callExec(args) {
     29  const { ctypes } = ChromeUtils.importESModule(
     30    "resource://gre/modules/ctypes.sys.mjs"
     31  );
     32  let { lib, cmd } = args;
     33  let libc = ctypes.open(lib);
     34  let exec = libc.declare(
     35    "execv",
     36    ctypes.default_abi,
     37    ctypes.int,
     38    ctypes.char.ptr
     39  );
     40  let rv = exec(cmd);
     41  libc.close();
     42  return rv;
     43 }
     44 
     45 // Calls the native fork syscall.
     46 function callFork(args) {
     47  const { ctypes } = ChromeUtils.importESModule(
     48    "resource://gre/modules/ctypes.sys.mjs"
     49  );
     50  let { lib } = args;
     51  let libc = ctypes.open(lib);
     52  let fork = libc.declare("fork", ctypes.default_abi, ctypes.int);
     53  let rv = fork();
     54  libc.close();
     55  return rv;
     56 }
     57 
     58 // Calls the native sysctl syscall.
     59 function callSysctl(args) {
     60  const { ctypes } = ChromeUtils.importESModule(
     61    "resource://gre/modules/ctypes.sys.mjs"
     62  );
     63  let { lib, name } = args;
     64  let libc = ctypes.open(lib);
     65  let sysctlbyname = libc.declare(
     66    "sysctlbyname",
     67    ctypes.default_abi,
     68    ctypes.int,
     69    ctypes.char.ptr,
     70    ctypes.voidptr_t,
     71    ctypes.size_t.ptr,
     72    ctypes.voidptr_t,
     73    ctypes.size_t.ptr
     74  );
     75  let rv = sysctlbyname(name, null, null, null, null);
     76  libc.close();
     77  return rv;
     78 }
     79 
     80 function callPrctl(args) {
     81  const { ctypes } = ChromeUtils.importESModule(
     82    "resource://gre/modules/ctypes.sys.mjs"
     83  );
     84  let { lib, option } = args;
     85  let libc = ctypes.open(lib);
     86  let prctl = libc.declare(
     87    "prctl",
     88    ctypes.default_abi,
     89    ctypes.int,
     90    ctypes.int, // option
     91    ctypes.unsigned_long, // arg2
     92    ctypes.unsigned_long, // arg3
     93    ctypes.unsigned_long, // arg4
     94    ctypes.unsigned_long // arg5
     95  );
     96  let rv = prctl(option, 0, 0, 0, 0);
     97  if (rv == -1) {
     98    rv = ctypes.errno;
     99  }
    100  libc.close();
    101  return rv;
    102 }
    103 
    104 // Calls the native open/close syscalls.
    105 function callOpen(args) {
    106  const { ctypes } = ChromeUtils.importESModule(
    107    "resource://gre/modules/ctypes.sys.mjs"
    108  );
    109  let { lib, path, flags } = args;
    110  let libc = ctypes.open(lib);
    111  let open = libc.declare(
    112    "open",
    113    ctypes.default_abi,
    114    ctypes.int,
    115    ctypes.char.ptr,
    116    ctypes.int
    117  );
    118  let close = libc.declare("close", ctypes.default_abi, ctypes.int, ctypes.int);
    119  let fd = open(path, flags);
    120  close(fd);
    121  libc.close();
    122  return fd;
    123 }
    124 
    125 // Verify faccessat2
    126 function callFaccessat2(args) {
    127  const { ctypes } = ChromeUtils.importESModule(
    128    "resource://gre/modules/ctypes.sys.mjs"
    129  );
    130  let { lib, dirfd, path, mode, flag } = args;
    131  let libc = ctypes.open(lib);
    132  let faccessat = libc.declare(
    133    "faccessat",
    134    ctypes.default_abi,
    135    ctypes.int,
    136    ctypes.int, // dirfd
    137    ctypes.char.ptr, // path
    138    ctypes.int, // mode
    139    ctypes.int // flag
    140  );
    141  let rv = faccessat(dirfd, path, mode, flag);
    142  if (rv == -1) {
    143    rv = ctypes.errno;
    144  }
    145  libc.close();
    146  return rv;
    147 }
    148 
    149 // Returns the name of the native library needed for native syscalls
    150 function getOSLib() {
    151  switch (Services.appinfo.OS) {
    152    case "WINNT":
    153      return "kernel32.dll";
    154    case "Darwin":
    155      return "libc.dylib";
    156    case "Linux":
    157      return "libc.so.6";
    158    default:
    159      Assert.ok(false, "Unknown OS");
    160      return 0;
    161  }
    162 }
    163 
    164 // Reading a header might be weird, but the alternatives to read a stable
    165 // version number we can easily check against are not much more fun
    166 async function getKernelVersion() {
    167  let header = await IOUtils.readUTF8("/usr/include/linux/version.h");
    168  let hr = header.split("\n");
    169  for (let line in hr) {
    170    let hrs = hr[line].split(" ");
    171    if (hrs[0] === "#define" && hrs[1] === "LINUX_VERSION_CODE") {
    172      return Number(hrs[2]);
    173    }
    174  }
    175  throw Error("No LINUX_VERSION_CODE");
    176 }
    177 
    178 // This is how it is done in /usr/include/linux/version.h
    179 function computeKernelVersion(major, minor, dot) {
    180  return (major << 16) + (minor << 8) + dot;
    181 }
    182 
    183 function getGlibcVersion() {
    184  const { ctypes } = ChromeUtils.importESModule(
    185    "resource://gre/modules/ctypes.sys.mjs"
    186  );
    187  let libc = ctypes.open(getOSLib());
    188  let gnu_get_libc_version = libc.declare(
    189    "gnu_get_libc_version",
    190    ctypes.default_abi,
    191    ctypes.char.ptr
    192  );
    193  let rv = gnu_get_libc_version().readString();
    194  libc.close();
    195  let ar = rv.split(".");
    196  // return a number made of MAJORMINOR
    197  return Number(ar[0] + ar[1]);
    198 }
    199 
    200 // Returns a harmless command to execute with execv
    201 function getOSExecCmd() {
    202  Assert.ok(!isWin());
    203  return "/bin/cat";
    204 }
    205 
    206 // Returns true if the current content sandbox level, passed in
    207 // the |level| argument, supports syscall sandboxing.
    208 function areContentSyscallsSandboxed(level) {
    209  let syscallsSandboxMinLevel = 0;
    210 
    211  // Set syscallsSandboxMinLevel to the lowest level that has
    212  // syscall sandboxing enabled. For now, this varies across
    213  // Windows, Mac, Linux, other.
    214  switch (Services.appinfo.OS) {
    215    case "WINNT":
    216      syscallsSandboxMinLevel = 1;
    217      break;
    218    case "Darwin":
    219      syscallsSandboxMinLevel = 1;
    220      break;
    221    case "Linux":
    222      syscallsSandboxMinLevel = 1;
    223      break;
    224    default:
    225      Assert.ok(false, "Unknown OS");
    226  }
    227 
    228  return level >= syscallsSandboxMinLevel;
    229 }
    230 
    231 //
    232 // Drive tests for a single content process.
    233 //
    234 // Tests executing OS API calls in the content process. Limited to Mac
    235 // and Linux calls for now.
    236 //
    237 add_task(async function () {
    238  // This test is only relevant in e10s
    239  if (!gMultiProcessBrowser) {
    240    ok(false, "e10s is enabled");
    241    info("e10s is not enabled, exiting");
    242    return;
    243  }
    244 
    245  let level = 0;
    246  let prefExists = true;
    247 
    248  // Read the security.sandbox.content.level pref.
    249  // If the pref isn't set and we're running on Linux on !isNightly(),
    250  // exit without failing. The Linux content sandbox is only enabled
    251  // on Nightly at this time.
    252  // eslint-disable-next-line mozilla/use-default-preference-values
    253  try {
    254    level = Services.prefs.getIntPref("security.sandbox.content.level");
    255  } catch (e) {
    256    prefExists = false;
    257  }
    258 
    259  ok(prefExists, "pref security.sandbox.content.level exists");
    260  if (!prefExists) {
    261    return;
    262  }
    263 
    264  info(`security.sandbox.content.level=${level}`);
    265  Assert.greater(level, 0, "content sandbox is enabled.");
    266 
    267  let areSyscallsSandboxed = areContentSyscallsSandboxed(level);
    268 
    269  // Content sandbox enabled, but level doesn't include syscall sandboxing.
    270  ok(areSyscallsSandboxed, "content syscall sandboxing is enabled.");
    271  if (!areSyscallsSandboxed) {
    272    info("content sandbox level too low for syscall tests, exiting\n");
    273    return;
    274  }
    275 
    276  let browser = gBrowser.selectedBrowser;
    277  let lib = getOSLib();
    278 
    279  // use execv syscall
    280  // (causes content process to be killed on Linux)
    281  if (isMac()) {
    282    // exec something harmless, this should fail
    283    let cmd = getOSExecCmd();
    284    let rv = await SpecialPowers.spawn(browser, [{ lib, cmd }], callExec);
    285    Assert.equal(rv, -1, `exec(${cmd}) is not permitted`);
    286  }
    287 
    288  // use open syscall
    289  if (isLinux() || isMac()) {
    290    // open a file for writing in $HOME, this should fail
    291    let path = fileInHomeDir().path;
    292    let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY;
    293    let fd = await SpecialPowers.spawn(
    294      browser,
    295      [{ lib, path, flags }],
    296      callOpen
    297    );
    298    Assert.less(fd, 0, "opening a file for writing in home is not permitted");
    299  }
    300 
    301  // use open syscall
    302  if (isLinux() || isMac()) {
    303    // open a file for writing in the content temp dir, this should fail on
    304    // macOS and Linux. The open handler in the content process closes the file
    305    // for us
    306    let path = fileInTempDir().path;
    307    let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY;
    308    let fd = await SpecialPowers.spawn(
    309      browser,
    310      [{ lib, path, flags }],
    311      callOpen
    312    );
    313    Assert.strictEqual(
    314      fd,
    315      -1,
    316      "opening a file for writing in content temp is not permitted"
    317    );
    318  }
    319 
    320  // use fork syscall
    321  if (isLinux() || isMac()) {
    322    let rv = await SpecialPowers.spawn(browser, [{ lib }], callFork);
    323    Assert.equal(rv, -1, "calling fork is not permitted");
    324  }
    325 
    326  // On macOS before 10.10 the |sysctl-name| predicate didn't exist for
    327  // filtering |sysctl| access. Check the Darwin version before running the
    328  // tests (Darwin 14.0.0 is macOS 10.10). This branch can be removed when we
    329  // remove support for macOS 10.9.
    330  if (isMac() && Services.sysinfo.getProperty("version") >= "14.0.0") {
    331    let rv = await SpecialPowers.spawn(
    332      browser,
    333      [{ lib, name: "kern.boottime" }],
    334      callSysctl
    335    );
    336    Assert.equal(rv, -1, "calling sysctl('kern.boottime') is not permitted");
    337 
    338    rv = await SpecialPowers.spawn(
    339      browser,
    340      [{ lib, name: "net.inet.ip.ttl" }],
    341      callSysctl
    342    );
    343    Assert.equal(rv, -1, "calling sysctl('net.inet.ip.ttl') is not permitted");
    344 
    345    rv = await SpecialPowers.spawn(
    346      browser,
    347      [{ lib, name: "hw.ncpu" }],
    348      callSysctl
    349    );
    350    Assert.equal(rv, 0, "calling sysctl('hw.ncpu') is permitted");
    351  }
    352 
    353  if (isLinux()) {
    354    // These constants are not portable.
    355 
    356    // verify we block PR_CAPBSET_READ with EINVAL
    357    let option = lazy.LIBC.PR_CAPBSET_READ;
    358    let rv = await SpecialPowers.spawn(browser, [{ lib, option }], callPrctl);
    359    Assert.strictEqual(
    360      rv,
    361      lazy.LIBC.EINVAL,
    362      "prctl(PR_CAPBSET_READ) is blocked"
    363    );
    364 
    365    const kernelVersion = await getKernelVersion();
    366    const glibcVersion = getGlibcVersion();
    367    // faccessat2 is only used with kernel 5.8+ by glibc 2.33+
    368    if (glibcVersion >= 233 && kernelVersion >= computeKernelVersion(5, 8, 0)) {
    369      info("Linux v5.8+, glibc 2.33+, checking faccessat2");
    370      const dirfd = 0;
    371      const path = "/";
    372      const mode = 0;
    373      // the value 0x01 is just one we know should get rejected
    374      let rv = await SpecialPowers.spawn(
    375        browser,
    376        [{ lib, dirfd, path, mode, flag: 0x01 }],
    377        callFaccessat2
    378      );
    379      Assert.strictEqual(
    380        rv,
    381        lazy.LIBC.ENOSYS,
    382        "faccessat2 (flag=0x01) was blocked with ENOSYS"
    383      );
    384 
    385      rv = await SpecialPowers.spawn(
    386        browser,
    387        [{ lib, dirfd, path, mode, flag: lazy.LIBC.AT_EACCESS }],
    388        callFaccessat2
    389      );
    390      Assert.strictEqual(
    391        rv,
    392        lazy.LIBC.EACCES,
    393        "faccessat2 (flag=0x200) was allowed, errno=EACCES"
    394      );
    395    } else {
    396      info(
    397        "Unsupported kernel (" +
    398          kernelVersion +
    399          " )/glibc (" +
    400          glibcVersion +
    401          "), skipping faccessat2"
    402      );
    403    }
    404  }
    405 });