Bug 1456911 - Rewrite the fd shuffling to be simpler & handle identity mappings correctly. r?froydnj draft
authorJed Davis <jld@mozilla.com>
Wed, 25 Apr 2018 17:44:08 -0600
changeset 805056 3aa5ae4f2ee766030904fc27799e5ee3fe928cbe
parent 805055 c9864ea2cde1fb930aa26890177b677139302b5c
push id112540
push userbmo:jld@mozilla.com
push dateThu, 07 Jun 2018 00:46:01 +0000
reviewersfroydnj
bugs1456911
milestone62.0a1
Bug 1456911 - Rewrite the fd shuffling to be simpler & handle identity mappings correctly. r?froydnj This replaces some old Chromium code that tries to minimally disentangle an arbitrary file descriptor mapping with simpler algorithm, for several reasons: 1. Do something appropriate when a file descriptor is mapped to the same fd number in the child; currently they're ignored, which means they'll be closed if they were close-on-exec. This implementation duplicates the fd twice in that case, which seems to be uncommon in practice; this isn't maximally efficient but avoids special-case code. 2. Make this more generally applicable; the previous design is specialized for arbitrary code running between fork and exec, but we also want to use this on OS X with posix_spawn, which exposes a very limited set of operations. 3. Avoid the use of C++ standard library iterators in async signal safe code; the Chromium developers mention that this is a potential problem in some debugging implementations that take locks. 4. In general the algorithm is simpler and should be more "obviously correct"; more concretely, it should get complete coverage just by being run normally in a debug build. As a convenient side benefit, CloseSuperfluousFds now takes an arbitrary predicate for which fds to leave open, which means it can be used in other code that needs it without creating a fake fd mapping. MozReview-Commit-ID: EoiRttrbrKL
ipc/chromium/moz.build
ipc/chromium/src/base/file_descriptor_shuffle.cc
ipc/chromium/src/base/file_descriptor_shuffle.h
ipc/chromium/src/base/process_util.h
ipc/chromium/src/base/process_util_linux.cc
ipc/chromium/src/base/process_util_posix.cc
ipc/glue/FileDescriptorShuffle.cpp
ipc/glue/FileDescriptorShuffle.h
ipc/glue/moz.build
security/sandbox/linux/launch/SandboxLaunch.cpp
--- a/ipc/chromium/moz.build
+++ b/ipc/chromium/moz.build
@@ -53,17 +53,16 @@ if os_win:
     ]
 
 elif not CONFIG['MOZ_SYSTEM_LIBEVENT']:
     DIRS += ['src/third_party']
 
 if os_posix:
     UNIFIED_SOURCES += [
         'src/base/condition_variable_posix.cc',
-        'src/base/file_descriptor_shuffle.cc',
         'src/base/file_util_posix.cc',
         'src/base/lock_impl_posix.cc',
         'src/base/message_pump_libevent.cc',
         'src/base/platform_thread_posix.cc',
         'src/base/process_util_posix.cc',
         'src/base/rand_util_posix.cc',
         'src/base/shared_memory_posix.cc',
         'src/base/string16.cc',
deleted file mode 100644
--- a/ipc/chromium/src/base/file_descriptor_shuffle.cc
+++ /dev/null
@@ -1,96 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-// Copyright (c) 2009 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/file_descriptor_shuffle.h"
-
-#include <errno.h>
-#include <unistd.h>
-
-#include "base/eintr_wrapper.h"
-#include "base/logging.h"
-
-namespace base {
-
-bool PerformInjectiveMultimapDestructive(
-    InjectiveMultimap* m, InjectionDelegate* delegate) {
-  static const size_t kMaxExtraFDs = 16;
-  int extra_fds[kMaxExtraFDs];
-  unsigned next_extra_fd = 0;
-
-  // DANGER: this function may not allocate.
-
-  for (InjectiveMultimap::iterator i = m->begin(); i != m->end(); ++i) {
-    int temp_fd = -1;
-
-    // We DCHECK the injectiveness of the mapping.
-    for (InjectiveMultimap::iterator j = i + 1; j != m->end(); ++j) {
-      DCHECK(i->dest != j->dest) << "Both fd " << i->source
-          << " and " << j->source << " map to " << i->dest;
-    }
-
-    const bool is_identity = i->source == i->dest;
-
-    for (InjectiveMultimap::iterator j = i + 1; j != m->end(); ++j) {
-      if (!is_identity && i->dest == j->source) {
-        if (temp_fd == -1) {
-          if (!delegate->Duplicate(&temp_fd, i->dest))
-            return false;
-          if (next_extra_fd < kMaxExtraFDs) {
-            extra_fds[next_extra_fd++] = temp_fd;
-          } else {
-              DLOG(ERROR) << "PerformInjectiveMultimapDestructive overflowed "
-                          << "extra_fds. Leaking file descriptors!";
-          }
-        }
-
-        j->source = temp_fd;
-        j->close = false;
-      }
-
-      if (i->close && i->source == j->dest)
-        i->close = false;
-
-      if (i->close && i->source == j->source) {
-        i->close = false;
-        j->close = true;
-      }
-    }
-
-    if (!is_identity) {
-      if (!delegate->Move(i->source, i->dest))
-        return false;
-    }
-
-    if (!is_identity && i->close)
-      delegate->Close(i->source);
-  }
-
-  for (unsigned i = 0; i < next_extra_fd; i++)
-    delegate->Close(extra_fds[i]);
-
-  return true;
-}
-
-bool PerformInjectiveMultimap(const InjectiveMultimap& m_in,
-                              InjectionDelegate* delegate) {
-    InjectiveMultimap m(m_in);
-    return PerformInjectiveMultimapDestructive(&m, delegate);
-}
-
-bool FileDescriptorTableInjection::Duplicate(int* result, int fd) {
-  *result = HANDLE_EINTR(dup(fd));
-  return *result >= 0;
-}
-
-bool FileDescriptorTableInjection::Move(int src, int dest) {
-  return HANDLE_EINTR(dup2(src, dest)) != -1;
-}
-
-void FileDescriptorTableInjection::Close(int fd) {
-  IGNORE_EINTR(close(fd));
-}
-
-}  // namespace base
deleted file mode 100644
--- a/ipc/chromium/src/base/file_descriptor_shuffle.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-// Copyright (c) 2009 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef BASE_FILE_DESCRIPTOR_SHUFFLE_H_
-#define BASE_FILE_DESCRIPTOR_SHUFFLE_H_
-
-#include "mozilla/Attributes.h"
-
-// This code exists to perform the shuffling of file descriptors which is
-// commonly needed when forking subprocesses. The naive approve is very simple,
-// just call dup2 to setup the desired descriptors, but wrong. It's tough to
-// handle the edge cases (like mapping 0 -> 1, 1 -> 0) correctly.
-//
-// In order to unittest this code, it's broken into the abstract action (an
-// injective multimap) and the concrete code for dealing with file descriptors.
-// Users should use the code like this:
-//   base::InjectiveMultimap file_descriptor_map;
-//   file_descriptor_map.push_back(base::InjectionArc(devnull, 0, true));
-//   file_descriptor_map.push_back(base::InjectionArc(devnull, 2, true));
-//   file_descriptor_map.push_back(base::InjectionArc(pipe[1], 1, true));
-//   base::ShuffleFileDescriptors(file_descriptor_map);
-//
-// and trust the the Right Thing will get done.
-
-#include <vector>
-
-namespace base {
-
-// A Delegate which performs the actions required to perform an injective
-// multimapping in place.
-class InjectionDelegate {
- public:
-  // Duplicate |fd|, an element of the domain, and write a fresh element of the
-  // domain into |result|. Returns true iff successful.
-  virtual bool Duplicate(int* result, int fd) = 0;
-  // Destructively move |src| to |dest|, overwriting |dest|. Returns true iff
-  // successful.
-  virtual bool Move(int src, int dest) = 0;
-  // Delete an element of the domain.
-  virtual void Close(int fd) = 0;
-};
-
-// An implementation of the InjectionDelegate interface using the file
-// descriptor table of the current process as the domain.
-class FileDescriptorTableInjection : public InjectionDelegate {
-  virtual bool Duplicate(int* result, int fd) override;
-  virtual bool Move(int src, int dest) override;
-  virtual void Close(int fd) override;
-};
-
-// A single arc of the directed graph which describes an injective multimapping.
-struct InjectionArc {
-  InjectionArc(int in_source, int in_dest, bool in_close)
-      : source(in_source),
-        dest(in_dest),
-        close(in_close) {
-  }
-
-  int source;
-  int dest;
-  bool close;  // if true, delete the source element after performing the
-               // mapping.
-};
-
-typedef std::vector<InjectionArc> InjectiveMultimap;
-
-bool PerformInjectiveMultimap(const InjectiveMultimap& map,
-                              InjectionDelegate* delegate);
-bool PerformInjectiveMultimapDestructive(InjectiveMultimap* map,
-                                         InjectionDelegate* delegate);
-
-// This function will not call malloc but will mutate |map|
-static inline bool ShuffleFileDescriptors(InjectiveMultimap *map) {
-  FileDescriptorTableInjection delegate;
-  return PerformInjectiveMultimapDestructive(map, &delegate);
-}
-
-}  // namespace base
-
-#endif  // !BASE_FILE_DESCRIPTOR_SHUFFLE_H_
--- a/ipc/chromium/src/base/process_util.h
+++ b/ipc/chromium/src/base/process_util.h
@@ -22,32 +22,29 @@
 #elif defined(OS_LINUX) || defined(__GLIBC__)
 #include <dirent.h>
 #include <limits.h>
 #include <sys/types.h>
 #elif defined(OS_MACOSX)
 #include <mach/mach.h>
 #endif
 
+#include <functional>
 #include <map>
 #include <string>
 #include <vector>
 #include <stdio.h>
 #include <stdlib.h>
 #ifndef OS_WIN
 #include <unistd.h>
 #endif
 
 #include "base/command_line.h"
 #include "base/process.h"
 
-#if defined(OS_POSIX)
-#include "base/file_descriptor_shuffle.h"
-#endif
-
 #include "mozilla/UniquePtr.h"
 #include "mozilla/ipc/EnvironmentMap.h"
 
 #if defined(OS_MACOSX)
 struct kinfo_proc;
 #endif
 
 namespace base {
@@ -87,20 +84,21 @@ ProcessId GetProcId(ProcessHandle proces
 
 #if defined(OS_POSIX)
 // Sets all file descriptors to close on exec except for stdin, stdout
 // and stderr.
 // TODO(agl): remove this function
 // WARNING: do not use. It's inherently race-prone in the face of
 // multi-threading.
 void SetAllFDsToCloseOnExec();
-// Close all file descriptors, expect those which are a destination in the
-// given multimap. Only call this function in a child process where you know
-// that there aren't any other threads.
-void CloseSuperfluousFds(const base::InjectiveMultimap& saved_map);
+// Close all file descriptors, except those for which the given
+// function returns true (and std{in,out,err}).  Only call this
+// function in a child process where you know that there aren't any
+// other threads.
+void CloseSuperfluousFds(std::function<bool(int)>&& should_preserve);
 
 typedef std::vector<std::pair<int, int> > file_handle_mapping_vector;
 typedef std::map<std::string, std::string> environment_map;
 #endif
 
 struct LaunchOptions {
   // If true, wait for the process to terminate.  Otherwise, return
   // immediately.
--- a/ipc/chromium/src/base/process_util_linux.cc
+++ b/ipc/chromium/src/base/process_util_linux.cc
@@ -8,54 +8,55 @@
 
 #include <string>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
 #include "base/eintr_wrapper.h"
 #include "base/logging.h"
-#include "mozilla/Move.h"
+#include "mozilla/ipc/FileDescriptorShuffle.h"
 #include "mozilla/UniquePtr.h"
 
 namespace {
 
 static mozilla::EnvironmentLog gProcessLog("MOZ_PROCESS_LOG");
 
 }  // namespace
 
 namespace base {
 
 bool LaunchApp(const std::vector<std::string>& argv,
                const LaunchOptions& options,
                ProcessHandle* process_handle)
 {
   mozilla::UniquePtr<char*[]> argv_cstr(new char*[argv.size() + 1]);
-  // Illegal to allocate memory after fork and before execvp
-  InjectiveMultimap fd_shuffle1, fd_shuffle2;
-  fd_shuffle1.reserve(options.fds_to_remap.size());
-  fd_shuffle2.reserve(options.fds_to_remap.size());
 
   EnvironmentArray envp = BuildEnvironmentArray(options.env_map);
+  mozilla::ipc::FileDescriptorShuffle shuffle;
+  if (!shuffle.Init(options.fds_to_remap)) {
+    return false;
+  }
 
   pid_t pid = options.fork_delegate ? options.fork_delegate->Fork() : fork();
   if (pid < 0)
     return false;
 
   if (pid == 0) {
     // In the child:
-    for (const auto& fd_map : options.fds_to_remap) {
-      fd_shuffle1.push_back(InjectionArc(fd_map.first, fd_map.second, false));
-      fd_shuffle2.push_back(InjectionArc(fd_map.first, fd_map.second, false));
+    for (const auto& fds : shuffle.Dup2Sequence()) {
+      if (HANDLE_EINTR(dup2(fds.first, fds.second)) != fds.second) {
+        // This shouldn't happen, but check for it.  And see below
+        // about logging being unsafe here, so this is debug only.
+        DLOG(ERROR) << "dup2 failed";
+        _exit(127);
+      }
     }
 
-    if (!ShuffleFileDescriptors(&fd_shuffle1))
-      _exit(127);
-
-    CloseSuperfluousFds(fd_shuffle2);
+    CloseSuperfluousFds(shuffle.MapsToFunc());
 
     for (size_t i = 0; i < argv.size(); i++)
       argv_cstr[i] = const_cast<char*>(argv[i].c_str());
     argv_cstr[argv.size()] = NULL;
 
     execve(argv_cstr[0], argv_cstr.get(), envp.get());
     // if we get here, we're in serious trouble and should complain loudly
     // NOTE: This is async signal unsafe; it could deadlock instead.  (But
--- a/ipc/chromium/src/base/process_util_posix.cc
+++ b/ipc/chromium/src/base/process_util_posix.cc
@@ -116,19 +116,20 @@ class ScopedDIRClose {
     if (x) {
       closedir(x);
     }
   }
 };
 typedef mozilla::UniquePtr<DIR, ScopedDIRClose> ScopedDIR;
 
 
-void CloseSuperfluousFds(const base::InjectiveMultimap& saved_mapping) {
-  // DANGER: no calls to malloc are allowed from now on:
-  // http://crbug.com/36678
+void CloseSuperfluousFds(std::function<bool(int)>&& should_preserve) {
+  // DANGER: no calls to malloc (or locks, etc.) are allowed from now on:
+  // https://crbug.com/36678
+  // Also, beware of STL iterators: https://crbug.com/331459
 #if defined(ANDROID)
   static const rlim_t kSystemDefaultMaxFds = 1024;
   static const char kFDDir[] = "/proc/self/fd";
 #elif defined(OS_LINUX) || defined(OS_SOLARIS)
   static const rlim_t kSystemDefaultMaxFds = 8192;
   static const char kFDDir[] = "/proc/self/fd";
 #elif defined(OS_MACOSX)
   static const rlim_t kSystemDefaultMaxFds = 256;
@@ -155,56 +156,46 @@ void CloseSuperfluousFds(const base::Inj
     max_fds = INT_MAX;
 
   DirReaderPosix fd_dir(kFDDir);
 
   if (!fd_dir.IsValid()) {
     // Fallback case: Try every possible fd.
     for (rlim_t i = 0; i < max_fds; ++i) {
       const int fd = static_cast<int>(i);
-      if (fd == STDIN_FILENO || fd == STDOUT_FILENO || fd == STDERR_FILENO)
+      if (fd == STDIN_FILENO || fd == STDOUT_FILENO || fd == STDERR_FILENO ||
+          should_preserve(fd)) {
         continue;
-      InjectiveMultimap::const_iterator j;
-      for (j = saved_mapping.begin(); j != saved_mapping.end(); j++) {
-        if (fd == j->dest)
-          break;
       }
-      if (j != saved_mapping.end())
-        continue;
 
       // Since we're just trying to close anything we can find,
       // ignore any error return values of close().
-      IGNORE_EINTR(close(fd));
+      close(fd);
     }
     return;
   }
 
   const int dir_fd = fd_dir.fd();
 
   for ( ; fd_dir.Next(); ) {
     // Skip . and .. entries.
     if (fd_dir.name()[0] == '.')
       continue;
 
     char *endptr;
     errno = 0;
     const long int fd = strtol(fd_dir.name(), &endptr, 10);
     if (fd_dir.name()[0] == 0 || *endptr || fd < 0 || errno)
       continue;
-    if (fd == STDIN_FILENO || fd == STDOUT_FILENO || fd == STDERR_FILENO)
-      continue;
-    InjectiveMultimap::const_iterator i;
-    for (i = saved_mapping.begin(); i != saved_mapping.end(); i++) {
-      if (fd == i->dest)
-        break;
-    }
-    if (i != saved_mapping.end())
-      continue;
     if (fd == dir_fd)
       continue;
+    if (fd == STDIN_FILENO || fd == STDOUT_FILENO || fd == STDERR_FILENO ||
+        should_preserve(fd)) {
+      continue;
+    }
 
     // When running under Valgrind, Valgrind opens several FDs for its
     // own use and will complain if we try to close them.  All of
     // these FDs are >= |max_fds|, so we can check against that here
     // before closing.  See https://bugs.kde.org/show_bug.cgi?id=191758
     if (fd < static_cast<int>(max_fds)) {
       int ret = IGNORE_EINTR(close(fd));
       if (ret != 0) {
new file mode 100644
--- /dev/null
+++ b/ipc/glue/FileDescriptorShuffle.cpp
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "FileDescriptorShuffle.h"
+
+#include "base/eintr_wrapper.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+
+#include <algorithm>
+#include <unistd.h>
+#include <fcntl.h>
+
+namespace mozilla {
+namespace ipc {
+
+// F_DUPFD_CLOEXEC is like F_DUPFD (see below) but atomically makes
+// the new fd close-on-exec.  This is useful to prevent accidental fd
+// leaks into processes created by plain fork/exec, but IPC uses
+// CloseSuperfluousFds so it's not essential.
+//
+// F_DUPFD_CLOEXEC is in POSIX 2008, but as of 2018 there are still
+// some OSes that don't support it.  (Specifically: Solaris 10 doesn't
+// have it, and Android should have kernel support but doesn't define
+// the constant until API 21 (Lollipop).  We also don't use this for
+// IPC child launching on Android, so there's no point in hard-coding
+// the definitions like we do for Android in some other cases.)
+#ifdef F_DUPFD_CLOEXEC
+static const int kDupFdCmd = F_DUPFD_CLOEXEC;
+#else
+static const int kDupFdCmd = F_DUPFD;
+#endif
+
+// This implementation ensures that the *ranges* of the source and
+// destination fds don't overlap, which is unnecessary but sufficient
+// to avoid conflicts or identity mappings.
+//
+// In practice, the source fds will usually be large and the
+// destination fds will all be relatively small, so there mostly won't
+// be temporary fds.  This approach has the advantage of being simple
+// (and linear-time, but hopefully there aren't enough fds for that to
+// matter).
+bool
+FileDescriptorShuffle::Init(MappingRef aMapping)
+{
+  MOZ_ASSERT(mMapping.IsEmpty());
+
+  // Find the maximum destination fd; any source fds not greater than
+  // this will be duplicated.
+  int maxDst = STDERR_FILENO;
+  for (const auto& elem : aMapping) {
+    maxDst = std::max(maxDst, elem.second);
+  }
+  mMaxDst = maxDst;
+
+#ifdef DEBUG
+  // Increase the limit to make sure the F_DUPFD case gets test coverage.
+  if (!aMapping.IsEmpty()) {
+    // Try to find a value that will create a nontrivial partition.
+    int fd0 = aMapping[0].first;
+    int fdn = aMapping.rbegin()->first;
+    maxDst = std::max(maxDst, (fd0 + fdn) / 2);
+  }
+#endif
+
+  for (const auto& elem : aMapping) {
+    int src = elem.first;
+    // F_DUPFD is like dup() but allows placing a lower bound
+    // on the new fd, which is exactly what we want.
+    if (src <= maxDst) {
+      src = fcntl(src, kDupFdCmd, maxDst + 1);
+      if (src < 0) {
+        return false;
+      }
+      mTempFds.AppendElement(src);
+    }
+    MOZ_ASSERT(src > maxDst);
+#ifdef DEBUG
+    // Check for accidentally mapping two different fds to the same
+    // destination.  (This is O(n^2) time, but it shouldn't matter.)
+    for (const auto& otherElem : mMapping) {
+      MOZ_ASSERT(elem.second != otherElem.second);
+    }
+#endif
+    mMapping.AppendElement(std::make_pair(src, elem.second));
+  }
+  return true;
+}
+
+bool
+FileDescriptorShuffle::MapsTo(int aFd) const
+{
+  // Prune fds that are too large to be a destination, rather than
+  // searching; this should be the common case.
+  if (aFd > mMaxDst) {
+    return false;
+  }
+  for (const auto& elem : mMapping) {
+    if (elem.second == aFd) {
+      return true;
+    }
+  }
+  return false;
+}
+
+FileDescriptorShuffle::~FileDescriptorShuffle()
+{
+  for (const auto& fd : mTempFds) {
+    mozilla::DebugOnly<int> rv = IGNORE_EINTR(close(fd));
+    MOZ_ASSERT(rv == 0);
+  }
+}
+
+} // namespace ipc
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/ipc/glue/FileDescriptorShuffle.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_ipc_FileDescriptorShuffle_h
+#define mozilla_ipc_FileDescriptorShuffle_h
+
+#include "mozilla/Span.h"
+#include "nsTArray.h"
+
+#include <functional>
+#include <utility>
+
+// This class converts a set of file descriptor mapping, which may
+// contain conflicts (like {a->b, b->c} or {a->b, b->a}) into a
+// sequence of dup2() operations that can be performed between fork
+// and exec, or with posix_spawn_file_actions_adddup2.  It may create
+// temporary duplicates of fds to use as the source of a dup2; they
+// are closed on destruction.
+//
+// The dup2 sequence is guaranteed to not contain dup2(x, x) for any
+// x; if such an element is present in the input, it will be dup2()ed
+// from a temporary fd to ensure that the close-on-exec bit is cleared.
+//
+// In general, this is *not* guaranteed to minimize the use of
+// temporary fds.
+
+namespace mozilla {
+namespace ipc {
+
+class FileDescriptorShuffle
+{
+public:
+  FileDescriptorShuffle() = default;
+  ~FileDescriptorShuffle();
+
+  using MappingRef = mozilla::Span<const std::pair<int, int>>;
+
+  // Translate the given mapping, creating temporary fds as needed.
+  // Can fail (return false) on failure to duplicate fds.
+  bool Init(MappingRef aMapping);
+
+  // Accessor for the dup2() sequence.  Do not use the returned value
+  // or the fds contained in it after this object is destroyed.
+  MappingRef Dup2Sequence() const { return mMapping; }
+
+  // Tests whether the given fd is used as a destination in this mapping.
+  // Can be used to close other fds after performing the dup2()s.
+  bool MapsTo(int aFd) const;
+
+  // Wraps MapsTo in a function object, as a convenience for use with
+  // base::CloseSuperfluousFds.
+  std::function<bool(int)> MapsToFunc() const {
+    return [this](int fd) { return MapsTo(fd); };
+  }
+
+private:
+  nsTArray<std::pair<int, int>> mMapping;
+  nsTArray<int> mTempFds;
+  int mMaxDst;
+
+  FileDescriptorShuffle(const FileDescriptorShuffle&) = delete;
+  void operator=(const FileDescriptorShuffle&) = delete;
+};
+
+} // namespace ipc
+} // namespace mozilla
+
+#endif // mozilla_ipc_FileDescriptorShuffle_h
--- a/ipc/glue/moz.build
+++ b/ipc/glue/moz.build
@@ -121,16 +121,24 @@ elif CONFIG['OS_ARCH'] == 'Darwin':
     UNIFIED_SOURCES += [
         'ProcessUtils_mac.mm'
     ]
 else:
     UNIFIED_SOURCES += [
         'ProcessUtils_none.cpp',
     ]
 
+if CONFIG['OS_ARCH'] != 'WINNT':
+    EXPORTS.mozilla.ipc += [
+        'FileDescriptorShuffle.h',
+    ]
+    UNIFIED_SOURCES += [
+        'FileDescriptorShuffle.cpp',
+    ]
+
 EXPORTS.ipc += [
     'IPCMessageUtils.h',
 ]
 
 UNIFIED_SOURCES += [
     'BackgroundImpl.cpp',
     'BackgroundUtils.cpp',
     'BrowserProcessSubThread.cpp',
--- a/security/sandbox/linux/launch/SandboxLaunch.cpp
+++ b/security/sandbox/linux/launch/SandboxLaunch.cpp
@@ -207,18 +207,16 @@ public:
 
   void PrepareMapping(base::file_handle_mapping_vector* aMap);
   pid_t Fork() override;
 
 private:
   int mFlags;
   int mChrootServer;
   int mChrootClient;
-  // For CloseSuperfluousFds in the chroot helper process:
-  base::InjectiveMultimap mChrootMap;
 
   void StartChrootServer();
   SandboxFork(const SandboxFork&) = delete;
   SandboxFork& operator=(const SandboxFork&) = delete;
 };
 
 static int
 GetEffectiveSandboxLevel(GeckoProcessType aType)
@@ -342,20 +340,16 @@ SandboxFork::SandboxFork(int aFlags, boo
     int fds[2];
     int rv = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds);
     if (rv != 0) {
       SANDBOX_LOG_ERROR("socketpair: %s", strerror(errno));
       MOZ_CRASH("socketpair failed");
     }
     mChrootClient = fds[0];
     mChrootServer = fds[1];
-    // Do this here because the child process won't be able to malloc.
-    mChrootMap.push_back(base::InjectionArc(mChrootServer,
-                                            mChrootServer,
-                                            false));
   }
 }
 
 void
 SandboxFork::PrepareMapping(base::file_handle_mapping_vector* aMap)
 {
   if (mChrootClient >= 0) {
     aMap->push_back({ mChrootClient, kSandboxChrootClientFd });
@@ -591,17 +585,17 @@ SandboxFork::StartChrootServer()
 
   LinuxCapabilities caps;
   caps.Effective(CAP_SYS_CHROOT) = true;
   if (!caps.SetCurrent()) {
     SANDBOX_LOG_ERROR("capset (chroot helper): %s", strerror(errno));
     MOZ_DIAGNOSTIC_ASSERT(false);
   }
 
-  CloseSuperfluousFds(mChrootMap);
+  base::CloseSuperfluousFds([this](int fd) { return fd == mChrootServer; });
 
   char msg;
   ssize_t msgLen = HANDLE_EINTR(read(mChrootServer, &msg, 1));
   if (msgLen == 0) {
     // Process exited before chrooting (or chose not to chroot?).
     _exit(0);
   }
   MOZ_RELEASE_ASSERT(msgLen == 1);