diff options
author | ulfjack <ulfjack@google.com> | 2017-07-12 16:51:20 +0200 |
---|---|---|
committer | László Csomor <laszlocsomor@google.com> | 2017-07-13 09:49:19 +0200 |
commit | a63da8e99b19a5cdd103998f073fab57e883b82e (patch) | |
tree | 59aed5039018055b66a3e67576193f27865d4c64 /src/main/java/com/google/devtools | |
parent | 67bd6fc6354f2abbbc149fcd0120228b538842d3 (diff) |
Split the sandbox strategies into SpawnRunner implementations and strategies
This adds a bunch of classes that only implement the SpawnRunner interface, and
will allow us to support remote caching in combination with local sandboxed
execution in a subsequent change.
PiperOrigin-RevId: 161664556
Diffstat (limited to 'src/main/java/com/google/devtools')
10 files changed, 946 insertions, 801 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java new file mode 100644 index 0000000000..d1b6e98452 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java @@ -0,0 +1,228 @@ +// 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 static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.actions.ActionExecutionMetadata; +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; +import com.google.devtools.build.lib.actions.Spawn; +import com.google.devtools.build.lib.actions.UserExecException; +import com.google.devtools.build.lib.exec.SpawnExecException; +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; +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.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; + +/** Abstract common ancestor for sandbox spawn runners implementing the common parts. */ +abstract class AbstractSandboxSpawnRunner implements SpawnRunner { + 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 Path sandboxBase; + private final SandboxOptions sandboxOptions; + private final ImmutableSet<Path> inaccessiblePaths; + + public AbstractSandboxSpawnRunner( + CommandEnvironment cmdEnv, + Path sandboxBase, + SandboxOptions sandboxOptions) { + this.sandboxBase = sandboxBase; + this.sandboxOptions = sandboxOptions; + this.inaccessiblePaths = + sandboxOptions.getInaccessiblePaths(cmdEnv.getDirectories().getFileSystem()); + } + + @Override + public SpawnResult exec(Spawn spawn, SpawnExecutionPolicy policy) + throws ExecException, InterruptedException { + ActionExecutionMetadata owner = spawn.getResourceOwner(); + policy.report(ProgressStatus.SCHEDULING); + try (ResourceHandle ignored = + ResourceManager.instance().acquireResources(owner, spawn.getLocalResources())) { + policy.report(ProgressStatus.EXECUTING); + return actuallyExec(spawn, policy); + } catch (IOException e) { + throw new UserExecException("I/O exception during sandboxed execution", e); + } + } + + protected abstract SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) + throws ExecException, InterruptedException, IOException; + + protected SpawnResult runSpawn( + Spawn originalSpawn, + SandboxedSpawn sandbox, + SpawnExecutionPolicy policy, + Path execRoot, + int timeoutSeconds) + throws ExecException, IOException, InterruptedException { + try { + 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 (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 { + 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(); + } + + 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; + } + + /** + * Returns a temporary directory that should be used as the sandbox directory for a single action. + */ + protected Path getSandboxRoot() throws IOException { + return sandboxBase.getRelative( + java.nio.file.Files.createTempDirectory( + java.nio.file.Paths.get(sandboxBase.getPathString()), "") + .getFileName() + .toString()); + } + + /** + * Gets the list of directories that the spawn will assume to be writable. + * + * @throws IOException because we might resolve symlinks, which throws {@link IOException}. + */ + protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) + throws IOException { + // We have to make the TEST_TMPDIR directory writable if it is specified. + ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder(); + writablePaths.add(sandboxExecRoot); + 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 is below sandboxExecRoot (and thus already 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) { + Path path = fileSystem.getPath(writablePath); + writablePaths.add(path); + writablePaths.add(path.resolveSymbolicLinks()); + } + + return writablePaths.build(); + } + + protected ImmutableSet<Path> getInaccessiblePaths() { + return inaccessiblePaths; + } + + protected abstract String getName(); +} diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java new file mode 100644 index 0000000000..446daa1c94 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java @@ -0,0 +1,272 @@ +// 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.collect.ImmutableSet; +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.SpawnResult; +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.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.OS; +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.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.TimeUnit; + +/** Spawn runner that uses Darwin (macOS) sandboxing to execute a process. */ +@ExecutionStrategy( + name = {"sandboxed", "darwin-sandbox"}, + contextType = SpawnActionContext.class +) +final class DarwinSandboxedSpawnRunner extends AbstractSandboxSpawnRunner { + private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec"; + + public static boolean isSupported(CommandEnvironment 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 allowNetwork; + private final String productName; + private final Path processWrapper; + + /** + * The set of directories that always should be writable, independent of the Spawn itself. + * + * <p>We cache this, because creating it involves executing {@code getconf}, which is expensive. + */ + private final ImmutableSet<Path> alwaysWritableDirs; + private final LocalEnvProvider localEnvProvider; + + DarwinSandboxedSpawnRunner( + CommandEnvironment cmdEnv, + BuildRequest buildRequest, + Path sandboxBase, + String productName) throws IOException { + super( + cmdEnv, + sandboxBase, + buildRequest.getOptions(SandboxOptions.class)); + this.execRoot = cmdEnv.getExecRoot(); + this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions()); + this.productName = productName; + this.alwaysWritableDirs = getAlwaysWritableDirs(cmdEnv.getDirectories().getFileSystem()); + this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv); + this.localEnvProvider = new XCodeLocalEnvProvider(); + } + + private static void addPathToSetIfExists(FileSystem fs, Set<Path> paths, String path) + throws IOException { + if (path != null) { + addPathToSetIfExists(paths, fs.getPath(path)); + } + } + + private static void addPathToSetIfExists(Set<Path> paths, Path path) throws IOException { + if (path.exists()) { + paths.add(path.resolveSymbolicLinks()); + } + } + + private static ImmutableSet<Path> getAlwaysWritableDirs(FileSystem fs) throws IOException { + HashSet<Path> writableDirs = new HashSet<>(); + + addPathToSetIfExists(fs, writableDirs, "/dev"); + addPathToSetIfExists(fs, writableDirs, System.getenv("TMPDIR")); + addPathToSetIfExists(fs, writableDirs, "/tmp"); + addPathToSetIfExists(fs, writableDirs, "/private/tmp"); + addPathToSetIfExists(fs, writableDirs, "/private/var/tmp"); + + // On macOS, in addition to what is specified in $TMPDIR, two other temporary directories may be + // written to by processes. We have to get their location by calling "getconf". + addPathToSetIfExists(fs, writableDirs, getConfStr("DARWIN_USER_TEMP_DIR")); + addPathToSetIfExists(fs, writableDirs, getConfStr("DARWIN_USER_CACHE_DIR")); + + // ~/Library/Cache and ~/Library/Logs need to be writable (cf. issue #2231). + Path homeDir = fs.getPath(System.getProperty("user.home")); + addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Cache")); + addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Logs")); + + // Certain Xcode tools expect to be able to write to this path. + addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Developer")); + + return ImmutableSet.copyOf(writableDirs); + } + + /** + * Returns the value of a POSIX or X/Open system configuration variable. + */ + private static String getConfStr(String confVar) throws IOException { + String[] commandArr = new String[2]; + commandArr[0] = "/usr/bin/getconf"; + commandArr[1] = confVar; + Command cmd = new Command(commandArr); + CommandResult res; + try { + res = cmd.execute(); + } catch (CommandException e) { + throw new IOException("getconf failed", e); + } + return new String(res.getStdout(), UTF_8).trim(); + } + + @Override + 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 = + localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName); + + final HashSet<Path> writableDirs = new HashSet<>(alwaysWritableDirs); + ImmutableSet<Path> extraWritableDirs = getWritableDirs(sandboxExecRoot, spawnEnvironment); + writableDirs.addAll(extraWritableDirs); + + ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); + + 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) { + @Override + public void createFileSystem() throws IOException { + super.createFileSystem(); + writeConfig( + sandboxConfigPath, writableDirs, getInaccessiblePaths(), allowNetworkForThisSpawn); + } + }; + return runSpawn(spawn, sandbox, policy, execRoot, timeoutSeconds); + } + + 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; + } + + 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/DarwinSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedStrategy.java index 89b961edf9..38c8645f2c 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 @@ -14,279 +14,29 @@ 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.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.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.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.OS; -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.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.TimeUnit; /** Strategy that uses sandboxing to execute a process, for Darwin */ +//TODO(ulfjack): This class only exists for this annotation. Find a better way to handle this! @ExecutionStrategy( name = {"sandboxed", "darwin-sandbox"}, contextType = SpawnActionContext.class ) -public class DarwinSandboxedStrategy extends SandboxStrategy { - private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec"; - - public static boolean isSupported(CommandEnvironment 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 allowNetwork; - private final String productName; - private final Path processWrapper; - - /** - * The set of directories that always should be writable, independent of the Spawn itself. - * - * <p>We cache this, because creating it involves executing {@code getconf}, which is expensive. - */ - private final ImmutableSet<Path> alwaysWritableDirs; - private final LocalEnvProvider localEnvProvider; - - private DarwinSandboxedStrategy( +final class DarwinSandboxedStrategy extends SandboxStrategy { + DarwinSandboxedStrategy( CommandEnvironment cmdEnv, BuildRequest buildRequest, Path sandboxBase, boolean verboseFailures, - String productName, - ImmutableSet<Path> alwaysWritableDirs) { + String productName) throws IOException { super( - cmdEnv, - sandboxBase, - verboseFailures, - buildRequest.getOptions(SandboxOptions.class)); - this.execRoot = cmdEnv.getExecRoot(); - this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions()); - this.productName = productName; - this.alwaysWritableDirs = alwaysWritableDirs; - this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv); - this.localEnvProvider = new XCodeLocalEnvProvider(); - } - - public static DarwinSandboxedStrategy create( - CommandEnvironment cmdEnv, - BuildRequest buildRequest, - Path sandboxBase, - boolean verboseFailures, - String productName) - throws IOException { - return new DarwinSandboxedStrategy( - cmdEnv, - buildRequest, - sandboxBase, verboseFailures, - productName, - getAlwaysWritableDirs(cmdEnv.getDirectories().getFileSystem())); - } - - private static void addPathToSetIfExists(FileSystem fs, Set<Path> paths, String path) - throws IOException { - if (path != null) { - addPathToSetIfExists(paths, fs.getPath(path)); - } - } - - private static void addPathToSetIfExists(Set<Path> paths, Path path) throws IOException { - if (path.exists()) { - paths.add(path.resolveSymbolicLinks()); - } - } - - private static ImmutableSet<Path> getAlwaysWritableDirs(FileSystem fs) throws IOException { - HashSet<Path> writableDirs = new HashSet<>(); - - addPathToSetIfExists(fs, writableDirs, "/dev"); - addPathToSetIfExists(fs, writableDirs, System.getenv("TMPDIR")); - addPathToSetIfExists(fs, writableDirs, "/tmp"); - addPathToSetIfExists(fs, writableDirs, "/private/tmp"); - addPathToSetIfExists(fs, writableDirs, "/private/var/tmp"); - - // On macOS, in addition to what is specified in $TMPDIR, two other temporary directories may be - // written to by processes. We have to get their location by calling "getconf". - addPathToSetIfExists(fs, writableDirs, getConfStr("DARWIN_USER_TEMP_DIR")); - addPathToSetIfExists(fs, writableDirs, getConfStr("DARWIN_USER_CACHE_DIR")); - - // ~/Library/Cache and ~/Library/Logs need to be writable (cf. issue #2231). - Path homeDir = fs.getPath(System.getProperty("user.home")); - addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Cache")); - addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Logs")); - - // Certain Xcode tools expect to be able to write to this path. - addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Developer")); - - return ImmutableSet.copyOf(writableDirs); - } - - /** - * Returns the value of a POSIX or X/Open system configuration variable. - */ - private static String getConfStr(String confVar) throws IOException { - String[] commandArr = new String[2]; - commandArr[0] = "/usr/bin/getconf"; - commandArr[1] = confVar; - Command cmd = new Command(commandArr); - CommandResult res; - try { - res = cmd.execute(); - } catch (CommandException e) { - throw new IOException("getconf failed", e); - } - return new String(res.getStdout(), UTF_8).trim(); - } - - @Override - 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 = - localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName); - - final HashSet<Path> writableDirs = new HashSet<>(alwaysWritableDirs); - ImmutableSet<Path> extraWritableDirs = getWritableDirs(sandboxExecRoot, spawnEnvironment); - writableDirs.addAll(extraWritableDirs); - - ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); - - 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) { - @Override - public void createFileSystem() throws IOException { - super.createFileSystem(); - writeConfig( - sandboxConfigPath, writableDirs, getInaccessiblePaths(), allowNetworkForThisSpawn); - } - }; - return runSpawn(spawn, sandbox, policy, execRoot, timeoutSeconds); - } - - 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; - } - - 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"; + buildRequest.getOptions(SandboxOptions.class), + new DarwinSandboxedSpawnRunner(cmdEnv, buildRequest, sandboxBase, productName)); } } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java new file mode 100644 index 0000000000..0730fc6934 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java @@ -0,0 +1,309 @@ +// 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.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.io.ByteStreams; +import com.google.devtools.build.lib.actions.ExecException; +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.buildtool.BuildRequest; +import com.google.devtools.build.lib.exec.SpawnResult; +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.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.TimeUnit; + +/** Spawn runner that uses linux sandboxing APIs to execute a local subprocess. */ +final class LinuxSandboxedSpawnRunner extends AbstractSandboxSpawnRunner { + private static final String LINUX_SANDBOX = "linux-sandbox"; + + public static boolean isSupported(CommandEnvironment 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 allowNetwork; + private final Path linuxSandbox; + private final Path inaccessibleHelperFile; + private final Path inaccessibleHelperDir; + + LinuxSandboxedSpawnRunner( + CommandEnvironment cmdEnv, + BuildRequest buildRequest, + Path sandboxBase, + Path inaccessibleHelperFile, + Path inaccessibleHelperDir) { + super( + cmdEnv, + sandboxBase, + buildRequest.getOptions(SandboxOptions.class)); + this.sandboxOptions = cmdEnv.getOptions().getOptions(SandboxOptions.class); + this.blazeDirs = cmdEnv.getDirectories(); + this.execRoot = cmdEnv.getExecRoot(); + this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions()); + this.linuxSandbox = getLinuxSandbox(cmdEnv); + this.inaccessibleHelperFile = inaccessibleHelperFile; + this.inaccessibleHelperDir = inaccessibleHelperDir; + } + + @Override + protected SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) + throws IOException, ExecException, InterruptedException { + // 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()); + ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); + 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); + } + + 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()); + + 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 + protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) + throws IOException { + ImmutableSet.Builder<Path> writableDirs = ImmutableSet.builder(); + writableDirs.addAll(super.getWritableDirs(sandboxExecRoot, env)); + + FileSystem fs = sandboxExecRoot.getFileSystem(); + writableDirs.add(fs.getPath("/dev/shm").resolveSymbolicLinks()); + writableDirs.add(fs.getPath("/tmp")); + + return writableDirs.build(); + } + + private ImmutableSet<Path> getTmpfsPaths() { + ImmutableSet.Builder<Path> tmpfsPaths = ImmutableSet.builder(); + for (String tmpfsPath : sandboxOptions.sandboxTmpfsPath) { + tmpfsPaths.add(blazeDirs.getFileSystem().getPath(tmpfsPath)); + } + return tmpfsPaths.build(); + } + + private SortedMap<Path, Path> getReadOnlyBindMounts( + BlazeDirectories blazeDirs, Path sandboxExecRoot) throws UserExecException { + Path tmpPath = blazeDirs.getFileSystem().getPath("/tmp"); + final SortedMap<Path, Path> bindMounts = Maps.newTreeMap(); + if (blazeDirs.getWorkspace().startsWith(tmpPath)) { + bindMounts.put(blazeDirs.getWorkspace(), blazeDirs.getWorkspace()); + } + if (blazeDirs.getOutputBase().startsWith(tmpPath)) { + bindMounts.put(blazeDirs.getOutputBase(), blazeDirs.getOutputBase()); + } + for (ImmutableMap.Entry<String, String> additionalMountPath : + sandboxOptions.sandboxAdditionalMounts) { + try { + final Path mountTarget = blazeDirs.getFileSystem().getPath(additionalMountPath.getValue()); + // If source path is relative, treat it as a relative path inside the execution root + final Path mountSource = sandboxExecRoot.getRelative(additionalMountPath.getKey()); + // If a target has more than one source path, the latter one will take effect. + bindMounts.put(mountTarget, mountSource); + } catch (IllegalArgumentException e) { + throw new UserExecException( + String.format("Error occurred when analyzing bind mount pairs. %s", e.getMessage())); + } + } + for (Path inaccessiblePath : getInaccessiblePaths()) { + if (inaccessiblePath.isDirectory(Symlinks.NOFOLLOW)) { + bindMounts.put(inaccessiblePath, inaccessibleHelperDir); + } else { + bindMounts.put(inaccessiblePath, inaccessibleHelperFile); + } + } + validateBindMounts(bindMounts); + return bindMounts; + } + + /** + * This method does the following things: - If mount source does not exist on the host system, + * throw an error message - If mount target exists, check whether the source and target are of the + * same type - If mount target does not exist on the host system, throw an error message + * + * @param bindMounts the bind mounts map with target as key and source as value + * @throws UserExecException + */ + private void validateBindMounts(SortedMap<Path, Path> bindMounts) throws UserExecException { + for (SortedMap.Entry<Path, Path> bindMount : bindMounts.entrySet()) { + final Path source = bindMount.getValue(); + final Path target = bindMount.getKey(); + // Mount source should exist in the file system + if (!source.exists()) { + throw new UserExecException(String.format("Mount source '%s' does not exist.", source)); + } + // If target exists, but is not of the same type as the source, then we cannot mount it. + if (target.exists()) { + boolean areBothDirectories = source.isDirectory() && target.isDirectory(); + boolean isSourceFile = source.isFile() || source.isSymbolicLink(); + boolean isTargetFile = target.isFile() || target.isSymbolicLink(); + boolean areBothFiles = isSourceFile && isTargetFile; + if (!(areBothDirectories || areBothFiles)) { + // Source and target are not of the same type; we cannot mount it. + throw new UserExecException( + String.format( + "Mount target '%s' is not of the same type as mount source '%s'.", + target, source)); + } + } else { + // Mount target should exist in the file system + throw new UserExecException( + String.format( + "Mount target '%s' does not exist. Bazel only supports bind mounting on top of " + + "existing files/directories. Please create an empty file or directory at " + + "the mount target path according to the type of mount source.", + target)); + } + } + } +} 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 f0d3031e66..3d556d3bfc 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 @@ -14,94 +14,21 @@ 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.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.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.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.TimeUnit; /** Strategy that uses sandboxing to execute a process. */ +// TODO(ulfjack): This class only exists for this annotation. Find a better way to handle this! @ExecutionStrategy( name = {"sandboxed", "linux-sandbox"}, contextType = SpawnActionContext.class ) -public class LinuxSandboxedStrategy extends SandboxStrategy { - private static final String LINUX_SANDBOX = "linux-sandbox"; - - public static boolean isSupported(CommandEnvironment 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 allowNetwork; - private final Path linuxSandbox; - private final Path inaccessibleHelperFile; - private final Path inaccessibleHelperDir; - +public final class LinuxSandboxedStrategy extends SandboxStrategy { private LinuxSandboxedStrategy( CommandEnvironment cmdEnv, BuildRequest buildRequest, @@ -110,17 +37,10 @@ public class LinuxSandboxedStrategy extends SandboxStrategy { Path inaccessibleHelperFile, Path inaccessibleHelperDir) { super( - cmdEnv, - sandboxBase, verboseFailures, - buildRequest.getOptions(SandboxOptions.class)); - this.sandboxOptions = cmdEnv.getOptions().getOptions(SandboxOptions.class); - this.blazeDirs = cmdEnv.getDirectories(); - this.execRoot = cmdEnv.getExecRoot(); - this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions()); - this.linuxSandbox = getLinuxSandbox(cmdEnv); - this.inaccessibleHelperFile = inaccessibleHelperFile; - this.inaccessibleHelperDir = inaccessibleHelperDir; + buildRequest.getOptions(SandboxOptions.class), + new LinuxSandboxedSpawnRunner( + cmdEnv, buildRequest, sandboxBase, inaccessibleHelperFile, inaccessibleHelperDir)); } static LinuxSandboxedStrategy create( @@ -149,198 +69,4 @@ public class LinuxSandboxedStrategy extends SandboxStrategy { inaccessibleHelperFile, inaccessibleHelperDir); } - - @Override - protected SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy) - throws IOException, ExecException, InterruptedException { - // 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()); - ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn); - 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); - } - - 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()); - - 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 - protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) - throws IOException { - ImmutableSet.Builder<Path> writableDirs = ImmutableSet.builder(); - writableDirs.addAll(super.getWritableDirs(sandboxExecRoot, env)); - - FileSystem fs = sandboxExecRoot.getFileSystem(); - writableDirs.add(fs.getPath("/dev/shm").resolveSymbolicLinks()); - writableDirs.add(fs.getPath("/tmp")); - - return writableDirs.build(); - } - - private ImmutableSet<Path> getTmpfsPaths() { - ImmutableSet.Builder<Path> tmpfsPaths = ImmutableSet.builder(); - for (String tmpfsPath : sandboxOptions.sandboxTmpfsPath) { - tmpfsPaths.add(blazeDirs.getFileSystem().getPath(tmpfsPath)); - } - return tmpfsPaths.build(); - } - - private SortedMap<Path, Path> getReadOnlyBindMounts( - BlazeDirectories blazeDirs, Path sandboxExecRoot) throws UserExecException { - Path tmpPath = blazeDirs.getFileSystem().getPath("/tmp"); - final SortedMap<Path, Path> bindMounts = Maps.newTreeMap(); - if (blazeDirs.getWorkspace().startsWith(tmpPath)) { - bindMounts.put(blazeDirs.getWorkspace(), blazeDirs.getWorkspace()); - } - if (blazeDirs.getOutputBase().startsWith(tmpPath)) { - bindMounts.put(blazeDirs.getOutputBase(), blazeDirs.getOutputBase()); - } - for (ImmutableMap.Entry<String, String> additionalMountPath : - sandboxOptions.sandboxAdditionalMounts) { - try { - final Path mountTarget = blazeDirs.getFileSystem().getPath(additionalMountPath.getValue()); - // If source path is relative, treat it as a relative path inside the execution root - final Path mountSource = sandboxExecRoot.getRelative(additionalMountPath.getKey()); - // If a target has more than one source path, the latter one will take effect. - bindMounts.put(mountTarget, mountSource); - } catch (IllegalArgumentException e) { - throw new UserExecException( - String.format("Error occurred when analyzing bind mount pairs. %s", e.getMessage())); - } - } - for (Path inaccessiblePath : getInaccessiblePaths()) { - if (inaccessiblePath.isDirectory(Symlinks.NOFOLLOW)) { - bindMounts.put(inaccessiblePath, inaccessibleHelperDir); - } else { - bindMounts.put(inaccessiblePath, inaccessibleHelperFile); - } - } - validateBindMounts(bindMounts); - return bindMounts; - } - - /** - * This method does the following things: - If mount source does not exist on the host system, - * throw an error message - If mount target exists, check whether the source and target are of the - * same type - If mount target does not exist on the host system, throw an error message - * - * @param bindMounts the bind mounts map with target as key and source as value - * @throws UserExecException - */ - private void validateBindMounts(SortedMap<Path, Path> bindMounts) throws UserExecException { - for (SortedMap.Entry<Path, Path> bindMount : bindMounts.entrySet()) { - final Path source = bindMount.getValue(); - final Path target = bindMount.getKey(); - // Mount source should exist in the file system - if (!source.exists()) { - throw new UserExecException(String.format("Mount source '%s' does not exist.", source)); - } - // If target exists, but is not of the same type as the source, then we cannot mount it. - if (target.exists()) { - boolean areBothDirectories = source.isDirectory() && target.isDirectory(); - boolean isSourceFile = source.isFile() || source.isSymbolicLink(); - boolean isTargetFile = target.isFile() || target.isSymbolicLink(); - boolean areBothFiles = isSourceFile && isTargetFile; - if (!(areBothDirectories || areBothFiles)) { - // Source and target are not of the same type; we cannot mount it. - throw new UserExecException( - String.format( - "Mount target '%s' is not of the same type as mount source '%s'.", - target, source)); - } - } else { - // Mount target should exist in the file system - throw new UserExecException( - String.format( - "Mount target '%s' does not exist. Bazel only supports bind mounting on top of " - + "existing files/directories. Please create an empty file or directory at " - + "the mount target path according to the type of mount source.", - target)); - } - } - } } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedSpawnRunner.java new file mode 100644 index 0000000000..6394b4e7b7 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/sandbox/ProcessWrapperSandboxedSpawnRunner.java @@ -0,0 +1,88 @@ +// Copyright 2017 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.Spawn; +import com.google.devtools.build.lib.buildtool.BuildRequest; +import com.google.devtools.build.lib.exec.SpawnResult; +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.Path; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** Strategy that uses sandboxing to execute a process. */ +final class ProcessWrapperSandboxedSpawnRunner extends AbstractSandboxSpawnRunner { + + public static boolean isSupported(CommandEnvironment cmdEnv) { + return OS.isPosixCompatible() && ProcessWrapperRunner.isSupported(cmdEnv); + } + + private final Path execRoot; + private final String productName; + private final Path processWrapper; + private final LocalEnvProvider localEnvProvider; + + ProcessWrapperSandboxedSpawnRunner( + CommandEnvironment cmdEnv, + BuildRequest buildRequest, + Path sandboxBase, + String productName) { + super( + cmdEnv, + sandboxBase, + buildRequest.getOptions(SandboxOptions.class)); + this.execRoot = cmdEnv.getExecRoot(); + this.productName = productName; + this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv); + this.localEnvProvider = OS.getCurrent() == OS.DARWIN + ? new XCodeLocalEnvProvider() + : LocalEnvProvider.UNMODIFIED; + } + + @Override + 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()); + + 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); + + 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); + } + + @Override + protected String getName() { + return "processwrapper-sandbox"; + } +} 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 6fed1505ae..8c770a8bb2 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,39 +14,19 @@ package com.google.devtools.build.lib.sandbox; -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.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.Path; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; /** Strategy that uses sandboxing to execute a process. */ +//TODO(ulfjack): This class only exists for this annotation. Find a better way to handle this! @ExecutionStrategy( name = {"sandboxed", "processwrapper-sandbox"}, contextType = SpawnActionContext.class ) -public class ProcessWrapperSandboxedStrategy extends SandboxStrategy { - - public static boolean isSupported(CommandEnvironment cmdEnv) { - return OS.isPosixCompatible() && ProcessWrapperRunner.isSupported(cmdEnv); - } - - private final Path execRoot; - private final String productName; - private final Path processWrapper; - private final LocalEnvProvider localEnvProvider; - +final class ProcessWrapperSandboxedStrategy extends SandboxStrategy { ProcessWrapperSandboxedStrategy( CommandEnvironment cmdEnv, BuildRequest buildRequest, @@ -54,44 +34,8 @@ public class ProcessWrapperSandboxedStrategy extends SandboxStrategy { boolean verboseFailures, String productName) { super( - cmdEnv, - sandboxBase, verboseFailures, - buildRequest.getOptions(SandboxOptions.class)); - this.execRoot = cmdEnv.getExecRoot(); - this.productName = productName; - this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv); - this.localEnvProvider = OS.getCurrent() == OS.DARWIN - ? new XCodeLocalEnvProvider() - : LocalEnvProvider.UNMODIFIED; - } - - @Override - 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()); - - 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); - - 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); - } - - @Override - protected String getName() { - return "processwrapper-sandbox"; + buildRequest.getOptions(SandboxOptions.class), + new ProcessWrapperSandboxedSpawnRunner(cmdEnv, buildRequest, sandboxBase, productName)); } } 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 653c9dafa7..64aaf908ad 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 @@ -36,9 +36,9 @@ final class SandboxActionContextConsumer implements ActionContextConsumer { ImmutableMultimap.builder(); ImmutableMap.Builder<String, String> spawnContexts = ImmutableMap.builder(); - if (LinuxSandboxedStrategy.isSupported(cmdEnv) - || DarwinSandboxedStrategy.isSupported(cmdEnv) - || ProcessWrapperSandboxedStrategy.isSupported(cmdEnv)) { + if (LinuxSandboxedSpawnRunner.isSupported(cmdEnv) + || DarwinSandboxedSpawnRunner.isSupported(cmdEnv) + || ProcessWrapperSandboxedSpawnRunner.isSupported(cmdEnv)) { // This makes the "sandboxed" strategy available via --spawn_strategy=sandboxed, // but it is not necessarily the default. contexts.put(SpawnActionContext.class, "sandboxed"); 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 d9829a2cb7..c2f2055002 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 @@ -42,22 +42,22 @@ final class SandboxActionContextProvider extends ActionContextProvider { // This works on most platforms, but isn't the best choice, so we put it first and let later // platform-specific sandboxing strategies become the default. - if (ProcessWrapperSandboxedStrategy.isSupported(cmdEnv)) { + if (ProcessWrapperSandboxedSpawnRunner.isSupported(cmdEnv)) { contexts.add( new ProcessWrapperSandboxedStrategy( cmdEnv, buildRequest, sandboxBase, verboseFailures, productName)); } // This is the preferred sandboxing strategy on Linux. - if (LinuxSandboxedStrategy.isSupported(cmdEnv)) { + if (LinuxSandboxedSpawnRunner.isSupported(cmdEnv)) { contexts.add( LinuxSandboxedStrategy.create(cmdEnv, buildRequest, sandboxBase, verboseFailures)); } // This is the preferred sandboxing strategy on macOS. - if (DarwinSandboxedStrategy.isSupported(cmdEnv)) { + if (DarwinSandboxedSpawnRunner.isSupported(cmdEnv)) { contexts.add( - DarwinSandboxedStrategy.create( + new DarwinSandboxedStrategy( cmdEnv, buildRequest, sandboxBase, verboseFailures, productName)); } 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 513b8d4df8..f290b812c1 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,24 +14,17 @@ 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; import com.google.devtools.build.lib.actions.SandboxedSpawnActionContext; 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.exec.SpawnExecException; import com.google.devtools.build.lib.exec.SpawnInputExpander; import com.google.devtools.build.lib.exec.SpawnResult; @@ -39,48 +32,29 @@ 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.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 Path sandboxBase; private final boolean verboseFailures; private final SandboxOptions sandboxOptions; - private final ImmutableSet<Path> inaccessiblePaths; private final SpawnInputExpander spawnInputExpander; + private final AbstractSandboxSpawnRunner spawnRunner; public SandboxStrategy( - CommandEnvironment cmdEnv, - Path sandboxBase, boolean verboseFailures, - SandboxOptions sandboxOptions) { - this.sandboxBase = sandboxBase; + SandboxOptions sandboxOptions, + AbstractSandboxSpawnRunner spawnRunner) { this.verboseFailures = verboseFailures; this.sandboxOptions = sandboxOptions; - this.inaccessiblePaths = - sandboxOptions.getInaccessiblePaths(cmdEnv.getDirectories().getFileSystem()); this.spawnInputExpander = new SpawnInputExpander(false); + this.spawnRunner = spawnRunner; } @Override @@ -101,27 +75,11 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { return; } - EventBus eventBus = actionExecutionContext.getEventBus(); - ActionExecutionMetadata owner = spawn.getResourceOwner(); - 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); + if (actionExecutionContext.reportsSubcommands()) { + actionExecutionContext.reportSubcommand(spawn); } - } - - private void actuallyExec( - Spawn spawn, - ActionExecutionContext actionExecutionContext, - AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) - throws ExecException, IOException, InterruptedException { final int timeoutSeconds = Spawns.getTimeoutSeconds(spawn); + final EventBus eventBus = actionExecutionContext.getEventBus(); SpawnExecutionPolicy policy = new SpawnExecutionPolicy() { @Override public ActionInputFileCache getActionInputFileCache() { @@ -164,161 +122,31 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { @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 SpawnResult runSpawn( - Spawn originalSpawn, - SandboxedSpawn sandbox, - SpawnExecutionPolicy policy, - Path execRoot, - int timeoutSeconds) - throws ExecException, IOException, InterruptedException { - try { - 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 (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; + // TODO(ulfjack): We should report more details to the UI. + switch (state) { + case EXECUTING: + String strategyName = spawnRunner.getName(); + eventBus.post( + ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), strategyName)); + break; + case SCHEDULING: + eventBus.post(ActionStatusMessage.schedulingStrategy(spawn.getResourceOwner())); + break; + default: + break; } - throw new SpawnExecException( - message, result, /*forciblyRunRemotely=*/false, /*catastrophe=*/false); - } - return result; - } finally { - if (!sandboxOptions.sandboxDebug) { - sandbox.delete(); } + }; + SpawnResult result = spawnRunner.exec(spawn, policy); + if (result.status() != Status.SUCCESS || result.exitCode() != 0) { + String message = + CommandFailureUtils.describeCommandFailure( + true, spawn.getArguments(), spawn.getEnvironment(), null); + throw new SpawnExecException( + message, result, /*forciblyRunRemotely=*/false, /*catastrophe=*/false); } } - 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 { - 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(); - } - - 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; - } - - /** - * Returns a temporary directory that should be used as the sandbox directory for a single action. - */ - protected Path getSandboxRoot() throws IOException { - return sandboxBase.getRelative( - java.nio.file.Files.createTempDirectory( - java.nio.file.Paths.get(sandboxBase.getPathString()), "") - .getFileName() - .toString()); - } - - /** - * Gets the list of directories that the spawn will assume to be writable. - * - * @throws IOException because we might resolve symlinks, which throws {@link IOException}. - */ - protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, String> env) - throws IOException { - // We have to make the TEST_TMPDIR directory writable if it is specified. - ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder(); - writablePaths.add(sandboxExecRoot); - 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) { - Path path = fileSystem.getPath(writablePath); - writablePaths.add(path.resolveSymbolicLinks()); - } - - return writablePaths.build(); - } - - protected ImmutableSet<Path> getInaccessiblePaths() { - return inaccessiblePaths; - } - - protected abstract String getName(); - @Override public String toString() { return "sandboxed"; |