Bug 1309394 - automated tests to validate content process sandboxing works as intended; r=gcp,bobowen draft
authorHaik Aftandilian <haftandilian@mozilla.com>
Tue, 10 Jan 2017 22:01:03 -0800
changeset 459192 791532efc3e28054ddf03e8bf8754884f5606369
parent 456009 509cb24089fc87f1be04f9d2ab476e9238b70a02
child 541835 ee15c5f9ff78c010b244df64cca2937c0fd97ce4
push id41159
push userhaftandilian@mozilla.com
push dateWed, 11 Jan 2017 16:19:00 +0000
reviewersgcp, bobowen
bugs1309394
milestone53.0a1
Bug 1309394 - automated tests to validate content process sandboxing works as intended; r=gcp,bobowen Adds security/sandbox/test/browser_content_sandbox_fs.js for validating content sandbox file I/O restrictions. Adds security/sandbox/test/browser_content_sandbox_syscalls.js for validating OS-level calls are sandboxed as intended. Uses js-ctypes to invoke native library routines. Windows tests yet to be added here. Adds security/sandbox/test/browser_content_sandbox_utils.js with some shared utility functions. MozReview-Commit-ID: 5zfCLctfuN5
security/sandbox/moz.build
security/sandbox/test/browser.ini
security/sandbox/test/browser_content_sandbox_fs.js
security/sandbox/test/browser_content_sandbox_syscalls.js
security/sandbox/test/browser_content_sandbox_utils.js
--- a/security/sandbox/moz.build
+++ b/security/sandbox/moz.build
@@ -1,14 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
 with Files('**'):
     BUG_COMPONENT = ('Core', 'Security: Process Sandboxing')
 
 if CONFIG['OS_ARCH'] == 'Linux':
     DIRS += ['linux']
 elif CONFIG['OS_ARCH'] == 'Darwin':
     DIRS += ['mac']
 elif CONFIG['OS_ARCH'] == 'WINNT':
new file mode 100644
--- /dev/null
+++ b/security/sandbox/test/browser.ini
@@ -0,0 +1,11 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+tags = contentsandbox
+support-files =
+  browser_content_sandbox_utils.js
+
+skip-if = !e10s
+[browser_content_sandbox_fs.js]
+skip-if = !e10s
+[browser_content_sandbox_syscalls.js]
new file mode 100644
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_fs.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var prefs = Cc["@mozilla.org/preferences-service;1"]
+            .getService(Ci.nsIPrefBranch);
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/" +
+    "security/sandbox/test/browser_content_sandbox_utils.js", this);
+
+/*
+ * This test exercises file I/O from the content process using OS.File
+ * methods to validate that calls that are meant to be blocked by content
+ * sandboxing are blocked.
+ */
+
+// Creates file at |path| and returns a promise that resolves with true
+// if the file was successfully created, otherwise false. Include imports
+// so this can be safely serialized and run remotely by ContentTask.spawn.
+function createFile(path) {
+  Components.utils.import("resource://gre/modules/osfile.jsm");
+  let encoder = new TextEncoder();
+  let array = encoder.encode("WRITING FROM CONTENT PROCESS");
+  return OS.File.writeAtomic(path, array).then(function(value) {
+    return true;
+  }, function(reason) {
+    return false;
+  });
+}
+
+// Deletes file at |path| and returns a promise that resolves with true
+// if the file was successfully deleted, otherwise false. Include imports
+// so this can be safely serialized and run remotely by ContentTask.spawn.
+function deleteFile(path) {
+  Components.utils.import("resource://gre/modules/osfile.jsm");
+  return OS.File.remove(path, {ignoreAbsent: false}).then(function(value) {
+    return true;
+  }).catch(function(err) {
+    return false;
+  });
+}
+
+// Returns true if the current content sandbox level, passed in
+// the |level| argument, supports filesystem sandboxing.
+function isContentFileIOSandboxed(level) {
+  let fileIOSandboxMinLevel = 0;
+
+  // Set fileIOSandboxMinLevel to the lowest level that has
+  // content filesystem sandboxing enabled. For now, this
+  // varies across Windows, Mac, Linux, other.
+  switch (Services.appinfo.OS) {
+    case "WINNT":
+      fileIOSandboxMinLevel = 1;
+      break;
+    case "Darwin":
+      fileIOSandboxMinLevel = 1;
+      break;
+    case "Linux":
+      fileIOSandboxMinLevel = 2;
+      break;
+    default:
+      Assert.ok(false, "Unknown OS");
+  }
+
+  return (level >= fileIOSandboxMinLevel);
+}
+
+//
+// Drive tests for a single content process.
+//
+// Tests attempting to write to a file in the home directory from the
+// content process--expected to fail.
+//
+// Tests attempting to write to a file in the content temp directory
+// from the content process--expected to succeed. On Mac and Windows,
+// use "ContentTmpD", but on Linux use "TmpD" until Linux uses the
+// content temp dir key.
+//
+add_task(function*() {
+  // This test is only relevant in e10s
+  if (!gMultiProcessBrowser) {
+    ok(false, "e10s is enabled");
+    info("e10s is not enabled, exiting");
+    return;
+  }
+
+  let level = 0;
+  let prefExists = true;
+
+  // Read the security.sandbox.content.level pref.
+  // If the pref isn't set and we're running on Linux on !isNightly(),
+  // exit without failing. The Linux content sandbox is only enabled
+  // on Nightly at this time.
+  try {
+    level = prefs.getIntPref("security.sandbox.content.level");
+  } catch (e) {
+    prefExists = false;
+  }
+
+  // Special case Linux on !isNightly
+  if (isLinux() && !isNightly()) {
+    todo(prefExists, "pref security.sandbox.content.level exists");
+    if (!prefExists) {
+      return;
+    }
+  }
+
+  ok(prefExists, "pref security.sandbox.content.level exists");
+  if (!prefExists) {
+    return;
+  }
+
+  // Special case Linux on !isNightly
+  if (isLinux() && !isNightly()) {
+    todo(level > 0, "content sandbox enabled for !nightly.");
+    return;
+  }
+
+  info(`security.sandbox.content.level=${level}`);
+  ok(level > 0, "content sandbox is enabled.");
+  if (level == 0) {
+    info("content sandbox is not enabled, exiting");
+    return;
+  }
+
+  let isFileIOSandboxed = isContentFileIOSandboxed(level);
+
+  // Special case Linux on !isNightly
+  if (isLinux() && !isNightly()) {
+    todo(isFileIOSandboxed, "content file I/O sandbox enabled for !nightly.");
+    return;
+  }
+
+  // Content sandbox enabled, but level doesn't include file I/O sandboxing.
+  ok(isFileIOSandboxed, "content file I/O sandboxing is enabled.");
+  if (!isFileIOSandboxed) {
+    info("content sandbox level too low for file I/O tests, exiting\n");
+    return;
+  }
+
+  let browser = gBrowser.selectedBrowser;
+
+  {
+    // test if the content process can create in $HOME, this should fail
+    let homeFile = fileInHomeDir();
+    let path = homeFile.path;
+    let fileCreated = yield ContentTask.spawn(browser, path, createFile);
+    ok(fileCreated == false, "creating a file in home dir is not permitted");
+    if (fileCreated == true) {
+      // content process successfully created the file, now remove it
+      homeFile.remove(false);
+    }
+  }
+
+  {
+    // test if the content process can create a temp file, should pass
+    let path = fileInTempDir().path;
+    let fileCreated = yield ContentTask.spawn(browser, path, createFile);
+    if (!fileCreated && isWin()) {
+      // TODO: fix 1329294 and enable this test for Windows.
+      // Not using todo() because this only fails on automation.
+      info("ignoring failure to write to content temp due to 1329294\n");
+      return;
+    }
+    ok(fileCreated == true, "creating a file in content temp is permitted");
+    // now delete the file
+    let fileDeleted = yield ContentTask.spawn(browser, path, deleteFile);
+    ok(fileDeleted == true, "deleting a file in content temp is permitted");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_syscalls.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var prefs = Cc["@mozilla.org/preferences-service;1"]
+            .getService(Ci.nsIPrefBranch);
+
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/" +
+    "security/sandbox/test/browser_content_sandbox_utils.js", this);
+
+/*
+ * This test is for executing system calls in content processes to validate
+ * that calls that are meant to be blocked by content sandboxing are blocked.
+ * We use the term system calls loosely so that any OS API call such as
+ * fopen could be included.
+ */
+
+// Calls the native execv library function. Include imports so this can be
+// safely serialized and run remotely by ContentTask.spawn.
+function callExec(args) {
+  Components.utils.import("resource://gre/modules/ctypes.jsm");
+  let {lib, cmd} = args;
+  let libc = ctypes.open(lib);
+  let exec = libc.declare("execv", ctypes.default_abi,
+      ctypes.int, ctypes.char.ptr);
+  let rv = exec(cmd);
+  libc.close();
+  return (rv);
+}
+
+// Calls the native fork syscall.
+function callFork(args) {
+  Components.utils.import("resource://gre/modules/ctypes.jsm");
+  let {lib} = args;
+  let libc = ctypes.open(lib);
+  let fork = libc.declare("fork", ctypes.default_abi, ctypes.int);
+  let rv = fork();
+  libc.close();
+  return (rv);
+}
+
+// Calls the native open/close syscalls.
+function callOpen(args) {
+  Components.utils.import("resource://gre/modules/ctypes.jsm");
+  let {lib, path, flags} = args;
+  let libc = ctypes.open(lib);
+  let open = libc.declare("open", ctypes.default_abi,
+                          ctypes.int, ctypes.char.ptr, ctypes.int);
+  let close = libc.declare("close", ctypes.default_abi,
+                           ctypes.int, ctypes.int);
+  let fd = open(path, flags);
+  close(fd);
+  libc.close();
+  return (fd);
+}
+
+// open syscall flags
+function openWriteCreateFlags() {
+  Assert.ok(isMac() || isLinux());
+  if (isMac()) {
+    let O_WRONLY = 0x001;
+    let O_CREAT  = 0x200;
+    return (O_WRONLY | O_CREAT);
+  } else {
+    // Linux
+    let O_WRONLY = 0x01;
+    let O_CREAT  = 0x40;
+    return (O_WRONLY | O_CREAT);
+  }
+}
+
+// Returns the name of the native library needed for native syscalls
+function getOSLib() {
+  switch (Services.appinfo.OS) {
+    case "WINNT":
+      return "kernel32.dll";
+    case "Darwin":
+      return "libc.dylib";
+    case "Linux":
+      return "libc.so.6";
+    default:
+      Assert.ok(false, "Unknown OS");
+  }
+}
+
+// Returns a harmless command to execute with execv
+function getOSExecCmd() {
+  Assert.ok(!isWin());
+  return ("/bin/cat");
+}
+
+// Returns true if the current content sandbox level, passed in
+// the |level| argument, supports syscall sandboxing.
+function areContentSyscallsSandboxed(level) {
+  let syscallsSandboxMinLevel = 0;
+
+  // Set syscallsSandboxMinLevel to the lowest level that has
+  // syscall sandboxing enabled. For now, this varies across
+  // Windows, Mac, Linux, other.
+  switch (Services.appinfo.OS) {
+    case "WINNT":
+      syscallsSandboxMinLevel = 1;
+      break;
+    case "Darwin":
+      syscallsSandboxMinLevel = 1;
+      break;
+    case "Linux":
+      syscallsSandboxMinLevel = 2;
+      break;
+    default:
+      Assert.ok(false, "Unknown OS");
+  }
+
+  return (level >= syscallsSandboxMinLevel);
+}
+
+//
+// Drive tests for a single content process.
+//
+// Tests executing OS API calls in the content process. Limited to Mac
+// and Linux calls for now.
+//
+add_task(function*() {
+  // This test is only relevant in e10s
+  if (!gMultiProcessBrowser) {
+    ok(false, "e10s is enabled");
+    info("e10s is not enabled, exiting");
+    return;
+  }
+
+  let level = 0;
+  let prefExists = true;
+
+  // Read the security.sandbox.content.level pref.
+  // If the pref isn't set and we're running on Linux on !isNightly(),
+  // exit without failing. The Linux content sandbox is only enabled
+  // on Nightly at this time.
+  try {
+    level = prefs.getIntPref("security.sandbox.content.level");
+  } catch (e) {
+    prefExists = false;
+  }
+
+  // Special case Linux on !isNightly
+  if (isLinux() && !isNightly()) {
+    todo(prefExists, "pref security.sandbox.content.level exists");
+    if (!prefExists) {
+      return;
+    }
+  }
+
+  ok(prefExists, "pref security.sandbox.content.level exists");
+  if (!prefExists) {
+    return;
+  }
+
+  // Special case Linux on !isNightly
+  if (isLinux() && !isNightly()) {
+    todo(level > 0, "content sandbox enabled for !nightly.");
+    return;
+  }
+
+  info(`security.sandbox.content.level=${level}`);
+  ok(level > 0, "content sandbox is enabled.");
+  if (level == 0) {
+    info("content sandbox is not enabled, exiting");
+    return;
+  }
+
+  let areSyscallsSandboxed = areContentSyscallsSandboxed(level);
+
+  // Special case Linux on !isNightly
+  if (isLinux() && !isNightly()) {
+    todo(areSyscallsSandboxed, "content syscall sandbox enabled for !nightly.");
+    return;
+  }
+
+  // Content sandbox enabled, but level doesn't include syscall sandboxing.
+  ok(areSyscallsSandboxed, "content syscall sandboxing is enabled.");
+  if (!areSyscallsSandboxed) {
+    info("content sandbox level too low for syscall tests, exiting\n");
+    return;
+  }
+
+  let browser = gBrowser.selectedBrowser;
+  let lib = getOSLib();
+
+  // use execv syscall
+  // (causes content process to be killed on Linux)
+  if (isMac()) {
+    // exec something harmless, this should fail
+    let cmd = getOSExecCmd();
+    let rv = yield ContentTask.spawn(browser, {lib, cmd}, callExec);
+    ok(rv == -1, `exec(${cmd}) is not permitted`);
+  }
+
+  // use open syscall
+  if (isLinux() || isMac())
+  {
+    // open a file for writing in $HOME, this should fail
+    let path = fileInHomeDir().path;
+    let flags = openWriteCreateFlags();
+    let fd = yield ContentTask.spawn(browser, {lib, path, flags}, callOpen);
+    ok(fd < 0, "opening a file for writing in home is not permitted");
+  }
+
+  // use open syscall
+  if (isLinux() || isMac())
+  {
+    // open a file for writing in the content temp dir, this should work
+    // and the open handler in the content process closes the file for us
+    let path = fileInTempDir().path;
+    let flags = openWriteCreateFlags();
+    let fd = yield ContentTask.spawn(browser, {lib, path, flags}, callOpen);
+    ok(fd >= 0, "opening a file for writing in content temp is permitted");
+  }
+
+  // use fork syscall
+  if (isLinux() || isMac())
+  {
+    let rv = yield ContentTask.spawn(browser, {lib}, callFork);
+    ok(rv == -1, "calling fork is not permitted");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_utils.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                      .getService(Ci.nsIUUIDGenerator);
+
+/*
+ * Utility functions for the browser content sandbox tests.
+ */
+
+function isMac() { return Services.appinfo.OS == "Darwin" }
+function isWin() { return Services.appinfo.OS == "WINNT" }
+function isLinux() { return Services.appinfo.OS == "Linux" }
+
+function isNightly() {
+  let version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].
+    getService(SpecialPowers.Ci.nsIXULAppInfo).version;
+  return (version.endsWith("a1"));
+}
+
+function uuid() {
+  return uuidGenerator.generateUUID().toString();
+}
+
+// Returns a file object for a new file in the home dir ($HOME/<UUID>).
+function fileInHomeDir() {
+  // get home directory, make sure it exists
+  let homeDir = Services.dirsvc.get("Home", Ci.nsILocalFile);
+  Assert.ok(homeDir.exists(), "Home dir exists");
+  Assert.ok(homeDir.isDirectory(), "Home dir is a directory");
+
+  // build a file object for a new file named $HOME/<UUID>
+  let homeFile = homeDir.clone();
+  homeFile.appendRelativePath(uuid());
+  Assert.ok(!homeFile.exists(), homeFile.path + " does not exist");
+  return (homeFile);
+}
+
+// Returns a file object for a new file in the content temp dir (.../<UUID>).
+function fileInTempDir() {
+  let contentTempKey = "ContentTmpD";
+  if (Services.appinfo.OS == "Linux") {
+    // Linux builds don't use the content-specific temp key
+    contentTempKey = "TmpD";
+  }
+
+  // get the content temp dir, make sure it exists
+  let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsILocalFile);
+  Assert.ok(ctmp.exists(), "Content temp dir exists");
+  Assert.ok(ctmp.isDirectory(), "Content temp dir is a directory");
+
+  // build a file object for a new file in content temp
+  let tempFile = ctmp.clone();
+  tempFile.appendRelativePath(uuid());
+  Assert.ok(!tempFile.exists(), tempFile.path + " does not exist");
+  return (tempFile);
+}