Bug 1456911 - Rewrite the fd shuffling to be simpler & handle identity mappings correctly. r=froydnj
authorJed Davis <jld@mozilla.com>
Wed, 25 Apr 2018 17:44:08 -0600
changeset 476563 69ffa8eab9d9a58a4916b6ce3dae1e70ea2ead28
parent 476562 808a2fe2aaab24a235e619b9030c9b3bfd239158
child 476564 80190d88549cf49b79fea1d350e65d8828f33810
push id9374
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:43:20 +0000
treeherdermozilla-beta@160e085dfb0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj
bugs1456911
milestone62.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 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 for std{in,out,err} and those
+// for which the given function returns true.  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);