Bug 1404298 - Crashes with read-access content sandboxing triggered by mounted volumes. r?Alex_Gaynor draft
authorHaik Aftandilian <haftandilian@mozilla.com>
Mon, 18 Dec 2017 12:58:30 -0800
changeset 713229 56b97bad4f05a251236b94f9b450de6c89b8bfe7
parent 713123 99bdcfe1725a596017b1c43308c34e2f6e114375
child 744284 b4d92ec1aa6ff74591c6762d4ed501be954bc1e6
push id93587
push userhaftandilian@mozilla.com
push dateTue, 19 Dec 2017 22:08:37 +0000
reviewersAlex_Gaynor
bugs1404298
milestone59.0a1
Bug 1404298 - Crashes with read-access content sandboxing triggered by mounted volumes. r?Alex_Gaynor Allow read-metadata access to top-level directory entries. MozReview-Commit-ID: 1Q7QXN2gX36
security/sandbox/mac/SandboxPolicies.h
security/sandbox/test/browser_content_sandbox_fs.js
--- a/security/sandbox/mac/SandboxPolicies.h
+++ b/security/sandbox/mac/SandboxPolicies.h
@@ -70,23 +70,21 @@ static const char contentSandboxRules[] 
   ; Allow read access to standard system paths.
   (allow file-read*
     (require-all (file-mode #o0004)
       (require-any (subpath "/Library/Filesystems/NetFSPlugins")
         (subpath "/System")
         (subpath "/usr/lib")
         (subpath "/usr/share"))))
 
+  ; Top-level directory metadata access (bug 1404298)
+  (allow file-read-metadata (regex #"^/[^/]+$"))
+
   (allow file-read-metadata
-    (literal "/etc")
-    (literal "/tmp")
-    (literal "/var")
     (literal "/private/etc/localtime")
-    (literal "/home")
-    (literal "/net")
     (regex #"^/private/tmp/KSInstallAction\."))
 
   ; Allow read access to standard special files.
   (allow file-read*
     (literal "/dev/autofs_nowait")
     (literal "/dev/random")
     (literal "/dev/urandom"))
 
--- a/security/sandbox/test/browser_content_sandbox_fs.js
+++ b/security/sandbox/test/browser_content_sandbox_fs.js
@@ -83,16 +83,29 @@ function readFile(path) {
   let promise = OS.File.read(path).then(function (binaryData) {
     return {ok: true};
   }).catch(function (error) {
     return {ok: false};
   });
   return promise;
 }
 
+// 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) {
+  Components.utils.import("resource://gre/modules/osfile.jsm");
+  let promise = OS.File.stat(path).then(function (stat) {
+    return {ok: true};
+  }).catch(function (error) {
+    return {ok: false};
+  });
+  return promise;
+}
+
 // 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.
@@ -342,91 +355,99 @@ async function testFileAccess() {
 
       let fontFile = GetFile(fontPath);
       tests.push({
         desc:     "font file",                  // description
         ok:       true,                         // expected to succeed?
         browser:  webBrowser,                   // browser to run test in
         file:     fontFile,                     // nsIFile object
         minLevel: minHomeReadSandboxLevel(),    // min level to enable test
+        func:     readFile,                     // the test function to use
       });
     }
     for (let fontPath of badFontTestPaths) {
       let result = await createFile(fontPath);
       Assert.ok(result, `${fontPath} created`);
 
       let fontFile = GetFile(fontPath);
       tests.push({
         desc:     "invalid font file",          // description
         ok:       false,                        // expected to succeed?
         browser:  webBrowser,                   // browser to run test in
         file:     fontFile,                     // nsIFile object
         minLevel: minHomeReadSandboxLevel(),    // min level to enable test
+        func:     readFile,                     // the test function to use
       });
     }
   }
 
   // The Linux test runners create the temporary profile in the same
   // system temp dir we give write access to, so this gives a false
   // positive.
   let profileDir = GetProfileDir();
   if (!isLinux()) {
     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 sysExtDevDir = GetSystemExtensionsDevDir();
   tests.push({
     desc:     "system extensions dev dir",
     ok:       true,
     browser:  webBrowser,
     file:     sysExtDevDir,
     minLevel: 0,
+    func:     readDir,
   });
 
   if (isWin()) {
     let extDir = GetPerUserExtensionDir();
     tests.push({
       desc:       "per-user extensions dir",
       ok:         true,
       browser:    webBrowser,
       file:       extDir,
       minLevel:   minHomeReadSandboxLevel(),
+      func:       readDir,
     });
   }
 
   if (isMac()) {
     // 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");
@@ -440,16 +461,17 @@ async function testFileAccess() {
         minLevel = 0;
       }
       tests.push({
         desc:     "home library cache temp dir",
         ok:       shouldBeReadable,
         browser:  webBrowser,
         file:     homeTempDir,
         minLevel,
+        func:     readDir,
       });
     }
   }
 
   if (isMac() || isLinux()) {
     let varDir = GetDir("/var");
 
     if (isMac()) {
@@ -460,24 +482,26 @@ async function testFileAccess() {
     }
 
     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,
       });
     }
   }
 
   if (isMac()) {
     // 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.
@@ -488,126 +512,202 @@ async function testFileAccess() {
       "$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,
       });
     }
 
     // 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 /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
+    // stat something within /Library. This test uses "/Library"
+    // because it's a path that is expected to always be present
+    // and isn't something content processes have read access to
+    // (just read-metadata).
+    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,
+    });
+    let libraryWidgetsDir = GetDir("/Library/Widgets");
+    tests.push({
+      desc:     "/Library/Widgets",
+      ok:       false,
+      browser:  webBrowser,
+      file:     libraryWidgetsDir,
+      minLevel: minHomeReadSandboxLevel(),
+      func:     statPath,
+    });
+
+    // 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,
+    });
+    let privateEtcDir = GetFile("/private/etc");
+    tests.push({
+      desc:     "/private/etc",
+      ok:       false,
+      browser:  webBrowser,
+      file:     privateEtcDir,
+      minLevel: minHomeReadSandboxLevel(),
+      func:     statPath,
     });
   }
 
   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()) {
     // On Linux, the temporary profile used for tests is in the system
     // temp dir which content has read access to, so this test fails.
     if (!isLinux()) {
       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`);
   }
 
   // remove tests not enabled by the current sandbox level
   tests = tests.filter((test) => (test.minLevel <= level));
 
   for (let test of tests) {
-    let testFunc = test.file.isDirectory() ? readDir : readFile;
     let okString = test.ok ? "allowed" : "blocked";
     let processType = test.browser === webBrowser ? "web" : "file";
 
+    // 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,
-        testFunc);
+        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.file.isDirectory() && !test.ok) {
+    if (test.func === readDir && !test.ok) {
       ok(result.numEntries == 0, `directory list is empty (${test.file.path})`);
     }
   }
 
   if (fileContentProcessEnabled) {
     gBrowser.removeTab(gBrowser.selectedTab);
   }
 }