summaryrefslogtreecommitdiffstats
path: root/security/sandbox/test
diff options
context:
space:
mode:
Diffstat (limited to 'security/sandbox/test')
-rw-r--r--security/sandbox/test/browser.ini27
-rw-r--r--security/sandbox/test/browser_bug1393259.js196
-rw-r--r--security/sandbox/test/browser_bug1717599_XDG-CONFIG-DIRS.ini9
-rw-r--r--security/sandbox/test/browser_bug1717599_XDG-CONFIG-HOME.ini9
-rw-r--r--security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js11
-rw-r--r--security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js11
-rw-r--r--security/sandbox/test/browser_content_sandbox_fs.js56
-rw-r--r--security/sandbox/test/browser_content_sandbox_fs_snap.js31
-rw-r--r--security/sandbox/test/browser_content_sandbox_fs_tests.js694
-rw-r--r--security/sandbox/test/browser_content_sandbox_fs_xdg.js31
-rw-r--r--security/sandbox/test/browser_content_sandbox_syscalls.js402
-rw-r--r--security/sandbox/test/browser_content_sandbox_utils.js462
-rw-r--r--security/sandbox/test/browser_sandbox_test.js59
-rw-r--r--security/sandbox/test/browser_snap.ini14
-rw-r--r--security/sandbox/test/browser_xdg.ini14
-rw-r--r--security/sandbox/test/bug1393259.html19
-rwxr-xr-xsecurity/sandbox/test/mac_register_font.py85
17 files changed, 2130 insertions, 0 deletions
diff --git a/security/sandbox/test/browser.ini b/security/sandbox/test/browser.ini
new file mode 100644
index 0000000000..0148cdc4db
--- /dev/null
+++ b/security/sandbox/test/browser.ini
@@ -0,0 +1,27 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+skip-if = ccov || (os == "linux" && (asan || tsan)) # bug 1784517
+tags = contentsandbox
+support-files =
+ browser_content_sandbox_utils.js
+ browser_content_sandbox_fs_tests.js
+ mac_register_font.py
+ ../../../layout/reftests/fonts/fira/FiraSans-Regular.otf
+# Bug 1718210, we need to disable rdd in gpu process to avoid leaks at shutdown
+prefs =
+ media.gpu-process-decoder=false
+
+[browser_content_sandbox_fs.js]
+skip-if =
+ (debug && os == 'win') # bug 1379635
+
+[browser_content_sandbox_syscalls.js]
+[browser_bug1393259.js]
+support-files =
+ bug1393259.html
+skip-if = (os != 'mac') # This is a Mac-specific test
+
+[browser_sandbox_test.js]
+skip-if =
+ !debug
diff --git a/security/sandbox/test/browser_bug1393259.js b/security/sandbox/test/browser_bug1393259.js
new file mode 100644
index 0000000000..1bbb79f696
--- /dev/null
+++ b/security/sandbox/test/browser_bug1393259.js
@@ -0,0 +1,196 @@
+/* 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/. */
+"use strict";
+
+/*
+ * This test validates that an OTF font installed in a directory not
+ * accessible to content processes is rendered correctly by checking that
+ * content displayed never uses the OS fallback font "LastResort". When
+ * a content process renders a page with the fallback font, that is an
+ * indication the content process failed to read or load the computed font.
+ * The test uses a version of the Fira Sans font and depends on the font
+ * not being already installed and enabled.
+ */
+
+const kPageURL =
+ "http://example.com/browser/security/sandbox/test/bug1393259.html";
+
+// Parameters for running the python script that registers/unregisters fonts.
+const kPythonPath = "/usr/bin/python";
+const kFontInstallerPath = "browser/security/sandbox/test/mac_register_font.py";
+const kUninstallFlag = "-u";
+const kVerboseFlag = "-v";
+
+// Where to find the font in the test environment.
+const kRepoFontPath = "browser/security/sandbox/test/FiraSans-Regular.otf";
+
+// Font name strings to check for.
+const kLastResortFontName = "LastResort";
+const kTestFontName = "Fira Sans";
+
+// Home-relative path to install a private font. Where a private font is
+// a font at a location not readable by content processes.
+const kPrivateFontSubPath = "/FiraSans-Regular.otf";
+
+add_task(async function() {
+ await new Promise(resolve => waitForFocus(resolve, window));
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: kPageURL,
+ },
+ async function(aBrowser) {
+ function runProcess(aCmd, aArgs, blocking = true) {
+ let cmdFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ cmdFile.initWithPath(aCmd);
+
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ process.init(cmdFile);
+ process.run(blocking, aArgs, aArgs.length);
+ return process.exitValue;
+ }
+
+ // Register the font at path |fontPath| and wait
+ // for the browser to detect the change.
+ async function registerFont(fontPath) {
+ let fontRegistered = getFontNotificationPromise();
+ let exitCode = runProcess(kPythonPath, [
+ kFontInstallerPath,
+ kVerboseFlag,
+ fontPath,
+ ]);
+ Assert.ok(exitCode == 0, "registering font" + fontPath);
+ if (exitCode == 0) {
+ // Wait for the font registration to be detected by the browser.
+ await fontRegistered;
+ }
+ }
+
+ // Unregister the font at path |fontPath|. If |waitForUnreg| is true,
+ // don't wait for the browser to detect the change and don't use
+ // the verbose arg for the unregister command.
+ async function unregisterFont(fontPath, waitForUnreg = true) {
+ let args = [kFontInstallerPath, kUninstallFlag];
+ let fontUnregistered;
+
+ if (waitForUnreg) {
+ args.push(kVerboseFlag);
+ fontUnregistered = getFontNotificationPromise();
+ }
+
+ let exitCode = runProcess(kPythonPath, args.concat(fontPath));
+ if (waitForUnreg) {
+ Assert.ok(exitCode == 0, "unregistering font" + fontPath);
+ if (exitCode == 0) {
+ await fontUnregistered;
+ }
+ }
+ }
+
+ // Returns a promise that resolves when font info is changed.
+ let getFontNotificationPromise = () =>
+ new Promise(resolve => {
+ const kTopic = "font-info-updated";
+ function observe() {
+ Services.obs.removeObserver(observe, kTopic);
+ resolve();
+ }
+
+ Services.obs.addObserver(observe, kTopic);
+ });
+
+ let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
+ let privateFontPath = homeDir.path + kPrivateFontSubPath;
+
+ registerCleanupFunction(function() {
+ unregisterFont(privateFontPath, /* waitForUnreg = */ false);
+ runProcess("/bin/rm", [privateFontPath], /* blocking = */ false);
+ });
+
+ // Copy the font file to the private path.
+ runProcess("/bin/cp", [kRepoFontPath, privateFontPath]);
+
+ // Cleanup previous aborted tests.
+ unregisterFont(privateFontPath, /* waitForUnreg = */ false);
+
+ // Get the original width, using the fallback monospaced font
+ let origWidth = await SpecialPowers.spawn(aBrowser, [], async function() {
+ let window = content.window.wrappedJSObject;
+ let contentDiv = window.document.getElementById("content");
+ return contentDiv.offsetWidth;
+ });
+
+ // Activate the font we want to test at a non-standard path.
+ await registerFont(privateFontPath);
+
+ // Assign the new font to the content.
+ await SpecialPowers.spawn(aBrowser, [], async function() {
+ let window = content.window.wrappedJSObject;
+ let contentDiv = window.document.getElementById("content");
+ contentDiv.style.fontFamily = "'Fira Sans', monospace";
+ });
+
+ // Wait until the width has changed, indicating the content process
+ // has recognized the newly-activated font.
+ while (true) {
+ let width = await SpecialPowers.spawn(aBrowser, [], async function() {
+ let window = content.window.wrappedJSObject;
+ let contentDiv = window.document.getElementById("content");
+ return contentDiv.offsetWidth;
+ });
+ if (width != origWidth) {
+ break;
+ }
+ // If the content wasn't ready yet, wait a little before re-checking.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 100));
+ }
+
+ // Get a list of fonts now being used to display the web content.
+ let fontList = await SpecialPowers.spawn(aBrowser, [], async function() {
+ let window = content.window.wrappedJSObject;
+ let range = window.document.createRange();
+ let contentDiv = window.document.getElementById("content");
+ range.selectNode(contentDiv);
+ let fonts = InspectorUtils.getUsedFontFaces(range);
+
+ let fontList = [];
+ for (let i = 0; i < fonts.length; i++) {
+ fontList.push({ name: fonts[i].name });
+ }
+ return fontList;
+ });
+
+ let lastResortFontUsed = false;
+ let testFontUsed = false;
+
+ for (let font of fontList) {
+ // Did we fall back to the "LastResort" font?
+ if (!lastResortFontUsed && font.name.includes(kLastResortFontName)) {
+ lastResortFontUsed = true;
+ continue;
+ }
+ // Did we render using our test font as expected?
+ if (!testFontUsed && font.name.includes(kTestFontName)) {
+ testFontUsed = true;
+ continue;
+ }
+ }
+
+ Assert.ok(
+ !lastResortFontUsed,
+ `The ${kLastResortFontName} fallback font was not used`
+ );
+
+ Assert.ok(testFontUsed, `The test font "${kTestFontName}" was used`);
+
+ await unregisterFont(privateFontPath);
+ }
+ );
+});
diff --git a/security/sandbox/test/browser_bug1717599_XDG-CONFIG-DIRS.ini b/security/sandbox/test/browser_bug1717599_XDG-CONFIG-DIRS.ini
new file mode 100644
index 0000000000..6ef46a6652
--- /dev/null
+++ b/security/sandbox/test/browser_bug1717599_XDG-CONFIG-DIRS.ini
@@ -0,0 +1,9 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+skip-if = ccov || (os == "linux" && (asan || tsan)) # bug 1784517
+tags = contentsandbox
+environment=XDG_CONFIG_DIRS=:/opt
+
+[browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js]
+run-if = (os == 'linux')
diff --git a/security/sandbox/test/browser_bug1717599_XDG-CONFIG-HOME.ini b/security/sandbox/test/browser_bug1717599_XDG-CONFIG-HOME.ini
new file mode 100644
index 0000000000..d0e936d02c
--- /dev/null
+++ b/security/sandbox/test/browser_bug1717599_XDG-CONFIG-HOME.ini
@@ -0,0 +1,9 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+skip-if = ccov || (os == "linux" && (asan || tsan)) # bug 1784517
+tags = contentsandbox
+environment=XDG_CONFIG_HOME=
+
+[browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js]
+run-if = (os == 'linux')
diff --git a/security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js b/security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js
new file mode 100644
index 0000000000..98a446b507
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js
@@ -0,0 +1,11 @@
+/* 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";
+
+//
+// Just test that browser does not die on empty env var
+//
+add_task(async function() {
+ ok(true, "Process can run");
+});
diff --git a/security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js b/security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js
new file mode 100644
index 0000000000..98a446b507
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js
@@ -0,0 +1,11 @@
+/* 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";
+
+//
+// Just test that browser does not die on empty env var
+//
+add_task(async function() {
+ ok(true, "Process can run");
+});
diff --git a/security/sandbox/test/browser_content_sandbox_fs.js b/security/sandbox/test/browser_content_sandbox_fs.js
new file mode 100644
index 0000000000..8077b8b697
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_fs.js
@@ -0,0 +1,56 @@
+/* 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
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/" +
+ "security/sandbox/test/browser_content_sandbox_fs_tests.js",
+ this
+);
+
+/*
+ * This test exercises file I/O from web and file content processes using
+ * nsIFile etc. methods to validate that calls that are meant to be blocked by
+ * content sandboxing are blocked.
+ */
+
+//
+// Checks that sandboxing is enabled and at the appropriate level
+// setting before triggering tests that do the file I/O.
+//
+// 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. Uses "ContentTmpD".
+//
+// Tests reading various files and directories from file and web
+// content processes.
+//
+add_task(async function() {
+ sanityChecks();
+
+ // Test creating a file in the home directory from a web content process
+ add_task(createFileInHome); // eslint-disable-line no-undef
+
+ // Test creating a file content temp from a web content process
+ add_task(createTempFile); // eslint-disable-line no-undef
+
+ // Test reading files/dirs from web and file content processes
+ add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef
+
+ add_task(testFileAccessMacOnly); // eslint-disable-line no-undef
+
+ add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
+
+ add_task(testFileAccessWindowsOnly); // eslint-disable-line no-undef
+
+ add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
+});
diff --git a/security/sandbox/test/browser_content_sandbox_fs_snap.js b/security/sandbox/test/browser_content_sandbox_fs_snap.js
new file mode 100644
index 0000000000..f5bec6d12d
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_fs_snap.js
@@ -0,0 +1,31 @@
+/* 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
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/" +
+ "security/sandbox/test/browser_content_sandbox_fs_tests.js",
+ this
+);
+
+add_task(async function() {
+ // Ensure that SNAP is there
+ const snap = Services.env.get("SNAP");
+ ok(snap.length > 1, "SNAP is defined");
+
+ // If it is there, do actual testing
+ sanityChecks();
+
+ add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
+
+ add_task(testFileAccessLinuxSnap); // eslint-disable-line no-undef
+
+ add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
+});
diff --git a/security/sandbox/test/browser_content_sandbox_fs_tests.js b/security/sandbox/test/browser_content_sandbox_fs_tests.js
new file mode 100644
index 0000000000..fe4cd4f220
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_fs_tests.js
@@ -0,0 +1,694 @@
+/* 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";
+
+// Test if the content process can create in $HOME, this should fail
+async function createFileInHome() {
+ let browser = gBrowser.selectedBrowser;
+ let homeFile = fileInHomeDir();
+ let path = homeFile.path;
+ let fileCreated = await SpecialPowers.spawn(browser, [path], createFile);
+ ok(!fileCreated.ok, "creating a file in home dir is not permitted");
+ if (fileCreated.ok) {
+ // content process successfully created the file, now remove it
+ homeFile.remove(false);
+ }
+}
+
+// Test if the content process can create a temp file, this is disallowed on
+// macOS but allowed everywhere else. Also test that the content process cannot
+// create symlinks or delete files.
+async function createTempFile() {
+ let browser = gBrowser.selectedBrowser;
+ let path = fileInTempDir().path;
+ let fileCreated = await SpecialPowers.spawn(browser, [path], createFile);
+ if (isMac()) {
+ ok(!fileCreated.ok, "creating a file in content temp is not permitted");
+ } else {
+ ok(!!fileCreated.ok, "creating a file in content temp is permitted");
+ }
+ // now delete the file
+ let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile);
+ if (isMac()) {
+ // On macOS we do not allow file deletion - it is not needed by the content
+ // process itself, and macOS uses a different permission to control access
+ // so revoking it is easy.
+ ok(!fileDeleted.ok, "deleting a file in content temp is not permitted");
+
+ let path = fileInTempDir().path;
+ let symlinkCreated = await SpecialPowers.spawn(
+ browser,
+ [path],
+ createSymlink
+ );
+ ok(
+ !symlinkCreated.ok,
+ "created a symlink in content temp is not permitted"
+ );
+ } else {
+ ok(!!fileDeleted.ok, "deleting a file in content temp is permitted");
+ }
+}
+
+// Test reading files and dirs from web and file content processes.
+async function testFileAccessAllPlatforms() {
+ let webBrowser = GetWebBrowser();
+ let fileContentProcessEnabled = isFileContentProcessEnabled();
+ let fileBrowser = GetFileBrowser();
+
+ // Directories/files to test accessing from content processes.
+ // For directories, we test whether a directory listing is allowed
+ // or blocked. For files, we test if we can read from the file.
+ // Each entry in the array represents a test file or directory
+ // that will be read from either a web or file process.
+ let tests = [];
+
+ let profileDir = GetProfileDir();
+ tests.push({
+ desc: "profile dir", // description
+ ok: false, // expected to succeed?
+ browser: webBrowser, // browser to run test in
+ file: profileDir, // nsIFile object
+ minLevel: minProfileReadSandboxLevel(), // min level to enable test
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: "profile dir",
+ ok: true,
+ browser: fileBrowser,
+ file: profileDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+
+ let homeDir = GetHomeDir();
+ tests.push({
+ desc: "home dir",
+ ok: false,
+ browser: webBrowser,
+ file: homeDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: "home dir",
+ ok: true,
+ browser: fileBrowser,
+ file: homeDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+
+ let extensionsDir = GetProfileEntry("extensions");
+ if (extensionsDir.exists() && extensionsDir.isDirectory()) {
+ tests.push({
+ desc: "extensions dir",
+ ok: true,
+ browser: webBrowser,
+ file: extensionsDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ } else {
+ ok(false, `${extensionsDir.path} is a valid dir`);
+ }
+
+ let chromeDir = GetProfileEntry("chrome");
+ if (chromeDir.exists() && chromeDir.isDirectory()) {
+ tests.push({
+ desc: "chrome dir",
+ ok: true,
+ browser: webBrowser,
+ file: chromeDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ } else {
+ ok(false, `${chromeDir.path} is valid dir`);
+ }
+
+ let cookiesFile = GetProfileEntry("cookies.sqlite");
+ if (cookiesFile.exists() && !cookiesFile.isDirectory()) {
+ tests.push({
+ desc: "cookies file",
+ ok: false,
+ browser: webBrowser,
+ file: cookiesFile,
+ minLevel: minProfileReadSandboxLevel(),
+ func: readFile,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: "cookies file",
+ ok: true,
+ browser: fileBrowser,
+ file: cookiesFile,
+ minLevel: 0,
+ func: readFile,
+ });
+ }
+ } else {
+ ok(false, `${cookiesFile.path} is a valid file`);
+ }
+
+ if (isMac() || isLinux()) {
+ let varDir = GetDir("/var");
+
+ if (isMac()) {
+ // Mac sandbox rules use /private/var because /var is a symlink
+ // to /private/var on OS X. Make sure that hasn't changed.
+ varDir.normalize();
+ Assert.ok(
+ varDir.path === "/private/var",
+ "/var resolves to /private/var"
+ );
+ }
+
+ tests.push({
+ desc: "/var",
+ ok: false,
+ browser: webBrowser,
+ file: varDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: "/var",
+ ok: true,
+ browser: fileBrowser,
+ file: varDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+ }
+
+ await runTestsList(tests);
+}
+
+async function testFileAccessMacOnly() {
+ if (!isMac()) {
+ return;
+ }
+
+ let webBrowser = GetWebBrowser();
+ let fileContentProcessEnabled = isFileContentProcessEnabled();
+ let fileBrowser = GetFileBrowser();
+ let level = GetSandboxLevel();
+
+ let tests = [];
+
+ // If ~/Library/Caches/TemporaryItems exists, when level <= 2 we
+ // make sure it's readable. For level 3, we make sure it isn't.
+ let homeTempDir = GetHomeDir();
+ homeTempDir.appendRelativePath("Library/Caches/TemporaryItems");
+ if (homeTempDir.exists()) {
+ let shouldBeReadable, minLevel;
+ if (level >= minHomeReadSandboxLevel()) {
+ shouldBeReadable = false;
+ minLevel = minHomeReadSandboxLevel();
+ } else {
+ shouldBeReadable = true;
+ minLevel = 0;
+ }
+ tests.push({
+ desc: "home library cache temp dir",
+ ok: shouldBeReadable,
+ browser: webBrowser,
+ file: homeTempDir,
+ minLevel,
+ func: readDir,
+ });
+ }
+
+ // Test if we can read from $TMPDIR because we expect it
+ // to be within /private/var. Reading from it should be
+ // prevented in a 'web' process.
+ let macTempDir = GetDirFromEnvVariable("TMPDIR");
+
+ macTempDir.normalize();
+ Assert.ok(
+ macTempDir.path.startsWith("/private/var"),
+ "$TMPDIR is in /private/var"
+ );
+
+ tests.push({
+ desc: `$TMPDIR (${macTempDir.path})`,
+ ok: false,
+ browser: webBrowser,
+ file: macTempDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: `$TMPDIR (${macTempDir.path})`,
+ ok: true,
+ browser: fileBrowser,
+ file: macTempDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+
+ // The font registry directory is in the Darwin user cache dir which is
+ // accessible with the getconf(1) library call using DARWIN_USER_CACHE_DIR.
+ // For this test, assume the cache dir is located at $TMPDIR/../C and use
+ // the $TMPDIR to derive the path to the registry.
+ let fontRegistryDir = macTempDir.parent.clone();
+ fontRegistryDir.appendRelativePath("C/com.apple.FontRegistry");
+ if (fontRegistryDir.exists()) {
+ tests.push({
+ desc: `FontRegistry (${fontRegistryDir.path})`,
+ ok: true,
+ browser: webBrowser,
+ file: fontRegistryDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ // Check that we can read the file named `font` which typically
+ // exists in the the font registry directory.
+ let fontFile = fontRegistryDir.clone();
+ fontFile.appendRelativePath("font");
+ if (fontFile.exists()) {
+ tests.push({
+ desc: `FontRegistry file (${fontFile.path})`,
+ ok: true,
+ browser: webBrowser,
+ file: fontFile,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readFile,
+ });
+ }
+ }
+
+ // Test that we cannot read from /Volumes at level 3
+ let volumes = GetDir("/Volumes");
+ tests.push({
+ desc: "/Volumes",
+ ok: false,
+ browser: webBrowser,
+ file: volumes,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+
+ // /Network is not present on macOS 10.15 (xnu 19). Don't
+ // test this directory on 10.15 and later.
+ if (AppConstants.isPlatformAndVersionAtMost("macosx", 18)) {
+ // Test that we cannot read from /Network at level 3
+ let network = GetDir("/Network");
+ tests.push({
+ desc: "/Network",
+ ok: false,
+ browser: webBrowser,
+ file: network,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ }
+ // Test that we cannot read from /Users at level 3
+ let users = GetDir("/Users");
+ tests.push({
+ desc: "/Users",
+ ok: false,
+ browser: webBrowser,
+ file: users,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+
+ // Test that we can stat /Users at level 3
+ tests.push({
+ desc: "/Users",
+ ok: true,
+ browser: webBrowser,
+ file: users,
+ minLevel: minHomeReadSandboxLevel(),
+ func: statPath,
+ });
+
+ // Test that we can stat /Library at level 3, but can't get a
+ // directory listing of /Library. This test uses "/Library"
+ // because it's a path that is expected to always be present.
+ let libraryDir = GetDir("/Library");
+ tests.push({
+ desc: "/Library",
+ ok: true,
+ browser: webBrowser,
+ file: libraryDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: statPath,
+ });
+ tests.push({
+ desc: "/Library",
+ ok: false,
+ browser: webBrowser,
+ file: libraryDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+
+ // Similarly, test that we can stat /private, but not /private/etc.
+ let privateDir = GetDir("/private");
+ tests.push({
+ desc: "/private",
+ ok: true,
+ browser: webBrowser,
+ file: privateDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: statPath,
+ });
+
+ await runTestsList(tests);
+}
+
+async function testFileAccessLinuxOnly() {
+ if (!isLinux()) {
+ return;
+ }
+
+ let webBrowser = GetWebBrowser();
+ let fileContentProcessEnabled = isFileContentProcessEnabled();
+ let fileBrowser = GetFileBrowser();
+
+ let tests = [];
+
+ // Test /proc/self/fd, because that can be used to unfreeze
+ // frozen shared memory.
+ let selfFdDir = GetDir("/proc/self/fd");
+ tests.push({
+ desc: "/proc/self/fd",
+ ok: false,
+ browser: webBrowser,
+ file: selfFdDir,
+ minLevel: isContentFileIOSandboxed(),
+ func: readDir,
+ });
+
+ let cacheFontConfigDir = GetHomeSubdir(".cache/fontconfig/");
+ tests.push({
+ desc: `$HOME/.cache/fontconfig/ (${cacheFontConfigDir.path})`,
+ ok: true,
+ browser: webBrowser,
+ file: cacheFontConfigDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+
+ // allows to handle both $HOME/.config/ or $XDG_CONFIG_HOME
+ let configDir = GetHomeSubdir(".config");
+
+ const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME");
+
+ if (xdgConfigHome.length > 1) {
+ configDir = GetDir(xdgConfigHome);
+ configDir.normalize();
+
+ tests.push({
+ desc: `$XDG_CONFIG_HOME (${configDir.path})`,
+ ok: true,
+ browser: webBrowser,
+ file: configDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ }
+
+ // $HOME/.config/ or $XDG_CONFIG_HOME/ should have rdonly access
+ tests.push({
+ desc: `${configDir.path} dir`,
+ ok: true,
+ browser: webBrowser,
+ file: configDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: `${configDir.path} dir`,
+ ok: true,
+ browser: fileBrowser,
+ file: configDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+
+ if (xdgConfigHome.length > 1) {
+ // When XDG_CONFIG_HOME is set, dont allow $HOME/.config
+ const homeConfigDir = GetHomeSubdir(".config");
+ tests.push({
+ desc: `${homeConfigDir.path} dir`,
+ ok: false,
+ browser: webBrowser,
+ file: homeConfigDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: `${homeConfigDir.path} dir`,
+ ok: true,
+ browser: fileBrowser,
+ file: homeConfigDir,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+ } else {
+ // WWhen XDG_CONFIG_HOME is not set, verify we do not allow $HOME/.configlol
+ // (i.e., check allow the dir and not the prefix)
+ //
+ // Checking $HOME/.config is already done above.
+ const homeConfigPrefix = GetHomeSubdir(".configlol");
+ tests.push({
+ desc: `${homeConfigPrefix.path} dir`,
+ ok: false,
+ browser: webBrowser,
+ file: homeConfigPrefix,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: `${homeConfigPrefix.path} dir`,
+ ok: false,
+ browser: fileBrowser,
+ file: homeConfigPrefix,
+ minLevel: 0,
+ func: readDir,
+ });
+ }
+ }
+
+ // Create a file under $HOME/.config/ or $XDG_CONFIG_HOME and ensure we can
+ // read it
+ let fileUnderConfig = GetSubdirFile(configDir);
+ await IOUtils.writeUTF8(fileUnderConfig.path, "TEST FILE DUMMY DATA");
+ ok(
+ await IOUtils.exists(fileUnderConfig.path),
+ `File ${fileUnderConfig.path} was properly created`
+ );
+
+ tests.push({
+ desc: `${configDir.path}/xxx is readable (${fileUnderConfig.path})`,
+ ok: true,
+ browser: webBrowser,
+ file: fileUnderConfig,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readFile,
+ cleanup: aPath => IOUtils.remove(aPath),
+ });
+
+ let configFile = GetSubdirFile(configDir);
+ tests.push({
+ desc: `${configDir.path} file write`,
+ ok: false,
+ browser: webBrowser,
+ file: configFile,
+ minLevel: minHomeReadSandboxLevel(),
+ func: createFile,
+ });
+ if (fileContentProcessEnabled) {
+ tests.push({
+ desc: `${configDir.path} file write`,
+ ok: false,
+ browser: fileBrowser,
+ file: configFile,
+ minLevel: 0,
+ func: createFile,
+ });
+ }
+
+ // Create a $HOME/.config/mozilla/ or $XDG_CONFIG_HOME/mozilla/ if none
+ // exists and assert content process cannot access it
+ let configMozilla = GetSubdir(configDir, "mozilla");
+ const emptyFileName = ".test_run_browser_sandbox.tmp";
+ let emptyFile = configMozilla.clone();
+ emptyFile.appendRelativePath(emptyFileName);
+
+ let populateFakeConfigMozilla = async aPath => {
+ // called with configMozilla
+ await IOUtils.makeDirectory(aPath, { permissions: 0o700 });
+ await IOUtils.writeUTF8(emptyFile.path, "");
+ ok(
+ await IOUtils.exists(emptyFile.path),
+ `Temp file ${emptyFile.path} was created`
+ );
+ };
+
+ let unpopulateFakeConfigMozilla = async aPath => {
+ // called with emptyFile
+ await IOUtils.remove(aPath);
+ ok(!(await IOUtils.exists(aPath)), `Temp file ${aPath} was removed`);
+ const parentDir = PathUtils.parent(aPath);
+ try {
+ await IOUtils.remove(parentDir, { recursive: false });
+ } catch (ex) {
+ if (
+ !DOMException.isInstance(ex) ||
+ ex.name !== "OperationError" ||
+ /Could not remove the non-empty directory/.test(ex.message)
+ ) {
+ // If we get here it means the directory was not empty and since we assert
+ // earlier we removed the temp file we created it means we should not
+ // worrying about removing this directory ...
+ throw ex;
+ }
+ }
+ };
+
+ await populateFakeConfigMozilla(configMozilla.path);
+
+ tests.push({
+ desc: `stat ${configDir.path}/mozilla (${configMozilla.path})`,
+ ok: false,
+ browser: webBrowser,
+ file: configMozilla,
+ minLevel: minHomeReadSandboxLevel(),
+ func: statPath,
+ });
+
+ tests.push({
+ desc: `read ${configDir.path}/mozilla (${configMozilla.path})`,
+ ok: false,
+ browser: webBrowser,
+ file: configMozilla,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+
+ tests.push({
+ desc: `stat ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`,
+ ok: false,
+ browser: webBrowser,
+ file: emptyFile,
+ minLevel: minHomeReadSandboxLevel(),
+ func: statPath,
+ });
+
+ tests.push({
+ desc: `read ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`,
+ ok: false,
+ browser: webBrowser,
+ file: emptyFile,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readFile,
+ cleanup: unpopulateFakeConfigMozilla,
+ });
+
+ // Only needed to perform cleanup
+ if (xdgConfigHome.length > 1) {
+ tests.push({
+ desc: `$XDG_CONFIG_HOME (${configDir.path}) cleanup`,
+ ok: true,
+ browser: webBrowser,
+ file: configDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ }
+
+ await runTestsList(tests);
+}
+
+async function testFileAccessLinuxSnap() {
+ let webBrowser = GetWebBrowser();
+
+ let tests = [];
+
+ // Assert that if we run with SNAP= env, then we allow access to it in the
+ // content process
+ let snap = Services.env.get("SNAP");
+ let snapExpectedResult = false;
+ if (snap.length > 1) {
+ snapExpectedResult = true;
+ } else {
+ snap = "/tmp/.snap_firefox_current/";
+ }
+
+ let snapDir = GetDir(snap);
+ snapDir.normalize();
+
+ let snapFile = GetSubdirFile(snapDir);
+ await createFile(snapFile.path);
+ ok(await IOUtils.exists(snapFile.path), `SNAP ${snapFile.path} was created`);
+ info(`SNAP (file) ${snapFile.path} was created`);
+
+ tests.push({
+ desc: `$SNAP (${snapDir.path} => ${snapFile.path})`,
+ ok: snapExpectedResult,
+ browser: webBrowser,
+ file: snapFile,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readFile,
+ });
+
+ await runTestsList(tests);
+}
+
+async function testFileAccessWindowsOnly() {
+ if (!isWin()) {
+ return;
+ }
+
+ let webBrowser = GetWebBrowser();
+
+ let tests = [];
+
+ let extDir = GetPerUserExtensionDir();
+ tests.push({
+ desc: "per-user extensions dir",
+ ok: true,
+ browser: webBrowser,
+ file: extDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+
+ await runTestsList(tests);
+}
+
+function cleanupBrowserTabs() {
+ let fileBrowser = GetFileBrowser();
+ if (fileBrowser.selectedTab) {
+ gBrowser.removeTab(fileBrowser.selectedTab);
+ }
+
+ let webBrowser = GetWebBrowser();
+ if (webBrowser.selectedTab) {
+ gBrowser.removeTab(webBrowser.selectedTab);
+ }
+
+ let tab1 = gBrowser.tabs[1];
+ if (tab1) {
+ gBrowser.removeTab(tab1);
+ }
+}
diff --git a/security/sandbox/test/browser_content_sandbox_fs_xdg.js b/security/sandbox/test/browser_content_sandbox_fs_xdg.js
new file mode 100644
index 0000000000..141c339c47
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_fs_xdg.js
@@ -0,0 +1,31 @@
+/* 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
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/" +
+ "security/sandbox/test/browser_content_sandbox_fs_tests.js",
+ this
+);
+
+add_task(async function() {
+ // Ensure that XDG_CONFIG_HOME is there
+ const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME");
+ ok(xdgConfigHome.length > 1, "XDG_CONFIG_HOME is defined");
+
+ // If it is there, do actual testing
+ sanityChecks();
+
+ // The linux only tests are the ones that can behave differently based on
+ // existence of XDG_CONFIG_HOME
+ add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
+
+ add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
+});
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..1816e1dbbb
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_syscalls.js
@@ -0,0 +1,402 @@
+/* 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";
+
+/* global OS */
+Cc["@mozilla.org/net/osfileconstantsservice;1"]
+ .getService(Ci.nsIOSFileConstantsService)
+ .init();
+
+registerCleanupFunction(() => {
+ delete window.OS;
+});
+
+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) {
+ const { ctypes } = ChromeUtils.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) {
+ const { ctypes } = ChromeUtils.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 sysctl syscall.
+function callSysctl(args) {
+ const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+ 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.import("resource://gre/modules/ctypes.jsm");
+ 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.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;
+}
+
+// Verify faccessat2
+function callFaccessat2(args) {
+ const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+ 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.import("resource://gre/modules/ctypes.jsm");
+ 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()) {
+ // verify we block PR_CAPBSET_READ with EINVAL
+ let option = OS.Constants.libc.PR_CAPBSET_READ;
+ let rv = await SpecialPowers.spawn(browser, [{ lib, option }], callPrctl);
+ ok(rv == OS.Constants.libc.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 == OS.Constants.libc.ENOSYS,
+ "faccessat2 (flag=0x01) was blocked with ENOSYS"
+ );
+
+ rv = await SpecialPowers.spawn(
+ browser,
+ [{ lib, dirfd, path, mode, flag: OS.Constants.libc.AT_EACCESS }],
+ callFaccessat2
+ );
+ ok(
+ rv == OS.Constants.libc.EACCES,
+ "faccessat2 (flag=0x200) was allowed, errno=EACCES"
+ );
+ } else {
+ info(
+ "Unsupported kernel (" +
+ kernelVersion +
+ " )/glibc (" +
+ glibcVersion +
+ "), skipping faccessat2"
+ );
+ }
+ }
+});
diff --git a/security/sandbox/test/browser_content_sandbox_utils.js b/security/sandbox/test/browser_content_sandbox_utils.js
new file mode 100644
index 0000000000..3891c5d6df
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_utils.js
@@ -0,0 +1,462 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const uuidGenerator = Services.uuid;
+
+/*
+ * Utility functions for the browser content sandbox tests.
+ */
+
+function sanityChecks() {
+ // 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.
+ // 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 isFileIOSandboxed = isContentFileIOSandboxed(level);
+
+ // 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");
+ }
+}
+
+// Creates file at |path| and returns a promise that resolves with an object
+// with .ok boolean to indicate 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) {
+ const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ try {
+ const fstream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+
+ fstream.init(
+ new FileUtils.File(path),
+ -1, // readonly mode
+ -1, // default permissions
+ 0
+ ); // behaviour flags
+
+ const ostream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ ostream.setOutputStream(fstream);
+
+ const data = "TEST FILE DUMMY DATA";
+ ostream.writeBytes(data, data.length);
+
+ ostream.close();
+ fstream.close();
+ } catch (e) {
+ return { ok: false };
+ }
+
+ return { ok: true };
+}
+
+// Creates a symlink at |path| and returns a promise that resolves with an
+// object with .ok boolean to indicate true if the symlink was successfully
+// created, otherwise false. Include imports so this can be safely serialized
+// and run remotely by ContentTask.spawn.
+function createSymlink(path) {
+ const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+
+ try {
+ const libc = ctypes.open(
+ Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so"
+ );
+
+ const symlink = libc.declare(
+ "symlink",
+ ctypes.default_abi,
+ ctypes.int, // return value
+ ctypes.char.ptr, // target
+ ctypes.char.ptr //linkpath
+ );
+
+ if (symlink("/etc", path)) {
+ return { ok: false };
+ }
+ } catch (e) {
+ return { ok: false };
+ }
+
+ return { ok: true };
+}
+
+// Deletes file at |path| and returns a promise that resolves with an object
+// with .ok boolean to indicate 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) {
+ const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ try {
+ const file = new FileUtils.File(path);
+ file.remove(false);
+ } catch (e) {
+ return { ok: false };
+ }
+
+ return { ok: true };
+}
+
+// Reads the directory at |path| and returns a promise that resolves when
+// iteration over the directory finishes or encounters an error. The promise
+// resolves with an object where .ok indicates success or failure and
+// .numEntries is the number of directory entries found.
+function readDir(path) {
+ const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ let numEntries = 0;
+
+ try {
+ const file = new FileUtils.File(path);
+ const enumerator = file.directoryEntries;
+
+ while (enumerator.hasMoreElements()) {
+ void enumerator.nextFile;
+ numEntries++;
+ }
+ } catch (e) {
+ return { ok: false, numEntries };
+ }
+
+ return { ok: true, numEntries };
+}
+
+// Reads the file at |path| and returns a promise that resolves when
+// reading is completed. Returned object has boolean .ok to indicate
+// success or failure.
+function readFile(path) {
+ const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ try {
+ const file = new FileUtils.File(path);
+
+ const fstream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ fstream.init(file, -1, -1, 0);
+
+ const istream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ istream.setInputStream(fstream);
+
+ const available = istream.available();
+ void istream.readBytes(available);
+ } catch (e) {
+ return { ok: false };
+ }
+
+ return { ok: true };
+}
+
+// Does a stat of |path| and returns a promise that resolves if the
+// stat is successful. Returned object has boolean .ok to indicate
+// success or failure.
+function statPath(path) {
+ const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ try {
+ const file = new FileUtils.File(path);
+ void file.lastModifiedTime;
+ } catch (e) {
+ return { ok: false };
+ }
+
+ return { ok: true };
+}
+
+// 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;
+}
+
+// Returns the lowest sandbox level where blanket reading of the profile
+// directory from the content process should be blocked by the sandbox.
+function minProfileReadSandboxLevel(level) {
+ switch (Services.appinfo.OS) {
+ case "WINNT":
+ return 3;
+ case "Darwin":
+ return 2;
+ case "Linux":
+ return 3;
+ default:
+ Assert.ok(false, "Unknown OS");
+ return 0;
+ }
+}
+
+// Returns the lowest sandbox level where blanket reading of the home
+// directory from the content process should be blocked by the sandbox.
+function minHomeReadSandboxLevel(level) {
+ switch (Services.appinfo.OS) {
+ case "WINNT":
+ return 3;
+ case "Darwin":
+ return 3;
+ case "Linux":
+ return 3;
+ default:
+ Assert.ok(false, "Unknown OS");
+ return 0;
+ }
+}
+
+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.Services.appinfo.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.nsIFile);
+ 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";
+
+ // get the content temp dir, make sure it exists
+ let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile);
+ 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;
+}
+
+function GetProfileDir() {
+ // get profile directory
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ return profileDir;
+}
+
+function GetHomeDir() {
+ // get home directory
+ let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
+ return homeDir;
+}
+
+function GetHomeSubdir(subdir) {
+ return GetSubdir(GetHomeDir(), subdir);
+}
+
+function GetHomeSubdirFile(subdir) {
+ return GetSubdirFile(GetHomeSubdir(subdir));
+}
+
+function GetSubdir(dir, subdir) {
+ let newSubdir = dir.clone();
+ newSubdir.appendRelativePath(subdir);
+ return newSubdir;
+}
+
+function GetSubdirFile(dir) {
+ let newFile = dir.clone();
+ newFile.appendRelativePath(uuid());
+ return newFile;
+}
+
+function GetPerUserExtensionDir() {
+ return Services.dirsvc.get("XREUSysExt", Ci.nsIFile);
+}
+
+// Returns a file object for the file or directory named |name| in the
+// profile directory.
+function GetProfileEntry(name) {
+ let entry = GetProfileDir();
+ entry.append(name);
+ return entry;
+}
+
+function GetDir(path) {
+ let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath(path);
+ Assert.ok(dir.isDirectory(), `${path} is a directory`);
+ return dir;
+}
+
+function GetDirFromEnvVariable(varName) {
+ return GetDir(Services.env.get(varName));
+}
+
+function GetFile(path) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+ return file;
+}
+
+function GetBrowserType(type) {
+ let browserType = undefined;
+
+ if (!GetBrowserType[type]) {
+ if (type === "web") {
+ GetBrowserType[type] = gBrowser.selectedBrowser;
+ } else {
+ // open a tab in a `type` content process
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ preferredRemoteType: type,
+ });
+ // get the browser for the `type` process tab
+ GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ }
+ }
+
+ browserType = GetBrowserType[type];
+ ok(
+ browserType.remoteType === type,
+ `GetBrowserType(${type}) returns a ${type} process`
+ );
+ return browserType;
+}
+
+function GetWebBrowser() {
+ return GetBrowserType("web");
+}
+
+function isFileContentProcessEnabled() {
+ // Ensure that the file content process is enabled.
+ let fileContentProcessEnabled = Services.prefs.getBoolPref(
+ "browser.tabs.remote.separateFileUriProcess"
+ );
+ ok(fileContentProcessEnabled, "separate file content process is enabled");
+ return fileContentProcessEnabled;
+}
+
+function GetFileBrowser() {
+ if (!isFileContentProcessEnabled()) {
+ return undefined;
+ }
+ return GetBrowserType("file");
+}
+
+function GetSandboxLevel() {
+ // Current level
+ return Services.prefs.getIntPref("security.sandbox.content.level");
+}
+
+async function runTestsList(tests) {
+ let level = GetSandboxLevel();
+
+ // remove tests not enabled by the current sandbox level
+ tests = tests.filter(test => test.minLevel <= level);
+
+ for (let test of tests) {
+ let okString = test.ok ? "allowed" : "blocked";
+ let processType = test.browser.remoteType;
+
+ // ensure the file/dir exists before we ask a content process to stat
+ // it so we know a failure is not due to a nonexistent file/dir
+ if (test.func === statPath) {
+ ok(test.file.exists(), `${test.file.path} exists`);
+ }
+
+ let result = await ContentTask.spawn(
+ test.browser,
+ test.file.path,
+ test.func
+ );
+
+ ok(
+ result.ok == test.ok,
+ `reading ${test.desc} from a ${processType} process ` +
+ `is ${okString} (${test.file.path})`
+ );
+
+ // if the directory is not expected to be readable,
+ // ensure the listing has zero entries
+ if (test.func === readDir && !test.ok) {
+ ok(result.numEntries == 0, `directory list is empty (${test.file.path})`);
+ }
+
+ if (test.cleanup != undefined) {
+ await test.cleanup(test.file.path);
+ }
+ }
+}
diff --git a/security/sandbox/test/browser_sandbox_test.js b/security/sandbox/test/browser_sandbox_test.js
new file mode 100644
index 0000000000..e0d456b236
--- /dev/null
+++ b/security/sandbox/test/browser_sandbox_test.js
@@ -0,0 +1,59 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+
+ // Types of processes to test, taken from GeckoProcessTypes.h
+ // GPU process might not run depending on the platform, so we need it to be
+ // the last one of the list to allow the remainingTests logic below to work
+ // as expected.
+ //
+ // For UtilityProcess, allow constructing a string made of the process type
+ // and the sandbox variant we want to test, e.g.,
+ // utility:0 for GENERIC_UTILITY
+ // utility:1 for AppleMedia/WMF on macOS/Windows
+ var processTypes = ["tab", "socket", "rdd", "gmplugin", "utility:0", "gpu"];
+
+ const platform = SpecialPowers.Services.appinfo.OS;
+ if (platform === "WINNT" || platform === "Darwin") {
+ processTypes.push("utility:1");
+ }
+
+ // A callback called after each test-result.
+ let sandboxTestResult = (subject, topic, data) => {
+ let { testid, passed, message } = JSON.parse(data);
+ ok(
+ passed,
+ "Test " + testid + (passed ? " passed: " : " failed: ") + message
+ );
+ };
+ Services.obs.addObserver(sandboxTestResult, "sandbox-test-result");
+
+ var remainingTests = processTypes.length;
+
+ // A callback that is notified when a child process is done running tests.
+ let sandboxTestDone = () => {
+ remainingTests = remainingTests - 1;
+ if (remainingTests == 0) {
+ Services.obs.removeObserver(sandboxTestResult, "sandbox-test-result");
+ Services.obs.removeObserver(sandboxTestDone, "sandbox-test-done");
+
+ // Notify SandboxTest component that it should terminate the connection
+ // with the child processes.
+ comp.finishTests();
+ // Notify mochitest that all process tests are complete.
+ finish();
+ }
+ };
+ Services.obs.addObserver(sandboxTestDone, "sandbox-test-done");
+
+ var comp = Cc["@mozilla.org/sandbox/sandbox-test;1"].getService(
+ Ci.mozISandboxTest
+ );
+
+ comp.startTests(processTypes);
+}
diff --git a/security/sandbox/test/browser_snap.ini b/security/sandbox/test/browser_snap.ini
new file mode 100644
index 0000000000..ad7b058cb1
--- /dev/null
+++ b/security/sandbox/test/browser_snap.ini
@@ -0,0 +1,14 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+skip-if = ccov || (os == "linux" && (asan || tsan)) # bug 1784517
+tags = contentsandbox
+support-files =
+ browser_content_sandbox_utils.js
+ browser_content_sandbox_fs_tests.js
+test-directories =
+ /tmp/.snap_firefox_current_real/
+environment=SNAP=/tmp/.snap_firefox_current_real/
+
+[browser_content_sandbox_fs_snap.js]
+run-if = (os == 'linux')
diff --git a/security/sandbox/test/browser_xdg.ini b/security/sandbox/test/browser_xdg.ini
new file mode 100644
index 0000000000..aee4c63c5a
--- /dev/null
+++ b/security/sandbox/test/browser_xdg.ini
@@ -0,0 +1,14 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+skip-if = ccov || (os == "linux" && (asan || tsan)) # bug 1784517
+tags = contentsandbox
+support-files =
+ browser_content_sandbox_utils.js
+ browser_content_sandbox_fs_tests.js
+test-directories =
+ /tmp/.xdg_config_home_test
+environment=XDG_CONFIG_HOME=/tmp/.xdg_config_home_test
+
+[browser_content_sandbox_fs_xdg.js]
+run-if = (os == 'linux')
diff --git a/security/sandbox/test/bug1393259.html b/security/sandbox/test/bug1393259.html
new file mode 100644
index 0000000000..b1e3cca99a
--- /dev/null
+++ b/security/sandbox/test/bug1393259.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8"/>
+</head>
+<style>
+#content { display: inline-block; }
+.monospace_fallback { font: 3em monospace; }
+</style>
+<body>
+
+<div id="content" class="monospace_fallback">
+abcdefghijklmnopqrstuvwxyz<br>
+<b>abcdefghijklmnopqrstuvwxyz</b><br>
+<i>abcdefghijklmnopqrstuvwxyz</i>
+</div>
+
+</body>
+</html>
diff --git a/security/sandbox/test/mac_register_font.py b/security/sandbox/test/mac_register_font.py
new file mode 100755
index 0000000000..549becf565
--- /dev/null
+++ b/security/sandbox/test/mac_register_font.py
@@ -0,0 +1,85 @@
+#!/usr/bin/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/. */
+
+"""
+mac_register_font.py
+
+Mac-specific utility command to register a font file with the OS.
+"""
+
+import argparse
+import sys
+
+import Cocoa
+import CoreText
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="print verbose registration failures",
+ default=False,
+ )
+ parser.add_argument(
+ "file", nargs="*", help="font file to register or unregister", default=[]
+ )
+ parser.add_argument(
+ "-u",
+ "--unregister",
+ action="store_true",
+ help="unregister the provided fonts",
+ default=False,
+ )
+ parser.add_argument(
+ "-p",
+ "--persist-user",
+ action="store_true",
+ help="permanently register the font",
+ default=False,
+ )
+
+ args = parser.parse_args()
+
+ if args.persist_user:
+ scope = CoreText.kCTFontManagerScopeUser
+ scopeDesc = "user"
+ else:
+ scope = CoreText.kCTFontManagerScopeSession
+ scopeDesc = "session"
+
+ failureCount = 0
+ for fontPath in args.file:
+ fontURL = Cocoa.NSURL.fileURLWithPath_(fontPath)
+ (result, error) = register_or_unregister_font(fontURL, args.unregister, scope)
+ if result:
+ print(
+ "%sregistered font %s with %s scope"
+ % (("un" if args.unregister else ""), fontPath, scopeDesc)
+ )
+ else:
+ print(
+ "Failed to %sregister font %s with %s scope"
+ % (("un" if args.unregister else ""), fontPath, scopeDesc)
+ )
+ if args.verbose:
+ print(error)
+ failureCount += 1
+
+ sys.exit(failureCount)
+
+
+def register_or_unregister_font(fontURL, unregister, scope):
+ return (
+ CoreText.CTFontManagerUnregisterFontsForURL(fontURL, scope, None)
+ if unregister
+ else CoreText.CTFontManagerRegisterFontsForURL(fontURL, scope, None)
+ )
+
+
+if __name__ == "__main__":
+ main()