summaryrefslogtreecommitdiffstats
path: root/security/sandbox/test/browser_content_sandbox_syscalls.js
diff options
context:
space:
mode:
Diffstat (limited to 'security/sandbox/test/browser_content_sandbox_syscalls.js')
-rw-r--r--security/sandbox/test/browser_content_sandbox_syscalls.js436
1 files changed, 436 insertions, 0 deletions
diff --git a/security/sandbox/test/browser_content_sandbox_syscalls.js b/security/sandbox/test/browser_content_sandbox_syscalls.js
new file mode 100644
index 0000000000..63cc921781
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_syscalls.js
@@ -0,0 +1,436 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from browser_content_sandbox_utils.js */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/" +
+ "security/sandbox/test/browser_content_sandbox_utils.js",
+ this
+);
+
+const ERRNO = {
+ EACCES: 13,
+ EINVAL: 22,
+ get ENOSYS() {
+ const os = Services.appinfo.OS;
+
+ if (["Linux", "Android"].includes(os)) {
+ // https://github.com/torvalds/linux/blob/9a48d604672220545d209e9996c2a1edbb5637f6/include/uapi/asm-generic/errno.h#L18
+ return 38;
+ } else if (
+ ["Darwin", "DragonFly", "FreeBSD", "OpenBSD", "NetBSD"].includes(os)
+ ) {
+ /*
+ * Darwin: https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/errno.h.auto.html
+ * DragonFly: https://github.com/DragonFlyBSD/DragonFlyBSD/blob/5e488df32cb01056a5b714a522e51c69ab7b4612/sys/sys/errno.h#L172
+ * FreeBSD: https://github.com/freebsd/freebsd-src/blob/7232e6dcc89b978825b30a537bca2e7d3a9b71bb/sys/sys/errno.h#L157
+ * OpenBSD: https://github.com/openbsd/src/blob/025fffe4c6e0113862ce4e1927e67517a2841502/sys/sys/errno.h#L151
+ * NetBSD: https://github.com/NetBSD/src/blob/ff24f695f5f53540b23b6bb4fa5c0b9d79b369e4/sys/sys/errno.h#L137
+ */
+ return 78;
+ } else if (os === "WINNT") {
+ // https://learn.microsoft.com/en-us/cpp/c-runtime-library/errno-constants?view=msvc-170
+ return 40;
+ }
+ throw new Error("Unsupported OS");
+ },
+};
+
+/*
+ * 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) {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ 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) {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ 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 sysctl syscall.
+function callSysctl(args) {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ let { lib, name } = args;
+ let libc = ctypes.open(lib);
+ let sysctlbyname = libc.declare(
+ "sysctlbyname",
+ ctypes.default_abi,
+ ctypes.int,
+ ctypes.char.ptr,
+ ctypes.voidptr_t,
+ ctypes.size_t.ptr,
+ ctypes.voidptr_t,
+ ctypes.size_t.ptr
+ );
+ let rv = sysctlbyname(name, null, null, null, null);
+ libc.close();
+ return rv;
+}
+
+function callPrctl(args) {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ let { lib, option } = args;
+ let libc = ctypes.open(lib);
+ let prctl = libc.declare(
+ "prctl",
+ ctypes.default_abi,
+ ctypes.int,
+ ctypes.int, // option
+ ctypes.unsigned_long, // arg2
+ ctypes.unsigned_long, // arg3
+ ctypes.unsigned_long, // arg4
+ ctypes.unsigned_long // arg5
+ );
+ let rv = prctl(option, 0, 0, 0, 0);
+ if (rv == -1) {
+ rv = ctypes.errno;
+ }
+ libc.close();
+ return rv;
+}
+
+// Calls the native open/close syscalls.
+function callOpen(args) {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ 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;
+}
+
+// Verify faccessat2
+function callFaccessat2(args) {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ let { lib, dirfd, path, mode, flag } = args;
+ let libc = ctypes.open(lib);
+ let faccessat = libc.declare(
+ "faccessat",
+ ctypes.default_abi,
+ ctypes.int,
+ ctypes.int, // dirfd
+ ctypes.char.ptr, // path
+ ctypes.int, // mode
+ ctypes.int // flag
+ );
+ let rv = faccessat(dirfd, path, mode, flag);
+ if (rv == -1) {
+ rv = ctypes.errno;
+ }
+ libc.close();
+ return rv;
+}
+
+// open syscall flags
+function openWriteCreateFlags() {
+ Assert.ok(isMac() || isLinux());
+ if (isMac()) {
+ let O_WRONLY = 0x001;
+ let O_CREAT = 0x200;
+ return O_WRONLY | O_CREAT;
+ }
+ // 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");
+ return 0;
+ }
+}
+
+// Reading a header might be weird, but the alternatives to read a stable
+// version number we can easily check against are not much more fun
+async function getKernelVersion() {
+ let header = await IOUtils.readUTF8("/usr/include/linux/version.h");
+ let hr = header.split("\n");
+ for (let line in hr) {
+ let hrs = hr[line].split(" ");
+ if (hrs[0] === "#define" && hrs[1] === "LINUX_VERSION_CODE") {
+ return Number(hrs[2]);
+ }
+ }
+ throw Error("No LINUX_VERSION_CODE");
+}
+
+// This is how it is done in /usr/include/linux/version.h
+function computeKernelVersion(major, minor, dot) {
+ return (major << 16) + (minor << 8) + dot;
+}
+
+function getGlibcVersion() {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ let libc = ctypes.open(getOSLib());
+ let gnu_get_libc_version = libc.declare(
+ "gnu_get_libc_version",
+ ctypes.default_abi,
+ ctypes.char.ptr
+ );
+ let rv = gnu_get_libc_version().readString();
+ libc.close();
+ let ar = rv.split(".");
+ // return a number made of MAJORMINOR
+ return Number(ar[0] + ar[1]);
+}
+
+// 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 = 1;
+ 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(async 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.
+ // eslint-disable-next-line mozilla/use-default-preference-values
+ try {
+ level = Services.prefs.getIntPref("security.sandbox.content.level");
+ } catch (e) {
+ prefExists = false;
+ }
+
+ ok(prefExists, "pref security.sandbox.content.level exists");
+ if (!prefExists) {
+ return;
+ }
+
+ info(`security.sandbox.content.level=${level}`);
+ ok(level > 0, "content sandbox is enabled.");
+
+ let areSyscallsSandboxed = areContentSyscallsSandboxed(level);
+
+ // 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 = await SpecialPowers.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 = await SpecialPowers.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 fail on
+ // macOS and work on Linux. The open handler in the content process closes
+ // the file for us
+ let path = fileInTempDir().path;
+ let flags = openWriteCreateFlags();
+ let fd = await SpecialPowers.spawn(
+ browser,
+ [{ lib, path, flags }],
+ callOpen
+ );
+ if (isMac()) {
+ ok(
+ fd === -1,
+ "opening a file for writing in content temp is not permitted"
+ );
+ } else {
+ ok(fd >= 0, "opening a file for writing in content temp is permitted");
+ }
+ }
+
+ // use fork syscall
+ if (isLinux() || isMac()) {
+ let rv = await SpecialPowers.spawn(browser, [{ lib }], callFork);
+ ok(rv == -1, "calling fork is not permitted");
+ }
+
+ // On macOS before 10.10 the |sysctl-name| predicate didn't exist for
+ // filtering |sysctl| access. Check the Darwin version before running the
+ // tests (Darwin 14.0.0 is macOS 10.10). This branch can be removed when we
+ // remove support for macOS 10.9.
+ if (isMac() && Services.sysinfo.getProperty("version") >= "14.0.0") {
+ let rv = await SpecialPowers.spawn(
+ browser,
+ [{ lib, name: "kern.boottime" }],
+ callSysctl
+ );
+ ok(rv == -1, "calling sysctl('kern.boottime') is not permitted");
+
+ rv = await SpecialPowers.spawn(
+ browser,
+ [{ lib, name: "net.inet.ip.ttl" }],
+ callSysctl
+ );
+ ok(rv == -1, "calling sysctl('net.inet.ip.ttl') is not permitted");
+
+ rv = await SpecialPowers.spawn(
+ browser,
+ [{ lib, name: "hw.ncpu" }],
+ callSysctl
+ );
+ ok(rv == 0, "calling sysctl('hw.ncpu') is permitted");
+ }
+
+ if (isLinux()) {
+ // These constants are not portable.
+ const AT_EACCESS = 512;
+ const PR_CAPBSET_READ = 23;
+
+ // verify we block PR_CAPBSET_READ with EINVAL
+ let option = PR_CAPBSET_READ;
+ let rv = await SpecialPowers.spawn(browser, [{ lib, option }], callPrctl);
+ ok(rv === ERRNO.EINVAL, "prctl(PR_CAPBSET_READ) is blocked");
+
+ const kernelVersion = await getKernelVersion();
+ const glibcVersion = getGlibcVersion();
+ // faccessat2 is only used with kernel 5.8+ by glibc 2.33+
+ if (glibcVersion >= 233 && kernelVersion >= computeKernelVersion(5, 8, 0)) {
+ info("Linux v5.8+, glibc 2.33+, checking faccessat2");
+ const dirfd = 0;
+ const path = "/";
+ const mode = 0;
+ // the value 0x01 is just one we know should get rejected
+ let rv = await SpecialPowers.spawn(
+ browser,
+ [{ lib, dirfd, path, mode, flag: 0x01 }],
+ callFaccessat2
+ );
+ ok(rv === ERRNO.ENOSYS, "faccessat2 (flag=0x01) was blocked with ENOSYS");
+
+ rv = await SpecialPowers.spawn(
+ browser,
+ [{ lib, dirfd, path, mode, flag: AT_EACCESS }],
+ callFaccessat2
+ );
+ ok(
+ rv === ERRNO.EACCES,
+ "faccessat2 (flag=0x200) was allowed, errno=EACCES"
+ );
+ } else {
+ info(
+ "Unsupported kernel (" +
+ kernelVersion +
+ " )/glibc (" +
+ glibcVersion +
+ "), skipping faccessat2"
+ );
+ }
+ }
+});