Refactor our sandboxing code.

--
MOS_MIGRATED_REVID=131817068
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxRunner.java
index b871d08..e3da6ab 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxRunner.java
@@ -15,70 +15,48 @@
 package com.google.devtools.build.lib.sandbox;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.io.ByteStreams;
-import com.google.devtools.build.lib.actions.ExecException;
-import com.google.devtools.build.lib.actions.UserExecException;
-import com.google.devtools.build.lib.shell.AbnormalTerminationException;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
-import com.google.devtools.build.lib.shell.TerminationStatus;
+import com.google.devtools.build.lib.shell.KillableObserver;
 import com.google.devtools.build.lib.shell.TimeoutKillableObserver;
-import com.google.devtools.build.lib.util.CommandFailureUtils;
-import com.google.devtools.build.lib.util.io.FileOutErr;
-import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
-import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.nio.file.Files;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Helper class for running the namespace sandbox. This runner prepares environment inside the
  * sandbox, handles sandbox output, performs cleanup and changes invocation if necessary.
  */
-public class DarwinSandboxRunner {
+final class DarwinSandboxRunner extends SandboxRunner {
+  private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec";
 
-  private final Path execRoot;
-  private final Path sandboxPath;
   private final Path sandboxExecRoot;
   private final Path argumentsFilePath;
-  private final ImmutableSet<PathFragment> createDirs;
-  private final boolean verboseFailures;
-  private final boolean sandboxDebug;
+  private final Set<Path> writableDirs;
+  private final Set<Path> inaccessiblePaths;
 
-  private final Path sandboxConfigPath;
-  private final ImmutableMap<PathFragment, Path> linkPaths;
-
-  public DarwinSandboxRunner(
-      Path execRoot,
+  DarwinSandboxRunner(
       Path sandboxPath,
       Path sandboxExecRoot,
-      Path sandboxConfigPath,
-      ImmutableMap<PathFragment, Path> linkPaths,
-      ImmutableSet<PathFragment> createDirs,
-      boolean verboseFailures,
-      boolean sandboxDebug) {
-    this.execRoot = execRoot;
-    this.sandboxPath = sandboxPath;
+      Set<Path> writableDirs,
+      Set<Path> inaccessiblePaths,
+      boolean verboseFailures) {
+    super(sandboxPath, sandboxExecRoot, verboseFailures);
     this.sandboxExecRoot = sandboxExecRoot;
-    this.argumentsFilePath =
-        sandboxPath.getParentDirectory().getRelative(sandboxPath.getBaseName() + ".params");
-    this.createDirs = createDirs;
-    this.verboseFailures = verboseFailures;
-    this.sandboxDebug = sandboxDebug;
-    this.sandboxConfigPath = sandboxConfigPath;
-    this.linkPaths = linkPaths;
+    this.argumentsFilePath = sandboxPath.getRelative("sandbox.sb");
+    this.writableDirs = writableDirs;
+    this.inaccessiblePaths = inaccessiblePaths;
   }
 
   static boolean isSupported() {
     List<String> args = new ArrayList<>();
-    args.add("sandbox-exec");
+    args.add(SANDBOX_EXEC);
     args.add("-p");
     args.add("(version 1) (allow default)");
     args.add("/usr/bin/true");
@@ -101,193 +79,66 @@
     return true;
   }
 
-  /**
-   * Runs given command inside the sandbox.
-   *
-   * @param spawnArguments - arguments of spawn to run inside the sandbox
-   * @param env - environment to run sandbox in
-   * @param outErr - error output to capture sandbox's and command's stderr
-   * @param outputs - files to extract from the sandbox, paths are relative to the exec root
-   * @throws ExecException
-   */
-  public void run(
-      List<String> spawnArguments,
-      ImmutableMap<String, String> env,
-      FileOutErr outErr,
-      Collection<PathFragment> outputs,
-      int timeout)
-      throws IOException, ExecException {
-    createFileSystem(outputs);
-
-    List<String> commandLineArgs = sandboxPreperationAndGetArgs(spawnArguments, outErr);
-
-    Command cmd =
-        new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile());
-
-    try {
-      cmd.execute(
-          /* stdin */ new byte[] {},
-          (timeout >= 0) ? new TimeoutKillableObserver(timeout * 1000) : Command.NO_OBSERVER,
-          outErr.getOutputStream(),
-          outErr.getErrorStream(),
-          /* killSubprocessOnInterrupt */ true);
-    } catch (CommandException e) {
-      boolean timedOut = false;
-      if (e instanceof AbnormalTerminationException) {
-        TerminationStatus status =
-            ((AbnormalTerminationException) e).getResult().getTerminationStatus();
-        timedOut = !status.exited() && (status.getTerminatingSignal() == 15 /* SIGTERM */);
-      }
-      String message =
-          CommandFailureUtils.describeCommandFailure(
-              verboseFailures, commandLineArgs, env, sandboxExecRoot.getPathString());
-      throw new UserExecException(message, e, timedOut);
-    } finally {
-      copyOutputs(outputs);
-    }
-  }
-
-  private void createFileSystem(Collection<PathFragment> outputs) throws IOException {
-    FileSystemUtils.createDirectoryAndParents(sandboxPath);
-
-    // Prepare the output directories in the sandbox.
-    for (PathFragment output : outputs) {
-      FileSystemUtils.createDirectoryAndParents(
-          sandboxExecRoot.getRelative(output.getParentDirectory()));
-    }
-  }
-
-  private void copyOutputs(Collection<PathFragment> outputs) throws IOException {
-    for (PathFragment output : outputs) {
-      Path source = sandboxExecRoot.getRelative(output);
-      Path target = execRoot.getRelative(output);
-      FileSystemUtils.createDirectoryAndParents(target.getParentDirectory());
-      if (source.isFile() || source.isSymbolicLink()) {
-        com.google.common.io.Files.move(source.getPathFile(), target.getPathFile());
-      }
-    }
-  }
-
-  public void cleanup() throws IOException {
-    if (sandboxPath.exists()) {
-      FileSystemUtils.deleteTree(sandboxPath);
-    }
-    if (!sandboxDebug && argumentsFilePath.exists()) {
-      argumentsFilePath.delete();
-    }
-  }
-
-  private List<String> sandboxPreperationAndGetArgs(List<String> spawnArguments, FileOutErr outErr)
+  @Override
+  protected Command getCommand(
+      List<String> arguments, Map<String, String> environment, int timeout, boolean allowNetwork)
       throws IOException {
-    FileSystem fs = sandboxPath.getFileSystem();
-    PrintWriter errWriter = new PrintWriter(outErr.getErrorStream());
+    writeConfig(allowNetwork);
+
     List<String> commandLineArgs = new ArrayList<>();
-
-    if (sandboxDebug) {
-      errWriter.printf("sandbox root is %s\n", sandboxPath.toString());
-      errWriter.printf("working dir is %s\n", sandboxExecRoot.toString());
-    }
-
-    // Create all needed directories.
-    for (PathFragment createDir : createDirs) {
-      Path dir;
-      if (createDir.isAbsolute()) {
-        dir = fs.getPath(createDir);
-      } else {
-        dir = sandboxPath.getRelative(createDir);
-      }
-      if (sandboxDebug) {
-        errWriter.printf("createdir: %s\n", dir);
-      }
-      FileSystemUtils.createDirectoryAndParents(dir);
-    }
-
-    // Link all the inputs.
-    linkInputs(linkPaths, errWriter);
-
-    errWriter.flush();
-
-    commandLineArgs.add("/usr/bin/sandbox-exec");
+    commandLineArgs.add(SANDBOX_EXEC);
     commandLineArgs.add("-f");
-    commandLineArgs.add(sandboxConfigPath.getPathString());
-    commandLineArgs.addAll(spawnArguments);
-
-    return commandLineArgs;
+    commandLineArgs.add(argumentsFilePath.getPathString());
+    commandLineArgs.addAll(arguments);
+    return new Command(
+        commandLineArgs.toArray(new String[0]), environment, sandboxExecRoot.getPathFile());
   }
 
-  /**
-   * Make all specified inputs available in the sandbox.
-   *
-   * We want the sandboxed process to have access only to these input files and not anything else
-   * from the workspace. Furthermore, the process should not be able to modify these input files.
-   * We achieve this by hardlinking all input files into a temporary "inputs" directory, then
-   * symlinking them into their correct place inside the sandbox.
-   *
-   * The hardlinks / symlinks combination (as opposed to simply directly hardlinking to the final
-   * destination) is necessary, because we build a solib symlink tree for shared libraries where the
-   * original file and the created symlink have two different file names (libblaze_util.so vs.
-   * src_Stest_Scpp_Sblaze_Uutil_Utest.so) and our cc_wrapper.sh needs to be able to figure out both
-   * names (by following solib symlinks back) to modify the paths to the shared libraries in
-   * cc_binaries.
-   */
-  private void linkInputs(ImmutableMap<PathFragment, Path> inputs, PrintWriter errWriter)
-      throws IOException {
-    // create directory for input files
-    Path inputsDir = sandboxPath.getRelative("inputs");
-    if (!inputsDir.exists()) {
-      inputsDir.createDirectory();
-    }
+  private void writeConfig(boolean allowNetwork) throws IOException {
+    try (PrintWriter out = new PrintWriter(argumentsFilePath.getOutputStream())) {
+      // Note: In Apple's sandbox configuration language, the *last* matching rule wins.
+      out.println("(version 1)");
+      out.println("(debug deny)");
+      out.println("(allow default)");
 
-    for (ImmutableMap.Entry<PathFragment, Path> entry : inputs.entrySet()) {
-      // hardlink, resolve symlink here instead in finalizeLinks
-      Path hardlinkOldPath = entry.getValue().resolveSymbolicLinks();
-      Path hardlinkNewPath =
-          hardlinkOldPath.startsWith(execRoot)
-              ? inputsDir.getRelative(hardlinkOldPath.relativeTo(execRoot))
-              : inputsDir.getRelative(entry.getKey());
-      if (sandboxDebug) {
-        errWriter.printf("hardlink: %s -> %s\n", hardlinkNewPath, hardlinkOldPath);
-      }
-      try {
-        createHardLink(hardlinkNewPath, hardlinkOldPath);
-      } catch (IOException e) {
-        // Creating a hardlink might fail when the input file and the sandbox directory are not on
-        // the same filesystem / device. Then we use symlink instead.
-        hardlinkNewPath.createSymbolicLink(hardlinkOldPath);
+      if (!allowNetwork) {
+        out.println("(deny network*)");
       }
 
-      // symlink
-      Path symlinkNewPath = sandboxExecRoot.getRelative(entry.getKey());
-      if (sandboxDebug) {
-        errWriter.printf("symlink: %s -> %s\n", hardlinkNewPath, symlinkNewPath);
+      out.println("(allow network* (local ip \"localhost:*\"))");
+      out.println("(allow network* (remote ip \"localhost:*\"))");
+      out.println("(allow network* (remote unix-socket (subpath \"/\")))");
+      out.println("(allow network* (local unix-socket (subpath \"/\")))");
+
+      for (Path inaccessiblePath : inaccessiblePaths) {
+        out.println("(deny file-read* (subpath \"" + inaccessiblePath + "\"))");
       }
-      FileSystemUtils.createDirectoryAndParents(symlinkNewPath.getParentDirectory());
-      symlinkNewPath.createSymbolicLink(hardlinkNewPath);
+
+      // Almost everything else is read-only.
+      out.println("(deny file-write* (subpath \"/\"))");
+
+      allowWriteSubpath(out, sandboxExecRoot);
+      for (Path path : writableDirs) {
+        allowWriteSubpath(out, path);
+      }
     }
   }
 
-  // TODO(yueg): import unix.FilesystemUtils and use FilesystemUtils.createHardLink() instead
-  private void createHardLink(Path target, Path source) throws IOException {
-    java.nio.file.Path targetNio = java.nio.file.Paths.get(target.toString());
-    java.nio.file.Path sourceNio = java.nio.file.Paths.get(source.toString());
+  private void allowWriteSubpath(PrintWriter out, Path path) throws IOException {
+    out.println("(allow file-write* (subpath \"" + path.getPathString() + "\"))");
+    Path resolvedPath = path.resolveSymbolicLinks();
+    if (!resolvedPath.equals(path)) {
+      out.println("(allow file-write* (subpath \"" + resolvedPath.getPathString() + "\"))");
+    }
+  }
 
-    if (!source.exists() || target.exists()) {
-      return;
-    }
-    // Regular file
-    if (source.isFile()) {
-      Path parentDir = target.getParentDirectory();
-      if (!parentDir.exists()) {
-        FileSystemUtils.createDirectoryAndParents(parentDir);
-      }
-      Files.createLink(targetNio, sourceNio);
-      // Directory
-    } else if (source.isDirectory()) {
-      Collection<Path> subpaths = source.getDirectoryEntries();
-      for (Path sourceSubpath : subpaths) {
-        Path targetSubpath = target.getRelative(sourceSubpath.relativeTo(source));
-        createHardLink(targetSubpath, sourceSubpath);
-      }
-    }
+  @Override
+  protected KillableObserver getCommandObserver(int timeout) {
+    return (timeout >= 0) ? new TimeoutKillableObserver(timeout * 1000) : Command.NO_OBSERVER;
+  }
+
+  @Override
+  protected int getSignalOnTimeout() {
+    return 15; /* SIGTERM */
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedStrategy.java
index df20524..6188856 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedStrategy.java
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -19,12 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.io.Files;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
-import com.google.devtools.build.lib.actions.ActionInput;
-import com.google.devtools.build.lib.actions.ActionInputHelper;
-import com.google.devtools.build.lib.actions.ActionStatusMessage;
-import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ExecutionStrategy;
@@ -32,20 +28,15 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 import com.google.devtools.build.lib.actions.UserExecException;
-import com.google.devtools.build.lib.analysis.AnalysisUtils;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.config.RunUnder;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
-import com.google.devtools.build.lib.cmdline.Label;
-import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
-import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
 import com.google.devtools.build.lib.rules.test.TestRunnerAction;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.shell.CommandResult;
 import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
 import com.google.devtools.build.lib.util.Preconditions;
-import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileStatus;
 import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -53,14 +44,11 @@
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.SearchPath;
 import com.google.devtools.build.lib.vfs.Symlinks;
-import java.io.File;
 import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -70,14 +58,14 @@
   name = {"sandboxed"},
   contextType = SpawnActionContext.class
 )
-public class DarwinSandboxedStrategy implements SpawnActionContext {
+public class DarwinSandboxedStrategy extends SandboxStrategy {
 
-  private final ExecutorService backgroundWorkers;
+  private final BuildRequest buildRequest;
   private final ImmutableMap<String, String> clientEnv;
   private final BlazeDirectories blazeDirs;
   private final Path execRoot;
-  private final BuildRequest buildRequest;
-  private final SandboxOptions sandboxOptions;
+  private final ExecutorService backgroundWorkers;
+  private final boolean sandboxDebug;
   private final boolean verboseFailures;
   private final String productName;
   private final ImmutableList<Path> confPaths;
@@ -93,12 +81,13 @@
       boolean verboseFailures,
       String productName,
       ImmutableList<Path> confPaths) {
+    super(blazeDirs, verboseFailures, buildRequest.getOptions(SandboxOptions.class));
     this.buildRequest = buildRequest;
-    this.sandboxOptions = buildRequest.getOptions(SandboxOptions.class);
     this.clientEnv = ImmutableMap.copyOf(clientEnv);
     this.blazeDirs = blazeDirs;
     this.execRoot = blazeDirs.getExecRoot();
     this.backgroundWorkers = Preconditions.checkNotNull(backgroundWorkers);
+    this.sandboxDebug = buildRequest.getOptions(SandboxOptions.class).sandboxDebug;
     this.verboseFailures = verboseFailures;
     this.productName = productName;
     this.confPaths = confPaths;
@@ -150,221 +139,114 @@
     return new String(res.getStdout(), UTF_8).trim();
   }
 
-  private int getTimeout(Spawn spawn) throws ExecException {
-    String timeoutStr = spawn.getExecutionInfo().get("timeout");
-    if (timeoutStr != null) {
-      try {
-        return Integer.parseInt(timeoutStr);
-      } catch (NumberFormatException e) {
-        throw new UserExecException("Could not parse timeout", e);
-      }
-    }
-    return -1;
-  }
-
   @Override
   public void exec(Spawn spawn, ActionExecutionContext actionExecutionContext)
       throws ExecException {
     Executor executor = actionExecutionContext.getExecutor();
 
     // Certain actions can't run remotely or in a sandbox - pass them on to the standalone strategy.
-    StandaloneSpawnStrategy standaloneStrategy =
-        Preconditions.checkNotNull(executor.getContext(StandaloneSpawnStrategy.class));
     if (!spawn.isRemotable()) {
-      standaloneStrategy.exec(spawn, actionExecutionContext);
+      SandboxHelpers.fallbackToNonSandboxedExecution(spawn, actionExecutionContext, executor);
       return;
     }
 
-    if (executor.reportsSubcommands()) {
-      executor.reportSubcommand(
-          Label.print(spawn.getOwner().getLabel())
-              + " ["
-              + spawn.getResourceOwner().prettyPrint()
-              + "]",
-          spawn.asShellCommand(executor.getExecRoot()));
-    }
-
-    executor
-        .getEventBus()
-        .post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), "sandbox"));
-
-    FileOutErr outErr = actionExecutionContext.getFileOutErr();
-
-    // The execId is a unique ID just for this invocation of "exec".
-    String execId = uuid + "-" + execCounter.getAndIncrement();
+    SandboxHelpers.reportSubcommand(executor, spawn);
+    SandboxHelpers.postActionStatusMessage(executor, spawn);
 
     // Each invocation of "exec" gets its own sandbox.
-    Path sandboxPath =
-        blazeDirs.getOutputBase().getRelative(productName + "-sandbox").getRelative(execId);
+    Path sandboxPath = SandboxHelpers.getSandboxRoot(blazeDirs, productName, uuid, execCounter);
+    Path sandboxExecRoot = sandboxPath.getRelative("execroot");
 
-    ImmutableSet<PathFragment> createDirs =
-        createImportantDirs(
-            standaloneStrategy.locallyDeterminedEnv(spawn.getEnvironment()), sandboxPath);
+    ImmutableMap<String, String> spawnEnvironment =
+        StandaloneSpawnStrategy.locallyDeterminedEnv(execRoot, productName, spawn.getEnvironment());
 
-    int timeout = getTimeout(spawn);
-
-    ImmutableSet.Builder<PathFragment> outputFiles = ImmutableSet.<PathFragment>builder();
-    final DarwinSandboxRunner runner =
-        getRunnerForExec(spawn, actionExecutionContext, sandboxPath, createDirs, outputFiles);
+    Set<Path> writableDirs = getWritableDirs(sandboxExecRoot, spawn.getEnvironment());
 
     try {
-      runner.run(
-          spawn.getArguments(),
-          standaloneStrategy.locallyDeterminedEnv(spawn.getEnvironment()),
-          outErr,
-          outputFiles.build(),
-          timeout);
+      HardlinkedExecRoot hardlinkedExecRoot =
+          new HardlinkedExecRoot(execRoot, sandboxPath, sandboxExecRoot);
+      ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn);
+      hardlinkedExecRoot.createFileSystem(
+          getMounts(spawn, actionExecutionContext), outputs, writableDirs);
+
+      DarwinSandboxRunner runner;
+      runner =
+          new DarwinSandboxRunner(
+              sandboxPath,
+              sandboxExecRoot,
+              getWritableDirs(sandboxExecRoot, spawnEnvironment),
+              getInaccessiblePaths(),
+              verboseFailures);
+
+      try {
+        runner.run(
+            spawn.getArguments(),
+            spawnEnvironment,
+            actionExecutionContext.getFileOutErr(),
+            SandboxHelpers.getTimeout(spawn),
+            SandboxHelpers.shouldAllowNetwork(buildRequest, spawn));
+      } finally {
+        hardlinkedExecRoot.copyOutputs(execRoot, outputs);
+        if (!sandboxDebug) {
+          SandboxHelpers.lazyCleanup(backgroundWorkers, runner);
+        }
+      }
     } catch (IOException e) {
       throw new UserExecException("I/O error during sandboxed execution", e);
-    } finally {
-      // By deleting the sandbox directory in the background, we avoid having to wait for it to
-      // complete before returning from the action, which improves performance.
-      backgroundWorkers.execute(
-          new Runnable() {
-            @Override
-            public void run() {
-              try {
-                while (!Thread.currentThread().isInterrupted()) {
-                  try {
-                    runner.cleanup();
-                    return;
-                  } catch (IOException e2) {
-                    // Sleep & retry.
-                    Thread.sleep(250);
-                  }
-                }
-              } catch (InterruptedException e) {
-                // Exit.
-              }
-            }
-          });
     }
   }
 
-  private DarwinSandboxRunner getRunnerForExec(
-      Spawn spawn,
-      ActionExecutionContext actionExecutionContext,
-      Path sandboxPath,
-      ImmutableSet<PathFragment> createDirs,
-      ImmutableSet.Builder<PathFragment> outputFiles)
+  @Override
+  protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) {
+    FileSystem fs = sandboxExecRoot.getFileSystem();
+    ImmutableSet.Builder<Path> writableDirs = ImmutableSet.builder();
+
+    writableDirs.addAll(super.getWritableDirs(sandboxExecRoot, env));
+    writableDirs.add(fs.getPath("/dev"));
+
+    String sysTmpDir = System.getenv("TMPDIR");
+    if (sysTmpDir != null) {
+      writableDirs.add(fs.getPath(sysTmpDir));
+    }
+
+    writableDirs.add(fs.getPath("/tmp"));
+
+    // Other temporary directories from getconf.
+    for (Path path : confPaths) {
+      if (path.exists()) {
+        writableDirs.add(path);
+      }
+    }
+
+    return writableDirs.build();
+  }
+
+  @Override
+  protected ImmutableSet<Path> getInaccessiblePaths() {
+    ImmutableSet.Builder<Path> inaccessiblePaths = ImmutableSet.builder();
+    inaccessiblePaths.addAll(super.getInaccessiblePaths());
+    inaccessiblePaths.add(blazeDirs.getWorkspace());
+    inaccessiblePaths.add(execRoot);
+    return inaccessiblePaths.build();
+  }
+
+  private Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext)
       throws ExecException {
-    ImmutableMap<PathFragment, Path> linkPaths;
     try {
-      // Gather all necessary linkPaths for the sandbox.
-      linkPaths = getMounts(spawn, actionExecutionContext);
+      Map<PathFragment, Path> mounts = new HashMap<>();
+      mountInputs(mounts, spawn, executionContext);
+
+      Map<PathFragment, Path> unfinalized = new HashMap<>();
+      mountRunfilesFromManifests(unfinalized, spawn);
+      mountRunfilesFromSuppliers(unfinalized, spawn);
+      mountFilesFromFilesetManifests(unfinalized, spawn, executionContext);
+      mountRunUnderCommand(unfinalized, spawn);
+      mounts.putAll(finalizeLinks(unfinalized));
+
+      return mounts;
     } catch (IllegalArgumentException | IOException e) {
       throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e);
     }
-
-    for (PathFragment optionalOutput : spawn.getOptionalOutputFiles()) {
-      Preconditions.checkArgument(!optionalOutput.isAbsolute());
-      outputFiles.add(optionalOutput);
-    }
-    for (ActionInput output : spawn.getOutputFiles()) {
-      outputFiles.add(new PathFragment(output.getExecPathString()));
-    }
-
-    DarwinSandboxRunner runner;
-    try {
-      Path sandboxConfigPath =
-          generateScriptFile(sandboxPath, SandboxHelpers.shouldAllowNetwork(buildRequest, spawn));
-      runner =
-          new DarwinSandboxRunner(
-              execRoot,
-              sandboxPath,
-              sandboxPath.getRelative("execroot"),
-              sandboxConfigPath,
-              linkPaths,
-              createDirs,
-              verboseFailures,
-              sandboxOptions.sandboxDebug);
-    } catch (IOException e) {
-      throw new UserExecException("I/O error during sandboxed execution", e);
-    }
-
-    return runner;
-  }
-
-  private ImmutableSet<PathFragment> createImportantDirs(
-      Map<String, String> env, Path sandboxPath) {
-    ImmutableSet.Builder<PathFragment> dirs = ImmutableSet.builder();
-    if (env.containsKey("TEST_TMPDIR")) {
-      PathFragment testTmpDir = new PathFragment(env.get("TEST_TMPDIR"));
-      if (!testTmpDir.isAbsolute()) {
-        testTmpDir = sandboxPath.asFragment().getRelative("execroot").getRelative(testTmpDir);
-      }
-      dirs.add(testTmpDir);
-    }
-    return dirs.build();
-  }
-
-  private Path generateScriptFile(Path sandboxPath, boolean allowNetwork) throws IOException {
-    FileSystemUtils.createDirectoryAndParents(sandboxPath);
-    Path sandboxConfigPath =
-        sandboxPath.getParentDirectory().getRelative(sandboxPath.getBaseName() + ".sb");
-    try (PrintWriter out = new PrintWriter(sandboxConfigPath.getOutputStream())) {
-      out.println("(version 1)");
-      out.println("(debug deny)");
-      out.println("(allow default)");
-
-      // check network
-      if (!allowNetwork) {
-        out.println("(deny network*)");
-      }
-      out.println("(allow network* (local ip \"localhost:*\"))");
-      out.println("(allow network* (remote ip \"localhost:*\"))");
-      out.println("(allow network* (remote unix-socket (subpath \"/\")))");
-      out.println("(allow network* (local unix-socket (subpath \"/\")))");
-
-      // Non-readable path: workspace && exec_root
-      out.println("(deny file-read* (subpath \"" + blazeDirs.getWorkspace() + "\"))");
-      out.println("(deny file-read* (subpath \"" + execRoot + "\"))");
-
-      // Almost everything is non-writable
-      out.println("(deny file-write* (subpath \"/\"))");
-
-      allowWriteSubpath(out, blazeDirs.getFileSystem().getPath("/dev"));
-
-      // Write access to sandbox
-      allowWriteSubpath(out, sandboxPath);
-
-      // Write access to the system TMPDIR
-      Path sysTmpDir = blazeDirs.getFileSystem().getPath(System.getenv("TMPDIR"));
-      allowWriteSubpath(out, sysTmpDir);
-      allowWriteSubpath(out, blazeDirs.getFileSystem().getPath("/tmp"));
-
-      // Other tmpdir from getconf
-      for (Path path : confPaths) {
-        if (path.exists()) {
-          allowWriteSubpath(out, path);
-        }
-      }
-    }
-
-    return sandboxConfigPath;
-  }
-
-  private void allowWriteSubpath(PrintWriter out, Path path) throws IOException {
-    out.println("(allow file-write* (subpath \"" + path.getPathString() + "\"))");
-    Path resolvedPath = path.resolveSymbolicLinks();
-    if (!resolvedPath.equals(path)) {
-      out.println("(allow file-write* (subpath \"" + resolvedPath.getPathString() + "\"))");
-    }
-  }
-
-  private ImmutableMap<PathFragment, Path> getMounts(
-      Spawn spawn, ActionExecutionContext executionContext) throws IOException, ExecException {
-    ImmutableMap.Builder<PathFragment, Path> result = new ImmutableMap.Builder<>();
-    result.putAll(mountInputs(spawn, executionContext));
-
-    Map<PathFragment, Path> unfinalized = new HashMap<>();
-    unfinalized.putAll(mountRunfilesFromManifests(spawn));
-    unfinalized.putAll(mountRunfilesFromSuppliers(spawn));
-    unfinalized.putAll(mountFilesFromFilesetManifests(spawn, executionContext));
-    unfinalized.putAll(mountRunUnderCommand(spawn));
-    result.putAll(finalizeLinks(unfinalized));
-
-    return result.build();
   }
 
   private ImmutableMap<PathFragment, Path> finalizeLinks(Map<PathFragment, Path> unfinalized)
@@ -400,87 +282,6 @@
     finalizedMounts.put(target, source);
   }
 
-  /** Mount all inputs of the spawn. */
-  private Map<PathFragment, Path> mountInputs(
-      Spawn spawn, ActionExecutionContext actionExecutionContext) {
-    Map<PathFragment, Path> mounts = new HashMap<>();
-
-    List<ActionInput> inputs =
-        ActionInputHelper.expandArtifacts(
-            spawn.getInputFiles(), actionExecutionContext.getArtifactExpander());
-
-    if (spawn.getResourceOwner() instanceof CppCompileAction) {
-      CppCompileAction action = (CppCompileAction) spawn.getResourceOwner();
-      if (action.shouldScanIncludes()) {
-        inputs.addAll(action.getAdditionalInputs());
-      }
-    }
-
-    for (ActionInput input : inputs) {
-      if (input.getExecPathString().contains("internal/_middlemen/")) {
-        continue;
-      }
-      Path mount = execRoot.getRelative(input.getExecPathString());
-      mounts.put(new PathFragment(input.getExecPathString()), mount);
-    }
-    return mounts;
-  }
-
-  /** Mount all runfiles that the spawn needs as specified in its runfiles manifests. */
-  private Map<PathFragment, Path> mountRunfilesFromManifests(Spawn spawn)
-      throws IOException, ExecException {
-    Map<PathFragment, Path> mounts = new HashMap<>();
-    for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
-      String manifestFilePath = manifest.getValue().getPath().getPathString();
-      Preconditions.checkState(!manifest.getKey().isAbsolute());
-
-      mounts.putAll(parseManifestFile(manifest.getKey(), new File(manifestFilePath), false, ""));
-    }
-    return mounts;
-  }
-
-  /** Mount all files that the spawn needs as specified in its fileset manifests. */
-  private Map<PathFragment, Path> mountFilesFromFilesetManifests(
-      Spawn spawn, ActionExecutionContext executionContext) throws IOException, ExecException {
-    final FilesetActionContext filesetContext =
-        executionContext.getExecutor().getContext(FilesetActionContext.class);
-    Map<PathFragment, Path> mounts = new HashMap<>();
-    for (Artifact fileset : spawn.getFilesetManifests()) {
-      Path manifest =
-          execRoot.getRelative(AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()));
-
-      mounts.putAll(
-          parseManifestFile(
-              fileset.getExecPath(),
-              manifest.getPathFile(),
-              true,
-              filesetContext.getWorkspaceName()));
-    }
-    return mounts;
-  }
-
-  /** Mount all runfiles that the spawn needs as specified via its runfiles suppliers. */
-  private Map<PathFragment, Path> mountRunfilesFromSuppliers(Spawn spawn) throws IOException {
-    Map<PathFragment, Path> mounts = new HashMap<>();
-    FileSystem fs = blazeDirs.getFileSystem();
-    Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings =
-        spawn.getRunfilesSupplier().getMappings();
-    for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
-        rootsAndMappings.entrySet()) {
-      PathFragment root =
-          fs.getRootDirectory().getRelative(rootAndMappings.getKey()).relativeTo(execRoot);
-      for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
-        Artifact sourceArtifact = mapping.getValue();
-        Path source = (sourceArtifact != null) ? sourceArtifact.getPath() : fs.getPath("/dev/null");
-
-        Preconditions.checkArgument(!mapping.getKey().isAbsolute());
-        PathFragment target = root.getRelative(mapping.getKey());
-        mounts.put(target, source);
-      }
-    }
-    return mounts;
-  }
-
   /**
    * If a --run_under= option is set and refers to a command via its path (as opposed to via its
    * label), we have to mount this. Note that this is best effort and works fine for shell scripts
@@ -489,9 +290,7 @@
    * <p>If --run_under= refers to a label, it is automatically provided in the spawn's input files,
    * so mountInputs() will catch that case.
    */
-  private Map<PathFragment, Path> mountRunUnderCommand(Spawn spawn) {
-    Map<PathFragment, Path> mounts = new HashMap<>();
-
+  private void mountRunUnderCommand(Map<PathFragment, Path> mounts, Spawn spawn) {
     if (spawn.getResourceOwner() instanceof TestRunnerAction) {
       TestRunnerAction testRunnerAction = ((TestRunnerAction) spawn.getResourceOwner());
       RunUnder runUnder = testRunnerAction.getExecutionSettings().getRunUnder();
@@ -514,71 +313,5 @@
         }
       }
     }
-    return mounts;
-  }
-
-  private Map<PathFragment, Path> parseManifestFile(
-      PathFragment targetDirectory,
-      File manifestFile,
-      boolean isFilesetManifest,
-      String workspaceName)
-      throws IOException, ExecException {
-    Map<PathFragment, Path> mounts = new HashMap<>();
-    int lineNum = 0;
-    for (String line : Files.readLines(manifestFile, StandardCharsets.UTF_8)) {
-      if (isFilesetManifest && (++lineNum % 2 == 0)) {
-        continue;
-      }
-      if (line.isEmpty()) {
-        continue;
-      }
-
-      String[] fields = line.trim().split(" ");
-
-      PathFragment targetPath;
-      if (isFilesetManifest) {
-        PathFragment targetPathFragment = new PathFragment(fields[0]);
-        if (!workspaceName.isEmpty()) {
-          if (!targetPathFragment.getSegment(0).equals(workspaceName)) {
-            throw new EnvironmentalExecException(
-                "Fileset manifest line must start with workspace name");
-          }
-          targetPathFragment = targetPathFragment.subFragment(1, targetPathFragment.segmentCount());
-        }
-        targetPath = targetDirectory.getRelative(targetPathFragment);
-      } else {
-        targetPath = targetDirectory.getRelative(fields[0]);
-      }
-
-      Path source;
-      switch (fields.length) {
-        case 1:
-          source = blazeDirs.getFileSystem().getPath("/dev/null");
-          break;
-        case 2:
-          source = blazeDirs.getFileSystem().getPath(fields[1]);
-          break;
-        default:
-          throw new IllegalStateException("'" + line + "' splits into more than 2 parts");
-      }
-
-      mounts.put(targetPath, source);
-    }
-    return mounts;
-  }
-
-  @Override
-  public boolean willExecuteRemotely(boolean remotable) {
-    return false;
-  }
-
-  @Override
-  public String toString() {
-    return "sandboxed";
-  }
-
-  @Override
-  public boolean shouldPropagateExecException() {
-    return verboseFailures && sandboxOptions.sandboxDebug;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedExecRoot.java
new file mode 100644
index 0000000..8aeabe3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedExecRoot.java
@@ -0,0 +1,149 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Creates an execRoot for a Spawn that contains input files as symlinks to hardlinks of the
+ * original input files.
+ */
+public class HardlinkedExecRoot implements SandboxExecRoot {
+
+  private final Path execRoot;
+  private final Path sandboxPath;
+  private final Path sandboxExecRoot;
+
+  public HardlinkedExecRoot(Path execRoot, Path sandboxPath, Path sandboxExecRoot) {
+    this.execRoot = execRoot;
+    this.sandboxPath = sandboxPath;
+    this.sandboxExecRoot = sandboxExecRoot;
+  }
+
+  @Override
+  public void createFileSystem(
+      Map<PathFragment, Path> inputs, Collection<PathFragment> outputs, Set<Path> writableDirs)
+      throws IOException {
+    Set<Path> createdDirs = new HashSet<>();
+    FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot);
+    createDirectoriesForOutputs(outputs, createdDirs);
+
+    // Create all needed directories.
+    for (Path createDir : writableDirs) {
+      FileSystemUtils.createDirectoryAndParents(createDir);
+    }
+
+    // Link all the inputs.
+    linkInputs(inputs);
+  }
+
+  private void createDirectoriesForOutputs(Collection<PathFragment> outputs, Set<Path> createdDirs)
+      throws IOException {
+    // Prepare the output directories in the sandbox.
+    for (PathFragment output : outputs) {
+      FileSystemUtils.createDirectoryAndParentsWithCache(
+          createdDirs, sandboxExecRoot.getRelative(output.getParentDirectory()));
+    }
+  }
+
+  /**
+   * Make all specified inputs available in the sandbox.
+   *
+   * <p>We want the sandboxed process to have access only to these input files and not anything else
+   * from the workspace. Furthermore, the process should not be able to modify these input files. We
+   * achieve this by hardlinking all input files into a temporary "inputs" directory, then
+   * symlinking them into their correct place inside the sandbox.
+   *
+   * <p>The hardlinks / symlinks combination (as opposed to simply directly hardlinking to the final
+   * destination) is necessary, because we build a solib symlink tree for shared libraries where the
+   * original file and the created symlink have two different file names (libblaze_util.so vs.
+   * src_Stest_Scpp_Sblaze_Uutil_Utest.so) and our cc_wrapper.sh needs to be able to figure out both
+   * names (by following solib symlinks back) to modify the paths to the shared libraries in
+   * cc_binaries.
+   */
+  private void linkInputs(Map<PathFragment, Path> inputs) throws IOException {
+    // Create directory for input files.
+    Path inputsDir = sandboxPath.getRelative("inputs");
+    if (!inputsDir.exists()) {
+      inputsDir.createDirectory();
+    }
+
+    for (ImmutableMap.Entry<PathFragment, Path> entry : inputs.entrySet()) {
+      // Hardlink, resolve symlink here instead in finalizeLinks.
+      Path source = entry.getValue().resolveSymbolicLinks();
+      Path target =
+          source.startsWith(execRoot)
+              ? inputsDir.getRelative(source.relativeTo(execRoot))
+              : inputsDir.getRelative(entry.getKey());
+      try {
+        createHardLink(target, source);
+      } catch (IOException e) {
+        // Creating a hardlink might fail when the input file and the sandbox directory are not on
+        // the same filesystem / device. Then we use symlink instead.
+        target.createSymbolicLink(source);
+      }
+
+      // symlink
+      Path symlinkNewPath = sandboxExecRoot.getRelative(entry.getKey());
+      FileSystemUtils.createDirectoryAndParents(symlinkNewPath.getParentDirectory());
+      symlinkNewPath.createSymbolicLink(target);
+    }
+  }
+
+  // TODO(yueg): import unix.FilesystemUtils and use FilesystemUtils.createHardLink() instead
+  private void createHardLink(Path target, Path source) throws IOException {
+    java.nio.file.Path targetNio = java.nio.file.Paths.get(target.toString());
+    java.nio.file.Path sourceNio = java.nio.file.Paths.get(source.toString());
+
+    if (!source.exists() || target.exists()) {
+      return;
+    }
+    // Regular file
+    if (source.isFile()) {
+      Path parentDir = target.getParentDirectory();
+      if (!parentDir.exists()) {
+        FileSystemUtils.createDirectoryAndParents(parentDir);
+      }
+      java.nio.file.Files.createLink(targetNio, sourceNio);
+      // Directory
+    } else if (source.isDirectory()) {
+      Collection<Path> subpaths = source.getDirectoryEntries();
+      for (Path sourceSubpath : subpaths) {
+        Path targetSubpath = target.getRelative(sourceSubpath.relativeTo(source));
+        createHardLink(targetSubpath, sourceSubpath);
+      }
+    }
+  }
+
+  @Override
+  public void copyOutputs(Path execRoot, Collection<PathFragment> outputs) throws IOException {
+    for (PathFragment output : outputs) {
+      Path source = sandboxExecRoot.getRelative(output);
+      if (source.isFile() || source.isSymbolicLink()) {
+        Path target = execRoot.getRelative(output);
+        Files.move(source.getPathFile(), target.getPathFile());
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxAlmostSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxAlmostSandboxRunner.java
deleted file mode 100644
index 73185f3..0000000
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxAlmostSandboxRunner.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2016 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.devtools.build.lib.sandbox;
-
-import com.google.devtools.build.lib.runtime.CommandEnvironment;
-import com.google.devtools.build.lib.util.OsUtils;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
-import com.google.devtools.build.lib.vfs.Path;
-import com.google.devtools.build.lib.vfs.PathFragment;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Helper class for running the Linux sandbox. This runner is a subclass of LinuxSandboxRunner which
- * uses process-wrapper instead of linux-sandbox in the same sandbox execution environment.
- */
-public class LinuxAlmostSandboxRunner extends LinuxSandboxRunner {
-
-  LinuxAlmostSandboxRunner(
-      Path execRoot,
-      Path sandboxExecRoot,
-      Set<Path> writablePaths,
-      List<Path> inaccessiblePaths,
-      boolean verboseFailures,
-      boolean sandboxDebug) {
-    super(
-        execRoot, sandboxExecRoot, writablePaths, inaccessiblePaths, verboseFailures, sandboxDebug);
-  }
-
-  static boolean isSupported(CommandEnvironment commandEnv) {
-    PathFragment embeddedTool =
-        commandEnv
-            .getBlazeWorkspace()
-            .getBinTools()
-            .getExecPath("process-wrapper" + OsUtils.executableExtension());
-    if (embeddedTool == null) {
-      // The embedded tool does not exist, meaning that we don't support sandboxing (e.g., while
-      // bootstrapping).
-      return false;
-    }
-    return true;
-  }
-
-  @Override
-  protected void runPreparation(int timeout, boolean allowNetwork) throws IOException {
-    // Create all needed directories.
-    for (Path writablePath : super.getWritablePaths()) {
-      if (writablePath.startsWith(super.getSandboxExecRoot())) {
-        FileSystemUtils.createDirectoryAndParents(writablePath);
-      }
-    }
-  }
-
-  @Override
-  protected List<String> runCommandLineArgs(List<String> spawnArguments, int timeout) {
-    List<String> commandLineArgs = new ArrayList<>();
-
-    commandLineArgs.add(super.getExecRoot().getRelative("_bin/process-wrapper").getPathString());
-    commandLineArgs.add(Integer.toString(timeout));
-    commandLineArgs.add("5"); /* kill delay: give some time to print stacktraces and whatnot. */
-    commandLineArgs.add("-"); /* stdout. */
-    commandLineArgs.add("-"); /* stderr. */
-
-    commandLineArgs.addAll(spawnArguments);
-
-    return commandLineArgs;
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java
index 38c4db5..cceed55 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java
@@ -16,18 +16,10 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
-import com.google.common.io.Files;
-import com.google.devtools.build.lib.actions.ExecException;
-import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
-import com.google.devtools.build.lib.shell.AbnormalTerminationException;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
-import com.google.devtools.build.lib.shell.TerminationStatus;
-import com.google.devtools.build.lib.util.CommandFailureUtils;
 import com.google.devtools.build.lib.util.OsUtils;
-import com.google.devtools.build.lib.util.Preconditions;
-import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -35,58 +27,38 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 
 /**
  * Helper class for running the Linux sandbox. This runner prepares environment inside the sandbox,
  * handles sandbox output, performs cleanup and changes invocation if necessary.
  */
-public class LinuxSandboxRunner {
+final class LinuxSandboxRunner extends SandboxRunner {
   protected static final String LINUX_SANDBOX = "linux-sandbox" + OsUtils.executableExtension();
 
-  protected Path getExecRoot() {
-    return execRoot;
-  }
-
-  protected Path getSandboxExecRoot() {
-    return sandboxExecRoot;
-  }
-
-  protected Set<Path> getWritablePaths() {
-    return writablePaths;
-  }
-
-  protected boolean isVerboseFailures() {
-    return verboseFailures;
-  }
-
   private final Path execRoot;
   private final Path sandboxExecRoot;
   private final Path argumentsFilePath;
-  private final Set<Path> writablePaths;
-  private final List<Path> inaccessiblePaths;
-  private final boolean verboseFailures;
+  private final Set<Path> writableDirs;
+  private final Set<Path> inaccessiblePaths;
   private final boolean sandboxDebug;
 
   LinuxSandboxRunner(
       Path execRoot,
+      Path sandboxPath,
       Path sandboxExecRoot,
-      Set<Path> writablePaths,
-      List<Path> inaccessiblePaths,
+      Set<Path> writableDirs,
+      Set<Path> inaccessiblePaths,
       boolean verboseFailures,
       boolean sandboxDebug) {
+    super(sandboxPath, sandboxExecRoot, verboseFailures);
     this.execRoot = execRoot;
     this.sandboxExecRoot = sandboxExecRoot;
-    this.argumentsFilePath =
-        sandboxExecRoot.getParentDirectory().getRelative(sandboxExecRoot.getBaseName() + ".params");
-    this.writablePaths = writablePaths;
+    this.argumentsFilePath = sandboxPath.getRelative("linux-sandbox.params");
+    this.writableDirs = writableDirs;
     this.inaccessiblePaths = inaccessiblePaths;
-    this.verboseFailures = verboseFailures;
     this.sandboxDebug = sandboxDebug;
   }
 
@@ -123,9 +95,21 @@
     return true;
   }
 
+  @Override
+  protected Command getCommand(
+      List<String> spawnArguments, Map<String, String> env, int timeout, boolean allowNetwork)
+      throws IOException {
+    writeConfig(timeout, allowNetwork);
 
-  protected void runPreparation(int timeout, boolean allowNetwork) throws IOException {
+    List<String> commandLineArgs = new ArrayList<>(3 + spawnArguments.size());
+    commandLineArgs.add(execRoot.getRelative("_bin/linux-sandbox").getPathString());
+    commandLineArgs.add("@" + argumentsFilePath.getPathString());
+    commandLineArgs.add("--");
+    commandLineArgs.addAll(spawnArguments);
+    return new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile());
+  }
 
+  private void writeConfig(int timeout, boolean allowNetwork) throws IOException {
     List<String> fileArgs = new ArrayList<>();
 
     if (sandboxDebug) {
@@ -143,12 +127,9 @@
     }
 
     // Create all needed directories.
-    for (Path writablePath : writablePaths) {
+    for (Path writablePath : writableDirs) {
       fileArgs.add("-w");
       fileArgs.add(writablePath.getPathString());
-      if (writablePath.startsWith(sandboxExecRoot)) {
-        FileSystemUtils.createDirectoryAndParents(writablePath);
-      }
     }
 
     for (Path inaccessiblePath : inaccessiblePaths) {
@@ -163,134 +144,4 @@
 
     FileSystemUtils.writeLinesAs(argumentsFilePath, StandardCharsets.ISO_8859_1, fileArgs);
   }
-
-  protected List<String> runCommandLineArgs(List<String> spawnArguments, int timeout) {
-    List<String> commandLineArgs = new ArrayList<>();
-
-    commandLineArgs.add(execRoot.getRelative("_bin/linux-sandbox").getPathString());
-
-    commandLineArgs.add("@" + argumentsFilePath.getPathString());
-
-    commandLineArgs.add("--");
-    commandLineArgs.addAll(spawnArguments);
-
-    return commandLineArgs;
-  }
-
-  /**
-   * Runs given
-   *
-   * @param spawnArguments - arguments of spawn to run inside the sandbox
-   * @param env - environment to run sandbox in
-   * @param outErr - error output to capture sandbox's and command's stderr
-   * @param outputs - files to extract from the sandbox, paths are relative to the exec root @throws
-   *     ExecException
-   */
-  public void run(
-      List<String> spawnArguments,
-      Map<String, String> env,
-      FileOutErr outErr,
-      Map<PathFragment, Path> inputs,
-      Collection<PathFragment> outputs,
-      int timeout,
-      boolean allowNetwork)
-      throws IOException, ExecException {
-    createFileSystem(inputs, outputs);
-
-    runPreparation(timeout, allowNetwork);
-
-    List<String> commandLineArgs = runCommandLineArgs(spawnArguments, timeout);
-    Command cmd =
-        new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile());
-
-    try {
-      cmd.execute(
-          /* stdin */ new byte[] {},
-          Command.NO_OBSERVER,
-          outErr.getOutputStream(),
-          outErr.getErrorStream(),
-          /* killSubprocessOnInterrupt */ true);
-    } catch (CommandException e) {
-      boolean timedOut = false;
-      if (e instanceof AbnormalTerminationException) {
-        TerminationStatus status =
-            ((AbnormalTerminationException) e).getResult().getTerminationStatus();
-        timedOut = !status.exited() && (status.getTerminatingSignal() == 14 /* SIGALRM */);
-      }
-      String message =
-          CommandFailureUtils.describeCommandFailure(
-              verboseFailures, commandLineArgs, env, sandboxExecRoot.getPathString());
-      throw new UserExecException(message, e, timedOut);
-    } finally {
-      copyOutputs(outputs);
-    }
-  }
-
-  protected void createFileSystem(Map<PathFragment, Path> inputs, Collection<PathFragment> outputs)
-      throws IOException {
-    Set<Path> createdDirs = new HashSet<>();
-    FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot);
-    createParentDirectoriesForInputs(createdDirs, inputs.keySet());
-    createSymlinksForInputs(inputs);
-    createDirectoriesForOutputs(createdDirs, outputs);
-  }
-
-  /**
-   * No input can be a child of another input, because otherwise we might try to create a symlink
-   * below another symlink we created earlier - which means we'd actually end up writing somewhere
-   * in the workspace.
-   *
-   * <p>If all inputs were regular files, this situation could naturally not happen - but
-   * unfortunately, we might get the occasional action that has directories in its inputs.
-   *
-   * <p>Creating all parent directories first ensures that we can safely create symlinks to
-   * directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested
-   * once we start creating the symlinks for all inputs.
-   */
-  private void createParentDirectoriesForInputs(Set<Path> createdDirs, Set<PathFragment> inputs)
-      throws IOException {
-    for (PathFragment inputPath : inputs) {
-      Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory();
-      Preconditions.checkArgument(dir.startsWith(sandboxExecRoot));
-      FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir);
-    }
-  }
-
-  private void createSymlinksForInputs(Map<PathFragment, Path> inputs) throws IOException {
-    // All input files are relative to the execroot.
-    for (Entry<PathFragment, Path> entry : inputs.entrySet()) {
-      Path key = sandboxExecRoot.getRelative(entry.getKey());
-      key.createSymbolicLink(entry.getValue());
-    }
-  }
-
-  /** Prepare the output directories in the sandbox. */
-  private void createDirectoriesForOutputs(Set<Path> createdDirs, Collection<PathFragment> outputs)
-      throws IOException {
-    for (PathFragment output : outputs) {
-      FileSystemUtils.createDirectoryAndParentsWithCache(
-          createdDirs, sandboxExecRoot.getRelative(output.getParentDirectory()));
-      FileSystemUtils.createDirectoryAndParentsWithCache(
-          createdDirs, execRoot.getRelative(output.getParentDirectory()));
-    }
-  }
-
-  protected void copyOutputs(Collection<PathFragment> outputs) throws IOException {
-    for (PathFragment output : outputs) {
-      Path source = sandboxExecRoot.getRelative(output);
-      if (source.isFile() || source.isSymbolicLink()) {
-        Path target = execRoot.getRelative(output);
-        Files.move(source.getPathFile(), target.getPathFile());
-      }
-    }
-  }
-
-  public void cleanup() throws IOException {
-    if (sandboxExecRoot.exists()) {
-      FileSystemUtils.deleteTree(sandboxExecRoot);
-    }
-    if (argumentsFilePath.exists()) {
-      argumentsFilePath.delete();
-    }
-  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
index 962d24a..f984af7 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
@@ -11,16 +11,11 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.io.Files;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
-import com.google.devtools.build.lib.actions.ActionInput;
-import com.google.devtools.build.lib.actions.ActionInputHelper;
-import com.google.devtools.build.lib.actions.ActionStatusMessage;
-import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ExecutionStrategy;
@@ -28,60 +23,48 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 import com.google.devtools.build.lib.actions.UserExecException;
-import com.google.devtools.build.lib.analysis.AnalysisUtils;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
-import com.google.devtools.build.lib.cmdline.Label;
-import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
-import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
-import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
 import com.google.devtools.build.lib.util.Preconditions;
-import com.google.devtools.build.lib.util.io.FileOutErr;
-import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
-import java.io.File;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
 
-/**
- * Strategy that uses sandboxing to execute a process.
- */
+/** Strategy that uses sandboxing to execute a process. */
 @ExecutionStrategy(
   name = {"sandboxed"},
   contextType = SpawnActionContext.class
 )
-public class LinuxSandboxedStrategy implements SpawnActionContext {
+public class LinuxSandboxedStrategy extends SandboxStrategy {
   private static Boolean sandboxingSupported = null;
 
   public static boolean isSupported(CommandEnvironment env) {
     if (sandboxingSupported == null) {
-      // Currently LinuxSandboxRunner support <= LinuxAlmostSandboxRunner support
       sandboxingSupported =
-          LinuxAlmostSandboxRunner.isSupported(env) || LinuxSandboxRunner.isSupported(env);
+          ProcessWrapperRunner.isSupported(env) || LinuxSandboxRunner.isSupported(env);
     }
     return sandboxingSupported.booleanValue();
   }
 
-  private final ExecutorService backgroundWorkers;
-
   private final BuildRequest buildRequest;
   private final SandboxOptions sandboxOptions;
   private final BlazeDirectories blazeDirs;
   private final Path execRoot;
+  private final ExecutorService backgroundWorkers;
   private final boolean verboseFailures;
-  private final UUID uuid = UUID.randomUUID();
-  private final AtomicInteger execCounter = new AtomicInteger();
   private final String productName;
   private final boolean fullySupported;
 
+  private final UUID uuid = UUID.randomUUID();
+  private final AtomicInteger execCounter = new AtomicInteger();
+
   LinuxSandboxedStrategy(
       BuildRequest buildRequest,
       BlazeDirectories blazeDirs,
@@ -89,6 +72,7 @@
       boolean verboseFailures,
       String productName,
       boolean fullySupported) {
+    super(blazeDirs, verboseFailures, buildRequest.getOptions(SandboxOptions.class));
     this.buildRequest = buildRequest;
     this.sandboxOptions = buildRequest.getOptions(SandboxOptions.class);
     this.blazeDirs = blazeDirs;
@@ -109,317 +93,69 @@
 
     // Certain actions can't run remotely or in a sandbox - pass them on to the standalone strategy.
     if (!spawn.isRemotable()) {
-      StandaloneSpawnStrategy standaloneStrategy =
-          Preconditions.checkNotNull(executor.getContext(StandaloneSpawnStrategy.class));
-      standaloneStrategy.exec(spawn, actionExecutionContext);
+      SandboxHelpers.fallbackToNonSandboxedExecution(spawn, actionExecutionContext, executor);
       return;
     }
 
-    if (executor.reportsSubcommands()) {
-      executor.reportSubcommand(
-          Label.print(spawn.getOwner().getLabel()) + " [" + spawn.getResourceOwner().prettyPrint()
-              + "]", spawn.asShellCommand(executor.getExecRoot()));
-    }
-
-    executor
-        .getEventBus()
-        .post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), "sandbox"));
-
-    FileOutErr outErr = actionExecutionContext.getFileOutErr();
-
-    // The execId is a unique ID just for this invocation of "exec".
-    String execId = uuid + "-" + execCounter.getAndIncrement();
+    SandboxHelpers.reportSubcommand(executor, spawn);
+    SandboxHelpers.postActionStatusMessage(executor, spawn);
 
     // Each invocation of "exec" gets its own sandbox.
-    Path sandboxExecRoot =
-        blazeDirs.getOutputBase().getRelative(productName + "-sandbox").getRelative(execId);
+    Path sandboxPath = SandboxHelpers.getSandboxRoot(blazeDirs, productName, uuid, execCounter);
+    Path sandboxExecRoot = sandboxPath.getRelative("execroot");
 
-    // Gather all necessary mounts for the sandbox.
-    Map<PathFragment, Path> mounts;
-    try {
-      mounts = getMounts(spawn, actionExecutionContext);
-    } catch (IllegalArgumentException | IOException e) {
-      throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e);
-    }
-
-    Map<String, String> env = new HashMap<>(spawn.getEnvironment());
-
-    ImmutableSet<Path> writablePaths = getWritablePaths(sandboxExecRoot, env);
-    ImmutableList<Path> inaccessiblePaths = getInaccessiblePaths();
-
-    int timeout = getTimeout(spawn);
-
-    ImmutableSet.Builder<PathFragment> outputFiles = ImmutableSet.builder();
-    for (PathFragment optionalOutput : spawn.getOptionalOutputFiles()) {
-      Preconditions.checkArgument(!optionalOutput.isAbsolute());
-      outputFiles.add(optionalOutput);
-    }
-    for (ActionInput output : spawn.getOutputFiles()) {
-      outputFiles.add(new PathFragment(output.getExecPathString()));
-    }
+    Set<Path> writableDirs = getWritableDirs(sandboxExecRoot, spawn.getEnvironment());
 
     try {
-      final LinuxSandboxRunner runner;
+      // Build the execRoot for the sandbox.
+      SymlinkedExecRoot symlinkedExecRoot = new SymlinkedExecRoot(sandboxExecRoot);
+      ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn);
+      symlinkedExecRoot.createFileSystem(
+          getMounts(spawn, actionExecutionContext), outputs, writableDirs);
+
+      final SandboxRunner runner;
       if (fullySupported) {
         runner =
             new LinuxSandboxRunner(
                 execRoot,
+                sandboxPath,
                 sandboxExecRoot,
-                writablePaths,
-                inaccessiblePaths,
+                getWritableDirs(sandboxExecRoot, spawn.getEnvironment()),
+                getInaccessiblePaths(),
                 verboseFailures,
                 sandboxOptions.sandboxDebug);
       } else {
-        // Then LinuxAlmostSandboxRunner must be supported
-        runner =
-            new LinuxAlmostSandboxRunner(
-                execRoot,
-                sandboxExecRoot,
-                writablePaths,
-                inaccessiblePaths,
-                verboseFailures,
-                sandboxOptions.sandboxDebug);
+        runner = new ProcessWrapperRunner(execRoot, sandboxPath, sandboxExecRoot, verboseFailures);
       }
       try {
         runner.run(
             spawn.getArguments(),
-            env,
-            outErr,
-            mounts,
-            outputFiles.build(),
-            timeout,
+            spawn.getEnvironment(),
+            actionExecutionContext.getFileOutErr(),
+            SandboxHelpers.getTimeout(spawn),
             SandboxHelpers.shouldAllowNetwork(buildRequest, spawn));
       } finally {
-        // Due to the Linux kernel behavior, if we try to remove the sandbox too quickly after the
-        // process has exited, we get "Device busy" errors because some of the mounts have not yet
-        // been undone. A second later it usually works. We will just clean the old sandboxes up
-        // using a background worker.
-        backgroundWorkers.execute(
-            new Runnable() {
-              @Override
-              public void run() {
-                try {
-                  while (!Thread.currentThread().isInterrupted()) {
-                    try {
-                      runner.cleanup();
-                      return;
-                    } catch (IOException e2) {
-                      // Sleep & retry.
-                      Thread.sleep(250);
-                    }
-                  }
-                } catch (InterruptedException e) {
-                  // Exit.
-                }
-              }
-            });
+        symlinkedExecRoot.copyOutputs(execRoot, outputs);
+        if (!sandboxOptions.sandboxDebug) {
+          SandboxHelpers.lazyCleanup(backgroundWorkers, runner);
+        }
       }
     } catch (IOException e) {
       throw new UserExecException("I/O error during sandboxed execution", e);
     }
   }
 
-  private int getTimeout(Spawn spawn) throws ExecException {
-    String timeoutStr = spawn.getExecutionInfo().get("timeout");
-    if (timeoutStr != null) {
-      try {
-        return Integer.parseInt(timeoutStr);
-      } catch (NumberFormatException e) {
-        throw new UserExecException("Could not parse timeout", e);
-      }
-    }
-    return -1;
-  }
-
-  /** Gets the list of directories that the spawn will assume to be writable. */
-  private ImmutableSet<Path> getWritablePaths(Path sandboxExecRoot, Map<String, String> env) {
-    ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder();
-    // We have to make the TEST_TMPDIR directory writable if it is specified.
-    if (env.containsKey("TEST_TMPDIR")) {
-      Path testTmpDir = sandboxExecRoot.getRelative(env.get("TEST_TMPDIR"));
-      writablePaths.add(testTmpDir);
-      env.put("TEST_TMPDIR", testTmpDir.getPathString());
-    }
-    return writablePaths.build();
-  }
-
-  private ImmutableList<Path> getInaccessiblePaths() {
-    ImmutableList.Builder<Path> inaccessiblePaths = ImmutableList.builder();
-    for (String path : sandboxOptions.sandboxBlockPath) {
-      inaccessiblePaths.add(blazeDirs.getFileSystem().getPath(path));
-    }
-    return inaccessiblePaths.build();
-  }
-
   private Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext)
-      throws IOException, ExecException {
-    Map<PathFragment, Path> mounts = new HashMap<>();
-    mountRunfilesFromManifests(mounts, spawn);
-    mountRunfilesFromSuppliers(mounts, spawn);
-    mountFilesFromFilesetManifests(mounts, spawn, executionContext);
-    mountInputs(mounts, spawn, executionContext);
-    return mounts;
-  }
-
-  /** Mount all runfiles that the spawn needs as specified in its runfiles manifests. */
-  private void mountRunfilesFromManifests(Map<PathFragment, Path> mounts, Spawn spawn)
-      throws IOException, ExecException {
-    for (Map.Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
-      String manifestFilePath = manifest.getValue().getPath().getPathString();
-      Preconditions.checkState(!manifest.getKey().isAbsolute());
-      PathFragment targetDirectory = manifest.getKey();
-
-      parseManifestFile(
-          blazeDirs.getFileSystem(),
-          mounts,
-          targetDirectory,
-          new File(manifestFilePath),
-          false,
-          "");
+      throws ExecException {
+    try {
+      Map<PathFragment, Path> mounts = new HashMap<>();
+      mountRunfilesFromManifests(mounts, spawn);
+      mountRunfilesFromSuppliers(mounts, spawn);
+      mountFilesFromFilesetManifests(mounts, spawn, executionContext);
+      mountInputs(mounts, spawn, executionContext);
+      return mounts;
+    } catch (IllegalArgumentException | IOException e) {
+      throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e);
     }
   }
-
-  /** Mount all files that the spawn needs as specified in its fileset manifests. */
-  private void mountFilesFromFilesetManifests(
-      Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext executionContext)
-      throws IOException, ExecException {
-    final FilesetActionContext filesetContext =
-        executionContext.getExecutor().getContext(FilesetActionContext.class);
-    for (Artifact fileset : spawn.getFilesetManifests()) {
-      File manifestFile =
-          new File(
-              execRoot.getPathString(),
-              AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()).getPathString());
-      PathFragment targetDirectory = fileset.getExecPath();
-
-      parseManifestFile(
-          blazeDirs.getFileSystem(),
-          mounts,
-          targetDirectory,
-          manifestFile,
-          true,
-          filesetContext.getWorkspaceName());
-    }
-  }
-
-  /** A parser for the MANIFEST files used by Filesets and runfiles. */
-  static void parseManifestFile(
-      FileSystem fs,
-      Map<PathFragment, Path> mounts,
-      PathFragment targetDirectory,
-      File manifestFile,
-      boolean isFilesetManifest,
-      String workspaceName)
-      throws IOException, ExecException {
-    int lineNum = 0;
-    for (String line : Files.readLines(manifestFile, StandardCharsets.UTF_8)) {
-      if (isFilesetManifest && (++lineNum % 2 == 0)) {
-        continue;
-      }
-      if (line.isEmpty()) {
-        continue;
-      }
-
-      String[] fields = line.trim().split(" ");
-
-      // The "target" field is always a relative path that is to be interpreted in this way:
-      // (1) If this is a fileset manifest and our workspace name is not empty, the first segment
-      // of each "target" path must be the workspace name, which is then stripped before further
-      // processing.
-      // (2) The "target" path is then appended to the "targetDirectory", which is a path relative
-      // to the execRoot. Together, this results in the full path in the execRoot in which place a
-      // symlink referring to "source" has to be created (see below).
-      PathFragment targetPath;
-      if (isFilesetManifest) {
-        PathFragment targetPathFragment = new PathFragment(fields[0]);
-        if (!workspaceName.isEmpty()) {
-          if (!targetPathFragment.getSegment(0).equals(workspaceName)) {
-            throw new EnvironmentalExecException(
-                "Fileset manifest line must start with workspace name");
-          }
-          targetPathFragment = targetPathFragment.subFragment(1, targetPathFragment.segmentCount());
-        }
-        targetPath = targetDirectory.getRelative(targetPathFragment);
-      } else {
-        targetPath = targetDirectory.getRelative(fields[0]);
-      }
-
-      // The "source" field, if it exists, is always an absolute path and may point to any file in
-      // the filesystem (it is not limited to files in the workspace or execroot).
-      Path source;
-      switch (fields.length) {
-        case 1:
-          source = fs.getPath("/dev/null");
-          break;
-        case 2:
-          source = fs.getPath(fields[1]);
-          break;
-        default:
-          throw new IllegalStateException("'" + line + "' splits into more than 2 parts");
-      }
-
-      mounts.put(targetPath, source);
-    }
-  }
-
-  /** Mount all runfiles that the spawn needs as specified via its runfiles suppliers. */
-  private void mountRunfilesFromSuppliers(Map<PathFragment, Path> mounts, Spawn spawn)
-      throws IOException {
-    Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings =
-        spawn.getRunfilesSupplier().getMappings();
-    for (Map.Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
-        rootsAndMappings.entrySet()) {
-      PathFragment root = rootAndMappings.getKey();
-      if (root.isAbsolute()) {
-        root = root.relativeTo(execRoot.asFragment());
-      }
-      for (Map.Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
-        Artifact sourceArtifact = mapping.getValue();
-        PathFragment source =
-            (sourceArtifact != null) ? sourceArtifact.getExecPath() : new PathFragment("/dev/null");
-
-        Preconditions.checkArgument(!mapping.getKey().isAbsolute());
-        PathFragment target = root.getRelative(mapping.getKey());
-        mounts.put(target, execRoot.getRelative(source));
-      }
-    }
-  }
-
-  /** Mount all inputs of the spawn. */
-  private void mountInputs(
-      Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext actionExecutionContext) {
-    List<ActionInput> inputs =
-        ActionInputHelper.expandArtifacts(
-            spawn.getInputFiles(), actionExecutionContext.getArtifactExpander());
-
-    if (spawn.getResourceOwner() instanceof CppCompileAction) {
-      CppCompileAction action = (CppCompileAction) spawn.getResourceOwner();
-      if (action.shouldScanIncludes()) {
-        inputs.addAll(action.getAdditionalInputs());
-      }
-    }
-
-    for (ActionInput input : inputs) {
-      if (input.getExecPathString().contains("internal/_middlemen/")) {
-        continue;
-      }
-      PathFragment mount = new PathFragment(input.getExecPathString());
-      mounts.put(mount, execRoot.getRelative(mount));
-    }
-  }
-
-  @Override
-  public boolean willExecuteRemotely(boolean remotable) {
-    return false;
-  }
-
-  @Override
-  public String toString() {
-    return "sandboxed";
-  }
-
-  @Override
-  public boolean shouldPropagateExecException() {
-    return verboseFailures && sandboxOptions.sandboxDebug;
-  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperRunner.java
new file mode 100644
index 0000000..0122b64
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperRunner.java
@@ -0,0 +1,68 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This runner runs process-wrapper inside a sandboxed execution root, which should work on most
+ * platforms and gives at least some isolation between running actions.
+ */
+final class ProcessWrapperRunner extends SandboxRunner {
+  private final Path execRoot;
+  private final Path sandboxExecRoot;
+
+  ProcessWrapperRunner(
+      Path execRoot, Path sandboxPath, Path sandboxExecRoot, boolean verboseFailures) {
+    super(sandboxPath, sandboxExecRoot, verboseFailures);
+    this.execRoot = execRoot;
+    this.sandboxExecRoot = sandboxExecRoot;
+  }
+
+  static boolean isSupported(CommandEnvironment commandEnv) {
+    PathFragment embeddedTool =
+        commandEnv
+            .getBlazeWorkspace()
+            .getBinTools()
+            .getExecPath("process-wrapper" + OsUtils.executableExtension());
+    if (embeddedTool == null) {
+      // The embedded tool does not exist, meaning that we don't support sandboxing (e.g., while
+      // bootstrapping).
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  protected Command getCommand(
+      List<String> spawnArguments, Map<String, String> env, int timeout, boolean allowNetwork) {
+    List<String> commandLineArgs = new ArrayList<>(5 + spawnArguments.size());
+    commandLineArgs.add(execRoot.getRelative("_bin/process-wrapper").getPathString());
+    commandLineArgs.add(Integer.toString(timeout));
+    commandLineArgs.add("5"); /* kill delay: give some time to print stacktraces and whatnot. */
+    commandLineArgs.add("-"); /* stdout. */
+    commandLineArgs.add("-"); /* stderr. */
+    commandLineArgs.addAll(spawnArguments);
+
+    return new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile());
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java
index 83edc09..cd468fd 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
 import com.google.common.collect.ImmutableMap;
@@ -26,7 +27,7 @@
  * {@link ActionContextConsumer} that requests the action contexts necessary for sandboxed
  * execution.
  */
-public class SandboxActionContextConsumer implements ActionContextConsumer {
+final class SandboxActionContextConsumer implements ActionContextConsumer {
 
   private final ImmutableMultimap<Class<? extends ActionContext>, String> contexts;
 
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
index 0c7d7aa..8153f1d 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
 import com.google.common.collect.ImmutableList;
@@ -27,7 +28,7 @@
 /**
  * Provides the sandboxed spawn strategy.
  */
-public class SandboxActionContextProvider extends ActionContextProvider {
+final class SandboxActionContextProvider extends ActionContextProvider {
 
   public static final String SANDBOX_NOT_SUPPORTED_MESSAGE =
       "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot "
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxExecRoot.java
new file mode 100644
index 0000000..0c392fd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxExecRoot.java
@@ -0,0 +1,56 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * SandboxExecRoot is responsible for making a list of input files available inside the directory,
+ * so that a process running inside the directory can access the files. It also handles moving the
+ * output files generated by the process out of the directory into a destination directory.
+ */
+public interface SandboxExecRoot {
+
+  /**
+   * Creates the sandboxed execution root, making all {@code inputs} available for reading, making
+   * sure that the parent directories of all {@code outputs} and that all {@code writableDirs}
+   * exist and can be written into.
+   *
+   * @param inputs  Specifies the input files to be made available inside the directory. The key of
+   *     the map is a relative path inside the sandboxed execution root, while the value is the
+   *     absolute path of the file in the filesystem.
+   * @param outputs  Output files that the process is expected to write to as relative paths to the
+   *     sandboxed execution root.
+   * @param writableDirs  Directories that the process may write into. All paths that are not inside
+   *     the sandboxed execution root must be ignored by this method.
+   * @throws IOException
+   */
+  void createFileSystem(
+      Map<PathFragment, Path> inputs, Collection<PathFragment> outputs, Set<Path> writableDirs)
+      throws IOException;
+
+  /**
+   * Moves all {@code outputs} to {@code execRoot} while keeping the directory structure.
+   *
+   * @throws IOException
+   */
+  void copyOutputs(Path execRoot, Collection<PathFragment> outputs) throws IOException;
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
index bd8c365..391748f 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
@@ -4,22 +4,109 @@
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
 //
-//    http://www.apache.org/licenses/LICENSE-2.0
+// http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSet.Builder;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionStatusMessage;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
 import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
+import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /** Helper methods that are shared by the different sandboxing strategies in this package. */
 final class SandboxHelpers {
 
+  static void lazyCleanup(ExecutorService backgroundWorkers, final SandboxRunner runner) {
+    // By deleting the sandbox directory in the background, we avoid having to wait for it to
+    // complete before returning from the action, which improves performance.
+    backgroundWorkers.execute(
+        new Runnable() {
+          @Override
+          public void run() {
+            try {
+              while (!Thread.currentThread().isInterrupted()) {
+                try {
+                  runner.cleanup();
+                  return;
+                } catch (IOException e2) {
+                  // Sleep & retry.
+                  Thread.sleep(250);
+                }
+              }
+            } catch (InterruptedException e) {
+              // Mark ourselves as interrupted and then exit.
+              Thread.currentThread().interrupt();
+            }
+          }
+        });
+  }
+
+  static void fallbackToNonSandboxedExecution(
+      Spawn spawn, ActionExecutionContext actionExecutionContext, Executor executor)
+      throws ExecException {
+    StandaloneSpawnStrategy standaloneStrategy =
+        Preconditions.checkNotNull(executor.getContext(StandaloneSpawnStrategy.class));
+    standaloneStrategy.exec(spawn, actionExecutionContext);
+  }
+
+  static void reportSubcommand(Executor executor, Spawn spawn) {
+    if (executor.reportsSubcommands()) {
+      executor.reportSubcommand(
+          Label.print(spawn.getOwner().getLabel())
+              + " ["
+              + spawn.getResourceOwner().prettyPrint()
+              + "]",
+          spawn.asShellCommand(executor.getExecRoot()));
+    }
+  }
+
+  static int getTimeout(Spawn spawn) throws ExecException {
+    String timeoutStr = spawn.getExecutionInfo().get("timeout");
+    if (timeoutStr != null) {
+      try {
+        return Integer.parseInt(timeoutStr);
+      } catch (NumberFormatException e) {
+        throw new UserExecException("Could not parse timeout", e);
+      }
+    }
+    return -1;
+  }
+
+  static ImmutableSet<PathFragment> getOutputFiles(Spawn spawn) {
+    Builder<PathFragment> outputFiles = ImmutableSet.builder();
+    for (PathFragment optionalOutput : spawn.getOptionalOutputFiles()) {
+      Preconditions.checkArgument(!optionalOutput.isAbsolute());
+      outputFiles.add(optionalOutput);
+    }
+    for (ActionInput output : spawn.getOutputFiles()) {
+      outputFiles.add(new PathFragment(output.getExecPathString()));
+    }
+    return outputFiles.build();
+  }
+
   static boolean shouldAllowNetwork(BuildRequest buildRequest, Spawn spawn) {
     // If we don't run tests, allow network access.
     if (!buildRequest.shouldRunTests()) {
@@ -42,4 +129,18 @@
 
     return false;
   }
+
+  static void postActionStatusMessage(Executor executor, Spawn spawn) {
+    executor
+        .getEventBus()
+        .post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), "sandbox"));
+  }
+
+  static Path getSandboxRoot(
+      BlazeDirectories blazeDirs, String productName, UUID uuid, AtomicInteger execCounter) {
+    return blazeDirs
+        .getOutputBase()
+        .getRelative(productName + "-sandbox")
+        .getRelative(uuid + "-" + execCounter.getAndIncrement());
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index 6b54a87..53c5c67 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
 import com.google.common.collect.ImmutableList;
@@ -33,7 +34,7 @@
 /**
  * This module provides the Sandbox spawn strategy.
  */
-public class SandboxModule extends BlazeModule {
+public final class SandboxModule extends BlazeModule {
   // Per-server state
   private ExecutorService backgroundWorkers;
 
@@ -69,9 +70,9 @@
 
   @Override
   public void beforeCommand(Command command, CommandEnvironment env) {
-    backgroundWorkers = Executors.newCachedThreadPool(new ThreadFactoryBuilder()
-        .setNameFormat("linux-sandbox-background-worker-%d")
-        .build());
+    backgroundWorkers =
+        Executors.newCachedThreadPool(
+            new ThreadFactoryBuilder().setNameFormat("sandbox-background-worker-%d").build());
     this.env = env;
     env.getEventBus().register(this);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
index f18792f..00ac908 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.devtools.build.lib.sandbox;
 
 import com.google.devtools.common.options.Option;
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java
new file mode 100644
index 0000000..bd9d09f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java
@@ -0,0 +1,120 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.shell.AbnormalTerminationException;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.shell.KillableObserver;
+import com.google.devtools.build.lib.shell.TerminationStatus;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/** A common interface of all sandbox runners, no matter which platform they're working on. */
+abstract class SandboxRunner {
+  private final Path sandboxPath;
+  private final boolean verboseFailures;
+  private final Path sandboxExecRoot;
+
+  SandboxRunner(Path sandboxPath, Path sandboxExecRoot, boolean verboseFailures) {
+    this.sandboxPath = sandboxPath;
+    this.sandboxExecRoot = sandboxExecRoot;
+    this.verboseFailures = verboseFailures;
+  }
+
+  /**
+   * Runs the command specified via {@code arguments} and {@code env} inside the sandbox.
+   *
+   * @param arguments - arguments of spawn to run inside the sandbox.
+   * @param environment - environment variables to pass to the spawn.
+   * @param outErr - error output to capture sandbox's and command's stderr
+   * @param timeout - after how many seconds should the process be killed
+   * @param allowNetwork - whether networking should be allowed for the process
+   */
+  void run(
+      List<String> arguments,
+      Map<String, String> environment,
+      OutErr outErr,
+      int timeout,
+      boolean allowNetwork)
+      throws IOException, ExecException {
+    Command cmd = getCommand(arguments, environment, timeout, allowNetwork);
+
+    try {
+      cmd.execute(
+          /* stdin */ new byte[] {},
+          getCommandObserver(timeout),
+          outErr.getOutputStream(),
+          outErr.getErrorStream(),
+          /* killSubprocessOnInterrupt */ true);
+    } catch (CommandException e) {
+      boolean timedOut = false;
+      if (e instanceof AbnormalTerminationException) {
+        TerminationStatus status =
+            ((AbnormalTerminationException) e).getResult().getTerminationStatus();
+        timedOut = !status.exited() && (status.getTerminatingSignal() == getSignalOnTimeout());
+      }
+      String message =
+          CommandFailureUtils.describeCommandFailure(
+              verboseFailures,
+              Arrays.asList(cmd.getCommandLineElements()),
+              environment,
+              sandboxExecRoot.getPathString());
+      throw new UserExecException(message, e, timedOut);
+    }
+  }
+
+  /**
+   * Returns the {@link Command} that the {@link #run} method will execute inside the sandbox.
+   *
+   * @param arguments - arguments of spawn to run inside the sandbox.
+   * @param environment - environment variables to pass to the spawn.
+   * @param timeout - after how many seconds should the process be killed
+   * @param allowNetwork - whether networking should be allowed for the process
+   */
+  protected abstract Command getCommand(
+      List<String> arguments, Map<String, String> environment, int timeout, boolean allowNetwork)
+      throws IOException;
+
+  /**
+   * Returns a {@link KillableObserver} that the {@link #run} method will use when executing the
+   * command returned by {@link #getCommand}.
+   */
+  protected KillableObserver getCommandObserver(int timeout) {
+    return Command.NO_OBSERVER;
+  }
+
+  /**
+   * Returns the signal code that the command returned by {@link #getCommand} exits with in case of
+   * a timeout.
+   */
+  protected int getSignalOnTimeout() {
+    return 14; /* SIGALRM */
+  }
+
+  void cleanup() throws IOException {
+    if (sandboxPath.exists()) {
+      FileSystemUtils.deleteTree(sandboxPath);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
new file mode 100644
index 0000000..645e669
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
@@ -0,0 +1,237 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSet.Builder;
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
+import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
+import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+/** Abstract common ancestor for sandbox strategies implementing the common parts. */
+abstract class SandboxStrategy implements SpawnActionContext {
+
+  private final BlazeDirectories blazeDirs;
+  private final Path execRoot;
+  private final boolean verboseFailures;
+  private final SandboxOptions sandboxOptions;
+
+  public SandboxStrategy(
+      BlazeDirectories blazeDirs, boolean verboseFailures, SandboxOptions sandboxOptions) {
+    this.blazeDirs = blazeDirs;
+    this.execRoot = blazeDirs.getExecRoot();
+    this.verboseFailures = verboseFailures;
+    this.sandboxOptions = sandboxOptions;
+  }
+
+  /** Gets the list of directories that the spawn will assume to be writable. */
+  protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) {
+    Builder<Path> writableDirs = ImmutableSet.builder();
+    // We have to make the TEST_TMPDIR directory writable if it is specified.
+    if (env.containsKey("TEST_TMPDIR")) {
+      writableDirs.add(sandboxExecRoot.getRelative(env.get("TEST_TMPDIR")));
+    }
+    return writableDirs.build();
+  }
+
+  protected ImmutableSet<Path> getInaccessiblePaths() {
+    ImmutableSet.Builder<Path> inaccessiblePaths = ImmutableSet.builder();
+    for (String path : sandboxOptions.sandboxBlockPath) {
+      inaccessiblePaths.add(blazeDirs.getFileSystem().getPath(path));
+    }
+    return inaccessiblePaths.build();
+  }
+
+  /** Mount all runfiles that the spawn needs as specified in its runfiles manifests. */
+  protected void mountRunfilesFromManifests(Map<PathFragment, Path> mounts, Spawn spawn)
+      throws IOException, ExecException {
+    for (Map.Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
+      String manifestFilePath = manifest.getValue().getPath().getPathString();
+      Preconditions.checkState(!manifest.getKey().isAbsolute());
+      PathFragment targetDirectory = manifest.getKey();
+
+      parseManifestFile(
+          blazeDirs.getFileSystem(),
+          mounts,
+          targetDirectory,
+          new File(manifestFilePath),
+          false,
+          "");
+    }
+  }
+
+  /** Mount all files that the spawn needs as specified in its fileset manifests. */
+  protected void mountFilesFromFilesetManifests(
+      Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext executionContext)
+      throws IOException, ExecException {
+    final FilesetActionContext filesetContext =
+        executionContext.getExecutor().getContext(FilesetActionContext.class);
+    for (Artifact fileset : spawn.getFilesetManifests()) {
+      File manifestFile =
+          new File(
+              execRoot.getPathString(),
+              AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()).getPathString());
+      PathFragment targetDirectory = fileset.getExecPath();
+
+      parseManifestFile(
+          blazeDirs.getFileSystem(),
+          mounts,
+          targetDirectory,
+          manifestFile,
+          true,
+          filesetContext.getWorkspaceName());
+    }
+  }
+
+  /** A parser for the MANIFEST files used by Filesets and runfiles. */
+  static void parseManifestFile(
+      FileSystem fs,
+      Map<PathFragment, Path> mounts,
+      PathFragment targetDirectory,
+      File manifestFile,
+      boolean isFilesetManifest,
+      String workspaceName)
+      throws IOException, ExecException {
+    int lineNum = 0;
+    for (String line : Files.readLines(manifestFile, StandardCharsets.UTF_8)) {
+      if (isFilesetManifest && (++lineNum % 2 == 0)) {
+        continue;
+      }
+      if (line.isEmpty()) {
+        continue;
+      }
+
+      String[] fields = line.trim().split(" ");
+
+      // The "target" field is always a relative path that is to be interpreted in this way:
+      // (1) If this is a fileset manifest and our workspace name is not empty, the first segment
+      // of each "target" path must be the workspace name, which is then stripped before further
+      // processing.
+      // (2) The "target" path is then appended to the "targetDirectory", which is a path relative
+      // to the execRoot. Together, this results in the full path in the execRoot in which place a
+      // symlink referring to "source" has to be created (see below).
+      PathFragment targetPath;
+      if (isFilesetManifest) {
+        PathFragment targetPathFragment = new PathFragment(fields[0]);
+        if (!workspaceName.isEmpty()) {
+          if (!targetPathFragment.getSegment(0).equals(workspaceName)) {
+            throw new EnvironmentalExecException(
+                "Fileset manifest line must start with workspace name");
+          }
+          targetPathFragment = targetPathFragment.subFragment(1, targetPathFragment.segmentCount());
+        }
+        targetPath = targetDirectory.getRelative(targetPathFragment);
+      } else {
+        targetPath = targetDirectory.getRelative(fields[0]);
+      }
+
+      // The "source" field, if it exists, is always an absolute path and may point to any file in
+      // the filesystem (it is not limited to files in the workspace or execroot).
+      Path source;
+      switch (fields.length) {
+        case 1:
+          source = fs.getPath("/dev/null");
+          break;
+        case 2:
+          source = fs.getPath(fields[1]);
+          break;
+        default:
+          throw new IllegalStateException("'" + line + "' splits into more than 2 parts");
+      }
+
+      mounts.put(targetPath, source);
+    }
+  }
+
+  /** Mount all runfiles that the spawn needs as specified via its runfiles suppliers. */
+  protected void mountRunfilesFromSuppliers(Map<PathFragment, Path> mounts, Spawn spawn)
+      throws IOException {
+    Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings =
+        spawn.getRunfilesSupplier().getMappings();
+    for (Map.Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
+        rootsAndMappings.entrySet()) {
+      PathFragment root = rootAndMappings.getKey();
+      if (root.isAbsolute()) {
+        root = root.relativeTo(execRoot.asFragment());
+      }
+      for (Map.Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
+        Artifact sourceArtifact = mapping.getValue();
+        PathFragment source =
+            (sourceArtifact != null) ? sourceArtifact.getExecPath() : new PathFragment("/dev/null");
+
+        Preconditions.checkArgument(!mapping.getKey().isAbsolute());
+        PathFragment target = root.getRelative(mapping.getKey());
+        mounts.put(target, execRoot.getRelative(source));
+      }
+    }
+  }
+
+  /** Mount all inputs of the spawn. */
+  protected void mountInputs(
+      Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext actionExecutionContext) {
+    List<ActionInput> inputs =
+        ActionInputHelper.expandArtifacts(
+            spawn.getInputFiles(), actionExecutionContext.getArtifactExpander());
+
+    if (spawn.getResourceOwner() instanceof CppCompileAction) {
+      CppCompileAction action = (CppCompileAction) spawn.getResourceOwner();
+      if (action.shouldScanIncludes()) {
+        inputs.addAll(action.getAdditionalInputs());
+      }
+    }
+
+    for (ActionInput input : inputs) {
+      if (input.getExecPathString().contains("internal/_middlemen/")) {
+        continue;
+      }
+      PathFragment mount = new PathFragment(input.getExecPathString());
+      mounts.put(mount, execRoot.getRelative(mount));
+    }
+  }
+
+  @Override
+  public boolean willExecuteRemotely(boolean remotable) {
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "sandboxed";
+  }
+
+  @Override
+  public boolean shouldPropagateExecException() {
+    return verboseFailures && sandboxOptions.sandboxDebug;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java
new file mode 100644
index 0000000..de0f31f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java
@@ -0,0 +1,115 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Creates an execRoot for a Spawn that contains input files as symlinks to their original
+ * destination.
+ */
+final class SymlinkedExecRoot implements SandboxExecRoot {
+
+  private final Path sandboxExecRoot;
+
+  public SymlinkedExecRoot(Path sandboxExecRoot) {
+    this.sandboxExecRoot = sandboxExecRoot;
+  }
+
+  @Override
+  public void createFileSystem(
+      Map<PathFragment, Path> inputs, Collection<PathFragment> outputs, Set<Path> writableDirs)
+      throws IOException {
+    Set<Path> createdDirs = new HashSet<>();
+    FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot);
+    createParentDirectoriesForInputs(createdDirs, inputs.keySet());
+    createSymlinksForInputs(inputs);
+    createWritableDirectories(createdDirs, writableDirs);
+    createDirectoriesForOutputs(createdDirs, outputs);
+  }
+
+  /**
+   * No input can be a child of another input, because otherwise we might try to create a symlink
+   * below another symlink we created earlier - which means we'd actually end up writing somewhere
+   * in the workspace.
+   *
+   * <p>If all inputs were regular files, this situation could naturally not happen - but
+   * unfortunately, we might get the occasional action that has directories in its inputs.
+   *
+   * <p>Creating all parent directories first ensures that we can safely create symlinks to
+   * directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested
+   * once we start creating the symlinks for all inputs.
+   */
+  private void createParentDirectoriesForInputs(Set<Path> createdDirs, Set<PathFragment> inputs)
+      throws IOException {
+    for (PathFragment inputPath : inputs) {
+      Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory();
+      Preconditions.checkArgument(dir.startsWith(sandboxExecRoot));
+      FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir);
+    }
+  }
+
+  private void createSymlinksForInputs(Map<PathFragment, Path> inputs) throws IOException {
+    // All input files are relative to the execroot.
+    for (Entry<PathFragment, Path> entry : inputs.entrySet()) {
+      Path key = sandboxExecRoot.getRelative(entry.getKey());
+      key.createSymbolicLink(entry.getValue());
+    }
+  }
+
+  private void createWritableDirectories(Set<Path> createdDirs, Set<Path> writableDirs)
+      throws IOException {
+    for (Path writablePath : writableDirs) {
+      if (writablePath.startsWith(sandboxExecRoot)) {
+        FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, writablePath);
+      }
+    }
+  }
+
+  /** Prepare the output directories in the sandbox. */
+  private void createDirectoriesForOutputs(Set<Path> createdDirs, Collection<PathFragment> outputs)
+      throws IOException {
+    for (PathFragment output : outputs) {
+      FileSystemUtils.createDirectoryAndParentsWithCache(
+          createdDirs, sandboxExecRoot.getRelative(output.getParentDirectory()));
+    }
+  }
+
+  /** Moves all {@code outputs} to {@code execRoot}. */
+  @Override
+  public void copyOutputs(Path execRoot, Collection<PathFragment> outputs) throws IOException {
+    Set<Path> createdDirs = new HashSet<>();
+    for (PathFragment output : outputs) {
+      Path source = sandboxExecRoot.getRelative(output);
+      if (source.isFile() || source.isSymbolicLink()) {
+        FileSystemUtils.createDirectoryAndParentsWithCache(
+            createdDirs, execRoot.getRelative(output.getParentDirectory()));
+
+        Path target = execRoot.getRelative(output);
+        Files.move(source.getPathFile(), target.getPathFile());
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
index 45cdafc..561942d 100644
--- a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
@@ -107,9 +107,12 @@
     args.addAll(spawn.getArguments());
 
     String cwd = executor.getExecRoot().getPathString();
-    Command cmd = new Command(args.toArray(new String[]{}),
-        locallyDeterminedEnv(spawn.getEnvironment()), new File(cwd),
-        OS.getCurrent() == OS.WINDOWS && timeoutSeconds >= 0 ? timeoutSeconds * 1000 : -1);
+    Command cmd =
+        new Command(
+            args.toArray(new String[] {}),
+            locallyDeterminedEnv(execRoot, productName, spawn.getEnvironment()),
+            new File(cwd),
+            OS.getCurrent() == OS.WINDOWS && timeoutSeconds >= 0 ? timeoutSeconds * 1000 : -1);
 
     FileOutErr outErr = actionExecutionContext.getFileOutErr();
     try {
@@ -158,7 +161,8 @@
    * @return the new environment, comprised of the old environment plus any new variables
    * @throws UserExecException if any variables dependent on system state could not be resolved
    */
-  public ImmutableMap<String, String> locallyDeterminedEnv(ImmutableMap<String, String> env)
+  public static ImmutableMap<String, String> locallyDeterminedEnv(
+      Path execRoot, String productName, ImmutableMap<String, String> env)
       throws UserExecException {
     // TODO(bazel-team): Remove apple-specific logic from this class.
     ImmutableMap.Builder<String, String> newEnvBuilder = ImmutableMap.builder();
@@ -168,7 +172,9 @@
     // should be explicitly set for build hermiticity.
     String developerDir = "";
     if (env.containsKey(AppleConfiguration.XCODE_VERSION_ENV_NAME)) {
-      developerDir = getDeveloperDir(env.get(AppleConfiguration.XCODE_VERSION_ENV_NAME));
+      developerDir =
+          getDeveloperDir(
+              execRoot, productName, env.get(AppleConfiguration.XCODE_VERSION_ENV_NAME));
       newEnvBuilder.put("DEVELOPER_DIR", developerDir);
     }
     if (env.containsKey(AppleConfiguration.APPLE_SDK_VERSION_ENV_NAME)) {
@@ -178,12 +184,15 @@
       }
       String iosSdkVersion = env.get(AppleConfiguration.APPLE_SDK_VERSION_ENV_NAME);
       String appleSdkPlatform = env.get(AppleConfiguration.APPLE_SDK_PLATFORM_ENV_NAME);
-      newEnvBuilder.put("SDKROOT", getSdkRootEnv(developerDir, iosSdkVersion, appleSdkPlatform));
+      newEnvBuilder.put(
+          "SDKROOT",
+          getSdkRootEnv(execRoot, productName, developerDir, iosSdkVersion, appleSdkPlatform));
     }
     return newEnvBuilder.build();
   }
 
-  private String getDeveloperDir(String xcodeVersion) throws UserExecException {
+  private static String getDeveloperDir(Path execRoot, String productName, String xcodeVersion)
+      throws UserExecException {
     if (OS.getCurrent() != OS.DARWIN) {
       throw new UserExecException(
           "Cannot locate xcode developer directory on non-darwin operating system");
@@ -192,8 +201,13 @@
         productName);
   }
 
-  private String getSdkRootEnv(String developerDir,
-      String iosSdkVersion, String appleSdkPlatform) throws UserExecException {
+  private static String getSdkRootEnv(
+      Path execRoot,
+      String productName,
+      String developerDir,
+      String iosSdkVersion,
+      String appleSdkPlatform)
+      throws UserExecException {
     if (OS.getCurrent() != OS.DARWIN) {
       throw new UserExecException("Cannot locate iOS SDK on non-darwin operating system");
     }