diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 771b243c6..4fc248847 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae944551f9 100644 +index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..ec11f894f18140b8e7ceb65e6ca4a2f27b5ba520 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -398,6 +398,7 @@ @@ -458,10 +458,10 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae + gZenWorkspaces._initialTab._shouldRemove = true; + } + } -+ } + } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; - } ++ } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { @@ -650,7 +650,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5744,13 +5908,13 @@ +@@ -5744,15 +5908,22 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -662,11 +662,20 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae let remainingTabs = Array.prototype.filter.call( this.visibleTabs, - tab => !excludeTabs.has(tab) -+ tab => !excludeTabs.has(tab) && gZenWorkspaces._shouldChangeToTab(tab) ++ tab => !excludeTabs.has(tab) && gZenWorkspaces._shouldChangeToTab(tab) && tab !== aTab ); ++ if (remainingTabs.length > 0 && Services.prefs.getBoolPref("zen.tabs.select-recently-used-on-close")) { ++ let mostRecentTab = remainingTabs.reduce((a, b) => ++ b.lastAccessed > a.lastAccessed ? b : a ++ ); ++ return gZenWorkspaces.findTabToBlur(mostRecentTab); ++ } ++ let tab = this.tabContainer.findNextTab(aTab, { -@@ -5766,7 +5930,7 @@ + direction: 1, + filter: _tab => remainingTabs.includes(_tab), +@@ -5766,7 +5937,7 @@ } if (tab) { @@ -675,7 +684,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } // If no qualifying visible tab was found, see if there is a tab in -@@ -5787,7 +5951,7 @@ +@@ -5787,7 +5958,7 @@ }); } @@ -684,7 +693,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } _blurTab(aTab) { -@@ -5798,7 +5962,7 @@ +@@ -5798,7 +5969,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -693,7 +702,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -5852,6 +6016,7 @@ +@@ -5852,6 +6023,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -701,7 +710,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -5863,7 +6028,7 @@ +@@ -5863,7 +6035,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -710,7 +719,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -5987,11 +6152,13 @@ +@@ -5987,11 +6159,13 @@ } // Finish tearing down the tab that's going away. @@ -724,7 +733,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae this.setTabTitle(aOurTab); -@@ -6193,10 +6360,10 @@ +@@ -6193,10 +6367,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -737,7 +746,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6254,7 +6421,8 @@ +@@ -6254,7 +6428,8 @@ * * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ @@ -747,7 +756,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae if (this.tabs.length == 1) { return null; } -@@ -6278,12 +6446,14 @@ +@@ -6278,12 +6453,14 @@ } // tell a new window to take the "dropped" tab @@ -763,7 +772,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } /** -@@ -6388,7 +6558,7 @@ +@@ -6388,7 +6565,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -772,7 +781,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } /** -@@ -6473,8 +6643,8 @@ +@@ -6473,8 +6650,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -783,7 +792,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6500,10 +6670,16 @@ +@@ -6500,10 +6677,16 @@ this.#handleTabMove( element, () => { @@ -802,7 +811,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6561,23 +6737,31 @@ +@@ -6561,23 +6744,31 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -840,7 +849,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6590,14 +6774,34 @@ +@@ -6590,14 +6781,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -876,7 +885,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6606,7 +6810,7 @@ +@@ -6606,7 +6817,7 @@ element, () => { if (moveBefore) { @@ -885,7 +894,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae } else if (targetElement) { targetElement.after(element); } else { -@@ -6676,10 +6880,10 @@ +@@ -6676,10 +6887,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToExistingGroup(aTab, aGroup, metricsContext) { @@ -898,7 +907,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6751,6 +6955,7 @@ +@@ -6751,6 +6962,7 @@ let state = { tabIndex: tab._tPos, @@ -906,7 +915,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6777,7 +6982,7 @@ +@@ -6777,7 +6989,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -915,7 +924,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6818,6 +7023,10 @@ +@@ -6818,6 +7030,10 @@ moveActionCallback(); @@ -926,7 +935,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -6869,6 +7078,19 @@ +@@ -6869,6 +7085,19 @@ * The new tab in the current window, null if the tab couldn't be adopted. */ adoptTab(aTab, { elementIndex, tabIndex, selectTab = false } = {}) { @@ -946,7 +955,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae // Swap the dropped tab with a new one we create and then close // it in the other window (making it seem to have moved between // windows). We also ensure that the tab we create to swap into has -@@ -6910,6 +7132,8 @@ +@@ -6910,6 +7139,8 @@ params.userContextId = aTab.getAttribute("usercontextid"); } let newTab = this.addWebTab("about:blank", params); @@ -955,7 +964,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae let newBrowser = this.getBrowserForTab(newTab); aTab.container.tabDragAndDrop.finishAnimateTabMove(); -@@ -7718,7 +7942,7 @@ +@@ -7718,7 +7949,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -964,7 +973,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae window.focus(); aEvent.preventDefault(); break; -@@ -7735,7 +7959,6 @@ +@@ -7735,7 +7966,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -972,7 +981,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae }); break; case "TabGroupCreateByUser": -@@ -7895,7 +8118,9 @@ +@@ -7895,7 +8125,9 @@ let filter = this._tabFilters.get(tab); if (filter) { @@ -982,7 +991,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae let listener = this._tabListeners.get(tab); if (listener) { -@@ -8698,6 +8923,7 @@ +@@ -8698,6 +8930,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -990,7 +999,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -8778,6 +9004,7 @@ +@@ -8778,6 +9011,7 @@ // known defaults. Note we use the original URL since about:newtab // redirects to a prerendered page. const shouldRemoveFavicon = @@ -998,7 +1007,7 @@ index 0eaca7a58e0026237b71b2ad515efe84d9e8c779..5f58cf2009dfe869d05b896b609c7bae !this.mBrowser.mIconURL && !ignoreBlank && !(originalLocation.spec in FAVICON_DEFAULTS); -@@ -9803,7 +10030,7 @@ var TabContextMenu = { +@@ -9803,7 +10037,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/toolkit/moz-configure.patch b/src/toolkit/moz-configure.patch index 501222625..cacaabb60 100644 --- a/src/toolkit/moz-configure.patch +++ b/src/toolkit/moz-configure.patch @@ -1,5 +1,5 @@ diff --git a/toolkit/moz.configure b/toolkit/moz.configure -index 226d0c5a93a9a2404e1974001da4e34b7b670067..18ac234b577eb514d3ef3467e24fceb0eb1ec8f8 100644 +index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d232f392d47 100644 --- a/toolkit/moz.configure +++ b/toolkit/moz.configure @@ -22,6 +22,7 @@ def check_moz_app_id(moz_app_id, build_project): @@ -55,3 +55,12 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..18ac234b577eb514d3ef3467e24fceb0 option( +@@ -3903,7 +3908,7 @@ with only_when(compile_environment): + return "Mozilla" + elif target.os == "Android": + return ".mozilla" +- return "mozilla" ++ return "zen" + + option( + "--with-user-appdir", diff --git a/src/zen/tests/manifest.toml b/src/zen/tests/manifest.toml index 5e48f8e67..60c0975dc 100644 --- a/src/zen/tests/manifest.toml +++ b/src/zen/tests/manifest.toml @@ -7,6 +7,16 @@ source = "browser/components/safebrowsing/content/test" is_direct_path = true +[sandbox] +source = "security/sandbox/test" +is_direct_path = true +disable = [ + "browser_bug1393259.js", +] + +[sandbox.replace-manifest] +"../../../" = "../../../../" + [shell] source = "browser/components/shell/test" is_direct_path = true @@ -17,4 +27,3 @@ disable = [ [tooltiptext] source = "toolkit/components/tooltiptext" - diff --git a/src/zen/tests/mochitests/moz.build b/src/zen/tests/mochitests/moz.build index 24b790fb9..efc1a106d 100644 --- a/src/zen/tests/mochitests/moz.build +++ b/src/zen/tests/mochitests/moz.build @@ -8,6 +8,7 @@ BROWSER_CHROME_MANIFESTS += [ "safebrowsing/browser.toml", + "sandbox/browser.toml", "shell/browser.toml", "tooltiptext/browser.toml", ] \ No newline at end of file diff --git a/src/zen/tests/mochitests/sandbox/browser.toml b/src/zen/tests/mochitests/sandbox/browser.toml new file mode 100644 index 000000000..9ab4a5d5c --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser.toml @@ -0,0 +1,37 @@ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && 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" +] + +["browser_bug1393259.js"] +disabled="Disabled by import_external_tests.py" +support-files = ["bug1393259.html"] +run-if = [ + "os == 'mac'", # This is a Mac-specific test +] +skip-if = [ + "os == 'mac' && os_version == '14.70' && arch == 'x86_64'", # Bug 1929424 +] +tags = "os_integration" + +["browser_content_sandbox_fs.js"] +skip-if = [ + "os == 'win' && os_version == '11.26100' && arch == 'x86' && debug", # bug 1379635 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # bug 1379635 +] + +["browser_content_sandbox_syscalls.js"] + +["browser_sandbox_test.js"] +run-if = [ + "debug", +] diff --git a/src/zen/tests/mochitests/sandbox/browser_bug1393259.js b/src/zen/tests/mochitests/sandbox/browser_bug1393259.js new file mode 100644 index 000000000..8fbeb1b9d --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_bug1393259.js @@ -0,0 +1,203 @@ +/* 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. +let kPythonPath = "/usr/bin/python"; +if (AppConstants.isPlatformAndVersionAtLeast("macosx", 23.0)) { + kPythonPath = "/usr/local/bin/python3"; +} +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.equal(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.equal(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/src/zen/tests/mochitests/sandbox/browser_bug1717599_XDG-CONFIG-DIRS.toml b/src/zen/tests/mochitests/sandbox/browser_bug1717599_XDG-CONFIG-DIRS.toml new file mode 100644 index 000000000..6a16ad95c --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_bug1717599_XDG-CONFIG-DIRS.toml @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && 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/src/zen/tests/mochitests/sandbox/browser_bug1717599_XDG-CONFIG-HOME.toml b/src/zen/tests/mochitests/sandbox/browser_bug1717599_XDG-CONFIG-HOME.toml new file mode 100644 index 000000000..7d524f137 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_bug1717599_XDG-CONFIG-HOME.toml @@ -0,0 +1,13 @@ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517 +] +tags = "contentsandbox" +environment = "XDG_CONFIG_HOME=" + +["browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js"] +run-if = [ + "os == 'linux'", +] diff --git a/src/zen/tests/mochitests/sandbox/browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js new file mode 100644 index 000000000..e45c0cb07 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js new file mode 100644 index 000000000..e45c0cb07 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs.js new file mode 100644 index 000000000..cff7a872f --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_snap.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_snap.js new file mode 100644 index 000000000..06f04c1d3 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/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"); + Assert.greater(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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_tests.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_tests.js new file mode 100644 index 000000000..7a415c2ed --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_tests.js @@ -0,0 +1,712 @@ +/* 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"; + +const lazy = {}; + +/* getLibcConstants is only present on *nix */ +ChromeUtils.defineLazyGetter(lazy, "LIBC", () => + ChromeUtils.getLibcConstants() +); + +// 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 failed"); + is( + fileCreated.code, + Cr.NS_ERROR_FILE_ACCESS_DENIED, + "creating a file in home dir failed with access denied" + ); + 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 forbidden on all +// platforms. Also test that the content process cannot create symlinks on +// macOS/Linux 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 isDbgWin = isWin() && SpecialPowers.isDebugBuild; + + let browser = gBrowser.selectedBrowser; + let path = fileInTempDir().path; + let fileCreated = await SpecialPowers.spawn(browser, [path], createFile); + if (isDbgWin) { + ok(fileCreated.ok, "creating a file in temp suceeded"); + } else { + ok(!fileCreated.ok, "creating a file in temp failed"); + is( + fileCreated.code, + Cr.NS_ERROR_FILE_ACCESS_DENIED, + "creating a file in temp failed with access denied" + ); + } + + // now delete the file + let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile); + if (isDbgWin) { + ok(fileDeleted.ok, "deleting a file in temp succeeded"); + } else { + ok(!fileDeleted.ok, "deleting a file in temp failed"); + const expectedError = isLinux() + ? Cr.NS_ERROR_FILE_ACCESS_DENIED + : Cr.NS_ERROR_FILE_NOT_FOUND; + is( + fileDeleted.code, + expectedError, + "deleting a file in temp failed with access denied" + ); + } + + // Test that symlink creation is not allowed on macOS/Linux. + if (isMac() || isLinux()) { + let path = fileInTempDir().path; + let symlinkCreated = await SpecialPowers.spawn( + browser, + [path], + createSymlink + ); + ok(!symlinkCreated.ok, "created a symlink in temp failed"); + const expectedError = isLinux() ? lazy.LIBC.EACCES : lazy.LIBC.EPERM; + is( + symlinkCreated.code, + expectedError, + "created a symlink in temp failed with access denied" + ); + } +} + +// 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.strictEqual( + 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, + }); + + // 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) { + configDir = GetDir(xdgConfigHome); + configDir.normalize(); + } + + tests.push({ + desc: `$XDG_CONFIG_HOME (${configDir.path})`, + ok: true, // access should not be granted outside of XDG support + browser: webBrowser, + file: configDir, + minLevel: minHomeReadSandboxLevel(), + func: readDir, + }); + + tests.push({ + desc: `XDG_CONFIG_HOME=${configDir.path} dir should have rdonly`, + ok: true, // should be allowed only if XDG support is there + browser: webBrowser, + file: configDir, + minLevel: minHomeReadSandboxLevel(), + func: readDir, + }); + + if (fileContentProcessEnabled) { + tests.push({ + desc: `${configDir.path} dir`, + ok: true, // should be allowed only if XDG support is there + browser: fileBrowser, + file: configDir, + minLevel: 0, + func: readDir, + }); + } + + if (isXdgEnabled() && xdgConfigHome) { + const homeConfigDir = GetHomeSubdir(".config"); + tests.push({ + desc: `XDG_CONFIG_HOME=${homeConfigDir.path} dir should deny $HOME/.config`, + 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: `No XDG_CONFIG_HOME we dont allow ${homeConfigPrefix.path} access`, + ok: false, + browser: webBrowser, + file: homeConfigPrefix, + minLevel: minHomeReadSandboxLevel(), + func: readDir, + }); + if (fileContentProcessEnabled) { + tests.push({ + desc: `No XDG_CONFIG_HOME we dont allow ${homeConfigPrefix.path} access`, + 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 (isXdgEnabled()) { + 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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_default.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_default.js new file mode 100644 index 000000000..f828308bf --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_default.js @@ -0,0 +1,40 @@ +/* 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 +); + +SimpleTest.requestCompleteLog(); + +add_setup(async function setup() { + const xdgConfigHome = Services.env.exists("XDG_CONFIG_HOME"); + Assert.equal(xdgConfigHome, false, `XDG_CONFIG_HOME is not set`); + + const mozLegacyHome = Services.env.exists("MOZ_LEGACY_HOME"); + Assert.equal(mozLegacyHome, false, "MOZ_LEGACY_HOME is not set"); + + // If it is there, do actual testing + sanityChecks(); +}); + +add_task(async function () { + // Make sure we dont break others. + add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef + + // 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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_mozLegacyHome.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_mozLegacyHome.js new file mode 100644 index 000000000..f76ee5d92 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_mozLegacyHome.js @@ -0,0 +1,42 @@ +/* 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 +); + +SimpleTest.requestCompleteLog(); + +add_setup(async function setup() { + const xdgConfigHome = Services.env.exists("XDG_CONFIG_HOME"); + Assert.equal(xdgConfigHome, true, "XDG_CONFIG_HOME is defined"); + + if (isXdgEnabled()) { + const mozLegacyHome = Services.env.get("MOZ_LEGACY_HOME"); + Assert.equal(mozLegacyHome, 1, "MOZ_LEGACY_HOME is set to 1"); + } + + // If it is there, do actual testing + sanityChecks(); +}); + +add_task(async function () { + // Make sure we dont break others. + add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef + + // 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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_xdgConfigHome.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_xdgConfigHome.js new file mode 100644 index 000000000..992435d7a --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_fs_xdg_xdgConfigHome.js @@ -0,0 +1,38 @@ +/* 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 +); + +SimpleTest.requestCompleteLog(); + +add_setup(async function setup() { + // Ensure that XDG_CONFIG_HOME is there + const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME"); + Assert.greater(xdgConfigHome.length, 1, "XDG_CONFIG_HOME is defined"); + + // If it is there, do actual testing + sanityChecks(); +}); + +add_task(async function () { + // Make sure we dont break others. + add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef + + // 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/src/zen/tests/mochitests/sandbox/browser_content_sandbox_syscalls.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_syscalls.js new file mode 100644 index 000000000..e59da7a00 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_syscalls.js @@ -0,0 +1,405 @@ +/* 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 lazy = {}; + +/* getLibcConstants is only present on *nix */ +ChromeUtils.defineLazyGetter(lazy, "LIBC", () => + ChromeUtils.getLibcConstants() +); + +/* + * 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; +} + +// 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}`); + Assert.greater(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); + Assert.equal(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 = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY; + let fd = await SpecialPowers.spawn( + browser, + [{ lib, path, flags }], + callOpen + ); + Assert.less(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 Linux. The open handler in the content process closes the file + // for us + let path = fileInTempDir().path; + let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY; + let fd = await SpecialPowers.spawn( + browser, + [{ lib, path, flags }], + callOpen + ); + Assert.strictEqual( + fd, + -1, + "opening a file for writing in content temp is not permitted" + ); + } + + // use fork syscall + if (isLinux() || isMac()) { + let rv = await SpecialPowers.spawn(browser, [{ lib }], callFork); + Assert.equal(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 + ); + Assert.equal(rv, -1, "calling sysctl('kern.boottime') is not permitted"); + + rv = await SpecialPowers.spawn( + browser, + [{ lib, name: "net.inet.ip.ttl" }], + callSysctl + ); + Assert.equal(rv, -1, "calling sysctl('net.inet.ip.ttl') is not permitted"); + + rv = await SpecialPowers.spawn( + browser, + [{ lib, name: "hw.ncpu" }], + callSysctl + ); + Assert.equal(rv, 0, "calling sysctl('hw.ncpu') is permitted"); + } + + if (isLinux()) { + // These constants are not portable. + + // verify we block PR_CAPBSET_READ with EINVAL + let option = lazy.LIBC.PR_CAPBSET_READ; + let rv = await SpecialPowers.spawn(browser, [{ lib, option }], callPrctl); + Assert.strictEqual( + rv, + lazy.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 + ); + Assert.strictEqual( + rv, + lazy.LIBC.ENOSYS, + "faccessat2 (flag=0x01) was blocked with ENOSYS" + ); + + rv = await SpecialPowers.spawn( + browser, + [{ lib, dirfd, path, mode, flag: lazy.LIBC.AT_EACCESS }], + callFaccessat2 + ); + Assert.strictEqual( + rv, + lazy.LIBC.EACCES, + "faccessat2 (flag=0x200) was allowed, errno=EACCES" + ); + } else { + info( + "Unsupported kernel (" + + kernelVersion + + " )/glibc (" + + glibcVersion + + "), skipping faccessat2" + ); + } + } +}); diff --git a/src/zen/tests/mochitests/sandbox/browser_content_sandbox_utils.js b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_utils.js new file mode 100644 index 000000000..26228554a --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_content_sandbox_utils.js @@ -0,0 +1,502 @@ +/* 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}`); + Assert.greater(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"); + } +} + +function isXdgEnabled() { + try { + return Services.prefs.getBoolPref("widget.support-xdg-config"); + } catch (ex) { + // if the pref is not there it means we dont have XDG support + if (ex.name === "NS_ERROR_UNEXPECTED") { + return false; + } + throw ex; + } +} + +// 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. +// +// Report the exception's error code in .code as well. +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, code: e.result }; + } + + 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. +// +// Report the exception's error code in .code as well. +// Report errno in .code if syscall returns -1. +function createSymlink(path) { + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + try { + // Trying to open "libc.so" on linux will fail with invalid elf header error + // because it would be a linker script. Using libc.so.6 avoids that. + const libc = ctypes.open( + Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so.6" + ); + + const symlink = libc.declare( + "symlink", + ctypes.default_abi, + ctypes.int, // return value + ctypes.char.ptr, // target + ctypes.char.ptr //linkpath + ); + + ctypes.errno = 0; + const rv = symlink("/etc", path); + const _errno = ctypes.errno; + if (rv < 0) { + return { ok: false, code: _errno }; + } + } catch (e) { + return { ok: false, code: e.result }; + } + + 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. +// +// Report the exception's error code in .code as well. +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, code: e.result }; + } + + 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. +// +// Report the exception's error code in .code as well. +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, code: e.result }; + } + + 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. +// +// Report the exception's error code in .code as well. +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, code: e.result }; + } + + 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. +// +// Report the exception's error code in .code as well. +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, code: e.result }; + } + + 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() { + 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() { + 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/). +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/ + 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 (.../). +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) { + info(`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, + allowInheritPrincipal: true, + }); + // get the browser for the `type` process tab + GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab); + } + } + + browserType = GetBrowserType[type]; + Assert.strictEqual( + 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 + ); + + Assert.equal( + 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) { + Assert.equal( + result.numEntries, + 0, + `directory list is empty (${test.file.path})` + ); + } + + if (test.cleanup != undefined) { + await test.cleanup(test.file.path); + } + } +} diff --git a/src/zen/tests/mochitests/sandbox/browser_profiler.toml b/src/zen/tests/mochitests/sandbox/browser_profiler.toml new file mode 100644 index 000000000..830c5478e --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_profiler.toml @@ -0,0 +1,22 @@ +[DEFAULT] +skip-if = [ + "!gecko_profiler", + "ccov", + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 for sandbox, bug 1885381 for profiler + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517 for sandbox, bug 1885381 for profiler +] +tags = "contentsandbox" + +# This is here to make sure we will not have prelaunched processes, which will +# mess with sandbox profiling interaction: we will miss launch-related markers +# and this makes the test intermittently fail on TV jobs +prefs = [ + "dom.ipc.processPrelaunch.fission.number=0" +] + +environment = "MOZ_SANDBOX_LOGGING_FOR_TESTS=1" + +["browser_sandbox_profiler.js"] +run-if = [ + "os == 'linux'", +] diff --git a/src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js b/src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js new file mode 100644 index 000000000..c20310b51 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ProfilerTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ProfilerTestUtils.sys.mjs" +); + +async function addTab() { + const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/browser", { + forceNewProcess: true, + }); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return tab; +} + +const sandboxSettingsEnabled = { + entries: 8 * 1024 * 1024, // 8M entries = 64MB + interval: 1, // ms + features: ["stackwalk", "sandbox"], + threads: ["SandboxProfilerEmitter"], +}; + +const sandboxSettingsDisabled = { + entries: 8 * 1024 * 1024, // 8M entries = 64MB + interval: 1, // ms + features: ["stackwalk"], + threads: ["SandboxProfilerEmitter"], +}; + +const kNewProcesses = 2; + +async function waitForMaybeSandboxProfilerData( + threadName, + name1, + withStacks, + enabled +) { + let tabs = []; + for (let i = 0; i < kNewProcesses; ++i) { + tabs.push(await addTab()); + } + + let profile; + let intercepted = undefined; + try { + await TestUtils.waitForCondition( + async () => { + profile = await Services.profiler.getProfileDataAsync(); + intercepted = profile.processes + .flatMap(ps => { + let sandboxThreads = ps.threads.filter( + th => th.name === threadName + ); + return sandboxThreads.flatMap(th => { + let markersData = th.markers.data; + return markersData.flatMap(d => { + let [, , , , , o] = d; + return o; + }); + }); + }) + .filter(x => "name1" in x && name1.includes(x.name1) >= 0); + return !!intercepted.length; + }, + `Wait for some samples from ${threadName}`, + /* interval*/ 100, + /* maxTries */ 25 + ); + Assert.greater( + intercepted.length, + 0, + `Should have collected some data from ${threadName}` + ); + } catch (ex) { + if (!enabled && ex.includes(`Wait for some samples from ${threadName}`)) { + Assert.equal( + intercepted.length, + 0, + `Should have NOT collected data from ${threadName}` + ); + } else { + throw ex; + } + } + + if (withStacks) { + let stacks = profile.processes.flatMap(ps => { + let sandboxThreads = ps.threads.filter(th => th.name === threadName); + return sandboxThreads.flatMap(th => { + let stackTableData = th.stackTable.data; + return stackTableData.flatMap(d => { + return [d]; + }); + }); + }); + if (enabled) { + Assert.greater(stacks.length, 0, "Should have some stack as well"); + } else { + Assert.equal(stacks.length, 0, "Should have NO stack as well"); + } + } + + for (let tab of tabs) { + await BrowserTestUtils.removeTab(tab); + } +} + +add_task(async () => { + await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled); + await waitForMaybeSandboxProfilerData( + "SandboxProfilerEmitterSyscalls", + ["id", "init"], + /* withStacks */ true, + /* enabled */ true + ); + await Services.profiler.StopProfiler(); +}); + +add_task(async () => { + await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled); + await waitForMaybeSandboxProfilerData( + "SandboxProfilerEmitterLogs", + ["log"], + /* withStacks */ false, + /* enabled */ true + ); + await Services.profiler.StopProfiler(); +}); + +add_task(async () => { + await ProfilerTestUtils.startProfiler(sandboxSettingsDisabled); + await waitForMaybeSandboxProfilerData( + "SandboxProfilerEmitterSyscalls", + ["id", "init"], + /* withStacks */ true, + /* enabled */ false + ); + await Services.profiler.StopProfiler(); +}); + +add_task(async () => { + await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled); + await waitForMaybeSandboxProfilerData( + "SandboxProfilerEmitterLogs", + ["log"], + /* withStacks */ false, + /* enabled */ false + ); + await Services.profiler.StopProfiler(); +}); diff --git a/src/zen/tests/mochitests/sandbox/browser_sandbox_test.js b/src/zen/tests/mochitests/sandbox/browser_sandbox_test.js new file mode 100644 index 000000000..58c37989c --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_sandbox_test.js @@ -0,0 +1,78 @@ +/* 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) { + // Clean up test file + if (homeTestFile.exists()) { + ok(homeTestFile.isFile(), "homeTestFile should be a file"); + if (homeTestFile.isFile()) { + homeTestFile.remove(false); + } + } + + 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 + ); + + let homeTestFile; + try { + homeTestFile = Services.dirsvc.get("Home", Ci.nsIFile); + homeTestFile.append(".mozilla_gpu_sandbox_read_test"); + if (!homeTestFile.exists()) { + homeTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } + } catch (e) { + ok(false, "Failed to create home test file: " + e); + } + + comp.startTests(processTypes); +} diff --git a/src/zen/tests/mochitests/sandbox/browser_snap.toml b/src/zen/tests/mochitests/sandbox/browser_snap.toml new file mode 100644 index 000000000..19a4a1004 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_snap.toml @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && 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/src/zen/tests/mochitests/sandbox/browser_xdg_default.toml b/src/zen/tests/mochitests/sandbox/browser_xdg_default.toml new file mode 100644 index 000000000..e32443de2 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_xdg_default.toml @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517 +] +tags = "contentsandbox" +support-files = [ + "browser_content_sandbox_utils.js", + "browser_content_sandbox_fs_tests.js", +] +# .config needs to exists for the sandbox to properly add it +test-directories = ["/tmp/.xdg_default_test", "/tmp/.xdg_default_test/.config"] +environment = [ + "HOME=/tmp/.xdg_default_test", +] + +["browser_content_sandbox_fs_xdg_default.js"] +run-if = ["os == 'linux'"] diff --git a/src/zen/tests/mochitests/sandbox/browser_xdg_mozLegacyHome.toml b/src/zen/tests/mochitests/sandbox/browser_xdg_mozLegacyHome.toml new file mode 100644 index 000000000..242c01464 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_xdg_mozLegacyHome.toml @@ -0,0 +1,23 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517 +] +tags = "contentsandbox" +support-files = [ + "browser_content_sandbox_utils.js", + "browser_content_sandbox_fs_tests.js", +] +test-directories = ["/tmp/.xdg_mozLegacyHome_test/.config", "/tmp/.xdg_config_home_test"] +environment = [ + "XDG_CONFIG_HOME=/tmp/.xdg_config_home_test", + "HOME=/tmp/.xdg_mozLegacyHome_test", + "MOZ_LEGACY_HOME=1", +] + +["browser_content_sandbox_fs_xdg_mozLegacyHome.js"] +run-if = ["os == 'linux'"] diff --git a/src/zen/tests/mochitests/sandbox/browser_xdg_xdgConfigHome.toml b/src/zen/tests/mochitests/sandbox/browser_xdg_xdgConfigHome.toml new file mode 100644 index 000000000..a6be37835 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/browser_xdg_xdgConfigHome.toml @@ -0,0 +1,23 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +[DEFAULT] +skip-if = [ + "ccov", + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && 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", + "MOZ_LEGACY_HOME=0", +] + +["browser_content_sandbox_fs_xdg_xdgConfigHome.js"] +run-if = [ + "os == 'linux'", +] diff --git a/src/zen/tests/mochitests/sandbox/bug1393259.html b/src/zen/tests/mochitests/sandbox/bug1393259.html new file mode 100644 index 000000000..b1e3cca99 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/bug1393259.html @@ -0,0 +1,19 @@ + + + + + + + + +
+abcdefghijklmnopqrstuvwxyz
+abcdefghijklmnopqrstuvwxyz
+abcdefghijklmnopqrstuvwxyz +
+ + + diff --git a/src/zen/tests/mochitests/sandbox/mac_register_font.py b/src/zen/tests/mochitests/sandbox/mac_register_font.py new file mode 100755 index 000000000..d536abac1 --- /dev/null +++ b/src/zen/tests/mochitests/sandbox/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()