Bug 1308400 - Support file process, whitelist path prefs. r?jld draft
authorGian-Carlo Pascutto <gcp@mozilla.com>
Fri, 30 Jun 2017 23:12:39 +0200
changeset 606219 5b94966972570e85f79b7289db04730dda865b0a
parent 604877 7529213e33752241cbc645f8d206c283959e074f
child 606220 ac58700cb02ece4ad83fb3c3851676c12fd5859e
push id67644
push usergpascutto@mozilla.com
push dateMon, 10 Jul 2017 18:21:54 +0000
reviewersjld
bugs1308400
milestone56.0a1
Bug 1308400 - Support file process, whitelist path prefs. r?jld MozReview-Commit-ID: 3eX06AioPZL
browser/app/profile/firefox.js
dom/ipc/ContentChild.cpp
dom/ipc/ContentParent.cpp
security/sandbox/linux/Sandbox.cpp
security/sandbox/linux/Sandbox.h
security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h
security/sandbox/linux/reporter/SandboxReporterCommon.h
security/sandbox/test/browser_content_sandbox_fs.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1080,30 +1080,32 @@ pref("security.sandbox.content.level", 1
 #endif
 #endif
 
 #if defined(XP_LINUX) && defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
 // This pref is introduced as part of bug 742434, the naming is inspired from
 // its Windows/Mac counterpart, but on Linux it's an integer which means:
 // 0 -> "no sandbox"
 // 1 -> "content sandbox using seccomp-bpf when available"
-// 2 -> "seccomp-bpf + file broker"
+// 2 -> "seccomp-bpf + write file broker"
+// 3 -> "seccomp-bpf + read/write file brokering"
 // Content sandboxing on Linux is currently in the stage of
 // 'just getting it enabled', which includes a very permissive whitelist. We
 // enable seccomp-bpf on nightly to see if everything is running, or if we need
 // to whitelist more system calls.
 //
 // So the purpose of this setting is to allow nightly users to disable the
 // sandbox while we fix their problems. This way, they won't have to wait for
 // another nightly release which disables seccomp-bpf again.
 //
 // This setting may not be required anymore once we decide to permanently
 // enable the content sandbox.
-pref("security.sandbox.content.level", 2);
+pref("security.sandbox.content.level", 3);
 pref("security.sandbox.content.write_path_whitelist", "");
+pref("security.sandbox.content.read_path_whitelist", "");
 pref("security.sandbox.content.syscall_whitelist", "");
 #endif
 
 #if defined(XP_MACOSX) || defined(XP_WIN)
 #if defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
 // ID (a UUID when set by gecko) that is used to form the name of a
 // sandbox-writable temporary directory to be used by content processes
 // when a temporary writable file is required in a level 1 sandbox.
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1613,17 +1613,20 @@ ContentChild::RecvSetProcessSandbox(cons
       for (const nsACString& callNrString : extraSyscalls.Split(',')) {
         nsresult rv;
         int callNr = PromiseFlatCString(callNrString).ToInteger(&rv);
         if (NS_SUCCEEDED(rv)) {
           syscallWhitelist.push_back(callNr);
         }
       }
     }
-    sandboxEnabled = SetContentProcessSandbox(brokerFd, syscallWhitelist);
+    ContentChild* cc = ContentChild::GetSingleton();
+    bool isFileProcess = cc->GetRemoteType().EqualsLiteral(FILE_REMOTE_TYPE);
+    sandboxEnabled = SetContentProcessSandbox(brokerFd, isFileProcess,
+                                              syscallWhitelist);
   }
 #elif defined(XP_WIN)
   mozilla::SandboxTarget::Instance()->StartSandbox();
 #elif defined(XP_MACOSX)
   sandboxEnabled = StartMacOSContentSandbox();
 #endif
 
 #if defined(MOZ_CRASHREPORTER)
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -2442,18 +2442,19 @@ ContentParent::InitInternal(ProcessPrior
   // should be changed so that it is required to restart firefox for the change
   // of value to take effect.
   shouldSandbox = (GetEffectiveContentSandboxLevel() > 0) &&
     !PR_GetEnv("MOZ_DISABLE_CONTENT_SANDBOX");
 
 #ifdef XP_LINUX
   if (shouldSandbox) {
     MOZ_ASSERT(!mSandboxBroker);
+    bool isFileProcess = mRemoteType.EqualsLiteral(FILE_REMOTE_TYPE);
     UniquePtr<SandboxBroker::Policy> policy =
-      sSandboxBrokerPolicyFactory->GetContentPolicy(Pid());
+      sSandboxBrokerPolicyFactory->GetContentPolicy(Pid(), isFileProcess);
     if (policy) {
       brokerFd = FileDescriptor();
       mSandboxBroker = SandboxBroker::Create(Move(policy), Pid(), brokerFd);
       if (!mSandboxBroker) {
         KillHard("SandboxBroker::Create failed");
         return;
       }
       MOZ_ASSERT(static_cast<const FileDescriptor&>(brokerFd).IsValid());
--- a/security/sandbox/linux/Sandbox.cpp
+++ b/security/sandbox/linux/Sandbox.cpp
@@ -670,26 +670,28 @@ SandboxEarlyInit(GeckoProcessType aType)
 #ifdef MOZ_CONTENT_SANDBOX
 /**
  * Starts the seccomp sandbox for a content process.  Should be called
  * only once, and before any potentially harmful content is loaded.
  *
  * Will normally make the process exit on failure.
 */
 bool
-SetContentProcessSandbox(int aBrokerFd, std::vector<int>& aSyscallWhitelist)
+SetContentProcessSandbox(int aBrokerFd, bool aFileProcess,
+                         std::vector<int>& aSyscallWhitelist)
 {
   if (!SandboxInfo::Get().Test(SandboxInfo::kEnabledForContent)) {
     if (aBrokerFd >= 0) {
       close(aBrokerFd);
     }
     return false;
   }
 
-  gSandboxReporterClient.emplace(SandboxReport::ProcType::CONTENT);
+  gSandboxReporterClient.emplace(aFileProcess ? SandboxReport::ProcType::FILE
+                                              : SandboxReport::ProcType::CONTENT);
 
   // This needs to live until the process exits.
   static Maybe<SandboxBrokerClient> sBroker;
   if (aBrokerFd >= 0) {
     sBroker.emplace(aBrokerFd);
   }
 
   SetCurrentProcessSandbox(GetContentSandboxPolicy(sBroker.ptrOr(nullptr),
--- a/security/sandbox/linux/Sandbox.h
+++ b/security/sandbox/linux/Sandbox.h
@@ -20,17 +20,19 @@ namespace mozilla {
 // This must be called early, while the process is still single-threaded.
 MOZ_EXPORT void SandboxEarlyInit(GeckoProcessType aType);
 
 #ifdef MOZ_CONTENT_SANDBOX
 // Call only if SandboxInfo::CanSandboxContent() returns true.
 // (No-op if MOZ_DISABLE_CONTENT_SANDBOX is set.)
 // aBrokerFd is the filesystem broker client file descriptor,
 // or -1 to allow direct filesystem access.
+// isFileProcess determines whether we allow system wide file reads.
 MOZ_EXPORT bool SetContentProcessSandbox(int aBrokerFd,
+                                         bool aFileProcess,
                                          std::vector<int>& aSyscallWhitelist);
 #endif
 
 #ifdef MOZ_GMP_SANDBOX
 // Call only if SandboxInfo::CanSandboxMedia() returns true.
 // (No-op if MOZ_DISABLE_GMP_SANDBOX is set.)
 // aFilePath is the path to the plugin file.
 MOZ_EXPORT void SetMediaPluginSandbox(const char *aFilePath);
--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
@@ -6,20 +6,23 @@
 
 #include "SandboxBrokerPolicyFactory.h"
 #include "SandboxInfo.h"
 #include "SandboxLogging.h"
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/SandboxSettings.h"
+#include "mozilla/dom/ContentChild.h"
 #include "nsPrintfCString.h"
 #include "nsString.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsAppDirectoryServiceDefs.h"
 #include "SpecialSystemDirectory.h"
 
 #ifdef ANDROID
 #include "cutils/properties.h"
 #endif
 
 #ifdef MOZ_WIDGET_GTK
 #include <glib.h>
@@ -118,24 +121,26 @@ SandboxBrokerPolicyFactory::SandboxBroke
   policy->AddTree(rdonly, "/system//usr/share/zoneinfo");
 
   policy->AddPath(rdonly, "/data/local/tmp/profiler.options",
                   SandboxBroker::Policy::AddAlways); // bug 1029337
 
   mCommonContentPolicy.reset(policy);
 #elif defined(MOZ_CONTENT_SANDBOX)
   SandboxBroker::Policy* policy = new SandboxBroker::Policy;
-  policy->AddDir(rdonly, "/");
   policy->AddDir(rdwrcr, "/dev/shm");
+  // Write permssions
+  //
   // Add write permissions on the temporary directory. This can come
   // from various environment variables (TMPDIR,TMP,TEMP,...) so
   // make sure to use the full logic.
   nsCOMPtr<nsIFile> tmpDir;
   nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory,
                                           getter_AddRefs(tmpDir));
+
   if (NS_SUCCEEDED(rv)) {
     nsAutoCString tmpPath;
     rv = tmpDir->GetNativePath(tmpPath);
     if (NS_SUCCEEDED(rv)) {
       policy->AddDir(rdwrcr, tmpPath.get());
     }
   }
   // If the above fails at any point, fall back to a very good guess.
@@ -158,71 +163,203 @@ SandboxBrokerPolicyFactory::SandboxBroke
   // Bug 1321134: DConf's single bit of shared memory
   if (const auto userDir = g_get_user_runtime_dir()) {
     // The leaf filename is "user" by default, but is configurable.
     nsPrintfCString shmPath("%s/dconf/", userDir);
     policy->AddPrefix(rdwrcr, shmPath.get());
   }
 #endif
 
+  // Read permissions
+  // No read blocking at level 2 and below
+  if (Preferences::GetInt("security.sandbox.content.level") <= 2) {
+    policy->AddDir(rdonly, "/");
+    mCommonContentPolicy.reset(policy);
+    return;
+  }
+  policy->AddPath(rdonly, "/dev/urandom");
+  policy->AddPath(rdonly, "/proc/cpuinfo");
+  policy->AddPath(rdonly, "/proc/meminfo");
+  policy->AddDir(rdonly, "/lib");
+  policy->AddDir(rdonly, "/etc");
+  policy->AddDir(rdonly, "/usr/share");
+  policy->AddDir(rdonly, "/usr/local/share");
+  policy->AddDir(rdonly, "/usr/lib");
+  policy->AddDir(rdonly, "/usr/lib32");
+  policy->AddDir(rdonly, "/usr/lib64");
+  policy->AddDir(rdonly, "/usr/X11R6/lib/X11/fonts");
+  policy->AddDir(rdonly, "/usr/tmp");
+  policy->AddDir(rdonly, "/var/tmp");
+  policy->AddDir(rdonly, "/sys/devices/cpu");
+  policy->AddDir(rdonly, "/sys/devices/system/cpu");
+
+  // Configuration dirs in the homedir that we want to allow read
+  // access to.
+  mozilla::Array<const char*, 3> confDirs = {
+    ".config",
+    ".themes",
+    ".fonts",
+  };
+
+  nsCOMPtr<nsIFile> homeDir;
+  rv = GetSpecialSystemDirectory(Unix_HomeDirectory, getter_AddRefs(homeDir));
+  if (NS_SUCCEEDED(rv)) {
+    nsCOMPtr<nsIFile> confDir;
+
+    for (auto dir : confDirs) {
+      rv = homeDir->Clone(getter_AddRefs(confDir));
+      if (NS_SUCCEEDED(rv)) {
+        rv = confDir->AppendNative(nsDependentCString(dir));
+        if (NS_SUCCEEDED(rv)) {
+          nsAutoCString tmpPath;
+          rv = confDir->GetNativePath(tmpPath);
+          if (NS_SUCCEEDED(rv)) {
+            policy->AddDir(rdonly, tmpPath.get());
+          }
+        }
+      }
+    }
+
+    // ~/.local/share (for themes)
+    rv = homeDir->Clone(getter_AddRefs(confDir));
+    if (NS_SUCCEEDED(rv)) {
+      rv = confDir->AppendNative(NS_LITERAL_CSTRING(".local"));
+      if (NS_SUCCEEDED(rv)) {
+        rv = confDir->AppendNative(NS_LITERAL_CSTRING("share"));
+      }
+      if (NS_SUCCEEDED(rv)) {
+        nsAutoCString tmpPath;
+        rv = confDir->GetNativePath(tmpPath);
+        if (NS_SUCCEEDED(rv)) {
+          policy->AddDir(rdonly, tmpPath.get());
+        }
+      }
+    }
+
+    // ~/.fonts.conf (Fontconfig)
+    rv = homeDir->Clone(getter_AddRefs(confDir));
+    if (NS_SUCCEEDED(rv)) {
+      rv = confDir->AppendNative(NS_LITERAL_CSTRING(".fonts.conf"));
+      if (NS_SUCCEEDED(rv)) {
+        nsAutoCString tmpPath;
+        rv = confDir->GetNativePath(tmpPath);
+        if (NS_SUCCEEDED(rv)) {
+          policy->AddPath(rdonly, tmpPath.get());
+        }
+      }
+    }
+
+    // .pangorc
+    rv = homeDir->Clone(getter_AddRefs(confDir));
+    if (NS_SUCCEEDED(rv)) {
+      rv = confDir->AppendNative(NS_LITERAL_CSTRING(".pangorc"));
+      if (NS_SUCCEEDED(rv)) {
+        nsAutoCString tmpPath;
+        rv = confDir->GetNativePath(tmpPath);
+        if (NS_SUCCEEDED(rv)) {
+          policy->AddPath(rdonly, tmpPath.get());
+        }
+      }
+    }
+  }
+
+  // Firefox binary dir.
+  // Note that unlike the previous cases, we use NS_GetSpecialDirectory
+  // instead of GetSpecialSystemDirectory. The former requires a working XPCOM
+  // system, which may not be the case for some tests. For quering for the
+  // location of XPCOM things, we can use it anyway.
+  nsCOMPtr<nsIFile> ffDir;
+  rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(ffDir));
+  if (NS_SUCCEEDED(rv)) {
+    nsAutoCString tmpPath;
+    rv = ffDir->GetNativePath(tmpPath);
+    if (NS_SUCCEEDED(rv)) {
+      policy->AddDir(rdonly, tmpPath.get());
+    }
+  }
+
+  if (mozilla::IsDevelopmentBuild()) {
+    // If this is a developer build the resources are symlinks to outside the binary dir.
+    // Therefore in non-release builds we allow reads from the whole repository.
+    // MOZ_DEVELOPER_REPO_DIR is set by mach run.
+    const char *developer_repo_dir = PR_GetEnv("MOZ_DEVELOPER_REPO_DIR");
+    if (developer_repo_dir) {
+      policy->AddDir(rdonly, developer_repo_dir);
+    }
+  }
+
   mCommonContentPolicy.reset(policy);
 #endif
 }
 
 #ifdef MOZ_CONTENT_SANDBOX
 UniquePtr<SandboxBroker::Policy>
-SandboxBrokerPolicyFactory::GetContentPolicy(int aPid)
+SandboxBrokerPolicyFactory::GetContentPolicy(int aPid, bool aFileProcess)
 {
   // Policy entries that vary per-process (currently the only reason
   // that can happen is because they contain the pid) are added here.
 
   MOZ_ASSERT(NS_IsMainThread());
   // File broker usage is controlled through a pref.
   if (GetEffectiveContentSandboxLevel() <= 1) {
     return nullptr;
   }
 
   MOZ_ASSERT(mCommonContentPolicy);
+  UniquePtr<SandboxBroker::Policy>
+    policy(new SandboxBroker::Policy(*mCommonContentPolicy));
+
 #if defined(MOZ_WIDGET_GONK)
   // Allow overriding "unsupported"ness with a pref, for testing.
   if (!IsSystemSupported()) {
     return nullptr;
   }
-  UniquePtr<SandboxBroker::Policy>
-    policy(new SandboxBroker::Policy(*mCommonContentPolicy));
 
   // Bug 1029337: where the profiler writes the data.
   nsPrintfCString profilerLogPath("/data/local/tmp/profile_%d_%d.txt",
                                   GeckoProcessType_Content, aPid);
   policy->AddPath(wrlog, profilerLogPath.get());
+#endif
 
   // Bug 1198550: the profiler's replacement for dl_iterate_phdr
   policy->AddPath(rdonly, nsPrintfCString("/proc/%d/maps", aPid).get());
 
   // Bug 1198552: memory reporting.
   policy->AddPath(rdonly, nsPrintfCString("/proc/%d/statm", aPid).get());
   policy->AddPath(rdonly, nsPrintfCString("/proc/%d/smaps", aPid).get());
 
-  return policy;
-#else
-  UniquePtr<SandboxBroker::Policy>
-    policy(new SandboxBroker::Policy(*mCommonContentPolicy));
-
+#ifndef MOZ_WIDGET_GONK
   // Now read any extra paths, this requires accessing user preferences
   // so we can only do it now. Our constructor is initialized before
   // user preferences are read in.
-  nsAdoptingCString extraPathString =
+  nsAdoptingCString extraReadPathString =
+    Preferences::GetCString("security.sandbox.content.read_path_whitelist");
+  AddDynamicPathList(policy.get(), extraReadPathString, rdonly);
+  nsAdoptingCString extraWritePathString =
     Preferences::GetCString("security.sandbox.content.write_path_whitelist");
-  if (extraPathString) {
-    for (const nsACString& path : extraPathString.Split(',')) {
-      nsCString trimPath(path);
-      trimPath.Trim(" ", true, true);
-      policy->AddDynamic(rdwr, trimPath.get());
-    }
+  AddDynamicPathList(policy.get(), extraWritePathString, rdwr);
+
+  // file:// processes get global read permissions
+  if (aFileProcess) {
+    policy->AddDir(rdonly, "/");
   }
+#endif
 
   // Return the common policy.
   return policy;
-#endif
+
+}
+
+void
+SandboxBrokerPolicyFactory::AddDynamicPathList(SandboxBroker::Policy *policy,
+                                               nsAdoptingCString& pathList,
+                                               int perms) {
+ if (pathList) {
+    for (const nsACString& path : pathList.Split(',')) {
+      nsCString trimPath(path);
+      trimPath.Trim(" ", true, true);
+      policy->AddDynamic(perms, trimPath.get());
+    }
+  }
 }
 
 #endif // MOZ_CONTENT_SANDBOX
 } // namespace mozilla
--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h
@@ -11,22 +11,25 @@
 
 namespace mozilla {
 
 class SandboxBrokerPolicyFactory {
 public:
   SandboxBrokerPolicyFactory();
 
 #ifdef MOZ_CONTENT_SANDBOX
-  UniquePtr<SandboxBroker::Policy> GetContentPolicy(int aPid);
+  UniquePtr<SandboxBroker::Policy> GetContentPolicy(int aPid, bool aFileProcess);
 #endif
 
 private:
   UniquePtr<const SandboxBroker::Policy> mCommonContentPolicy;
   // B2G devices tend to have hardware-specific paths used by device
   // drivers, so rollout of filesystem isolation will need per-device
   // testing.  This predicate allows that to happen gradually.
   static bool IsSystemSupported();
+  static void AddDynamicPathList(SandboxBroker::Policy *policy,
+                                 nsAdoptingCString& paths,
+                                 int perms);
 };
 
 } // namespace mozilla
 
 #endif // mozilla_SandboxBrokerPolicyFactory_h
--- a/security/sandbox/linux/reporter/SandboxReporterCommon.h
+++ b/security/sandbox/linux/reporter/SandboxReporterCommon.h
@@ -26,16 +26,17 @@ static const int kSandboxReporterFileDes
 // seccomp-bpf policy.
 struct SandboxReport {
   // In the future this may include finer distinctions than
   // GeckoProcessType -- e.g., whether a content process can load
   // file:/// URLs, or if it's reserved for content with certain
   // user-granted permissions.
   enum class ProcType : uint8_t {
     CONTENT,
+    FILE,
     MEDIA_PLUGIN,
   };
 
   // The syscall number and arguments are usually `unsigned long`, but
   // that causes ambiguous overload errors with nsACString::AppendInt.
   using ULong = UnsignedStdintTypeForSize<sizeof(unsigned long)>::Type;
 
   // This time uses CLOCK_MONOTONIC_COARSE.  Displaying or reporting
--- a/security/sandbox/test/browser_content_sandbox_fs.js
+++ b/security/sandbox/test/browser_content_sandbox_fs.js
@@ -243,24 +243,29 @@ async function testFileAccess() {
 
   // 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 = [];
 
+  // 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();
-  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
-  });
+  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
+    });
+  }
   if (fileContentProcessEnabled) {
     tests.push({
       desc:     "profile dir",
       ok:       true,
       browser:  fileBrowser,
       file:     profileDir,
       minLevel: 0,
     });
@@ -303,29 +308,25 @@ async function testFileAccess() {
         ok:       shouldBeReadable,
         browser:  webBrowser,
         file:     homeTempDir,
         minLevel,
       });
     }
   }
 
-  // Should we enable this /var test on Linux? Once we are running
-  // with read access restrictions on Linux, this todo will fail and
-  // should then be removed.
-  if (isLinux()) {
-    todo(level >= minHomeReadSandboxLevel(), "enable /var test on Linux?");
-  }
-  if (isMac()) {
+  if (isMac() || isLinux()) {
     let varDir = GetDir("/var");
 
-    // Mac sandbox rules use /private/var because /var is a symlink
-    // to /private/var on OS X. Make sure that hasn't changed.
-    varDir.normalize();
-    Assert.ok(varDir.path === "/private/var", "/var resolves to /private/var");
+    if (isMac()) {
+      // Mac sandbox rules use /private/var because /var is a symlink
+      // to /private/var on OS X. Make sure that hasn't changed.
+      varDir.normalize();
+      Assert.ok(varDir.path === "/private/var", "/var resolves to /private/var");
+    }
 
     tests.push({
       desc:     "/var",
       ok:       false,
       browser:  webBrowser,
       file:     varDir,
       minLevel: minHomeReadSandboxLevel(),
     });