Bug 1479960 - Add freezing of IPC shared memory. r=froydnj,kmag
authorJed Davis <jld@mozilla.com>
Wed, 14 Aug 2019 22:48:34 +0000
changeset 488142 225411558a4eaa436ef6036eee1513427a156413
parent 488141 ddfa5ff8106195214d932cedc963fe2f9191c5d0
child 488143 0f466f2a18c0fcf9e552ea07158e847f88ca9950
push id113900
push usercbrindusan@mozilla.com
push dateThu, 15 Aug 2019 09:53:50 +0000
treeherdermozilla-inbound@0db07ff50ab5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj, kmag
bugs1479960
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1479960 - Add freezing of IPC shared memory. r=froydnj,kmag This allows writing to shared memory and then making it read-only before sharing it to other processes, such that a malicious sandboxed process cannot regain write access. This is currently available only in the low-level base::SharedMemory interface. The freeze operation exposes the common subset of read-only shared memory that we can implement on all supported OSes: with some APIs (POSIX shm_open) we can't revoke writeability from existing capabilies, while for others (Android ashmem) we *must* revoke it. Thus, we require that the writeable capability not have been duplicated or shared to another process, and consume it as part of freezing. Also, because in some backends need special handling at creation time, freezeability must be explicitly requested. In particular, this doesn't allow giving an untrusted process read-only access to memory that the original process can write. Note that on MacOS before 10.12 this will use temporary files in order to avoid an OS security bug that allows regaining write access; those OS versions are no longer supported by Apple (but are supported by Firefox). Depends on D26742 Differential Revision: https://phabricator.services.mozilla.com/D26743
ipc/chromium/src/base/shared_memory.h
ipc/chromium/src/base/shared_memory_posix.cc
ipc/chromium/src/base/shared_memory_win.cc
--- a/ipc/chromium/src/base/shared_memory.h
+++ b/ipc/chromium/src/base/shared_memory.h
@@ -13,16 +13,17 @@
 #  include <sys/types.h>
 #  include <semaphore.h>
 #  include "base/file_descriptor_posix.h"
 #endif
 #include <string>
 
 #include "base/basictypes.h"
 #include "base/process.h"
+#include "mozilla/Attributes.h"
 #include "mozilla/UniquePtrExtensions.h"
 
 namespace base {
 
 // SharedMemoryHandle is a platform specific type which represents
 // the underlying OS handle to a shared memory segment.
 #if defined(OS_WIN)
 typedef HANDLE SharedMemoryHandle;
@@ -61,17 +62,23 @@ class SharedMemory {
   // IsHandleValid applied to this object's handle.
   bool IsValid() const;
 
   // Return invalid handle (see comment above for exact definition).
   static SharedMemoryHandle NULLHandle();
 
   // Creates a shared memory segment.
   // Returns true on success, false on failure.
-  bool Create(size_t size);
+  bool Create(size_t size) { return CreateInternal(size, false); }
+
+  // Creates a shared memory segment that supports the Freeze()
+  // method; see below.  (Warning: creating freezeable shared memory
+  // within a sandboxed process isn't possible on some platforms.)
+  // Returns true on success, false on failure.
+  bool CreateFreezeable(size_t size) { return CreateInternal(size, true); }
 
   // Maps the shared memory into the caller's address space.
   // Returns true on success, false otherwise.  The memory address
   // is accessed via the memory() accessor.
   //
   // If the specified fixed address is not null, it is the address that the
   // shared memory must be mapped at.  Returns false if the shared memory
   // could not be mapped at that address.
@@ -102,16 +109,26 @@ class SharedMemory {
   // Used only in gfx/ipc/SharedDIBWin.cpp; should be removable once
   // NPAPI goes away.
   HANDLE GetHandle() {
     freezeable_ = false;
     return mapped_file_;
   }
 #endif
 
+  // Make the shared memory object read-only, such that it cannot be
+  // written even if it's sent to an untrusted process.  If it was
+  // mapped in this process, it will be unmapped.  The object must
+  // have been created with CreateFreezeable(), and must not have
+  // already been shared to another process.
+  //
+  // (See bug 1479960 comment #0 for OS-specific implementation
+  // details.)
+  MOZ_MUST_USE bool Freeze();
+
   // Closes the open shared memory segment.
   // It is safe to call Close repeatedly.
   void Close(bool unmap_view = true);
 
   // Returns a page-aligned address at which the given number of bytes could
   // probably be mapped.  Returns NULL on error or if there is insufficient
   // contiguous address space to map the required number of pages.
   //
@@ -148,27 +165,31 @@ class SharedMemory {
   // (This is public so that the Linux sandboxing code can use it.)
   static bool AppendPosixShmPrefix(std::string* str, pid_t pid);
 #endif
 
  private:
   bool ShareToProcessCommon(ProcessId target_pid,
                             SharedMemoryHandle* new_handle, bool close_self);
 
+  bool CreateInternal(size_t size, bool freezeable);
+
 #if defined(OS_WIN)
   // If true indicates this came from an external source so needs extra checks
   // before being mapped.
   bool external_section_;
   HANDLE mapped_file_;
 #elif defined(OS_POSIX)
   int mapped_file_;
+  int frozen_file_;
   size_t mapped_size_;
 #endif
   void* memory_;
   bool read_only_;
+  bool freezeable_;
   size_t max_size_;
 
   DISALLOW_EVIL_CONSTRUCTORS(SharedMemory);
 };
 
 }  // namespace base
 
 #endif  // BASE_SHARED_MEMORY_H_
--- a/ipc/chromium/src/base/shared_memory_posix.cc
+++ b/ipc/chromium/src/base/shared_memory_posix.cc
@@ -22,57 +22,147 @@
 #include "mozilla/Atomics.h"
 #include "mozilla/UniquePtrExtensions.h"
 #include "prenv.h"
 
 namespace base {
 
 SharedMemory::SharedMemory()
     : mapped_file_(-1),
+      frozen_file_(-1),
       mapped_size_(0),
       memory_(nullptr),
       read_only_(false),
+      freezeable_(false),
       max_size_(0) {}
 
 SharedMemory::SharedMemory(SharedMemory&& other) {
   if (this == &other) {
     return;
   }
 
   mapped_file_ = other.mapped_file_;
   mapped_size_ = other.mapped_size_;
+  frozen_file_ = other.frozen_file_;
   memory_ = other.memory_;
   read_only_ = other.read_only_;
+  freezeable_ = other.freezeable_;
   max_size_ = other.max_size_;
 
   other.mapped_file_ = -1;
   other.mapped_size_ = 0;
+  other.frozen_file_ = -1;
   other.memory_ = nullptr;
 }
 
 SharedMemory::~SharedMemory() { Close(); }
 
 bool SharedMemory::SetHandle(SharedMemoryHandle handle, bool read_only) {
   DCHECK(mapped_file_ == -1);
+  DCHECK(frozen_file_ == -1);
 
+  freezeable_ = false;
   mapped_file_ = handle.fd;
   read_only_ = read_only;
   return true;
 }
 
 // static
 bool SharedMemory::IsHandleValid(const SharedMemoryHandle& handle) {
   return handle.fd >= 0;
 }
 
 bool SharedMemory::IsValid() const { return mapped_file_ >= 0; }
 
 // static
 SharedMemoryHandle SharedMemory::NULLHandle() { return SharedMemoryHandle(); }
 
+// Workaround for CVE-2018-4435 (https://crbug.com/project-zero/1671);
+// can be removed when minimum OS version is at least 10.12.
+#ifdef OS_MACOSX
+static const char* GetTmpDir() {
+  static const char* const kTmpDir = [] {
+    const char* tmpdir = PR_GetEnv("TMPDIR");
+    if (tmpdir) {
+      return tmpdir;
+    }
+    return "/tmp";
+  }();
+  return kTmpDir;
+}
+
+static int FakeShmOpen(const char* name, int oflag, int mode) {
+  CHECK(name[0] == '/');
+  std::string path(GetTmpDir());
+  path += name;
+  return open(path.c_str(), oflag | O_CLOEXEC | O_NOCTTY, mode);
+}
+
+static int FakeShmUnlink(const char* name) {
+  CHECK(name[0] == '/');
+  std::string path(GetTmpDir());
+  path += name;
+  return unlink(path.c_str());
+}
+
+static bool IsShmOpenSecure() {
+  static const bool kIsSecure = [] {
+    mozilla::UniqueFileHandle rwfd, rofd;
+    std::string name;
+    CHECK(SharedMemory::AppendPosixShmPrefix(&name, getpid()));
+    name += "sectest";
+    // The prefix includes the pid and this will be called at most
+    // once per process, so no need for a counter.
+    rwfd.reset(
+        HANDLE_EINTR(shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600)));
+    // An adversary could steal the name.  Handle this semi-gracefully.
+    DCHECK(rwfd);
+    if (!rwfd) {
+      return false;
+    }
+    rofd.reset(shm_open(name.c_str(), O_RDONLY, 0400));
+    CHECK(rofd);
+    CHECK(shm_unlink(name.c_str()) == 0);
+    CHECK(ftruncate(rwfd.get(), 1) == 0);
+    rwfd = nullptr;
+    void* map = mmap(nullptr, 1, PROT_READ, MAP_SHARED, rofd.get(), 0);
+    CHECK(map != MAP_FAILED);
+    bool ok = mprotect(map, 1, PROT_READ | PROT_WRITE) != 0;
+    munmap(map, 1);
+    return ok;
+  }();
+  return kIsSecure;
+}
+
+static int SafeShmOpen(bool freezeable, const char* name, int oflag, int mode) {
+  if (!freezeable || IsShmOpenSecure()) {
+    return shm_open(name, oflag, mode);
+  } else {
+    return FakeShmOpen(name, oflag, mode);
+  }
+}
+
+static int SafeShmUnlink(bool freezeable, const char* name) {
+  if (!freezeable || IsShmOpenSecure()) {
+    return shm_unlink(name);
+  } else {
+    return FakeShmUnlink(name);
+  }
+}
+
+#elif !defined(ANDROID)
+static int SafeShmOpen(bool freezeable, const char* name, int oflag, int mode) {
+  return shm_open(name, oflag, mode);
+}
+
+static int SafeShmUnlink(bool freezeable, const char* name) {
+  return shm_unlink(name);
+}
+#endif
+
 // static
 bool SharedMemory::AppendPosixShmPrefix(std::string* str, pid_t pid) {
 #if defined(ANDROID)
   return false;
 #else
   *str += '/';
 #  ifdef OS_LINUX
   // The Snap package environment doesn't provide a private /dev/shm
@@ -94,23 +184,25 @@ bool SharedMemory::AppendPosixShmPrefix(
 #  endif  // OS_LINUX
   // Hopefully the "implementation defined" name length limit is long
   // enough for this.
   StringAppendF(str, "org.mozilla.ipc.%d.", static_cast<int>(pid));
   return true;
 #endif    // !ANDROID
 }
 
-bool SharedMemory::Create(size_t size) {
+bool SharedMemory::CreateInternal(size_t size, bool freezeable) {
   read_only_ = false;
 
   DCHECK(size > 0);
   DCHECK(mapped_file_ == -1);
+  DCHECK(frozen_file_ == -1);
 
   mozilla::UniqueFileHandle fd;
+  mozilla::UniqueFileHandle frozen_fd;
   bool needs_truncate = true;
 
 #ifdef ANDROID
   // Android has its own shared memory facility:
   fd.reset(open("/" ASHMEM_NAME_DEF, O_RDWR, 0600));
   if (!fd) {
     CHROMIUM_LOG(WARNING) << "failed to open shm: " << strerror(errno);
     return false;
@@ -125,20 +217,31 @@ bool SharedMemory::Create(size_t size) {
   do {
     // The names don't need to be unique, but it saves time if they
     // usually are.
     static mozilla::Atomic<size_t> sNameCounter;
     std::string name;
     CHECK(AppendPosixShmPrefix(&name, getpid()));
     StringAppendF(&name, "%zu", sNameCounter++);
     // O_EXCL means the names being predictable shouldn't be a problem.
-    fd.reset(
-        HANDLE_EINTR(shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600)));
+    fd.reset(HANDLE_EINTR(SafeShmOpen(freezeable, name.c_str(),
+                                      O_RDWR | O_CREAT | O_EXCL, 0600)));
     if (fd) {
-      if (shm_unlink(name.c_str()) != 0) {
+      if (freezeable) {
+        frozen_fd.reset(HANDLE_EINTR(
+            SafeShmOpen(freezeable, name.c_str(), O_RDONLY, 0400)));
+        if (!frozen_fd) {
+          int open_err = errno;
+          SafeShmUnlink(freezeable, name.c_str());
+          DLOG(FATAL) << "failed to re-open freezeable shm: "
+                      << strerror(open_err);
+          return false;
+        }
+      }
+      if (SafeShmUnlink(freezeable, name.c_str()) != 0) {
         // This shouldn't happen, but if it does: assume the file is
         // in fact leaked, and bail out now while it's still 0-length.
         DLOG(FATAL) << "failed to unlink shm: " << strerror(errno);
         return false;
       }
     }
   } while (!fd && errno == EEXIST);
 #endif
@@ -151,17 +254,42 @@ bool SharedMemory::Create(size_t size) {
   if (needs_truncate) {
     if (HANDLE_EINTR(ftruncate(fd.get(), static_cast<off_t>(size))) != 0) {
       CHROMIUM_LOG(WARNING) << "failed to set shm size: " << strerror(errno);
       return false;
     }
   }
 
   mapped_file_ = fd.release();
+  frozen_file_ = frozen_fd.release();
   max_size_ = size;
+  freezeable_ = freezeable;
+  return true;
+}
+
+bool SharedMemory::Freeze() {
+  DCHECK(!read_only_);
+  CHECK(freezeable_);
+  Unmap();
+
+#ifdef ANDROID
+  if (ioctl(mapped_file_, ASHMEM_SET_PROT_MASK, PROT_READ) != 0) {
+    CHROMIUM_LOG(WARNING) << "failed to freeze shm: " << strerror(errno);
+    return false;
+  }
+#else
+  DCHECK(frozen_file_ >= 0);
+  DCHECK(mapped_file_ >= 0);
+  close(mapped_file_);
+  mapped_file_ = frozen_file_;
+  frozen_file_ = -1;
+#endif
+
+  read_only_ = true;
+  freezeable_ = false;
   return true;
 }
 
 bool SharedMemory::Map(size_t bytes, void* fixed_address) {
   if (mapped_file_ == -1) return false;
 
   // Don't use MAP_FIXED when a fixed_address was specified, since that can
   // replace pages that are alread mapped at that address.
@@ -201,16 +329,17 @@ void* SharedMemory::FindFreeAddressSpace
       mmap(NULL, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
   munmap(memory, size);
   return memory != MAP_FAILED ? memory : NULL;
 }
 
 bool SharedMemory::ShareToProcessCommon(ProcessId processId,
                                         SharedMemoryHandle* new_handle,
                                         bool close_self) {
+  freezeable_ = false;
   const int new_fd = dup(mapped_file_);
   DCHECK(new_fd >= -1);
   new_handle->fd = new_fd;
   new_handle->auto_close = true;
 
   if (close_self) Close();
 
   return true;
@@ -220,18 +349,25 @@ void SharedMemory::Close(bool unmap_view
   if (unmap_view) {
     Unmap();
   }
 
   if (mapped_file_ >= 0) {
     close(mapped_file_);
     mapped_file_ = -1;
   }
+  if (frozen_file_ >= 0) {
+    CHROMIUM_LOG(WARNING) << "freezeable shared memory was never frozen";
+    close(frozen_file_);
+    frozen_file_ = -1;
+  }
 }
 
 mozilla::UniqueFileHandle SharedMemory::TakeHandle() {
   mozilla::UniqueFileHandle fh(mapped_file_);
   mapped_file_ = -1;
-  Unmap();
+  // Now that the main fd is removed, reset everything else: close the
+  // frozen fd if present and unmap the memory if mapped.
+  Close();
   return fh;
 }
 
 }  // namespace base
--- a/ipc/chromium/src/base/shared_memory_win.cc
+++ b/ipc/chromium/src/base/shared_memory_win.cc
@@ -5,16 +5,20 @@
 // found in the LICENSE file.
 
 #include "base/shared_memory.h"
 
 #include "base/logging.h"
 #include "base/win_util.h"
 #include "base/string_util.h"
 #include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/RandomNum.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsDebug.h"
+#include "nsString.h"
 
 namespace {
 // NtQuerySection is an internal (but believed to be stable) API and the
 // structures it uses are defined in nt_internals.h.
 // So we have to define them ourselves.
 typedef enum _SECTION_INFORMATION_CLASS {
   SectionBasicInformation,
 } SECTION_INFORMATION_CLASS;
@@ -54,65 +58,114 @@ bool IsSectionSafeToMap(HANDLE handle) {
 
 namespace base {
 
 SharedMemory::SharedMemory()
     : external_section_(false),
       mapped_file_(NULL),
       memory_(NULL),
       read_only_(false),
+      freezeable_(false),
       max_size_(0) {}
 
 SharedMemory::SharedMemory(SharedMemory&& other) {
   if (this == &other) {
     return;
   }
 
   mapped_file_ = other.mapped_file_;
   memory_ = other.memory_;
   read_only_ = other.read_only_;
   max_size_ = other.max_size_;
+  freezeable_ = other.freezeable_;
   external_section_ = other.external_section_;
 
   other.mapped_file_ = nullptr;
   other.memory_ = nullptr;
 }
 
 SharedMemory::~SharedMemory() {
   external_section_ = true;
   Close();
 }
 
 bool SharedMemory::SetHandle(SharedMemoryHandle handle, bool read_only) {
   DCHECK(mapped_file_ == NULL);
 
   external_section_ = true;
+  freezeable_ = false;  // just in case
   mapped_file_ = handle;
   read_only_ = read_only;
   return true;
 }
 
 // static
 bool SharedMemory::IsHandleValid(const SharedMemoryHandle& handle) {
   return handle != NULL;
 }
 
 bool SharedMemory::IsValid() const { return mapped_file_ != NULL; }
 
 // static
 SharedMemoryHandle SharedMemory::NULLHandle() { return NULL; }
 
-bool SharedMemory::Create(size_t size) {
+bool SharedMemory::CreateInternal(size_t size, bool freezeable) {
   DCHECK(mapped_file_ == NULL);
   read_only_ = false;
-  mapped_file_ = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
-                                   0, static_cast<DWORD>(size), NULL);
+
+  SECURITY_ATTRIBUTES sa;
+  SECURITY_DESCRIPTOR sd;
+  ACL dacl;
+
+  sa.nLength = sizeof(sa);
+  sa.lpSecurityDescriptor = &sd;
+  sa.bInheritHandle = FALSE;
+
+  if (NS_WARN_IF(!InitializeAcl(&dacl, sizeof(dacl), ACL_REVISION)) ||
+      NS_WARN_IF(
+          !InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) ||
+      NS_WARN_IF(!SetSecurityDescriptorDacl(&sd, TRUE, &dacl, FALSE))) {
+    return false;
+  }
+
+  nsAutoStringN<sizeof("MozSharedMem_") + 16 * 4> name;
+  if (!mozilla::IsWin8Point1OrLater()) {
+    name.AssignLiteral("MozSharedMem_");
+    for (size_t i = 0; i < 4; ++i) {
+      mozilla::Maybe<uint64_t> randomNum = mozilla::RandomUint64();
+      if (NS_WARN_IF(randomNum.isNothing())) {
+        return false;
+      }
+      name.AppendPrintf("%016llx", *randomNum);
+    }
+  }
+
+  mapped_file_ = CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE,
+                                   0, static_cast<DWORD>(size),
+                                   name.IsEmpty() ? nullptr : name.get());
   if (!mapped_file_) return false;
 
   max_size_ = size;
+  freezeable_ = freezeable;
+  return true;
+}
+
+bool SharedMemory::Freeze() {
+  DCHECK(!read_only_);
+  CHECK(freezeable_);
+  Unmap();
+
+  if (!::DuplicateHandle(GetCurrentProcess(), mapped_file_, GetCurrentProcess(),
+                         &mapped_file_, GENERIC_READ | FILE_MAP_READ, false,
+                         DUPLICATE_CLOSE_SOURCE)) {
+    return false;
+  }
+
+  read_only_ = true;
+  freezeable_ = false;
   return true;
 }
 
 bool SharedMemory::Map(size_t bytes, void* fixed_address) {
   if (mapped_file_ == NULL) return false;
 
   if (external_section_ && !IsSectionSafeToMap(mapped_file_)) {
     return false;
@@ -143,16 +196,17 @@ void* SharedMemory::FindFreeAddressSpace
     VirtualFree(memory, 0, MEM_RELEASE);
   }
   return memory;
 }
 
 bool SharedMemory::ShareToProcessCommon(ProcessId processId,
                                         SharedMemoryHandle* new_handle,
                                         bool close_self) {
+  freezeable_ = false;
   *new_handle = 0;
   DWORD access = FILE_MAP_READ | SECTION_QUERY;
   DWORD options = 0;
   HANDLE mapped_file = mapped_file_;
   HANDLE result;
   if (!read_only_) access |= FILE_MAP_WRITE;
   if (close_self) {
     // DUPLICATE_CLOSE_SOURCE causes DuplicateHandle to close mapped_file.