diff options
author | 2017-07-12 12:10:11 +0200 | |
---|---|---|
committer | 2017-07-12 16:02:39 +0200 | |
commit | 264f40fb42ce1390f9ee420f923b77ef3a82327f (patch) | |
tree | 11e16746189e6ee28b57c9294f8570715e4edb87 /src/main/java/com/google/devtools/build/lib | |
parent | 423a46a11acd109467573c3344a4dd49f211aea6 (diff) |
Rewrite all the sandbox strategy implementations
- Make use of existing abstractions like SpawnRunner and SpawnExecutionPolicy.
- Instead of having the *Strategy create a *Runner, and then call back into
SandboxStrategy, create a single SandboxContainer which contains the full
command line, environment, and everything needed to create and delete the
sandbox directory.
- Do all the work in SandboxStrategy, including creation and deletion of the
sandbox directory.
- Use SpawnResult instead of throwing, catching, and rethrowing.
- Simplify the control flow a bit.
PiperOrigin-RevId: 161644979
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib')
19 files changed, 659 insertions, 815 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ExecException.java b/src/main/java/com/google/devtools/build/lib/actions/ExecException.java index 9c81cc22a1..16f4550195 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/ExecException.java +++ b/src/main/java/com/google/devtools/build/lib/actions/ExecException.java @@ -43,12 +43,10 @@ package com.google.devtools.build.lib.actions; public abstract class ExecException extends Exception { private final boolean catastrophe; - private final boolean timedOut; public ExecException(String message, boolean catastrophe) { super(message); this.catastrophe = catastrophe; - this.timedOut = false; } public ExecException(String message) { @@ -58,31 +56,17 @@ public abstract class ExecException extends Exception { public ExecException(Throwable cause) { super(cause); this.catastrophe = false; - this.timedOut = false; } public ExecException(String message, Throwable cause, boolean catastrophe) { super(message + ": " + cause.getMessage(), cause); this.catastrophe = catastrophe; - this.timedOut = false; } public ExecException(String message, Throwable cause) { this(message, cause, false); } - public ExecException(String message, boolean catastrophe, boolean timedOut) { - super(message); - this.catastrophe = catastrophe; - this.timedOut = timedOut; - } - - public ExecException(String message, Throwable cause, boolean catastrophe, boolean timedOut) { - super(message, cause); - this.catastrophe = catastrophe; - this.timedOut = timedOut; - } - /** * Catastrophic exceptions should stop the build, even if --keep_going. */ @@ -120,6 +104,6 @@ public abstract class ExecException extends Exception { * Tells if the execution exception was thrown because of the execution timing out. */ public boolean hasTimedOut() { - return timedOut; + return false; } } diff --git a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java index bdb52ab64e..5b06d3ecc3 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java +++ b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java @@ -28,18 +28,10 @@ public class UserExecException extends ExecException { super(cause); } - public UserExecException(String message, boolean timedOut) { - super(message, false, timedOut); - } - public UserExecException(String message, Throwable cause) { super(message, cause); } - public UserExecException(String message, Throwable cause, boolean timedOut) { - super(message, cause, false, timedOut); - } - @Override public ActionExecutionException toActionExecutionException(String messagePrefix, boolean verboseFailures, Action action) { diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java index 70a6ae4db3..4c9bd95372 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java +++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java @@ -50,6 +50,11 @@ public class SpawnExecException extends ExecException { } @Override + public boolean hasTimedOut() { + return getSpawnResult().status() == Status.TIMEOUT; + } + + @Override public ActionExecutionException toActionExecutionException(String messagePrefix, boolean verboseFailures, Action action) { TerminationStatus status = new TerminationStatus( diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java index a95f144ecc..9445171656 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java +++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java @@ -15,6 +15,7 @@ package com.google.devtools.build.lib.exec; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionInputFileCache; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.util.io.FileOutErr; @@ -122,6 +123,13 @@ public interface SpawnRunner { /** The input file cache for this specific spawn. */ ActionInputFileCache getActionInputFileCache(); + /** An artifact expander. */ + // TODO(ulfjack): This is only used for the sandbox runners to compute a set of empty + // directories. We shouldn't have this and the getInputMapping method; maybe there's a way to + // unify the two? Alternatively, maybe the input mapping should (optionally?) contain + // directories? Or maybe we need a separate method to return the set of directories? + ArtifactExpander getArtifactExpander(); + /** * All implementations must call this method before writing to the provided stdout / stderr or * to any of the output file locations. This method is used to coordinate - implementations diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnStrategy.java index 9a1c415589..ea5e43a7d2 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnStrategy.java @@ -18,6 +18,7 @@ import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionInputFileCache; import com.google.devtools.build.lib.actions.ActionStatusMessage; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; import com.google.devtools.build.lib.actions.EnvironmentalExecException; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; @@ -87,6 +88,11 @@ final class RemoteSpawnStrategy implements SpawnActionContext { } @Override + public ArtifactExpander getArtifactExpander() { + return actionExecutionContext.getArtifactExpander(); + } + + @Override public void lockOutputFiles() throws InterruptedException { // This is only needed for the dynamic spawn strategy, which we still need to actually // implement. 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 deleted file mode 100644 index a30d96de72..0000000000 --- a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxRunner.java +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2014 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 static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.collect.ImmutableMap; -import com.google.common.io.ByteStreams; -import com.google.devtools.build.lib.runtime.CommandEnvironment; -import com.google.devtools.build.lib.shell.Command; -import com.google.devtools.build.lib.shell.CommandException; -import com.google.devtools.build.lib.vfs.Path; -import java.io.BufferedWriter; -import java.io.File; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.util.ArrayList; -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. - */ -final class DarwinSandboxRunner extends SandboxRunner { - private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec"; - - private final Path sandboxExecRoot; - private final Path sandboxConfigPath; - private final Set<Path> writableDirs; - private final Set<Path> inaccessiblePaths; - - DarwinSandboxRunner( - Path sandboxPath, - Path sandboxExecRoot, - Set<Path> writableDirs, - Set<Path> inaccessiblePaths, - boolean verboseFailures) { - super(verboseFailures); - this.sandboxExecRoot = sandboxExecRoot; - this.sandboxConfigPath = sandboxPath.getRelative("sandbox.sb"); - this.writableDirs = writableDirs; - this.inaccessiblePaths = inaccessiblePaths; - } - - static boolean isSupported(CommandEnvironment cmdEnv) { - if (!ProcessWrapperRunner.isSupported(cmdEnv)) { - return false; - } - - List<String> args = new ArrayList<>(); - args.add(SANDBOX_EXEC); - args.add("-p"); - args.add("(version 1) (allow default)"); - args.add("/usr/bin/true"); - - ImmutableMap<String, String> env = ImmutableMap.of(); - File cwd = new File("/usr/bin"); - - Command cmd = new Command(args.toArray(new String[0]), env, cwd); - try { - cmd.execute( - /* stdin */ new byte[] {}, - Command.NO_OBSERVER, - ByteStreams.nullOutputStream(), - ByteStreams.nullOutputStream(), - /* killSubprocessOnInterrupt */ true); - } catch (CommandException e) { - return false; - } - - return true; - } - - @Override - protected Command getCommand( - CommandEnvironment cmdEnv, - List<String> arguments, - Map<String, String> env, - int timeout, - boolean allowNetwork, - boolean useFakeHostname, - boolean useFakeUsername) - throws IOException { - writeConfig(allowNetwork); - - List<String> commandLineArgs = new ArrayList<>(); - commandLineArgs.add(SANDBOX_EXEC); - commandLineArgs.add("-f"); - commandLineArgs.add(sandboxConfigPath.getPathString()); - commandLineArgs.addAll(ProcessWrapperRunner.getCommandLine(cmdEnv, arguments, timeout)); - return new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile()); - } - - private void writeConfig(boolean allowNetwork) throws IOException { - try (PrintWriter out = - new PrintWriter( - new BufferedWriter( - new OutputStreamWriter(sandboxConfigPath.getOutputStream(), UTF_8)))) { - // Note: In Apple's sandbox configuration language, the *last* matching rule wins. - out.println("(version 1)"); - out.println("(debug deny)"); - out.println("(allow default)"); - - if (!allowNetwork) { - out.println("(deny network*)"); - out.println("(allow network* (local ip \"localhost:*\"))"); - out.println("(allow network* (remote ip \"localhost:*\"))"); - } - - // By default, everything is read-only. - out.println("(deny file-write*)"); - - out.println("(allow file-write*"); - for (Path path : writableDirs) { - out.println(" (subpath \"" + path.getPathString() + "\")"); - } - out.println(")"); - - if (!inaccessiblePaths.isEmpty()) { - out.println("(deny file-read*"); - // The sandbox configuration file is not part of a cache key and sandbox-exec doesn't care - // about ordering of paths in expressions, so it's fine if the iteration order is random. - for (Path inaccessiblePath : inaccessiblePaths) { - out.println(" (subpath \"" + inaccessiblePath + "\")"); - } - out.println(")"); - } - } - } -} 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 9d8e5acfe9..89b961edf9 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 @@ -16,15 +16,16 @@ package com.google.devtools.build.lib.sandbox; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.devtools.build.lib.actions.ActionExecutionContext; -import com.google.devtools.build.lib.actions.ActionStatusMessage; +import com.google.common.io.ByteStreams; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.buildtool.BuildRequest; -import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.exec.SpawnResult; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; import com.google.devtools.build.lib.exec.apple.XCodeLocalEnvProvider; import com.google.devtools.build.lib.exec.local.LocalEnvProvider; import com.google.devtools.build.lib.runtime.CommandEnvironment; @@ -33,14 +34,19 @@ import com.google.devtools.build.lib.shell.CommandException; import com.google.devtools.build.lib.shell.CommandResult; import com.google.devtools.build.lib.util.OS; 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.BufferedWriter; +import java.io.File; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.TimeUnit; /** Strategy that uses sandboxing to execute a process, for Darwin */ @ExecutionStrategy( @@ -48,16 +54,44 @@ import java.util.concurrent.atomic.AtomicReference; contextType = SpawnActionContext.class ) public class DarwinSandboxedStrategy extends SandboxStrategy { + private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec"; public static boolean isSupported(CommandEnvironment cmdEnv) { - return OS.getCurrent() == OS.DARWIN && DarwinSandboxRunner.isSupported(cmdEnv); + if (OS.getCurrent() != OS.DARWIN) { + return false; + } + if (!ProcessWrapperRunner.isSupported(cmdEnv)) { + return false; + } + + List<String> args = new ArrayList<>(); + args.add(SANDBOX_EXEC); + args.add("-p"); + args.add("(version 1) (allow default)"); + args.add("/usr/bin/true"); + + ImmutableMap<String, String> env = ImmutableMap.of(); + File cwd = new File("/usr/bin"); + + Command cmd = new Command(args.toArray(new String[0]), env, cwd); + try { + cmd.execute( + /* stdin */ new byte[] {}, + Command.NO_OBSERVER, + ByteStreams.nullOutputStream(), + ByteStreams.nullOutputStream(), + /* killSubprocessOnInterrupt */ true); + } catch (CommandException e) { + return false; + } + + return true; } private final Path execRoot; - private final boolean sandboxDebug; - private final boolean verboseFailures; + private final boolean allowNetwork; private final String productName; - private final SpawnInputExpander spawnInputExpander; + private final Path processWrapper; /** * The set of directories that always should be writable, independent of the Spawn itself. @@ -76,16 +110,14 @@ public class DarwinSandboxedStrategy extends SandboxStrategy { ImmutableSet<Path> alwaysWritableDirs) { super( cmdEnv, - buildRequest, sandboxBase, verboseFailures, buildRequest.getOptions(SandboxOptions.class)); this.execRoot = cmdEnv.getExecRoot(); - this.sandboxDebug = buildRequest.getOptions(SandboxOptions.class).sandboxDebug; - this.verboseFailures = verboseFailures; + this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions()); this.productName = productName; this.alwaysWritableDirs = alwaysWritableDirs; - this.spawnInputExpander = new SpawnInputExpander(false); + this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv); this.localEnvProvider = new XCodeLocalEnvProvider(); } @@ -161,16 +193,8 @@ public class DarwinSandboxedStrategy extends SandboxStrategy { } @Override - protected void actuallyExec( - Spawn spawn, - ActionExecutionContext actionExecutionContext, - AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) - throws ExecException, InterruptedException, IOException { - actionExecutionContext - .getEventBus() - .post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), "darwin-sandbox")); - SandboxHelpers.reportSubcommand(actionExecutionContext, spawn); - + protected SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) + throws ExecException, IOException, InterruptedException { // Each invocation of "exec" gets its own sandbox. Path sandboxPath = getSandboxRoot(); Path sandboxExecRoot = sandboxPath.getRelative("execroot").getRelative(execRoot.getBaseName()); @@ -178,50 +202,91 @@ public class DarwinSandboxedStrategy extends SandboxStrategy { Map<String, String> spawnEnvironment = localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName); - HashSet<Path> writableDirs = new HashSet<>(alwaysWritableDirs); + final HashSet<Path> writableDirs = new HashSet<>(alwaysWritableDirs); ImmutableSet<Path> extraWritableDirs = getWritableDirs(sandboxExecRoot, spawnEnvironment); writableDirs.addAll(extraWritableDirs); - SymlinkedExecRoot symlinkedExecRoot = new SymlinkedExecRoot(sandboxExecRoot); ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); - symlinkedExecRoot.createFileSystem( - SandboxHelpers.getInputFiles( - spawnInputExpander, this.execRoot, spawn, actionExecutionContext), + + final Path sandboxConfigPath = sandboxPath.getRelative("sandbox.sb"); + int timeoutSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(policy.getTimeoutMillis()); + List<String> arguments = computeCommandLine( + spawn, timeoutSeconds, sandboxConfigPath); + Map<String, String> environment = + localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName); + + boolean allowNetworkForThisSpawn = allowNetwork || SandboxHelpers.shouldAllowNetwork(spawn); + SandboxedSpawn sandbox = new SymlinkedSandboxedSpawn( + sandboxPath, + sandboxExecRoot, + arguments, + environment, + SandboxHelpers.getInputFiles(spawn, policy, execRoot), outputs, - writableDirs); + writableDirs) { + @Override + public void createFileSystem() throws IOException { + super.createFileSystem(); + writeConfig( + sandboxConfigPath, writableDirs, getInaccessiblePaths(), allowNetworkForThisSpawn); + } + }; + return runSpawn(spawn, sandbox, policy, execRoot, timeoutSeconds); + } - // This will add the resolved versions of the spawn-dependant writable paths (e.g. its execroot - // or TEST_TMPDIR) to the set, now that they have been created by the SymlinkedExecRoot. - for (Path extraWritableDir : extraWritableDirs) { - addPathToSetIfExists(writableDirs, extraWritableDir); - } + private List<String> computeCommandLine(Spawn spawn, int timeoutSeconds, Path sandboxConfigPath) { + List<String> commandLineArgs = new ArrayList<>(); + commandLineArgs.add(SANDBOX_EXEC); + commandLineArgs.add("-f"); + commandLineArgs.add(sandboxConfigPath.getPathString()); + commandLineArgs.addAll( + ProcessWrapperRunner.getCommandLine(processWrapper, spawn.getArguments(), timeoutSeconds)); + return commandLineArgs; + } - DarwinSandboxRunner runner = - new DarwinSandboxRunner( - sandboxPath, sandboxExecRoot, writableDirs, getInaccessiblePaths(), verboseFailures); - try { - runSpawn( - spawn, - actionExecutionContext, - spawnEnvironment, - symlinkedExecRoot, - outputs, - runner, - writeOutputFiles); - } finally { - if (!sandboxDebug) { - try { - FileSystemUtils.deleteTree(sandboxPath); - } catch (IOException e) { - // This usually means that the Spawn itself exited, but still has children running that - // we couldn't wait for, which now block deletion of the sandbox directory. On Linux this - // should never happen, as we use PID namespaces and where they are not available the - // subreaper feature to make sure all children have been reliably killed before returning, - // but on other OS this might not always work. The SandboxModule will try to delete them - // again when the build is all done, at which point it hopefully works, so let's just go - // on here. + private void writeConfig( + Path sandboxConfigPath, + Set<Path> writableDirs, + Set<Path> inaccessiblePaths, + boolean allowNetwork) throws IOException { + try (PrintWriter out = + new PrintWriter( + new BufferedWriter( + new OutputStreamWriter(sandboxConfigPath.getOutputStream(), UTF_8)))) { + // Note: In Apple's sandbox configuration language, the *last* matching rule wins. + out.println("(version 1)"); + out.println("(debug deny)"); + out.println("(allow default)"); + + if (!allowNetwork) { + out.println("(deny network*)"); + out.println("(allow network* (local ip \"localhost:*\"))"); + out.println("(allow network* (remote ip \"localhost:*\"))"); + } + + // By default, everything is read-only. + out.println("(deny file-write*)"); + + out.println("(allow file-write*"); + for (Path path : writableDirs) { + out.println(" (subpath \"" + path.getPathString() + "\")"); + } + out.println(")"); + + if (!inaccessiblePaths.isEmpty()) { + out.println("(deny file-read*"); + // The sandbox configuration file is not part of a cache key and sandbox-exec doesn't care + // about ordering of paths in expressions, so it's fine if the iteration order is random. + for (Path inaccessiblePath : inaccessiblePaths) { + out.println(" (subpath \"" + inaccessiblePath + "\")"); } + out.println(")"); } } } + + @Override + protected String getName() { + return "darwin-sandbox"; + } } 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 deleted file mode 100644 index a960a81bc4..0000000000 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2014 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.ByteStreams; -import com.google.devtools.build.lib.runtime.CommandEnvironment; -import com.google.devtools.build.lib.shell.Command; -import com.google.devtools.build.lib.shell.CommandException; -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.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -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. - */ -final class LinuxSandboxRunner extends SandboxRunner { - private static final String LINUX_SANDBOX = "linux-sandbox" + OsUtils.executableExtension(); - - private final Path sandboxExecRoot; - private final Set<Path> writableDirs; - private final Set<Path> tmpfsPaths; - // a <target, source> mapping of paths to bind mount - private final Map<Path, Path> bindMounts; - private final boolean sandboxDebug; - - LinuxSandboxRunner( - Path sandboxExecRoot, - Set<Path> writableDirs, - Set<Path> tmpfsPaths, - Map<Path, Path> bindMounts, - boolean verboseFailures, - boolean sandboxDebug) { - super(verboseFailures); - this.sandboxExecRoot = sandboxExecRoot; - this.writableDirs = writableDirs; - this.tmpfsPaths = tmpfsPaths; - this.bindMounts = bindMounts; - this.sandboxDebug = sandboxDebug; - } - - static boolean isSupported(CommandEnvironment cmdEnv) { - Path embeddedTool = getLinuxSandbox(cmdEnv); - if (embeddedTool == null) { - // The embedded tool does not exist, meaning that we don't support sandboxing (e.g., while - // bootstrapping). - return false; - } - - Path execRoot = cmdEnv.getExecRoot(); - - List<String> args = new ArrayList<>(); - args.add(embeddedTool.getPathString()); - args.add("--"); - args.add("/bin/true"); - - ImmutableMap<String, String> env = ImmutableMap.of(); - File cwd = execRoot.getPathFile(); - - Command cmd = new Command(args.toArray(new String[0]), env, cwd); - try { - cmd.execute( - /* stdin */ new byte[] {}, - Command.NO_OBSERVER, - ByteStreams.nullOutputStream(), - ByteStreams.nullOutputStream(), - /* killSubprocessOnInterrupt */ true); - } catch (CommandException e) { - return false; - } - - return true; - } - - private static Path getLinuxSandbox(CommandEnvironment cmdEnv) { - PathFragment execPath = cmdEnv.getBlazeWorkspace().getBinTools().getExecPath(LINUX_SANDBOX); - return execPath != null ? cmdEnv.getExecRoot().getRelative(execPath) : null; - } - - @Override - protected Command getCommand( - CommandEnvironment cmdEnv, - List<String> spawnArguments, - Map<String, String> env, - int timeout, - boolean allowNetwork, - boolean useFakeHostname, - boolean useFakeUsername) - throws IOException { - List<String> commandLineArgs = new ArrayList<>(); - commandLineArgs.add(getLinuxSandbox(cmdEnv).getPathString()); - - if (sandboxDebug) { - commandLineArgs.add("-D"); - } - - // Kill the process after a timeout. - if (timeout != -1) { - commandLineArgs.add("-T"); - commandLineArgs.add(Integer.toString(timeout)); - } - - // Create all needed directories. - for (Path writablePath : writableDirs) { - commandLineArgs.add("-w"); - commandLineArgs.add(writablePath.getPathString()); - } - - for (Path tmpfsPath : tmpfsPaths) { - commandLineArgs.add("-e"); - commandLineArgs.add(tmpfsPath.getPathString()); - } - - for (ImmutableMap.Entry<Path, Path> bindMount : bindMounts.entrySet()) { - commandLineArgs.add("-M"); - commandLineArgs.add(bindMount.getValue().getPathString()); - - // The file is mounted in a custom location inside the sandbox. - if (!bindMount.getKey().equals(bindMount.getValue())) { - commandLineArgs.add("-m"); - commandLineArgs.add(bindMount.getKey().getPathString()); - } - } - - if (!allowNetwork) { - // Block network access out of the namespace. - commandLineArgs.add("-N"); - } - - if (useFakeHostname) { - // Use a fake hostname ("localhost") inside the sandbox. - commandLineArgs.add("-H"); - } - - if (useFakeUsername) { - // Use a fake username ("nobody") inside the sandbox. - commandLineArgs.add("-U"); - } - - commandLineArgs.add("--"); - 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/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java index 1d6e5ca4fd..f0d3031e66 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 @@ -17,8 +17,7 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; -import com.google.devtools.build.lib.actions.ActionExecutionContext; -import com.google.devtools.build.lib.actions.ActionStatusMessage; +import com.google.common.io.ByteStreams; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.Spawn; @@ -26,19 +25,25 @@ import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.buildtool.BuildRequest; -import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.exec.SpawnResult; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; import com.google.devtools.build.lib.runtime.CommandEnvironment; +import com.google.devtools.build.lib.shell.Command; +import com.google.devtools.build.lib.shell.CommandException; import com.google.devtools.build.lib.util.OS; 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 com.google.devtools.build.lib.vfs.Symlinks; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.TimeUnit; /** Strategy that uses sandboxing to execute a process. */ @ExecutionStrategy( @@ -46,16 +51,54 @@ import java.util.concurrent.atomic.AtomicReference; contextType = SpawnActionContext.class ) public class LinuxSandboxedStrategy extends SandboxStrategy { + private static final String LINUX_SANDBOX = "linux-sandbox"; public static boolean isSupported(CommandEnvironment cmdEnv) { - return OS.getCurrent() == OS.LINUX && LinuxSandboxRunner.isSupported(cmdEnv); + if (OS.getCurrent() != OS.LINUX) { + return false; + } + Path embeddedTool = getLinuxSandbox(cmdEnv); + if (embeddedTool == null) { + // The embedded tool does not exist, meaning that we don't support sandboxing (e.g., while + // bootstrapping). + return false; + } + + Path execRoot = cmdEnv.getExecRoot(); + + List<String> args = new ArrayList<>(); + args.add(embeddedTool.getPathString()); + args.add("--"); + args.add("/bin/true"); + + ImmutableMap<String, String> env = ImmutableMap.of(); + File cwd = execRoot.getPathFile(); + + Command cmd = new Command(args.toArray(new String[0]), env, cwd); + try { + cmd.execute( + /* stdin */ new byte[] {}, + Command.NO_OBSERVER, + ByteStreams.nullOutputStream(), + ByteStreams.nullOutputStream(), + /* killSubprocessOnInterrupt */ true); + } catch (CommandException e) { + return false; + } + + return true; + } + + private static Path getLinuxSandbox(CommandEnvironment cmdEnv) { + PathFragment execPath = cmdEnv.getBlazeWorkspace().getBinTools().getExecPath(LINUX_SANDBOX); + return execPath != null ? cmdEnv.getExecRoot().getRelative(execPath) : null; } private final SandboxOptions sandboxOptions; private final BlazeDirectories blazeDirs; private final Path execRoot; - private final boolean verboseFailures; - private final SpawnInputExpander spawnInputExpander; + private final boolean allowNetwork; + private final Path linuxSandbox; private final Path inaccessibleHelperFile; private final Path inaccessibleHelperDir; @@ -68,15 +111,14 @@ public class LinuxSandboxedStrategy extends SandboxStrategy { Path inaccessibleHelperDir) { super( cmdEnv, - buildRequest, sandboxBase, verboseFailures, buildRequest.getOptions(SandboxOptions.class)); - this.sandboxOptions = buildRequest.getOptions(SandboxOptions.class); + this.sandboxOptions = cmdEnv.getOptions().getOptions(SandboxOptions.class); this.blazeDirs = cmdEnv.getDirectories(); this.execRoot = cmdEnv.getExecRoot(); - this.verboseFailures = verboseFailures; - this.spawnInputExpander = new SpawnInputExpander(false); + this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions()); + this.linuxSandbox = getLinuxSandbox(cmdEnv); this.inaccessibleHelperFile = inaccessibleHelperFile; this.inaccessibleHelperDir = inaccessibleHelperDir; } @@ -109,61 +151,101 @@ public class LinuxSandboxedStrategy extends SandboxStrategy { } @Override - protected void actuallyExec( - Spawn spawn, - ActionExecutionContext actionExecutionContext, - AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) + protected SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) throws IOException, ExecException, InterruptedException { - actionExecutionContext - .getEventBus() - .post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), "linux-sandbox")); - SandboxHelpers.reportSubcommand(actionExecutionContext, spawn); - // Each invocation of "exec" gets its own sandbox. Path sandboxPath = getSandboxRoot(); Path sandboxExecRoot = sandboxPath.getRelative("execroot").getRelative(execRoot.getBaseName()); Set<Path> writableDirs = getWritableDirs(sandboxExecRoot, spawn.getEnvironment()); - SymlinkedExecRoot symlinkedExecRoot = new SymlinkedExecRoot(sandboxExecRoot); ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); - symlinkedExecRoot.createFileSystem( - SandboxHelpers.getInputFiles(spawnInputExpander, execRoot, spawn, actionExecutionContext), + int timeoutSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(policy.getTimeoutMillis()); + List<String> arguments = computeCommandLine( + spawn, + timeoutSeconds, + linuxSandbox, + writableDirs, + getTmpfsPaths(), + getReadOnlyBindMounts(blazeDirs, sandboxExecRoot), + allowNetwork || SandboxHelpers.shouldAllowNetwork(spawn)); + + SandboxedSpawn sandbox = new SymlinkedSandboxedSpawn( + sandboxPath, + sandboxExecRoot, + arguments, + spawn.getEnvironment(), + SandboxHelpers.getInputFiles(spawn, policy, execRoot), outputs, writableDirs); + return runSpawn(spawn, sandbox, policy, execRoot, timeoutSeconds); + } - SandboxRunner runner = - new LinuxSandboxRunner( - sandboxExecRoot, - writableDirs, - getTmpfsPaths(), - getReadOnlyBindMounts(blazeDirs, sandboxExecRoot), - verboseFailures, - sandboxOptions.sandboxDebug); + private List<String> computeCommandLine( + Spawn spawn, + int timeoutSeconds, + Path linuxSandbox, + Set<Path> writableDirs, + Set<Path> tmpfsPaths, + Map<Path, Path> bindMounts, + boolean allowNetwork) { + List<String> commandLineArgs = new ArrayList<>(); + commandLineArgs.add(linuxSandbox.getPathString()); - try { - runSpawn( - spawn, - actionExecutionContext, - spawn.getEnvironment(), - symlinkedExecRoot, - outputs, - runner, - writeOutputFiles); - } finally { - if (!sandboxOptions.sandboxDebug) { - try { - FileSystemUtils.deleteTree(sandboxPath); - } catch (IOException e) { - // This usually means that the Spawn itself exited, but still has children running that - // we couldn't wait for, which now block deletion of the sandbox directory. On Linux this - // should never happen, as we use PID namespaces and where they are not available the - // subreaper feature to make sure all children have been reliably killed before returning, - // but on other OS this might not always work. The SandboxModule will try to delete them - // again when the build is all done, at which point it hopefully works, so let's just go - // on here. - } + if (sandboxOptions.sandboxDebug) { + commandLineArgs.add("-D"); + } + + // Kill the process after a timeout. + if (timeoutSeconds != -1) { + commandLineArgs.add("-T"); + commandLineArgs.add(Integer.toString(timeoutSeconds)); + } + + // Create all needed directories. + for (Path writablePath : writableDirs) { + commandLineArgs.add("-w"); + commandLineArgs.add(writablePath.getPathString()); + } + + for (Path tmpfsPath : tmpfsPaths) { + commandLineArgs.add("-e"); + commandLineArgs.add(tmpfsPath.getPathString()); + } + + for (ImmutableMap.Entry<Path, Path> bindMount : bindMounts.entrySet()) { + commandLineArgs.add("-M"); + commandLineArgs.add(bindMount.getValue().getPathString()); + + // The file is mounted in a custom location inside the sandbox. + if (!bindMount.getKey().equals(bindMount.getValue())) { + commandLineArgs.add("-m"); + commandLineArgs.add(bindMount.getKey().getPathString()); } } + + if (!allowNetwork) { + // Block network access out of the namespace. + commandLineArgs.add("-N"); + } + + if (sandboxOptions.sandboxFakeHostname) { + // Use a fake hostname ("localhost") inside the sandbox. + commandLineArgs.add("-H"); + } + + if (sandboxOptions.sandboxFakeUsername) { + // Use a fake username ("nobody") inside the sandbox. + commandLineArgs.add("-U"); + } + + commandLineArgs.add("--"); + commandLineArgs.addAll(spawn.getArguments()); + return commandLineArgs; + } + + @Override + protected String getName() { + return "linux-sandbox"; } @Override 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 index e6a3061da5..1dace949e9 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperRunner.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperRunner.java @@ -15,28 +15,19 @@ 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 { +final class ProcessWrapperRunner { private static final String PROCESS_WRAPPER = "process-wrapper" + OsUtils.executableExtension(); - private final Path sandboxExecRoot; - - ProcessWrapperRunner(Path sandboxExecRoot, boolean verboseFailures) { - super(verboseFailures); - this.sandboxExecRoot = sandboxExecRoot; - } - static boolean isSupported(CommandEnvironment cmdEnv) { // We can only use this runner, if the process-wrapper exists in the embedded tools. // This might not always be the case, e.g. while bootstrapping. @@ -49,23 +40,10 @@ final class ProcessWrapperRunner extends SandboxRunner { return execPath != null ? cmdEnv.getExecRoot().getRelative(execPath) : null; } - @Override - protected Command getCommand( - CommandEnvironment cmdEnv, - List<String> spawnArguments, - Map<String, String> env, - int timeout, - boolean allowNetwork, - boolean useFakeHostname, - boolean useFakeUsername) { - List<String> commandLineArgs = getCommandLine(cmdEnv, spawnArguments, timeout); - return new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile()); - } - static List<String> getCommandLine( - CommandEnvironment cmdEnv, List<String> spawnArguments, int timeout) { + Path processWrapper, List<String> spawnArguments, int timeout) { List<String> commandLineArgs = new ArrayList<>(5 + spawnArguments.size()); - commandLineArgs.add(getProcessWrapper(cmdEnv).getPathString()); + commandLineArgs.add(processWrapper.getPathString()); commandLineArgs.add("--timeout=" + timeout); commandLineArgs.add("--kill_delay=5"); /* give some time to print stacktraces and whatnot. */ commandLineArgs.addAll(spawnArguments); diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedStrategy.java index 0ccdd989e3..6fed1505ae 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedStrategy.java @@ -14,26 +14,22 @@ package com.google.devtools.build.lib.sandbox; -import com.google.common.collect.ImmutableSet; -import com.google.devtools.build.lib.actions.ActionExecutionContext; -import com.google.devtools.build.lib.actions.ActionStatusMessage; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.buildtool.BuildRequest; -import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.exec.SpawnResult; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; import com.google.devtools.build.lib.exec.apple.XCodeLocalEnvProvider; import com.google.devtools.build.lib.exec.local.LocalEnvProvider; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.util.OS; -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.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.TimeUnit; /** Strategy that uses sandboxing to execute a process. */ @ExecutionStrategy( @@ -46,11 +42,9 @@ public class ProcessWrapperSandboxedStrategy extends SandboxStrategy { return OS.isPosixCompatible() && ProcessWrapperRunner.isSupported(cmdEnv); } - private final SandboxOptions sandboxOptions; private final Path execRoot; - private final boolean verboseFailures; private final String productName; - private final SpawnInputExpander spawnInputExpander; + private final Path processWrapper; private final LocalEnvProvider localEnvProvider; ProcessWrapperSandboxedStrategy( @@ -61,73 +55,43 @@ public class ProcessWrapperSandboxedStrategy extends SandboxStrategy { String productName) { super( cmdEnv, - buildRequest, sandboxBase, verboseFailures, buildRequest.getOptions(SandboxOptions.class)); - this.sandboxOptions = buildRequest.getOptions(SandboxOptions.class); this.execRoot = cmdEnv.getExecRoot(); - this.verboseFailures = verboseFailures; this.productName = productName; - this.spawnInputExpander = new SpawnInputExpander(false); + this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv); this.localEnvProvider = OS.getCurrent() == OS.DARWIN ? new XCodeLocalEnvProvider() : LocalEnvProvider.UNMODIFIED; } @Override - protected void actuallyExec( - Spawn spawn, - ActionExecutionContext actionExecutionContext, - AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) - throws ExecException, InterruptedException, IOException { - actionExecutionContext - .getEventBus() - .post( - ActionStatusMessage.runningStrategy( - spawn.getResourceOwner(), "processwrapper-sandbox")); - SandboxHelpers.reportSubcommand(actionExecutionContext, spawn); - + protected SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) + throws ExecException, IOException, InterruptedException { // Each invocation of "exec" gets its own sandbox. Path sandboxPath = getSandboxRoot(); Path sandboxExecRoot = sandboxPath.getRelative("execroot").getRelative(execRoot.getBaseName()); - Map<String, String> spawnEnvironment = + int timeoutSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(policy.getTimeoutMillis()); + List<String> arguments = + ProcessWrapperRunner.getCommandLine(processWrapper, spawn.getArguments(), timeoutSeconds); + Map<String, String> environment = localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName); - Set<Path> writableDirs = getWritableDirs(sandboxExecRoot, spawn.getEnvironment()); - SymlinkedExecRoot symlinkedExecRoot = new SymlinkedExecRoot(sandboxExecRoot); - ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); - symlinkedExecRoot.createFileSystem( - SandboxHelpers.getInputFiles( - spawnInputExpander, this.execRoot, spawn, actionExecutionContext), - outputs, - writableDirs); + SandboxedSpawn sandbox = new SymlinkedSandboxedSpawn( + sandboxPath, + sandboxExecRoot, + arguments, + environment, + SandboxHelpers.getInputFiles(spawn, policy, execRoot), + SandboxHelpers.getOutputFiles(spawn), + getWritableDirs(sandboxExecRoot, spawn.getEnvironment())); + return runSpawn(spawn, sandbox, policy, execRoot, timeoutSeconds); + } - SandboxRunner runner = new ProcessWrapperRunner(sandboxExecRoot, verboseFailures); - try { - runSpawn( - spawn, - actionExecutionContext, - spawnEnvironment, - symlinkedExecRoot, - outputs, - runner, - writeOutputFiles); - } finally { - if (!sandboxOptions.sandboxDebug) { - try { - FileSystemUtils.deleteTree(sandboxPath); - } catch (IOException e) { - // This usually means that the Spawn itself exited, but still has children running that - // we couldn't wait for, which now block deletion of the sandbox directory. On Linux this - // should never happen, as we use PID namespaces and where they are not available the - // subreaper feature to make sure all children have been reliably killed before returning, - // but on other OS this might not always work. The SandboxModule will try to delete them - // again when the build is all done, at which point it hopefully works, so let's just go - // on here. - } - } - } + @Override + protected String getName() { + return "processwrapper-sandbox"; } } 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 e7d605fb37..e26f37bb43 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 @@ -19,16 +19,18 @@ 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.Artifact; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; -import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; 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 com.google.devtools.common.options.OptionsProvider; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -47,12 +49,6 @@ public final class SandboxHelpers { standaloneStrategy.exec(spawn, actionExecutionContext); } - static void reportSubcommand(ActionExecutionContext actionExecutionContext, Spawn spawn) { - if (actionExecutionContext.reportsSubcommands()) { - actionExecutionContext.reportSubcommand(spawn); - } - } - /** * Returns the inputs of a Spawn as a map of PathFragments relative to an execRoot to paths in the * host filesystem where the input files can be found. @@ -69,6 +65,30 @@ public final class SandboxHelpers { executionContext.getArtifactExpander(), executionContext.getActionInputFileCache(), executionContext.getContext(FilesetActionContext.class)); + return postProcess(inputMap, spawn, executionContext.getArtifactExpander(), execRoot); + } + + /** + * Returns the inputs of a Spawn as a map of PathFragments relative to an execRoot to paths in the + * host filesystem where the input files can be found. + */ + public static Map<PathFragment, Path> getInputFiles( + Spawn spawn, + SpawnExecutionPolicy policy, + Path execRoot) + throws IOException { + return postProcess(policy.getInputMapping(), spawn, policy.getArtifactExpander(), execRoot); + } + + /** + * Returns the inputs of a Spawn as a map of PathFragments relative to an execRoot to paths in the + * host filesystem where the input files can be found. + */ + private static Map<PathFragment, Path> postProcess( + Map<PathFragment, ActionInput> inputMap, + Spawn spawn, + ArtifactExpander artifactExpander, + Path execRoot) { // SpawnInputExpander#getInputMapping uses ArtifactExpander#expandArtifacts to expand // middlemen and tree artifacts, which expands empty tree artifacts to no entry. However, // actions that accept TreeArtifacts as inputs generally expect that the empty directory is @@ -77,7 +97,7 @@ public final class SandboxHelpers { for (ActionInput input : spawn.getInputFiles()) { if (input instanceof Artifact && ((Artifact) input).isTreeArtifact()) { List<Artifact> containedArtifacts = new ArrayList<>(); - executionContext.getArtifactExpander().expand((Artifact) input, containedArtifacts); + artifactExpander.expand((Artifact) input, containedArtifacts); // Attempting to mount a non-empty directory results in ERR_DIRECTORY_NOT_EMPTY, so we // only mount empty TreeArtifacts as directories. if (containedArtifacts.isEmpty()) { @@ -105,17 +125,24 @@ public final class SandboxHelpers { return outputFiles.build(); } - static boolean shouldAllowNetwork(BuildRequest buildRequest, Spawn spawn) { + /** + * Returns true if the build options are set in a way that requires network access for all + * actions. This is separate from {@link #shouldAllowNetwork(Spawn)} to avoid having to keep a + * reference to the full set of build options (and also for performance, since this only needs to + * be checked once-per-build). + */ + static boolean shouldAllowNetwork(OptionsProvider buildOptions) { // Allow network access, when --java_debug is specified, otherwise we can't connect to the // remote debug server of the test. This intentionally overrides the "block-network" execution // tag. - if (buildRequest + return buildOptions .getOptions(BuildConfiguration.Options.class) .testArguments - .contains("--wrapper_script_flag=--debug")) { - return true; - } + .contains("--wrapper_script_flag=--debug"); + } + /** Returns true if this specific spawn requires network access. */ + static boolean shouldAllowNetwork(Spawn spawn) { // If the Spawn requests to block network access, do so. if (spawn.getExecutionInfo().containsKey("block-network")) { return false; 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 36a2aa9c01..d9949fdf4a 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 @@ -15,14 +15,19 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.common.options.Converter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParsingException; import com.google.devtools.common.options.proto.OptionFilters.OptionEffectTag; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** Options for sandboxed execution. */ @@ -173,4 +178,19 @@ public class SandboxOptions extends OptionsBase { help = "Add additional path pair to mount in sandbox." ) public List<ImmutableMap.Entry<String, String>> sandboxAdditionalMounts; + + public ImmutableSet<Path> getInaccessiblePaths(FileSystem fs) { + List<Path> inaccessiblePaths = new ArrayList<>(); + for (String path : sandboxBlockPath) { + Path blockedPath = fs.getPath(path); + try { + inaccessiblePaths.add(blockedPath.resolveSymbolicLinks()); + } catch (IOException e) { + // It's OK to block access to an invalid symlink. In this case we'll just make the symlink + // itself inaccessible, instead of the target, though. + inaccessiblePaths.add(blockedPath); + } + } + return ImmutableSet.copyOf(inaccessiblePaths); + } } 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 deleted file mode 100644 index 9591402024..0000000000 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java +++ /dev/null @@ -1,159 +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.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.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 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 static final String SANDBOX_DEBUG_SUGGESTION = - "\n\nUse --sandbox_debug to see verbose messages from the sandbox"; - - private final boolean verboseFailures; - - SandboxRunner(boolean verboseFailures) { - this.verboseFailures = verboseFailures; - } - - /** - * Runs the command specified via {@code arguments} and {@code env} inside the sandbox. - * - * @param cmdEnv - the current command environment. - * @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. - * @param sandboxDebug - whether debugging message should be printed. - * @param useFakeHostname - whether the hostname should be set to 'localhost' inside the sandbox. - * @param useFakeUsername - whether the username should be set to 'nobody' inside the sandbox. - */ - void run( - CommandEnvironment cmdEnv, - List<String> arguments, - Map<String, String> environment, - OutErr outErr, - int timeout, - boolean allowNetwork, - boolean sandboxDebug, - boolean useFakeHostname, - boolean useFakeUsername) - throws ExecException { - Command cmd; - try { - cmd = - getCommand( - cmdEnv, - arguments, - environment, - timeout, - allowNetwork, - useFakeHostname, - useFakeUsername); - } catch (IOException e) { - throw new UserExecException("I/O error during sandboxed execution", e); - } - - TerminationStatus status = null; - try { - cmd.execute( - /* stdin */ new byte[] {}, - getCommandObserver(timeout), - outErr.getOutputStream(), - outErr.getErrorStream(), - /* killSubprocessOnInterrupt */ true); - } catch (CommandException e) { - boolean timedOut = false; - if (e instanceof AbnormalTerminationException) { - status = ((AbnormalTerminationException) e).getResult().getTerminationStatus(); - timedOut = !status.exited() && (status.getTerminatingSignal() == getSignalOnTimeout()); - } - - String statusMessage = status + " [sandboxed]"; - - if (!verboseFailures) { - // Simplest possible error message. - throw new UserExecException(statusMessage, e, timedOut); - } - - List<String> commandList = arguments; - if (sandboxDebug) { - // When using --sandbox_debug, show the entire command-line including the part where we call - // the sandbox helper binary. - commandList = Arrays.asList(cmd.getCommandLineElements()); - } - - String commandFailureMessage = - CommandFailureUtils.describeCommandFailure(true, commandList, environment, null); - - if (!sandboxDebug) { - commandFailureMessage += SANDBOX_DEBUG_SUGGESTION; - } - - throw new UserExecException(commandFailureMessage, e, timedOut); - } - } - - /** - * Returns the {@link Command} that the {@link #run} method will execute inside the sandbox. - * - * @param cmdEnv - the current command environment. - * @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. - * @param useFakeHostname - whether the hostname should be set to 'localhost' inside the sandbox. - * @param useFakeUsername - whether the username should be set to 'nobody' inside the sandbox. - */ - protected abstract Command getCommand( - CommandEnvironment cmdEnv, - List<String> arguments, - Map<String, String> environment, - int timeout, - boolean allowNetwork, - boolean useFakeHostname, - boolean useFakeUsername) - 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 */ - } -} 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 index fe6a581fb2..513b8d4df8 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java @@ -14,11 +14,16 @@ package com.google.devtools.build.lib.sandbox; +import static java.nio.charset.StandardCharsets.UTF_8; + import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionExecutionMetadata; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ActionInputFileCache; import com.google.devtools.build.lib.actions.ActionStatusMessage; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ResourceManager; import com.google.devtools.build.lib.actions.ResourceManager.ResourceHandle; @@ -27,59 +32,57 @@ import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.Spawns; import com.google.devtools.build.lib.actions.UserExecException; -import com.google.devtools.build.lib.buildtool.BuildRequest; -import com.google.devtools.build.lib.events.Event; -import com.google.devtools.build.lib.events.EventHandler; +import com.google.devtools.build.lib.exec.SpawnExecException; +import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.exec.SpawnResult; +import com.google.devtools.build.lib.exec.SpawnResult.Status; +import com.google.devtools.build.lib.exec.SpawnRunner.ProgressStatus; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; +import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; 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.CommandResult; +import com.google.devtools.build.lib.util.CommandFailureUtils; +import com.google.devtools.build.lib.util.io.FileOutErr; import com.google.devtools.build.lib.util.io.OutErr; 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.IOException; import java.util.Map; -import java.util.Set; +import java.util.SortedMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** Abstract common ancestor for sandbox strategies implementing the common parts. */ abstract class SandboxStrategy implements SandboxedSpawnActionContext { + private static final int LOCAL_EXEC_ERROR = -1; + private static final int POSIX_TIMEOUT_EXIT_CODE = /*SIGNAL_BASE=*/128 + /*SIGALRM=*/14; + + private static final String SANDBOX_DEBUG_SUGGESTION = + "\n\nUse --sandbox_debug to see verbose messages from the sandbox"; - private final CommandEnvironment cmdEnv; - private final BuildRequest buildRequest; - private final Path execRoot; private final Path sandboxBase; private final boolean verboseFailures; private final SandboxOptions sandboxOptions; private final ImmutableSet<Path> inaccessiblePaths; + private final SpawnInputExpander spawnInputExpander; public SandboxStrategy( CommandEnvironment cmdEnv, - BuildRequest buildRequest, Path sandboxBase, boolean verboseFailures, SandboxOptions sandboxOptions) { - this.cmdEnv = cmdEnv; - this.buildRequest = buildRequest; - this.execRoot = cmdEnv.getExecRoot(); this.sandboxBase = sandboxBase; this.verboseFailures = verboseFailures; this.sandboxOptions = sandboxOptions; - - ImmutableSet.Builder<Path> inaccessiblePaths = ImmutableSet.builder(); - FileSystem fileSystem = cmdEnv.getDirectories().getFileSystem(); - for (String path : sandboxOptions.sandboxBlockPath) { - Path blockedPath = fileSystem.getPath(path); - try { - inaccessiblePaths.add(blockedPath.resolveSymbolicLinks()); - } catch (IOException e) { - // It's OK to block access to an invalid symlink. In this case we'll just make the symlink - // itself inaccessible, instead of the target, though. - inaccessiblePaths.add(blockedPath); - } - } - this.inaccessiblePaths = inaccessiblePaths.build(); + this.inaccessiblePaths = + sandboxOptions.getInaccessiblePaths(cmdEnv.getDirectories().getFileSystem()); + this.spawnInputExpander = new SpawnInputExpander(false); } - /** Executes the given {@code spawn}. */ @Override public void exec(Spawn spawn, ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException { @@ -103,73 +106,168 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { eventBus.post(ActionStatusMessage.schedulingStrategy(owner)); try (ResourceHandle ignored = ResourceManager.instance().acquireResources(owner, spawn.getLocalResources())) { + eventBus.post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), getName())); + if (actionExecutionContext.reportsSubcommands()) { + actionExecutionContext.reportSubcommand(spawn); + } actuallyExec(spawn, actionExecutionContext, writeOutputFiles); } catch (IOException e) { throw new UserExecException("I/O exception during sandboxed execution", e); } } - protected abstract void actuallyExec( + private void actuallyExec( Spawn spawn, ActionExecutionContext actionExecutionContext, AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) + throws ExecException, IOException, InterruptedException { + final int timeoutSeconds = Spawns.getTimeoutSeconds(spawn); + SpawnExecutionPolicy policy = new SpawnExecutionPolicy() { + @Override + public ActionInputFileCache getActionInputFileCache() { + return actionExecutionContext.getActionInputFileCache(); + } + + @Override + public ArtifactExpander getArtifactExpander() { + return actionExecutionContext.getArtifactExpander(); + } + + @Override + public void lockOutputFiles() throws InterruptedException { + Class<? extends SpawnActionContext> token = SandboxStrategy.this.getClass(); + if (writeOutputFiles != null + && writeOutputFiles.get() != token + && !writeOutputFiles.compareAndSet(null, token)) { + throw new InterruptedException(); + } + } + + @Override + public long getTimeoutMillis() { + return TimeUnit.SECONDS.toMillis(timeoutSeconds); + } + + @Override + public FileOutErr getFileOutErr() { + return actionExecutionContext.getFileOutErr(); + } + + @Override + public SortedMap<PathFragment, ActionInput> getInputMapping() throws IOException { + return spawnInputExpander.getInputMapping( + spawn, + actionExecutionContext.getArtifactExpander(), + actionExecutionContext.getActionInputFileCache(), + actionExecutionContext.getContext(FilesetActionContext.class)); + } + + @Override + public void report(ProgressStatus state) { + // We already reported the progress, so do nothing here. + } + }; + actuallyExec(spawn, policy); + } + + protected abstract SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) throws ExecException, InterruptedException, IOException; - protected void runSpawn( - Spawn spawn, - ActionExecutionContext actionExecutionContext, - Map<String, String> spawnEnvironment, - SandboxExecRoot sandboxExecRoot, - Set<PathFragment> outputs, - SandboxRunner runner, - AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) - throws ExecException, InterruptedException { - EventHandler eventHandler = actionExecutionContext.getEventHandler(); - ExecException execException = null; - OutErr outErr = actionExecutionContext.getFileOutErr(); + protected SpawnResult runSpawn( + Spawn originalSpawn, + SandboxedSpawn sandbox, + SpawnExecutionPolicy policy, + Path execRoot, + int timeoutSeconds) + throws ExecException, IOException, InterruptedException { try { - runner.run( - cmdEnv, - spawn.getArguments(), - spawnEnvironment, - outErr, - Spawns.getTimeoutSeconds(spawn), - SandboxHelpers.shouldAllowNetwork(buildRequest, spawn), - sandboxOptions.sandboxDebug, - sandboxOptions.sandboxFakeHostname, - sandboxOptions.sandboxFakeUsername); - } catch (ExecException e) { - execException = e; - } + sandbox.createFileSystem(); + OutErr outErr = policy.getFileOutErr(); + SpawnResult result = run(sandbox, outErr, timeoutSeconds); + + policy.lockOutputFiles(); + + try { + // We copy the outputs even when the command failed. + sandbox.copyOutputs(execRoot); + } catch (IOException e) { + throw new IOException("Could not move output artifacts from sandboxed execution", e); + } - if (writeOutputFiles != null && !writeOutputFiles.compareAndSet(null, SandboxStrategy.class)) { - throw new InterruptedException(); + if (result.status() != Status.SUCCESS || result.exitCode() != 0) { + String message; + if (sandboxOptions.sandboxDebug) { + message = + CommandFailureUtils.describeCommandFailure( + true, sandbox.getArguments(), sandbox.getEnvironment(), null); + } else { + message = + CommandFailureUtils.describeCommandFailure( + false, originalSpawn.getArguments(), originalSpawn.getEnvironment(), null) + + SANDBOX_DEBUG_SUGGESTION; + } + throw new SpawnExecException( + message, result, /*forciblyRunRemotely=*/false, /*catastrophe=*/false); + } + return result; + } finally { + if (!sandboxOptions.sandboxDebug) { + sandbox.delete(); + } } + } + + private final SpawnResult run(SandboxedSpawn sandbox, OutErr outErr, int timeoutSeconds) + throws IOException, InterruptedException { + Command cmd = new Command( + sandbox.getArguments().toArray(new String[0]), + sandbox.getEnvironment(), + sandbox.getSandboxExecRoot().getPathFile()); + long startTime = System.currentTimeMillis(); + CommandResult result; try { - // We copy the outputs even when the command failed, otherwise StandaloneTestStrategy - // won't be able to get the test logs of a failed test. (We should probably do this in - // some better way.) - sandboxExecRoot.copyOutputs(execRoot, outputs); - } catch (IOException e) { - if (execException == null) { - throw new UserExecException("Could not move output artifacts from sandboxed execution", e); - } else { - // Catch the IOException and turn it into an error message, otherwise this might hide an - // exception thrown during runner.run earlier. - eventHandler.handle( - Event.error( - "I/O exception while extracting output artifacts from sandboxed execution: " + e)); + result = cmd.execute( + /* stdin */ new byte[] {}, + Command.NO_OBSERVER, + outErr.getOutputStream(), + outErr.getErrorStream(), + /* killSubprocessOnInterrupt */ true); + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + } catch (AbnormalTerminationException e) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); } + result = e.getResult(); + } catch (CommandException e) { + // At the time this comment was written, this must be a ExecFailedException encapsulating an + // IOException from the underlying Subprocess.Factory. + String msg = e.getMessage() == null ? e.getClass().getName() : e.getMessage(); + outErr.getErrorStream().write(("Action failed to execute: " + msg + "\n").getBytes(UTF_8)); + outErr.getErrorStream().flush(); + return new SpawnResult.Builder() + .setStatus(Status.EXECUTION_FAILED) + .setExitCode(LOCAL_EXEC_ERROR) + .build(); } - if (execException != null) { - outErr.printErr( - "Use --strategy=" - + spawn.getMnemonic() - + "=standalone to disable sandboxing for the failing actions.\n"); - throw execException; - } + long wallTime = System.currentTimeMillis() - startTime; + boolean wasTimeout = wasTimeout(timeoutSeconds, wallTime); + Status status = wasTimeout ? Status.TIMEOUT : Status.SUCCESS; + int exitCode = status == Status.TIMEOUT + ? POSIX_TIMEOUT_EXIT_CODE + : result.getTerminationStatus().getRawExitCode(); + return new SpawnResult.Builder() + .setStatus(status) + .setExitCode(exitCode) + .setWallTimeMillis(wallTime) + .build(); + } + + private boolean wasTimeout(int timeoutSeconds, long wallTimeMillis) { + return timeoutSeconds > 0 && wallTimeMillis / 1000.0 > timeoutSeconds; } /** @@ -193,16 +291,23 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { // We have to make the TEST_TMPDIR directory writable if it is specified. ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder(); writablePaths.add(sandboxExecRoot); - if (env.containsKey("TEST_TMPDIR")) { - // We add this even though it may be below sandboxExecRoot (and thus would already be writable - // as a subpath) to take advantage of the side-effect that SymlinkedExecRoot also creates this - // needed directory if it doesn't exist yet. - writablePaths.add(sandboxExecRoot.getRelative(env.get("TEST_TMPDIR"))); + String tmpDirString = env.get("TEST_TMPDIR"); + if (tmpDirString != null) { + PathFragment testTmpDir = PathFragment.create(tmpDirString); + if (testTmpDir.isAbsolute()) { + writablePaths.add(sandboxExecRoot.getRelative(testTmpDir).resolveSymbolicLinks()); + } else { + // We add this even though it may is below sandboxExecRoot (and thus would already be + // writable as a subpath) to take advantage of the side-effect that SymlinkedExecRoot also + // creates this needed directory if it doesn't exist yet. + writablePaths.add(sandboxExecRoot.getRelative(testTmpDir)); + } } FileSystem fileSystem = sandboxExecRoot.getFileSystem(); for (String writablePath : sandboxOptions.sandboxWritablePath) { - writablePaths.add(fileSystem.getPath(writablePath)); + Path path = fileSystem.getPath(writablePath); + writablePaths.add(path.resolveSymbolicLinks()); } return writablePaths.build(); @@ -212,6 +317,8 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { return inaccessiblePaths; } + protected abstract String getName(); + @Override public String toString() { return "sandboxed"; diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxedSpawn.java index 0c392fd141..431a4b1d43 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxExecRoot.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxedSpawn.java @@ -15,42 +15,45 @@ 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.List; import java.util.Map; -import java.util.Set; /** - * SandboxExecRoot is responsible for making a list of input files available inside the directory, + * A description of a subprocess, as well as the necessary file system / sandbox setup. + * + * <p>Instances are responsible for making a list of input files available inside the sandbox root, * 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 { +interface SandboxedSpawn { + /** The path in which to execute the subprocess. */ + Path getSandboxExecRoot(); + + /** The command-line of the subprocess. */ + List<String> getArguments(); + + /** The environment variables to be set for the subprocess. */ + Map<String, String> getEnvironment(); /** * 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; + void createFileSystem() 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; + void copyOutputs(Path execRoot) throws IOException; + /** + * Deletes the sandbox directory. + */ + void delete(); } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedSandboxedSpawn.java index a158f02961..96da98097e 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedSandboxedSpawn.java @@ -24,6 +24,7 @@ import com.google.devtools.build.lib.vfs.Symlinks; import java.io.IOException; 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; @@ -32,18 +33,49 @@ import java.util.Set; * Creates an execRoot for a Spawn that contains input files as symlinks to their original * destination. */ -public final class SymlinkedExecRoot implements SandboxExecRoot { - +public class SymlinkedSandboxedSpawn implements SandboxedSpawn { + private final Path sandboxPath; private final Path sandboxExecRoot; + private final List<String> arguments; + private final Map<String, String> environment; + private final Map<PathFragment, Path> inputs; + private final Collection<PathFragment> outputs; + private final Set<Path> writableDirs; - public SymlinkedExecRoot(Path sandboxExecRoot) { + public SymlinkedSandboxedSpawn( + Path sandboxPath, + Path sandboxExecRoot, + List<String> arguments, + Map<String, String> environment, + Map<PathFragment, Path> inputs, + Collection<PathFragment> outputs, + Set<Path> writableDirs) { + this.sandboxPath = sandboxPath; this.sandboxExecRoot = sandboxExecRoot; + this.arguments = arguments; + this.environment = environment; + this.inputs = inputs; + this.outputs = outputs; + this.writableDirs = writableDirs; } @Override - public void createFileSystem( - Map<PathFragment, Path> inputs, Collection<PathFragment> outputs, Set<Path> writableDirs) - throws IOException { + public Path getSandboxExecRoot() { + return sandboxExecRoot; + } + + @Override + public List<String> getArguments() { + return arguments; + } + + @Override + public Map<String, String> getEnvironment() { + return environment; + } + + @Override + public void createFileSystem() throws IOException { Set<Path> createdDirs = new HashSet<>(); cleanFileSystem(inputs.keySet()); FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot); @@ -140,7 +172,7 @@ public final class SymlinkedExecRoot implements SandboxExecRoot { /** Moves all {@code outputs} to {@code execRoot}. */ @Override - public void copyOutputs(Path execRoot, Collection<PathFragment> outputs) throws IOException { + public void copyOutputs(Path execRoot) throws IOException { for (PathFragment output : outputs) { Path source = sandboxExecRoot.getRelative(output); Path target = execRoot.getRelative(output); @@ -157,4 +189,19 @@ public final class SymlinkedExecRoot implements SandboxExecRoot { } } } + + @Override + public void delete() { + try { + FileSystemUtils.deleteTree(sandboxPath); + } catch (IOException e) { + // This usually means that the Spawn itself exited, but still has children running that + // we couldn't wait for, which now block deletion of the sandbox directory. On Linux this + // should never happen, as we use PID namespaces and where they are not available the + // subreaper feature to make sure all children have been reliably killed before returning, + // but on other OS this might not always work. The SandboxModule will try to delete them + // again when the build is all done, at which point it hopefully works, so let's just go + // on here. + } + } } 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 886fa45a33..b1e0e9d93b 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 @@ -18,6 +18,7 @@ import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionInputFileCache; import com.google.devtools.build.lib.actions.ActionStatusMessage; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.ResourceManager; @@ -85,6 +86,11 @@ public class StandaloneSpawnStrategy implements SpawnActionContext { } @Override + public ArtifactExpander getArtifactExpander() { + return actionExecutionContext.getArtifactExpander(); + } + + @Override public void lockOutputFiles() throws InterruptedException { // Do nothing for now. } diff --git a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java index 7192c3e029..a06ddc0114 100644 --- a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java +++ b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java @@ -14,8 +14,10 @@ package com.google.devtools.build.lib.worker; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.devtools.build.lib.sandbox.SymlinkedExecRoot; +import com.google.devtools.build.lib.sandbox.SymlinkedSandboxedSpawn; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import java.io.IOException; @@ -23,30 +25,46 @@ import java.io.IOException; /** A {@link Worker} that runs inside a sandboxed execution root. */ final class SandboxedWorker extends Worker { private final Path workDir; - private final SymlinkedExecRoot symlinkedExecRoot; SandboxedWorker(WorkerKey workerKey, int workerId, Path workDir, Path logFile) { super(workerKey, workerId, workDir, logFile); this.workDir = workDir; - this.symlinkedExecRoot = new SymlinkedExecRoot(workDir); } @Override void destroy() throws IOException { super.destroy(); - if (symlinkedExecRoot != null) { - FileSystemUtils.deleteTree(workDir); - } + FileSystemUtils.deleteTree(workDir); } @Override public void prepareExecution(WorkerKey key) throws IOException { - symlinkedExecRoot.createFileSystem( - key.getInputFiles(), key.getOutputFiles(), ImmutableSet.<Path>of()); + // Note: the key passed in here may be different from the key passed to the constructor for + // subsequent invocations of the same worker. + // TODO(ulfjack): Remove WorkerKey.getInputFiles and WorkerKey.getOutputFiles; they are only + // used to pass information to this method and the method below. Instead, don't pass the + // WorkerKey to this method but only the input and output files. + new SymlinkedSandboxedSpawn( + workDir, + workDir, + ImmutableList.of("/does_not_exist"), + ImmutableMap.<String, String>of(), + key.getInputFiles(), + key.getOutputFiles(), + ImmutableSet.<Path>of()).createFileSystem(); } @Override public void finishExecution(WorkerKey key) throws IOException { - symlinkedExecRoot.copyOutputs(key.getExecRoot(), key.getOutputFiles()); + // Note: the key passed in here may be different from the key passed to the constructor for + // subsequent invocations of the same worker. + new SymlinkedSandboxedSpawn( + workDir, + workDir, + ImmutableList.of("/does_not_exist"), + ImmutableMap.<String, String>of(), + key.getInputFiles(), + key.getOutputFiles(), + ImmutableSet.<Path>of()).copyOutputs(key.getExecRoot()); } } |