diff options
Diffstat (limited to '')
17 files changed, 2173 insertions, 0 deletions
diff --git a/security/sandbox/test/browser.ini b/security/sandbox/test/browser.ini new file mode 100644 index 0000000000..b76f019131 --- /dev/null +++ b/security/sandbox/test/browser.ini @@ -0,0 +1,26 @@ +# 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_bug1393259.js] +support-files = + bug1393259.html +skip-if = (os != 'mac') # This is a Mac-specific test +[browser_content_sandbox_fs.js] +skip-if = + (debug && os == 'win') # bug 1379635 +[browser_content_sandbox_syscalls.js] +[browser_sandbox_test.js] +skip-if = + !debug + win11_2009 && msix && debug # bug 1823583
\ No newline at end of file diff --git a/security/sandbox/test/browser_bug1393259.js b/security/sandbox/test/browser_bug1393259.js new file mode 100644 index 0000000000..58ee9ca06f --- /dev/null +++ b/security/sandbox/test/browser_bug1393259.js @@ -0,0 +1,200 @@ +/* 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..e45c0cb078 --- /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..e45c0cb078 --- /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..cff7a872fe --- /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..a8b26a1e31 --- /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..12678ddbe0 --- /dev/null +++ b/security/sandbox/test/browser_content_sandbox_fs_tests.js @@ -0,0 +1,698 @@ +/* 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 and Windows but allowed everywhere else. Also test that the content +// process cannot create symlinks on macOS or delete files. +async function createTempFile() { + // On Windows we allow access to the temp dir for DEBUG builds, because of + // logging that uses that dir. + let isOptWin = isWin() && !SpecialPowers.isDebugBuild; + + let browser = gBrowser.selectedBrowser; + let path = fileInTempDir().path; + let fileCreated = await SpecialPowers.spawn(browser, [path], createFile); + if (isMac() || isOptWin) { + ok(!fileCreated.ok, "creating a file in temp is not permitted"); + } else { + ok(!!fileCreated.ok, "creating a file in temp is permitted"); + } + // now delete the file + let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile); + if (isMac() || isOptWin) { + // 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 temp is not permitted"); + } else { + ok(!!fileDeleted.ok, "deleting a file in temp is permitted"); + } + + // Test that symlink creation is not allowed on macOS. + if (isMac()) { + let path = fileInTempDir().path; + let symlinkCreated = await SpecialPowers.spawn( + browser, + [path], + createSymlink + ); + ok(!symlinkCreated.ok, "created a symlink in temp is not 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..f5150fc329 --- /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..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" + ); + } + } +}); 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..ce6ed39ff6 --- /dev/null +++ b/security/sandbox/test/browser_content_sandbox_utils.js @@ -0,0 +1,464 @@ +/* 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.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + 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 = "TmpD"; + + // get the content temp dir, make sure it exists + let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile); + Assert.ok(ctmp.exists(), "Temp dir exists"); + Assert.ok(ctmp.isDirectory(), "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() |