aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java4
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java456
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java266
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java7
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java43
-rw-r--r--src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java2
-rw-r--r--src/main/tools/BUILD11
-rw-r--r--src/main/tools/namespace-sandbox.c738
-rw-r--r--src/main/tools/process-tools.c139
-rw-r--r--src/main/tools/process-tools.h85
-rw-r--r--src/main/tools/process-wrapper.c263
-rw-r--r--src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java1
-rw-r--r--src/test/shell/bazel/BUILD15
-rwxr-xr-xsrc/test/shell/bazel/bazel_sandboxing_test.sh46
-rwxr-xr-xsrc/test/shell/bazel/namespace-runner_test.sh139
-rwxr-xr-xsrc/test/shell/bazel/process-wrapper_test.sh89
17 files changed, 1467 insertions, 839 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java
index a391a30869..377e95c00b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java
@@ -24,6 +24,7 @@ import com.google.devtools.build.lib.actions.ResourceManager;
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl;
import com.google.devtools.build.lib.analysis.config.BinTools;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.events.Event;
@@ -77,7 +78,6 @@ public class StandaloneTestStrategy extends TestStrategy {
.getChild(getTmpDirName(action.getExecutionSettings().getExecutable().getExecPath()));
Path workingDirectory = runfilesDir.getRelative(action.getRunfilesPrefix());
-
TestRunnerAction.ResolvedPaths resolvedPaths =
action.resolve(actionExecutionContext.getExecutor().getExecRoot());
Map<String, String> env = getEnv(action, runfilesDir, testTmpDir, resolvedPaths);
@@ -95,6 +95,8 @@ public class StandaloneTestStrategy extends TestStrategy {
getArgs(TEST_SETUP, "", action),
env,
info,
+ new RunfilesSupplierImpl(
+ runfilesDir.asFragment(), action.getExecutionSettings().getRunfiles()),
action,
action
.getTestProperties()
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
index 9b549e949c..febc5d76ff 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
@@ -12,10 +12,12 @@ java_library(
"//src/main/java:analysis-exec-rules-skyframe",
"//src/main/java:buildtool-runtime",
"//src/main/java:common",
+ "//src/main/java:events",
"//src/main/java:packages",
"//src/main/java:shell",
"//src/main/java:unix",
"//src/main/java:vfs",
+ "//src/main/java/com/google/devtools/build/lib/standalone",
"//third_party:guava",
],
)
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 4f7557cb7e..67aebeecc8 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
@@ -13,11 +13,17 @@
// limitations under the License.
package com.google.devtools.build.lib.sandbox;
-import com.google.common.collect.ImmutableList;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.Constants;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputHelper;
-import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ExecutionStrategy;
import com.google.devtools.build.lib.actions.Executor;
@@ -25,21 +31,34 @@ 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.analysis.config.RunUnder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
+import com.google.devtools.build.lib.rules.test.TestRunnerAction;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
import com.google.devtools.build.lib.syntax.Label;
import com.google.devtools.build.lib.unix.FilesystemUtils;
-import com.google.devtools.build.lib.util.CommandFailureUtils;
-import com.google.devtools.build.lib.util.DependencySet;
import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.SearchPath;
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
+import java.nio.charset.Charset;
+import java.util.HashSet;
import java.util.List;
-import java.util.TreeSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Strategy that uses sandboxing to execute a process.
@@ -47,12 +66,24 @@ import java.util.TreeSet;
@ExecutionStrategy(name = {"sandboxed"},
contextType = SpawnActionContext.class)
public class LinuxSandboxedStrategy implements SpawnActionContext {
+ private final ExecutorService backgroundWorkers;
+
+ private final BlazeRuntime blazeRuntime;
+ private final BlazeDirectories blazeDirs;
+ private final Path execRoot;
private final boolean verboseFailures;
- private final BlazeDirectories directories;
+ private final StandaloneSpawnStrategy standaloneStrategy;
+ private final UUID uuid = UUID.randomUUID();
+ private final AtomicInteger execCounter = new AtomicInteger();
- public LinuxSandboxedStrategy(BlazeDirectories blazeDirectories, boolean verboseFailures) {
- this.directories = blazeDirectories;
+ public LinuxSandboxedStrategy(
+ BlazeRuntime blazeRuntime, boolean verboseFailures, ExecutorService backgroundWorkers) {
+ this.blazeRuntime = blazeRuntime;
+ this.blazeDirs = blazeRuntime.getDirectories();
+ this.execRoot = blazeDirs.getExecRoot();
this.verboseFailures = verboseFailures;
+ this.backgroundWorkers = backgroundWorkers;
+ this.standaloneStrategy = new StandaloneSpawnStrategy(blazeDirs.getExecRoot(), verboseFailures);
}
/**
@@ -62,158 +93,331 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
public void exec(Spawn spawn, ActionExecutionContext actionExecutionContext)
throws ExecException {
Executor executor = actionExecutionContext.getExecutor();
+
+ // TODO(philwo) - this catches BuildInfo, which can't run in a sandbox. Is there a better way?
+ // Maybe add an annotation to actions that they can refuse to run under certain strategies?
+ if (spawn.getOwner().getLabel() == null
+ || spawn.getArguments().get(0).contains("build-runfiles")) {
+ standaloneStrategy.exec(spawn, actionExecutionContext);
+ return;
+ }
+
if (executor.reportsSubcommands()) {
executor.reportSubcommand(
Label.print(spawn.getOwner().getLabel()) + " [" + spawn.getResourceOwner().prettyPrint()
+ "]", spawn.asShellCommand(executor.getExecRoot()));
}
- boolean processHeaders = spawn.getResourceOwner() instanceof CppCompileAction;
-
- Path execPath = this.directories.getExecRoot();
- List<String> spawnArguments = new ArrayList<>();
- for (String arg : spawn.getArguments()) {
- if (arg.startsWith(execPath.getPathString())) {
- // make all paths relative for the sandbox
- spawnArguments.add(arg.substring(execPath.getPathString().length()));
- } else {
- spawnArguments.add(arg);
- }
- }
+ FileOutErr outErr = actionExecutionContext.getFileOutErr();
- List<? extends ActionInput> expandedInputs =
- ActionInputHelper.expandMiddlemen(spawn.getInputFiles(),
- actionExecutionContext.getMiddlemanExpander());
+ // The execId is a unique ID just for this invocation of "exec".
+ String execId = uuid + "-" + Integer.toString(execCounter.getAndIncrement());
- String cwd = executor.getExecRoot().getPathString();
+ // Each invocation of "exec" gets its own sandbox.
+ Path sandboxPath =
+ execRoot.getRelative(Constants.PRODUCT_NAME + "-sandbox").getRelative(execId);
- FileOutErr outErr = actionExecutionContext.getFileOutErr();
+ ImmutableMultimap<Path, Path> mounts;
try {
- PathFragment includePrefix = null; // null when there's no include mangling to do
- List<PathFragment> includeDirectories = ImmutableList.of();
- if (processHeaders) {
- CppCompileAction cppAction = (CppCompileAction) spawn.getResourceOwner();
- // headers are mounted in the sandbox in a separate include dir, so their names are mangled
- // when running the compilation and will have to be unmangled after it's done in the *.pic.d
- includeDirectories = extractIncludeDirs(execPath, cppAction, spawnArguments);
- includePrefix = getSandboxIncludeDir(cppAction);
- }
+ // Gather all necessary mounts for the sandbox.
+ mounts = getMounts(spawn, sandboxPath, actionExecutionContext);
+ } catch (IOException e) {
+ throw new UserExecException("Could not prepare mounts for sandbox execution", e);
+ }
+
+ int timeout = getTimeout(spawn);
- NamespaceSandboxRunner runner = new NamespaceSandboxRunner(directories, spawn, includePrefix,
- includeDirectories, verboseFailures);
- runner.setupSandbox(expandedInputs, spawn.getOutputFiles());
- runner.run(spawnArguments, spawn.getEnvironment(), new File(cwd), outErr);
- runner.copyOutputs(spawn.getOutputFiles(), outErr);
- if (processHeaders) {
- CppCompileAction cppAction = (CppCompileAction) spawn.getResourceOwner();
- unmangleHeaderFiles(cppAction);
+ try {
+ final NamespaceSandboxRunner runner =
+ new NamespaceSandboxRunner(execRoot, sandboxPath, mounts, verboseFailures);
+ try {
+ runner.run(
+ spawn.getArguments(),
+ spawn.getEnvironment(),
+ blazeDirs.getExecRoot().getPathFile(),
+ outErr,
+ spawn.getOutputFiles(),
+ timeout);
+ } finally {
+ // Due to the Linux kernel behavior, if we try to remove the sandbox too quickly after the
+ // process has exited, we get "Device busy" errors because some of the mounts have not yet
+ // been undone. A second later it usually works. We will just clean the old sandboxes up
+ // using a background worker.
+ backgroundWorkers.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ runner.cleanup();
+ return;
+ } catch (IOException e2) {
+ // Sleep & retry.
+ Thread.sleep(250);
+ }
+ }
+ } catch (InterruptedException e) {
+ // Exit.
+ }
+ }
+ });
}
- runner.cleanup();
} catch (CommandException e) {
- String message = CommandFailureUtils.describeCommandFailure(verboseFailures,
- spawn.getArguments(), spawn.getEnvironment(), cwd);
- throw new UserExecException(String.format("%s: %s", message, e));
+ EventHandler handler = actionExecutionContext.getExecutor().getEventHandler();
+ handler.handle(
+ Event.error("Sandboxed execution failed: " + spawn.getOwner().getLabel() + "."));
+ throw new UserExecException("Error during execution of spawn", e);
} catch (IOException e) {
- throw new UserExecException(e.getMessage());
+ EventHandler handler = actionExecutionContext.getExecutor().getEventHandler();
+ handler.handle(
+ Event.error(
+ "I/O error during sandboxed execution:\n" + Throwables.getStackTraceAsString(e)));
+ throw new UserExecException("Could not execute spawn", e);
}
}
- private void unmangleHeaderFiles(CppCompileAction cppCompileAction) throws IOException {
- Path execPath = this.directories.getExecRoot();
- CppCompileAction.DotdFile dotdfile = cppCompileAction.getDotdFile();
- DependencySet depset = new DependencySet(execPath).read(dotdfile.getPath());
- DependencySet unmangled = new DependencySet(execPath);
- PathFragment sandboxIncludeDir = getSandboxIncludeDir(cppCompileAction);
- PathFragment prefix = sandboxIncludeDir.getRelative(execPath.asFragment().relativeTo("/"));
- for (PathFragment dep : depset.getDependencies()) {
- if (dep.startsWith(prefix)) {
- dep = dep.relativeTo(prefix);
+ private int getTimeout(Spawn spawn) throws UserExecException {
+ String timeoutStr = spawn.getExecutionInfo().get("timeout");
+ if (timeoutStr != null) {
+ try {
+ return Integer.parseInt(timeoutStr);
+ } catch (NumberFormatException e) {
+ throw new UserExecException("could not parse timeout: " + e);
}
- unmangled.addDependency(dep);
}
- unmangled.write(execPath.getRelative(depset.getOutputFileName()), ".d");
+ return -1;
}
- private PathFragment getSandboxIncludeDir(CppCompileAction cppCompileAction) {
- return new PathFragment(
- "include-" + Actions.escapedPath(cppCompileAction.getPrimaryOutput().toString()));
- }
+ private ImmutableMultimap<Path, Path> getMounts(
+ Spawn spawn, Path sandboxPath, ActionExecutionContext actionExecutionContext)
+ throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+ mounts.putAll(mountUsualUnixDirs(sandboxPath));
+ mounts.putAll(setupBlazeUtils(sandboxPath));
+ mounts.putAll(mountRunfilesFromManifests(spawn, sandboxPath));
+ mounts.putAll(mountRunfilesFromSuppliers(spawn, sandboxPath));
+ mounts.putAll(mountRunfilesForTests(spawn, sandboxPath));
+ mounts.putAll(mountInputs(spawn, sandboxPath, actionExecutionContext));
+ mounts.putAll(mountRunUnderCommand(spawn, sandboxPath));
+
+ SetMultimap<Path, Path> fixedMounts = LinkedHashMultimap.create();
+ for (Entry<Path, Path> mount : mounts.build().entries()) {
+ Path source = mount.getKey();
+ Path target = mount.getValue();
+ validateAndAddMount(sandboxPath, fixedMounts, source, target);
+
+ // Iteratively resolve symlinks and mount the whole chain. Take care not to run into a cyclic
+ // symlink - when we already processed the source once, we can exit the loop. Skyframe will
+ // catch cyclic symlinks for declared inputs, but this won't help if there is one in the parts
+ // of the host system that we mount.
+ Set<Path> seenSources = new HashSet<>();
+ while (source.isSymbolicLink() && seenSources.add(source)) {
+ source = source.getParentDirectory().getRelative(source.readSymbolicLink());
+ target = sandboxPath.getRelative(source.asFragment().relativeTo("/"));
- private ImmutableList<PathFragment> extractIncludeDirs(Path execPath,
- CppCompileAction cppCompileAction, List<String> spawnArguments) throws IOException {
- List<PathFragment> includes = new ArrayList<>();
- includes.addAll(cppCompileAction.getQuoteIncludeDirs());
- includes.addAll(cppCompileAction.getIncludeDirs());
- includes.addAll(cppCompileAction.getSystemIncludeDirs());
-
- // gcc implicitly includes headers in the same dir as .cc file
- PathFragment sourceDirectory =
- cppCompileAction.getSourceFile().getPath().getParentDirectory().asFragment();
- includes.add(sourceDirectory);
- spawnArguments.add("-iquote");
- spawnArguments.add(sourceDirectory.toString());
-
- TreeSet<PathFragment> processedIncludes = new TreeSet<>();
- for (int i = 0; i < includes.size(); i++) {
- PathFragment absolutePath;
- if (!includes.get(i).isAbsolute()) {
- absolutePath = execPath.getRelative(includes.get(i)).asFragment();
- } else {
- absolutePath = includes.get(i);
+ validateAndAddMount(sandboxPath, fixedMounts, source, target);
}
- // CppCompileAction may provide execPath as one of the include directories. This is a big
- // overestimation of what is actually needed and doesn't make for very hermetic sandbox
- // (since everything from the workspace will be somehow accessed in the sandbox). To have
- // some more hermeticity in this situation we mount all the include dirs in:
- // sandbox-directory/include-prefix/actual-include-dir
- // (where include-prefix is obtained from this.getSandboxIncludeDir(cppCompileAction))
- // and make so gcc looks there for includes. This should prevent the user from accessing
- // files that technically should not be in the sandbox.
- // TODO(bazel-team): change CppCompileAction so that include dirs contain only subsets of the
- // execPath
- if (absolutePath.equals(execPath.asFragment())) {
- // we can't mount execPath because it will lead to a circular mount; instead mount its
- // subdirs inside (other than the ones containing sandbox)
- String[] subdirs = FilesystemUtils.readdir(absolutePath.toString());
- for (String dirName : subdirs) {
- if (dirName.equals("_bin") || dirName.equals("bazel-out")) {
- continue;
- }
- PathFragment child = absolutePath.getChild(dirName);
- processedIncludes.add(child);
+ }
+ return ImmutableMultimap.copyOf(fixedMounts);
+ }
+
+ /**
+ * Adds the new mount ("source" -> "target") to "mounts" after doing some validations on it.
+ *
+ * @return true if the mount was added to the multimap, or false if the multimap already contained
+ * the mount.
+ */
+ private static boolean validateAndAddMount(
+ Path sandboxPath, SetMultimap<Path, Path> mounts, Path source, Path target) {
+ // The source must exist.
+ Preconditions.checkArgument(source.exists(), source.toString() + " does not exist");
+
+ // We cannot mount two different things onto the same target.
+ if (!mounts.containsEntry(source, target) && mounts.containsValue(target)) {
+ // There is a conflicting entry, find it and error out.
+ for (Entry<Path, Path> mount : mounts.entries()) {
+ if (mount.getValue().equals(target)) {
+ throw new IllegalStateException(
+ String.format(
+ "Cannot mount both '%s' and '%s' onto '%s'", mount.getKey(), source, target));
}
- } else {
- processedIncludes.add(absolutePath);
}
}
- // pseudo random name for include directory inside sandbox, so it won't be accessed by accident
- String prefix = getSandboxIncludeDir(cppCompileAction).toString();
+ // Mounts must always mount into the sandbox, otherwise they might corrupt the host system.
+ Preconditions.checkArgument(
+ target.startsWith(sandboxPath),
+ String.format("(%s -> %s) does not mount into sandbox", source, target));
+
+ return mounts.put(source, target);
+ }
- // change names in the invocation
- for (int i = 0; i < spawnArguments.size(); i++) {
- if (spawnArguments.get(i).startsWith("-I")) {
- String argument = spawnArguments.get(i).substring(2);
- spawnArguments.set(i, setIncludeDirSandboxPath(execPath, argument, "-I" + prefix));
+ /**
+ * Mount a certain set of unix directories to make the usual tools and libraries available to the
+ * spawn that runs.
+ */
+ private ImmutableMultimap<Path, Path> mountUsualUnixDirs(Path sandboxPath) throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+ FileSystem fs = blazeDirs.getFileSystem();
+ mounts.put(fs.getPath("/bin"), sandboxPath.getRelative("bin"));
+ mounts.put(fs.getPath("/etc"), sandboxPath.getRelative("etc"));
+ for (String entry : FilesystemUtils.readdir("/")) {
+ if (entry.startsWith("lib")) {
+ mounts.put(fs.getRootDirectory().getRelative(entry), sandboxPath.getRelative(entry));
}
- if (spawnArguments.get(i).equals("-iquote") || spawnArguments.get(i).equals("-isystem")) {
- spawnArguments.set(i + 1, setIncludeDirSandboxPath(execPath,
- spawnArguments.get(i + 1), prefix));
+ }
+ for (String entry : FilesystemUtils.readdir("/usr")) {
+ if (!entry.equals("local")) {
+ mounts.put(
+ fs.getPath("/usr").getRelative(entry),
+ sandboxPath.getRelative("usr").getRelative(entry));
}
}
- return ImmutableList.copyOf(processedIncludes);
+ return mounts.build();
+ }
+
+ /**
+ * Mount the embedded tools.
+ */
+ private ImmutableMultimap<Path, Path> setupBlazeUtils(Path sandboxPath) throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+ Path source = blazeDirs.getEmbeddedBinariesRoot().getRelative("build-runfiles");
+ Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/"));
+ mounts.put(source, target);
+ return mounts.build();
}
- private String setIncludeDirSandboxPath(Path execPath, String argument, String prefix) {
- StringBuilder builder = new StringBuilder(prefix);
- if (argument.charAt(0) != '/') {
- // relative path
- builder.append(execPath);
- builder.append('/');
+ /**
+ * Mount all runfiles that the spawn needs as specified in its runfiles manifests.
+ */
+ private ImmutableMultimap<Path, Path> mountRunfilesFromManifests(Spawn spawn, Path sandboxPath)
+ throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+ FileSystem fs = blazeDirs.getFileSystem();
+ for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
+ String manifestFilePath = manifest.getValue().getPath().getPathString();
+ Preconditions.checkState(!manifest.getKey().isAbsolute());
+ Path targetDirectory = execRoot.getRelative(manifest.getKey());
+ for (String line : Files.readLines(new File(manifestFilePath), Charset.defaultCharset())) {
+ String[] fields = line.split(" ");
+ Preconditions.checkState(
+ fields.length == 2, "'" + line + "' does not split into exactly 2 parts");
+ Path source = fs.getPath(fields[1]);
+ Path targetPath = targetDirectory.getRelative(fields[0]);
+ Path targetInSandbox = sandboxPath.getRelative(targetPath.asFragment().relativeTo("/"));
+ mounts.put(source, targetInSandbox);
+ }
}
- builder.append(argument);
+ return mounts.build();
+ }
- return builder.toString();
+ /**
+ * Mount all runfiles that the spawn needs as specified via its runfiles suppliers.
+ */
+ private ImmutableMultimap<Path, Path> mountRunfilesFromSuppliers(Spawn spawn, Path sandboxPath)
+ throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+ FileSystem fs = blazeDirs.getFileSystem();
+ Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings =
+ spawn.getRunfilesSupplier().getMappings();
+ for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
+ rootsAndMappings.entrySet()) {
+ PathFragment root = rootAndMappings.getKey();
+ if (root.isAbsolute()) {
+ root = root.relativeTo("/");
+ }
+ for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
+ Artifact sourceArtifact = mapping.getValue();
+ Path source = (sourceArtifact != null) ? sourceArtifact.getPath() : fs.getPath("/dev/null");
+
+ Preconditions.checkArgument(!mapping.getKey().isAbsolute());
+ Path target = sandboxPath.getRelative(root.getRelative(mapping.getKey()));
+ mounts.put(source, target);
+ }
+ }
+ return mounts.build();
+ }
+
+ /**
+ * Tests are a special case and we have to mount the TEST_SRCDIR where the test expects it to be
+ * and also provide a TEST_TMPDIR to the test where it can store temporary files.
+ */
+ private ImmutableMultimap<Path, Path> mountRunfilesForTests(Spawn spawn, Path sandboxPath)
+ throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+ FileSystem fs = blazeDirs.getFileSystem();
+ if (spawn.getEnvironment().containsKey("TEST_TMPDIR")) {
+ Path source = fs.getPath(spawn.getEnvironment().get("TEST_TMPDIR"));
+ Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/"));
+ FileSystemUtils.createDirectoryAndParents(target);
+ }
+ return mounts.build();
+ }
+
+ /**
+ * Mount all inputs of the spawn.
+ */
+ private ImmutableMultimap<Path, Path> mountInputs(
+ Spawn spawn, Path sandboxPath, ActionExecutionContext actionExecutionContext)
+ throws IOException {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+
+ List<ActionInput> inputs =
+ ActionInputHelper.expandMiddlemen(
+ spawn.getInputFiles(), actionExecutionContext.getMiddlemanExpander());
+
+ if (spawn.getResourceOwner() instanceof CppCompileAction) {
+ CppCompileAction action = (CppCompileAction) spawn.getResourceOwner();
+ if (action.shouldScanIncludes()) {
+ inputs.addAll(action.getAdditionalInputs());
+ }
+ }
+
+ for (ActionInput input : inputs) {
+ if (input.getExecPathString().contains("internal/_middlemen/")) {
+ continue;
+ }
+ Path source = execRoot.getRelative(input.getExecPathString());
+ Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/"));
+ mounts.put(source, target);
+ }
+ return mounts.build();
+ }
+
+ /**
+ * If a --run_under= option is set and refers to a command via its path (as opposed to via its
+ * label), we have to mount this. Note that this is best effort and works fine for shell scripts
+ * and small binaries, but we can't track any further dependencies of this command.
+ *
+ * <p>If --run_under= refers to a label, it is automatically provided in the spawn's input files,
+ * so mountInputs() will catch that case.
+ */
+ private ImmutableMultimap<Path, Path> mountRunUnderCommand(Spawn spawn, Path sandboxPath) {
+ ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder();
+
+ if (spawn.getResourceOwner() instanceof TestRunnerAction) {
+ TestRunnerAction testRunnerAction = ((TestRunnerAction) spawn.getResourceOwner());
+ RunUnder runUnder = testRunnerAction.getExecutionSettings().getRunUnder();
+ if (runUnder != null && runUnder.getCommand() != null) {
+ PathFragment sourceFragment = new PathFragment(runUnder.getCommand());
+ Path source;
+ if (sourceFragment.isAbsolute()) {
+ source = blazeDirs.getFileSystem().getPath(sourceFragment);
+ } else if (blazeDirs.getExecRoot().getRelative(sourceFragment).exists()) {
+ source = blazeDirs.getExecRoot().getRelative(sourceFragment);
+ } else {
+ List<Path> searchPath =
+ SearchPath.parse(blazeDirs.getFileSystem(), blazeRuntime.getClientEnv().get("PATH"));
+ source = SearchPath.which(searchPath, runUnder.getCommand());
+ }
+ if (source != null) {
+ Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/"));
+ mounts.put(source, target);
+ }
+ }
+ }
+ return mounts.build();
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java
index 7720b787c6..b68590c1ca 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java
@@ -14,17 +14,13 @@
package com.google.devtools.build.lib.sandbox;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.io.Files;
import com.google.devtools.build.lib.actions.ActionInput;
-import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.Spawn;
-import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.shell.Command;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.unix.FilesystemUtils;
-import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
@@ -32,179 +28,30 @@ import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.File;
import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
/**
* Helper class for running the namespace sandbox. This runner prepares environment inside the
- * sandbox (copies inputs, creates file structure), handles sandbox output, performs cleanup and
- * changes invocation if necessary.
+ * sandbox, handles sandbox output, performs cleanup and changes invocation if necessary.
*/
public class NamespaceSandboxRunner {
- private final boolean debug;
- private final PathFragment sandboxDirectory;
- private final Path sandboxPath;
- private final List<String> mounts;
- private final Path embeddedBinaries;
- private final ImmutableList<PathFragment> includeDirectories;
- private final PathFragment includePrefix;
- private final Spawn spawn;
private final Path execRoot;
+ private final Path sandboxPath;
+ private final Path sandboxExecRoot;
+ private final ImmutableMultimap<Path, Path> mounts;
+ private final boolean debug;
- public NamespaceSandboxRunner(BlazeDirectories directories, Spawn spawn,
- PathFragment includePrefix, List<PathFragment> includeDirectories, boolean debug) {
- String md5sum = Fingerprint.md5Digest(spawn.getResourceOwner().getPrimaryOutput().toString());
- this.sandboxDirectory = new PathFragment("sandbox-root-" + md5sum);
- this.sandboxPath =
- directories.getExecRoot().getRelative("sandboxes").getRelative(sandboxDirectory);
+ public NamespaceSandboxRunner(
+ Path execRoot, Path sandboxPath, ImmutableMultimap<Path, Path> mounts, boolean debug) {
+ this.execRoot = execRoot;
+ this.sandboxPath = sandboxPath;
+ this.sandboxExecRoot = sandboxPath.getRelative(execRoot.asFragment().relativeTo("/"));
+ this.mounts = mounts;
this.debug = debug;
- this.mounts = new ArrayList<>();
- this.embeddedBinaries = directories.getEmbeddedBinariesRoot();
- this.includePrefix = includePrefix;
- this.includeDirectories = ImmutableList.copyOf(includeDirectories);
- this.spawn = spawn;
- this.execRoot = directories.getExecRoot();
}
- private void createFileSystem(Collection<? extends ActionInput> outputs) throws IOException {
- // create the sandboxes' parent directory if needed
- // TODO(bazel-team): create this with rest of the workspace dirs
- if (!sandboxPath.getParentDirectory().isDirectory()) {
- FilesystemUtils.mkdir(sandboxPath.getParentDirectory().getPathString(), 0755);
- }
-
- FilesystemUtils.mkdir(sandboxPath.getPathString(), 0755);
- String[] dirs = { "bin", "etc" };
- for (String dir : dirs) {
- FilesystemUtils.mkdir(sandboxPath.getChild(dir).getPathString(), 0755);
- mounts.add("/" + dir);
- }
-
- // usr
- String[] dirsUsr = { "bin", "include" };
- FilesystemUtils.mkdir(sandboxPath.getChild("usr").getPathString(), 0755);
- Path usr = sandboxPath.getChild("usr");
- for (String dir : dirsUsr) {
- FilesystemUtils.mkdir(usr.getChild(dir).getPathString(), 0755);
- mounts.add("/usr/" + dir);
- }
- FileSystemUtils.createDirectoryAndParents(usr.getChild("local").getChild("include"));
- mounts.add("/usr/local/include");
-
- // shared libs
- String[] rootDirs = FilesystemUtils.readdir("/");
- for (String entry : rootDirs) {
- if (entry.startsWith("lib")) {
- FilesystemUtils.mkdir(sandboxPath.getChild(entry).getPathString(), 0755);
- mounts.add("/" + entry);
- }
- }
-
- String[] usrDirs = FilesystemUtils.readdir("/usr/");
- for (String entry : usrDirs) {
- if (entry.startsWith("lib")) {
- String lib = usr.getChild(entry).getPathString();
- FilesystemUtils.mkdir(lib, 0755);
- mounts.add("/usr/" + entry);
- }
- }
-
- if (this.includePrefix != null) {
- FilesystemUtils.mkdir(sandboxPath.getRelative(includePrefix).getPathString(), 0755);
-
- for (PathFragment fullPath : includeDirectories) {
- // includeDirectories should be absolute paths like /usr/include/foo.h. we want to combine
- // them into something like sandbox/include-prefix/usr/include/foo.h - for that we remove
- // the leading '/' from the path string and concatenate with sandbox/include/prefix
- FileSystemUtils.createDirectoryAndParents(sandboxPath.getRelative(includePrefix)
- .getRelative(fullPath.getPathString().substring(1)));
- }
- }
-
- // output directories
- for (ActionInput output : outputs) {
- PathFragment parentDirectory =
- new PathFragment(output.getExecPathString()).getParentDirectory();
- FileSystemUtils.createDirectoryAndParents(sandboxPath.getRelative(parentDirectory));
- }
- }
-
- public void setupSandbox(List<? extends ActionInput> inputs,
- Collection<? extends ActionInput> outputs) throws IOException {
- createFileSystem(outputs);
- setupBlazeUtils();
- includeManifests();
- includeRunfiles();
- copyInputs(inputs);
- }
-
- private void copyInputs(List<? extends ActionInput> inputs) throws IOException {
- for (ActionInput input : inputs) {
- if (input.getExecPathString().contains("internal/_middlemen/")) {
- continue;
- }
- Path target = sandboxPath.getRelative(input.getExecPathString());
- Path source = execRoot.getRelative(input.getExecPathString());
- FileSystemUtils.createDirectoryAndParents(target.getParentDirectory());
- File targetFile = new File(target.getPathString());
- // TODO(bazel-team): mount inputs inside sandbox instead of copying
- Files.copy(new File(source.getPathString()), targetFile);
- FilesystemUtils.chmod(targetFile, 0755);
- }
- }
-
- private void includeRunfiles() throws IOException {
- Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings =
- spawn.getRunfilesSupplier().getMappings();
- for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
- rootsAndMappings.entrySet()) {
- PathFragment root = rootAndMappings.getKey();
- for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
- Artifact sourceArtifact = mapping.getValue();
- String sourcePath = (sourceArtifact != null) ? sourceArtifact.getPath().getPathString()
- : "/dev/null";
- File source = new File(sourcePath);
-
- String targetPath = root.getRelative(mapping.getKey()).getPathString();
- File target = new File(targetPath);
-
- Files.createParentDirs(target);
- Files.copy(source, target);
- }
- }
- }
-
- private void includeManifests() throws IOException {
- for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
- String path = manifest.getValue().getPath().getPathString();
- for (String line : Files.readLines(new File(path), Charset.defaultCharset())) {
- String[] fields = line.split(" ");
- String targetPath = sandboxPath.getPathString() + PathFragment.SEPARATOR_CHAR + fields[0];
- String sourcePath = fields[1];
- File source = new File(sourcePath);
- File target = new File(targetPath);
- Files.createParentDirs(target);
- Files.copy(source, target);
- }
- }
- }
-
- private void setupBlazeUtils() throws IOException {
- Path bin = this.sandboxPath.getChild("_bin");
- if (!bin.isDirectory()) {
- FilesystemUtils.mkdir(bin.getPathString(), 0755);
- }
- Files.copy(new File(this.embeddedBinaries.getChild("build-runfiles").getPathString()),
- new File(bin.getChild("build-runfiles").getPathString()));
- FilesystemUtils.chmod(bin.getChild("build-runfiles").getPathString(), 0755);
- }
-
-
/**
* Runs given
*
@@ -214,65 +61,86 @@ public class NamespaceSandboxRunner {
* @param outErr - error output to capture sandbox's and command's stderr
* @throws CommandException
*/
- public void run(List<String> spawnArguments, ImmutableMap<String, String> env, File cwd,
- FileOutErr outErr) throws CommandException {
- List<String> args = new ArrayList<>();
- args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString());
+ public void run(
+ List<String> spawnArguments,
+ ImmutableMap<String, String> env,
+ File cwd,
+ FileOutErr outErr,
+ Collection<? extends ActionInput> outputs,
+ int timeout)
+ throws IOException, CommandException {
+ createFileSystem(outputs);
- // Only for c++ compilation
- if (includePrefix != null) {
- for (PathFragment include : includeDirectories) {
- args.add("-n");
- args.add(include.getPathString());
- }
+ List<String> args = new ArrayList<>();
- args.add("-N");
- args.add(includePrefix.getPathString());
- }
+ args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString());
if (debug) {
args.add("-D");
}
+ // Sandbox directory.
args.add("-S");
args.add(sandboxPath.getPathString());
- for (String mount : mounts) {
+
+ // Working directory of the spawn.
+ args.add("-W");
+ args.add(cwd.toString());
+
+ // Kill the process after a timeout.
+ if (timeout != -1) {
+ args.add("-T");
+ args.add(Integer.toString(timeout));
+ }
+
+ // Mount all the inputs.
+ for (ImmutableMap.Entry<Path, Path> mount : mounts.entries()) {
+ args.add("-M");
+ args.add(mount.getKey().getPathString());
args.add("-m");
- args.add(mount);
+ args.add(mount.getValue().getPathString());
}
- args.add("-C");
+ args.add("--");
args.addAll(spawnArguments);
- Command cmd = new Command(args.toArray(new String[] {}), env, cwd);
+
+ Command cmd = new Command(args.toArray(new String[0]), env, cwd);
cmd.execute(
- /* stdin */new byte[] {},
- Command.NO_OBSERVER,
- outErr.getOutputStream(),
- outErr.getErrorStream(),
- /* killSubprocessOnInterrupt */true);
+ /* stdin */ new byte[] {},
+ Command.NO_OBSERVER,
+ outErr.getOutputStream(),
+ outErr.getErrorStream(),
+ /* killSubprocessOnInterrupt */ true);
+
+ copyOutputs(outputs);
}
+ private void createFileSystem(Collection<? extends ActionInput> outputs) throws IOException {
+ FileSystemUtils.createDirectoryAndParents(sandboxPath);
- public void cleanup() throws IOException {
- FilesystemUtils.rmTree(sandboxPath.getPathString());
+ // Prepare the output directories in the sandbox.
+ for (ActionInput output : outputs) {
+ PathFragment parentDirectory =
+ new PathFragment(output.getExecPathString()).getParentDirectory();
+ FileSystemUtils.createDirectoryAndParents(sandboxExecRoot.getRelative(parentDirectory));
+ }
}
-
- public void copyOutputs(Collection<? extends ActionInput> outputs, FileOutErr outErr)
- throws IOException {
+ private void copyOutputs(Collection<? extends ActionInput> outputs) throws IOException {
for (ActionInput output : outputs) {
- Path source = this.sandboxPath.getRelative(output.getExecPathString());
- Path target = this.execRoot.getRelative(output.getExecPathString());
+ Path source = sandboxExecRoot.getRelative(output.getExecPathString());
+ Path target = execRoot.getRelative(output.getExecPathString());
FileSystemUtils.createDirectoryAndParents(target.getParentDirectory());
- // TODO(bazel-team): eliminate cases when there are excessive outputs in spawns
- // (java compilation expects "srclist" file in its outputs which is sometimes not produced)
if (source.isFile()) {
Files.move(new File(source.getPathString()), new File(target.getPathString()));
- } else {
- outErr.getErrorStream().write(("Output wasn't created by action: " + output + "\n")
- .getBytes(StandardCharsets.UTF_8));
}
}
}
+
+ public void cleanup() throws IOException {
+ if (sandboxPath.exists()) {
+ FilesystemUtils.rmTree(sandboxPath.getPathString());
+ }
+ }
}
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 69b13af624..20b64c87ca 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
@@ -22,6 +22,8 @@ import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.util.OS;
+import java.util.concurrent.ExecutorService;
+
/**
* Provides the sandboxed spawn strategy.
*/
@@ -30,12 +32,13 @@ public class SandboxActionContextProvider extends ActionContextProvider {
@SuppressWarnings("unchecked")
private final ImmutableList<ActionContext> strategies;
- public SandboxActionContextProvider(BlazeRuntime runtime, BuildRequest buildRequest) {
+ public SandboxActionContextProvider(
+ BlazeRuntime runtime, BuildRequest buildRequest, ExecutorService backgroundWorkers) {
boolean verboseFailures = buildRequest.getOptions(ExecutionOptions.class).verboseFailures;
Builder<ActionContext> strategies = ImmutableList.builder();
if (OS.getCurrent() == OS.LINUX) {
- strategies.add(new LinuxSandboxedStrategy(runtime.getDirectories(), verboseFailures));
+ strategies.add(new LinuxSandboxedStrategy(runtime, verboseFailures, backgroundWorkers));
}
this.strategies = strategies.build();
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index 65d0c15379..bc8785b659 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -13,6 +13,7 @@
// limitations under the License.
package com.google.devtools.build.lib.sandbox;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.actions.ActionContextConsumer;
@@ -23,17 +24,22 @@ import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.Command;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
/**
* This module provides the Sandbox spawn strategy.
*/
public class SandboxModule extends BlazeModule {
+ private final ExecutorService backgroundWorkers = Executors.newCachedThreadPool();
private BuildRequest buildRequest;
private BlazeRuntime runtime;
@Override
public Iterable<ActionContextProvider> getActionContextProviders() {
return ImmutableList.<ActionContextProvider>of(
- new SandboxActionContextProvider(runtime, buildRequest));
+ new SandboxActionContextProvider(runtime, buildRequest, backgroundWorkers));
}
@Override
@@ -43,7 +49,12 @@ public class SandboxModule extends BlazeModule {
@Override
public void beforeCommand(BlazeRuntime runtime, Command command) {
- this.runtime = runtime;
+ if (this.runtime == null) {
+ this.runtime = runtime;
+ } else {
+ // The BlazeRuntime is guaranteed to never change.
+ Preconditions.checkArgument(runtime == this.runtime);
+ }
runtime.getEventBus().register(this);
}
@@ -51,4 +62,32 @@ public class SandboxModule extends BlazeModule {
public void buildStarting(BuildStartingEvent event) {
buildRequest = event.getRequest();
}
+
+ /**
+ * Shut down the background worker pool in the canonical way.
+ *
+ * <p>See https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
+ */
+ @Override
+ public void blazeShutdown() {
+ // Disable new tasks from being submitted
+ backgroundWorkers.shutdown();
+
+ try {
+ // Wait a while for existing tasks to terminate
+ if (!backgroundWorkers.awaitTermination(5, TimeUnit.SECONDS)) {
+ backgroundWorkers.shutdownNow(); // Cancel currently executing tasks
+
+ // Wait a while for tasks to respond to being cancelled and force-kill them if necessary
+ // after the timeout.
+ backgroundWorkers.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ } catch (InterruptedException ie) {
+ // (Re-)Cancel if current thread also interrupted
+ backgroundWorkers.shutdownNow();
+
+ // Preserve interrupt status
+ Thread.currentThread().interrupt();
+ }
+ }
}
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 92f7a3cad0..cdd2d70583 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
@@ -82,7 +82,7 @@ public class StandaloneSpawnStrategy implements SpawnActionContext {
// Disable it for now to make the setup easier and to avoid further PATH hacks.
// Ideally we should have a native implementation of process-wrapper for Windows.
args.add(processWrapper.getPathString());
- args.add("" + timeout);
+ args.add(Integer.toString(timeout));
args.add("5"); /* kill delay: give some time to print stacktraces and whatnot. */
// TODO(bazel-team): use process-wrapper redirection so we don't have to
diff --git a/src/main/tools/BUILD b/src/main/tools/BUILD
index 8fe91a5202..02f0b8d26a 100644
--- a/src/main/tools/BUILD
+++ b/src/main/tools/BUILD
@@ -1,9 +1,18 @@
package(default_visibility = ["//src:__subpackages__"])
+cc_library(
+ name = "process-tools",
+ srcs = ["process-tools.c"],
+ hdrs = ["process-tools.h"],
+ copts = ["-std=c99"],
+)
+
cc_binary(
name = "process-wrapper",
srcs = ["process-wrapper.c"],
copts = ["-std=c99"],
+ linkopts = ["-lm"],
+ deps = [":process-tools"],
)
cc_binary(
@@ -18,6 +27,8 @@ cc_binary(
"//conditions:default": ["namespace-sandbox.c"],
}),
copts = ["-std=c99"],
+ linkopts = ["-lm"],
+ deps = [":process-tools"],
)
filegroup(
diff --git a/src/main/tools/namespace-sandbox.c b/src/main/tools/namespace-sandbox.c
index 5cf6b433be..060356d13f 100644
--- a/src/main/tools/namespace-sandbox.c
+++ b/src/main/tools/namespace-sandbox.c
@@ -1,5 +1,3 @@
-#define _GNU_SOURCE
-
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,404 +12,502 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+#define _GNU_SOURCE
+
#include <errno.h>
#include <fcntl.h>
-#include <getopt.h>
-#include <limits.h>
-#include <linux/capability.h>
+#include <libgen.h>
+#include <pwd.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
-#include <sys/time.h>
#include <sys/types.h>
-#include <sys/wait.h>
#include <unistd.h>
-static int global_debug = 0;
+#include "process-tools.h"
-#define PRINT_DEBUG(...) do { if (global_debug) {fprintf(stderr, "sandbox.c: " __VA_ARGS__);}} while(0)
+#define PRINT_DEBUG(...) \
+ do { \
+ if (global_debug) { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \
+ } \
+ } while (0)
-#define CHECK_CALL(x) if ((x) == -1) { perror(#x); exit(1); }
-#define CHECK_NOT_NULL(x) if (x == NULL) { perror(#x); exit(1); }
-#define DIE() do { fprintf(stderr, "Error in %d\n", __LINE__); exit(-1); } while(0);
+static bool global_debug = false;
+static double global_kill_delay;
+static int global_child_pid;
+static volatile sig_atomic_t global_signal;
-const int kChildrenCleanupDelay = 1;
+// The uid and gid of the user and group 'nobody'.
+static const int kNobodyUid = 65534;
+static const int kNobodyGid = 65534;
-static volatile sig_atomic_t global_signal_received = 0;
-
-//
-// Options parsing result
-//
+// Options parsing result.
struct Options {
- char **args; // Command to run (-C / --)
- char *include_prefix; // Include prefix (-N)
- char *sandbox_root; // Sandbox root (-S)
- char *tools; // tools directory (-t)
- char **mounts; // List of directories to mount (-m)
- char **includes; // List of include directories (-n)
- int num_mounts; // size of mounts
- int num_includes; // size of includes
- int timeout; // Timeout (-T)
+ double timeout_secs; // How long to wait before killing the child (-T)
+ double kill_delay_secs; // How long to wait before sending SIGKILL in case of
+ // timeout (-t)
+ const char *stdout_path; // Where to redirect stdout (-l)
+ const char *stderr_path; // Where to redirect stderr (-L)
+ char *const *args; // Command to run (--)
+ const char *sandbox_root; // Sandbox root (-S)
+ const char *working_dir; // Working directory (-W)
+ char **mount_sources; // Map of directories to mount, from
+ char **mount_targets; // sources -> targets (-m)
+ int num_mounts; // How many mounts were specified
};
-// Print out a usage error. argc and argv are the argument counter
-// and vector, fmt is a format string for the error message to print.
-void Usage(int argc, char **argv, char *fmt, ...);
-// Parse the command line flags and return the result in an
-// Options structure passed as argument.
-void ParseCommandLine(int argc, char **argv, struct Options *opt);
-
-// Signal hanlding
-void PropagateSignals();
-void EnableAlarm();
-// Sandbox setup
-void SetupDirectories(struct Options* opt);
-void SetupSlashDev();
-void SetupUserNamespace(int uid, int gid);
-void ChangeRoot();
-// Write the file "filename" using a format string specified by "fmt".
-// Returns -1 on failure.
-int WriteFile(const char *filename, const char *fmt, ...);
-// Run the command specified by the argv array and kill it after
-// timeout seconds.
-void SpawnCommand(char **argv, int timeout);
-
-
-
-int main(int argc, char *argv[]) {
- struct Options opt = {
- .args = NULL,
- .include_prefix = NULL,
- .sandbox_root = NULL,
- .tools = NULL,
- .mounts = calloc(argc, sizeof(char*)),
- .includes = calloc(argc, sizeof(char*)),
- .num_mounts = 0,
- .num_includes = 0,
- .timeout = 0
- };
- ParseCommandLine(argc, argv, &opt);
- int uid = getuid();
- int gid = getgid();
-
- // parsed all arguments, now prepare sandbox
- PRINT_DEBUG("%s\n", opt.sandbox_root);
- // create new namespaces in which this process and its children will live
- CHECK_CALL(unshare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER));
- CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL));
- // Create the sandbox directory layout
- SetupDirectories(&opt);
- // Set the user namespace (user_namespaces(7))
- SetupUserNamespace(uid, gid);
- // make sandbox actually hermetic:
- ChangeRoot();
+// Print out a usage error. argc and argv are the argument counter and vector,
+// fmt is a format,
+// string for the error message to print.
+static void Usage(int argc, char *const *argv, const char *fmt, ...) {
+ int i;
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
- // Finally call the command
- free(opt.mounts);
- free(opt.includes);
- SpawnCommand(opt.args, opt.timeout);
- return 0;
+ fprintf(stderr,
+ "\nUsage: %s [-S sandbox-root] [-W working-dir] [-M source -m "
+ "target] -- command arg1\n",
+ argv[0]);
+ fprintf(stderr, " provided:");
+ for (i = 0; i < argc; i++) {
+ fprintf(stderr, " %s", argv[i]);
+ }
+ fprintf(stderr,
+ "\nMandatory arguments:\n"
+ " -S directory which will become the root of the sandbox\n"
+ " -- command to run inside sandbox, followed by arguments\n"
+ "\n"
+ "Optional arguments:\n"
+ " -W working directory\n"
+ " -t time to give the child to shutdown cleanly before sending it a "
+ "SIGKILL\n"
+ " -T timeout after which sandbox will be terminated\n"
+ " -t in case timeout occurs, how long to wait before killing the "
+ "child with SIGKILL\n"
+ " -M/-m system directory to mount inside the sandbox\n"
+ " Multiple directories can be specified and each of them will\n"
+ " be mounted readonly. The -M option specifies which directory\n"
+ " to mount, the -m option specifies where to mount it in the\n"
+ " sandbox.\n"
+ " -D if set, debug info will be printed\n"
+ " -l redirect stdout to a file\n"
+ " -L redirect stderr to a file\n");
+ exit(EXIT_FAILURE);
}
-void SpawnCommand(char **argv, int timeout) {
- for (int i = 0; argv[i] != NULL; i++) {
- PRINT_DEBUG("arg: %s\n", argv[i]);
- }
+// Parse the command line flags and return the result in an Options structure
+// passed as argument.
+static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) {
+ extern char *optarg;
+ extern int optind, optopt;
+ int c;
- // spawn child and wait until it finishes
- pid_t cpid = fork();
- if (cpid == 0) {
- CHECK_CALL(setpgid(0, 0));
- // if the execvp below fails with "No such file or directory" it means that:
- // a) the binary is not in the sandbox (which means it wasn't included in
- // the inputs)
- // b) the binary uses shared library which is not inside sandbox - you can
- // check for that by running "ldd ./a.out" (by default directories
- // starting with /lib* and /usr/lib* should be there)
- // c) the binary uses elf interpreter which is not inside sandbox - you can
- // check for that by running "readelf -a a.out | grep interpreter" (the
- // sandbox code assumes that it is either in /lib*/ or /usr/lib*/)
- CHECK_CALL(execvp(argv[0], argv));
- PRINT_DEBUG("Exec failed near %s:%d\n", __FILE__, __LINE__);
- exit(1);
- } else {
- // PARENT
- // make sure that all signals propagate to children (mostly useful to kill
- // entire sandbox)
- PropagateSignals();
- // after given timeout, kill children
- EnableAlarm(timeout);
- int status = 0;
- while (1) {
- PRINT_DEBUG("Waiting for the child...\n");
- pid_t pid = wait(&status);
- if (global_signal_received) {
- PRINT_DEBUG("Received signal: %s\n", strsignal(global_signal_received));
- CHECK_CALL(killpg(cpid, global_signal_received));
- // give children some time for cleanup before they terminate
- sleep(kChildrenCleanupDelay);
- CHECK_CALL(killpg(cpid, SIGKILL));
- exit(128 | global_signal_received);
- }
- if (errno == EINTR) {
- continue;
- }
- if (pid < 0) {
- perror("Wait failed:");
- exit(1);
- }
- if (WIFEXITED(status)) {
- PRINT_DEBUG("Child exited with status: %d\n", WEXITSTATUS(status));
- exit(WEXITSTATUS(status));
- }
- if (WIFSIGNALED(status)) {
- PRINT_DEBUG("Child terminated by a signal: %d\n", WTERMSIG(status));
- exit(WEXITSTATUS(status));
- }
- if (WIFSTOPPED(status)) {
- PRINT_DEBUG("Child stopped by a signal: %d\n", WSTOPSIG(status));
- }
+ while ((c = getopt(argc, argv, ":DS:W:t:T:M:m:l:L:")) != -1) {
+ switch (c) {
+ case 'S':
+ if (opt->sandbox_root == NULL) {
+ opt->sandbox_root = optarg;
+ } else {
+ Usage(argc, argv,
+ "Multiple sandbox roots (-S) specified, expected one.");
+ }
+ break;
+ case 'W':
+ if (opt->working_dir == NULL) {
+ opt->working_dir = optarg;
+ } else {
+ Usage(argc, argv,
+ "Multiple working directories (-W) specified, expected at most "
+ "one.");
+ }
+ break;
+ case 't':
+ if (sscanf(optarg, "%lf", &opt->kill_delay_secs) != 1 ||
+ opt->kill_delay_secs < 0) {
+ Usage(argc, argv, "Invalid kill delay (-t) value: %lf",
+ opt->kill_delay_secs);
+ }
+ break;
+ case 'T':
+ if (sscanf(optarg, "%lf", &opt->timeout_secs) != 1 ||
+ opt->timeout_secs < 0) {
+ Usage(argc, argv, "Invalid timeout (-T) value: %lf",
+ opt->timeout_secs);
+ }
+ break;
+ case 'M':
+ if (opt->mount_sources[opt->num_mounts] != NULL) {
+ Usage(argc, argv, "The -M option must be followed by an -m option.");
+ }
+ opt->mount_sources[opt->num_mounts] = optarg;
+ break;
+ case 'm':
+ if (opt->mount_sources[opt->num_mounts] == NULL) {
+ Usage(argc, argv, "The -m option must be preceded by an -M option.");
+ }
+ if (opt->sandbox_root == NULL) {
+ Usage(argc, argv,
+ "The sandbox root must be set via the -S option before "
+ "specifying an"
+ " -m option.");
+ }
+ if (strstr(optarg, opt->sandbox_root) != optarg) {
+ Usage(argc, argv,
+ "A path passed to the -m option must start with the sandbox "
+ "root.");
+ }
+ opt->mount_targets[opt->num_mounts++] = optarg;
+ break;
+ case 'D':
+ global_debug = true;
+ break;
+ case 'l':
+ if (opt->stdout_path == NULL) {
+ opt->stdout_path = optarg;
+ } else {
+ Usage(argc, argv,
+ "Cannot redirect stdout to more than one destination.");
+ }
+ break;
+ case 'L':
+ if (opt->stderr_path == NULL) {
+ opt->stderr_path = optarg;
+ } else {
+ Usage(argc, argv,
+ "Cannot redirect stderr to more than one destination.");
+ }
+ break;
+ case '?':
+ Usage(argc, argv, "Unrecognized argument: -%c (%d)", optopt, optind);
+ break;
+ case ':':
+ Usage(argc, argv, "Flag -%c requires an argument", optopt);
+ break;
}
}
-}
-int WriteFile(const char *filename, const char *fmt, ...) {
- int r;
- va_list ap;
- FILE *stream = fopen(filename, "w");
- if (stream == NULL) {
- return -1;
+ if (opt->sandbox_root == NULL) {
+ Usage(argc, argv, "Sandbox root (-S) must be specified");
}
- va_start(ap, fmt);
- r = vfprintf(stream, fmt, ap);
- va_end(ap);
- if (r >= 0) {
- r = fclose(stream);
+
+ if (opt->mount_sources[opt->num_mounts] != NULL &&
+ opt->mount_sources[opt->num_mounts] == NULL) {
+ Usage(argc, argv, "An -m option is missing.");
}
- return r;
-}
-//
-// Signal handling
-//
-void SignalHandler(int signum, siginfo_t *info, void *uctxt) {
- global_signal_received = signum;
+ opt->args = argv + optind;
+ if (argc <= optind) {
+ Usage(argc, argv, "No command specified.");
+ }
}
-void PropagateSignals() {
- // propagate some signals received by the parent to processes in sandbox, so
- // that it's easier to terminate entire sandbox
- struct sigaction action = {};
- action.sa_flags = SA_SIGINFO;
- action.sa_sigaction = SignalHandler;
-
- // handle all signals that could terminate the process
- int signals[] = {SIGHUP, SIGINT, SIGKILL, SIGPIPE, SIGALRM, SIGTERM, SIGPOLL,
- SIGPROF, SIGVTALRM,
- // signals below produce core dump by default, however at the moment we'll
- // just terminate
- SIGQUIT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS, SIGTRAP, SIGXCPU,
- SIGXFSZ, -1};
- for (int *p = signals; *p != -1; p++) {
- sigaction(*p, &action, NULL);
+static void CreateNamespaces() {
+ // This weird workaround is necessary due to unshare sometimes failing with EINVAL due to a race
+ // condition in the Linux kernel (see https://lkml.org/lkml/2015/7/28/833).
+ // An alternative would be to use clone/waitpid instead.
+ int delay = 1;
+ int tries = 0;
+ const int max_tries = 5000000;
+ while (tries++ < max_tries) {
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC) ==
+ 0) {
+ PRINT_DEBUG("unshare succeeded after %d tries\n", tries);
+ return;
+ } else {
+ if (errno != EINVAL) {
+ perror("unshare");
+ exit(EXIT_FAILURE);
+ }
+ }
+ usleep(delay);
+ delay = (delay * 3) / 2;
}
+ fprintf(stderr,
+ "unshare failed with EINVAL even after %d tries, giving up.\n",
+ tries);
+ exit(EXIT_FAILURE);
}
-void EnableAlarm(int timeout) {
- if (timeout <= 0) return;
-
- struct itimerval timer = {};
- timer.it_value.tv_sec = (long) timeout;
- CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL));
+static void CreateFile(const char *path) {
+ int handle;
+ CHECK_CALL(handle = open(path, O_CREAT | O_WRONLY | O_EXCL, 0666));
+ CHECK_CALL(close(handle));
}
-//
-// Sandbox setup
-//
-void SetupSlashDev() {
+static void SetupDevices() {
CHECK_CALL(mkdir("dev", 0755));
- const char *devs[] = {
- "/dev/null",
- "/dev/random",
- "/dev/urandom",
- "/dev/zero",
- NULL
- };
+ const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero",
+ NULL};
for (int i = 0; devs[i] != NULL; i++) {
- // open+close to create the file, which will become mount point for actual
- // device
- int handle = open(devs[i] + 1, O_CREAT | O_RDONLY, 0644);
- CHECK_CALL(handle);
- CHECK_CALL(close(handle));
+ CreateFile(devs[i] + 1);
CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL));
}
+
+ CHECK_CALL(symlink("/proc/self/fd", "dev/fd"));
}
-void SetupDirectories(struct Options *opt) {
- // Mount the sandbox and go there.
- CHECK_CALL(mount(opt->sandbox_root, opt->sandbox_root, NULL, MS_BIND | MS_NOSUID, NULL));
- CHECK_CALL(chdir(opt->sandbox_root));
- SetupSlashDev();
- // Mount blaze specific directories - tools/ and build-runfiles/.
- if (opt->tools != NULL) {
- PRINT_DEBUG("tools: %s\n", opt->tools);
- CHECK_CALL(mkdir("tools", 0755));
- CHECK_CALL(mount(opt->tools, "tools", NULL, MS_BIND | MS_RDONLY, NULL));
+// Recursively creates the file or directory specified in "path" and its parent
+// directories.
+static int CreateTarget(const char *path, bool is_directory) {
+ if (path == NULL) {
+ errno = EINVAL;
+ return -1;
}
- // Mount directories passed in argv; those are mostly dirs for shared libs.
- for (int i = 0; i < opt->num_mounts; i++) {
- CHECK_CALL(mount(opt->mounts[i], opt->mounts[i] + 1, NULL, MS_BIND | MS_RDONLY, NULL));
+ struct stat sb;
+ // If the path already exists...
+ if (stat(path, &sb) == 0) {
+ if (is_directory && S_ISDIR(sb.st_mode)) {
+ // and it's a directory and supposed to be a directory, we're done here.
+ return 0;
+ } else if (!is_directory && S_ISREG(sb.st_mode)) {
+ // and it's a regular file and supposed to be one, we're done here.
+ return 0;
+ } else {
+ // otherwise something is really wrong.
+ errno = is_directory ? ENOTDIR : EEXIST;
+ return -1;
+ }
+ } else {
+ // If stat failed because of any error other than "the path does not exist",
+ // this is an error.
+ if (errno != ENOENT) {
+ return -1;
+ }
}
- // C++ compilation
- // C++ headers go in a separate directory.
- if (opt->include_prefix != NULL) {
- CHECK_CALL(chdir(opt->include_prefix));
- for (int i = 0; i < opt->num_includes; i++) {
- // TODO(bazel-team): sometimes list of -iquote given by bazel contains
- // invalid (non-existing) entries, ideally we would like not to have them
- PRINT_DEBUG("include: %s\n", opt->includes[i]);
- if (mount(opt->includes[i], opt->includes[i] + 1 , NULL, MS_BIND, NULL) > -1) {
- continue;
- }
- if (errno == ENOENT) {
- continue;
- }
- CHECK_CALL(-1);
- }
- CHECK_CALL(chdir(".."));
+ // Create the parent directory.
+ CHECK_CALL(CreateTarget(dirname(strdupa(path)), true));
+
+ if (is_directory) {
+ CHECK_CALL(mkdir(path, 0755));
+ } else {
+ CreateFile(path);
}
+ return 0;
+}
+
+static void SetupDirectories(struct Options *opt) {
+ // Mount the sandbox and go there.
+ CHECK_CALL(mount(opt->sandbox_root, opt->sandbox_root, NULL,
+ MS_BIND | MS_NOSUID, NULL));
+ CHECK_CALL(chdir(opt->sandbox_root));
+
+ // Setup /dev.
+ SetupDevices();
+
CHECK_CALL(mkdir("proc", 0755));
CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL));
+
+ CHECK_CALL(mkdir("tmp", 0755));
+ CHECK_CALL(mount("tmpfs", "tmp", "tmpfs", MS_NOSUID | MS_NODEV,
+ "size=25%,mode=1777"));
+
+ // Make sure the home directory exists and is writable.
+ const char *homedir;
+ if ((homedir = getenv("HOME")) == NULL) {
+ homedir = getpwuid(getuid())->pw_dir;
+ }
+
+ if (homedir[0] != '/') {
+ DIE("Home directory of user nobody must be an absolute path, but is %s", homedir);
+ }
+
+ char *homedir_absolute = malloc(strlen(opt->sandbox_root) + strlen(homedir) + 1);
+ strcpy(homedir_absolute, opt->sandbox_root);
+ strcat(homedir_absolute, homedir);
+
+ CreateTarget(homedir_absolute, true);
+ CHECK_CALL(mount("tmpfs", homedir_absolute, "tmpfs", MS_NOSUID | MS_NODEV,
+ "size=25%,mode=1777"));
+
+ // Mount directories passed in argv
+ for (int i = 0; i < opt->num_mounts; i++) {
+ struct stat sb;
+ stat(opt->mount_sources[i], &sb);
+
+ CHECK_CALL(CreateTarget(opt->mount_targets[i], S_ISDIR(sb.st_mode)));
+
+ PRINT_DEBUG("mount -o rbind,ro %s %s\n", opt->mount_sources[i],
+ opt->mount_targets[i]);
+ CHECK_CALL(mount(opt->mount_sources[i], opt->mount_targets[i], NULL,
+ MS_REC | MS_BIND | MS_RDONLY, NULL));
+ }
}
-void SetupUserNamespace(int uid, int gid) {
+// Write the file "filename" using a format string specified by "fmt". Returns
+// -1 on failure.
+static int WriteFile(const char *filename, const char *fmt, ...) {
+ int r;
+ va_list ap;
+ FILE *stream = fopen(filename, "w");
+ if (stream == NULL) {
+ return -1;
+ }
+ va_start(ap, fmt);
+ r = vfprintf(stream, fmt, ap);
+ va_end(ap);
+ if (r >= 0) {
+ r = fclose(stream);
+ }
+ return r;
+}
+
+static void SetupUserNamespace(int uid, int gid) {
// Disable needs for CAP_SETGID
int r = WriteFile("/proc/self/setgroups", "deny");
if (r < 0 && errno != ENOENT) {
// Writing to /proc/self/setgroups might fail on earlier
// version of linux because setgroups does not exist, ignore.
perror("WriteFile(\"/proc/self/setgroups\", \"deny\")");
- exit(-1);
+ exit(EXIT_FAILURE);
}
- // set group and user mapping from outer namespace to inner:
- // no changes in the parent, be root in the child
- CHECK_CALL(WriteFile("/proc/self/uid_map", "0 %d 1\n", uid));
- CHECK_CALL(WriteFile("/proc/self/gid_map", "0 %d 1\n", gid));
- CHECK_CALL(setresuid(0, 0, 0));
- CHECK_CALL(setresgid(0, 0, 0));
+ // Set group and user mapping from outer namespace to inner:
+ // No changes in the parent, be nobody in the child.
+ //
+ // We can't be root in the child, because some code may assume that running as root grants it
+ // certain capabilities that it doesn't in fact have. It's safer to let the child think that it
+ // is just a normal user.
+ CHECK_CALL(WriteFile("/proc/self/uid_map", "%d %d 1\n", kNobodyUid, uid));
+ CHECK_CALL(WriteFile("/proc/self/gid_map", "%d %d 1\n", kNobodyGid, gid));
+
+ CHECK_CALL(setresuid(kNobodyUid, kNobodyUid, kNobodyUid));
+ CHECK_CALL(setresgid(kNobodyGid, kNobodyGid, kNobodyGid));
}
-void ChangeRoot() {
+static void ChangeRoot(struct Options *opt) {
// move the real root to old_root, then detach it
char old_root[16] = "old-root-XXXXXX";
- CHECK_NOT_NULL(mkdtemp(old_root));
+ if (mkdtemp(old_root) == NULL) {
+ perror("mkdtemp");
+ DIE("mkdtemp returned NULL\n");
+ }
+
// pivot_root has no wrapper in libc, so we need syscall()
CHECK_CALL(syscall(SYS_pivot_root, ".", old_root));
CHECK_CALL(chroot("."));
CHECK_CALL(umount2(old_root, MNT_DETACH));
CHECK_CALL(rmdir(old_root));
+
+ if (opt->working_dir != NULL) {
+ CHECK_CALL(chdir(opt->working_dir));
+ }
}
-//
-// Command line parsing
-//
-void Usage(int argc, char **argv, char *fmt, ...) {
- int i;
- va_list ap;
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
+// Called when timeout or signal occurs.
+void OnSignal(int sig) {
+ global_signal = sig;
- fprintf(stderr,
- "\nUsage: %s [-S sandbox-root] [-m mount] [-C|--] command arg1\n",
- argv[0]);
- fprintf(stderr, " provided:");
- for (i = 0; i < argc; i++) {
- fprintf(stderr, " %s", argv[i]);
+ // Nothing to do if we received a signal before spawning the child.
+ if (global_child_pid == -1) {
+ return;
+ }
+
+ if (sig == SIGALRM) {
+ // SIGALRM represents a timeout, so we should give the process a bit of
+ // time to die gracefully if it needs it.
+ KillEverything(global_child_pid, true, global_kill_delay);
+ } else {
+ // Signals should kill the process quickly, as it's typically blocking
+ // the return of the prompt after a user hits "Ctrl-C".
+ KillEverything(global_child_pid, false, global_kill_delay);
}
- fprintf(stderr,
- "\nMandatory arguments:\n"
- " [-C|--] command to run inside sandbox, followed by arguments\n"
- " -S directory which will become the root of the sandbox\n"
- "\n"
- "Optional arguments:\n"
- " -t absolute path to bazel tools directory\n"
- " -T timeout after which sandbox will be terminated\n"
- " -m system directory to mount inside the sandbox\n"
- " Multiple directories can be specified and each of them will\n"
- " be mount as readonly\n"
- " -D if set, debug info will be printed\n");
- exit(1);
}
-void ParseCommandLine(int argc, char **argv, struct Options *opt) {
- extern char *optarg;
- extern int optind, optopt;
- int c;
+// Run the command specified by the argv array and kill it after timeout
+// seconds.
+static void SpawnCommand(char *const *argv, double timeout_secs) {
+ for (int i = 0; argv[i] != NULL; i++) {
+ PRINT_DEBUG("arg: %s\n", argv[i]);
+ }
- opt->include_prefix = NULL;
- opt->sandbox_root = NULL;
- opt->tools = NULL;
- opt->mounts = malloc(argc * sizeof(char*));
- opt->includes = malloc(argc * sizeof(char*));
- opt->num_mounts = 0;
- opt->num_includes = 0;
- opt->timeout = 0;
-
- while ((c = getopt(argc, argv, "+:S:t:T:m:N:n:DC")) != -1) {
- switch(c) {
- case 'S':
- if (opt->sandbox_root == NULL) {
- opt->sandbox_root = optarg;
- } else {
- Usage(argc, argv,
- "Multiple sandbox roots (-S) specified (expected one).");
- }
- break;
- case 'm':
- opt->mounts[opt->num_mounts++] = optarg;
- break;
- case 'D':
- global_debug = 1;
- break;
- case 'T':
- sscanf(optarg, "%d", &opt->timeout);
- if (opt->timeout < 0) {
- Usage(argc, argv, "Invalid timeout (-T) value: %d", opt->timeout);
- }
- break;
- case 'N':
- opt->include_prefix = optarg;
- break;
- case 'n':
- opt->includes[opt->num_includes++] = optarg;
- break;
- case 'C':
- break; // deprecated, ignore.
- case 't':
- opt->tools = optarg;
- break;
- case '?':
- Usage(argc, argv, "Unrecognized argument: -%c (%d)", optopt, optind);
- break;
- case ':':
- Usage(argc, argv, "Flag -%c requires an argument", optopt);
- break;
+ CHECK_CALL(global_child_pid = fork());
+ if (global_child_pid == 0) {
+ // In child.
+ CHECK_CALL(setsid());
+ ClearSignalMask();
+
+ // Force umask to include read and execute for everyone, to make
+ // output permissions predictable.
+ umask(022);
+
+ // Does not return unless something went wrong.
+ CHECK_CALL(execvp(argv[0], argv));
+ } else {
+ // In parent.
+
+ // Set up a signal handler which kills all subprocesses when the given
+ // signal is triggered.
+ HandleSignal(SIGALRM, OnSignal);
+ HandleSignal(SIGTERM, OnSignal);
+ HandleSignal(SIGINT, OnSignal);
+ SetTimeout(timeout_secs);
+
+ int status = WaitChild(global_child_pid, argv[0]);
+
+ // The child is done for, but may have grandchildren that we still have to
+ // kill.
+ kill(-global_child_pid, SIGKILL);
+
+ if (global_signal > 0) {
+ // Don't trust the exit code if we got a timeout or signal.
+ UnHandle(global_signal);
+ raise(global_signal);
+ } else if (WIFEXITED(status)) {
+ exit(WEXITSTATUS(status));
+ } else {
+ int sig = WTERMSIG(status);
+ UnHandle(sig);
+ raise(sig);
}
}
+}
- opt->args = argv + optind;
- if (argc <= optind) {
- Usage(argc, argv, "No command specified");
- }
+int main(int argc, char *const argv[]) {
+ struct Options opt;
+ memset(&opt, 0, sizeof(opt));
+ opt.mount_sources = calloc(argc, sizeof(char *));
+ opt.mount_targets = calloc(argc, sizeof(char *));
+
+ ParseCommandLine(argc, argv, &opt);
+ global_kill_delay = opt.kill_delay_secs;
+
+ int uid = SwitchToEuid();
+ int gid = SwitchToEgid();
+
+ RedirectStdout(opt.stdout_path);
+ RedirectStderr(opt.stderr_path);
+
+ PRINT_DEBUG("sandbox root is %s\n", opt.sandbox_root);
+ PRINT_DEBUG("working dir is %s\n",
+ (opt.working_dir != NULL) ? opt.working_dir : "/ (default)");
+
+ CreateNamespaces();
+
+ // Make our mount namespace private, so that further mounts do not affect the
+ // outside environment.
+ CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ SetupDirectories(&opt);
+ SetupUserNamespace(uid, gid);
+ ChangeRoot(&opt);
+
+ SpawnCommand(opt.args, opt.timeout_secs);
+
+ free(opt.mount_sources);
+ free(opt.mount_targets);
+
+ return 0;
}
diff --git a/src/main/tools/process-tools.c b/src/main/tools/process-tools.c
new file mode 100644
index 0000000000..48441e8485
--- /dev/null
+++ b/src/main/tools/process-tools.c
@@ -0,0 +1,139 @@
+// Copyright 2015 Google Inc. 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.
+
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <signal.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+
+#include "process-tools.h"
+
+int SwitchToEuid() {
+ int uid = getuid();
+ int euid = geteuid();
+ if (uid != euid) {
+ CHECK_CALL(setreuid(euid, euid));
+ }
+ return euid;
+}
+
+int SwitchToEgid() {
+ int gid = getgid();
+ int egid = getegid();
+ if (gid != egid) {
+ CHECK_CALL(setregid(egid, egid));
+ }
+ return egid;
+}
+
+void Redirect(const char *target_path, int fd, const char *name) {
+ if (target_path != NULL && strcmp(target_path, "-") != 0) {
+ int fd_out;
+ CHECK_CALL(fd_out = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0666),
+ "Could not open %s for redirection of %s", target_path, name);
+ CHECK_CALL(dup2(fd_out, fd));
+ CHECK_CALL(close(fd_out));
+ }
+}
+
+void RedirectStdout(const char *stdout_path) {
+ Redirect(stdout_path, STDOUT_FILENO, "stdout");
+}
+
+void RedirectStderr(const char *stderr_path) {
+ Redirect(stderr_path, STDERR_FILENO, "stderr");
+}
+
+void KillEverything(int pgrp, bool gracefully, double graceful_kill_delay) {
+ if (gracefully) {
+ kill(-pgrp, SIGTERM);
+
+ // Round up fractional seconds in this polling implementation.
+ int kill_delay = (int)(ceil(graceful_kill_delay));
+
+ // If the process is still alive, give it some time to die gracefully.
+ while (kill_delay-- > 0 && kill(-pgrp, 0) == 0) {
+ sleep(1);
+ }
+ }
+
+ kill(-pgrp, SIGKILL);
+}
+
+void HandleSignal(int sig, void (*handler)(int)) {
+ struct sigaction sa = {.sa_handler = handler};
+ CHECK_CALL(sigemptyset(&sa.sa_mask));
+ CHECK_CALL(sigaction(sig, &sa, NULL));
+}
+
+void UnHandle(int sig) { HandleSignal(sig, SIG_DFL); }
+
+void ClearSignalMask() {
+ // Use an empty signal mask for the process.
+ sigset_t empty_sset;
+ CHECK_CALL(sigemptyset(&empty_sset));
+ CHECK_CALL(sigprocmask(SIG_SETMASK, &empty_sset, NULL));
+
+ // Set the default signal handler for all signals.
+ for (int i = 1; i < NSIG; ++i) {
+ if (i == SIGKILL || i == SIGSTOP) {
+ continue;
+ }
+ struct sigaction sa = {.sa_handler = SIG_DFL};
+ CHECK_CALL(sigemptyset(&sa.sa_mask));
+ // Ignore possible errors, because we might not be allowed to set the
+ // handler for certain signals, but we still want to try.
+ sigaction(i, &sa, NULL);
+ }
+}
+
+void SetTimeout(double timeout_secs) {
+ if (timeout_secs <= 0) {
+ return;
+ }
+
+ double int_val, fraction_val;
+ fraction_val = modf(timeout_secs, &int_val);
+
+ struct itimerval timer = {.it_interval.tv_sec = 0,
+ .it_interval.tv_usec = 0,
+ .it_value.tv_sec = (long)int_val,
+ .it_value.tv_usec = (long)(fraction_val * 1e6)};
+
+ CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL));
+}
+
+int WaitChild(pid_t pid, const char *name) {
+ int err, status;
+
+ do {
+ err = waitpid(pid, &status, 0);
+ } while (err == -1 && errno == EINTR);
+
+ if (err == -1) {
+ DIE("wait on %s (pid %d) failed\n", name, pid);
+ }
+
+ return status;
+}
diff --git a/src/main/tools/process-tools.h b/src/main/tools/process-tools.h
new file mode 100644
index 0000000000..ef9fcdccdf
--- /dev/null
+++ b/src/main/tools/process-tools.h
@@ -0,0 +1,85 @@
+// Copyright 2015 Google Inc. 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.
+
+#ifndef PROCESS_TOOLS_H__
+#define PROCESS_TOOLS_H__
+
+#include <sys/types.h>
+#include <stdbool.h>
+
+// see
+// http://stackoverflow.com/questions/5641427/how-to-make-preprocessor-generate-a-string-for-line-keyword
+#define S(x) #x
+#define S_(x) S(x)
+#define S__LINE__ S_(__LINE__)
+
+#define DIE(args...) \
+ { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": " args); \
+ exit(EXIT_FAILURE); \
+ }
+
+#define CHECK_CALL(x, ...) \
+ if ((x) == -1) { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \
+ perror(#x); \
+ exit(EXIT_FAILURE); \
+ }
+
+#define CHECK_NOT_NULL(x) \
+ if (x == NULL) { \
+ perror(#x); \
+ exit(EXIT_FAILURE); \
+ }
+
+// Switch completely to the effective uid.
+// Some programs (notably, bash) ignore the euid and just use the uid. This
+// limits the ability for us to use process-wrapper as a setuid binary for
+// security/user-isolation.
+int SwitchToEuid();
+
+// Switch completely to the effective gid.
+int SwitchToEgid();
+
+// Redirect stdout to the file stdout_path (but not if stdout_path is "-").
+void RedirectStdout(const char *stdout_path);
+
+// Redirect stderr to the file stdout_path (but not if stderr_path is "-").
+void RedirectStderr(const char *stderr_path);
+
+// Make sure the process group "pgrp" and all its subprocesses are killed.
+// If "gracefully" is true, sends SIGKILL first and after a timeout of
+// "graceful_kill_delay" seconds, sends SIGTERM.
+// If not, send SIGTERM immediately.
+void KillEverything(int pgrp, bool gracefully, double graceful_kill_delay);
+
+// Set up a signal handler for a signal.
+void HandleSignal(int sig, void (*handler)(int));
+
+// Revert signal handler for a signal to the default.
+void UnHandle(int sig);
+
+// Use an empty signal mask for the process and set all signal handlers to their
+// default.
+void ClearSignalMask();
+
+// Receive SIGALRM after the given timeout. No-op if the timeout is
+// non-positive.
+void SetTimeout(double timeout_secs);
+
+// Wait for "pid" to exit and return its exit code.
+// "name" is used for the error message only.
+int WaitChild(pid_t pid, const char *name);
+
+#endif // PROCESS_TOOLS_H__
diff --git a/src/main/tools/process-wrapper.c b/src/main/tools/process-wrapper.c
index 27601bf212..d156463726 100644
--- a/src/main/tools/process-wrapper.c
+++ b/src/main/tools/process-wrapper.c
@@ -22,220 +22,117 @@
// die with raise(SIGTERM) even if the child process handles SIGTERM with
// exit(0).
-#ifndef _GNU_SOURCE
#define _GNU_SOURCE
-#endif
#include <errno.h>
-#include <fcntl.h>
-#include <math.h>
#include <signal.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
-#include <sys/wait.h>
#include <unistd.h>
+#include "process-tools.h"
+
// Not in headers on OSX.
extern char **environ;
-static int global_pid; // Returned from fork().
-static int global_signal = -1;
-static double global_kill_delay = 0.0;
-
-#define DIE(args...) { \
- fprintf(stderr, args); \
- fprintf(stderr, " --- "); \
- perror(NULL); \
- fprintf(stderr, "\n"); \
- exit(EXIT_FAILURE); \
+static double global_kill_delay;
+static int global_child_pid;
+static volatile sig_atomic_t global_signal;
+
+// Options parsing result.
+struct Options {
+ double timeout_secs;
+ double kill_delay_secs;
+ const char *stdout_path;
+ const char *stderr_path;
+ char *const *args;
+};
+
+// Print out a usage error. argc and argv are the argument counter and vector,
+// fmt is a format,
+// string for the error message to print.
+static void Usage(char *const *argv) {
+ fprintf(stderr,
+ "Usage: %s <timeout-secs> <kill-delay-secs> <stdout-redirect> "
+ "<stderr-redirect> <command> [args] ...\n",
+ argv[0]);
+ exit(EXIT_FAILURE);
}
-#define CHECK_CALL(x) if (x != 0) { perror(#x); exit(1); }
-
-// Make sure the process and all subprocesses are killed.
-static void KillEverything(int pgrp) {
- kill(-pgrp, SIGTERM);
-
- // Round up fractional seconds in this polling implementation.
- int kill_delay = (int)(global_kill_delay+0.999) ;
- // If the process is still alive, give it some time to die gracefully.
- while (kill(-pgrp, 0) == 0 && kill_delay-- > 0) {
- sleep(1);
+// Parse the command line flags and return the result in an Options structure
+// passed as argument.
+static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) {
+ if (argc <= 5) {
+ Usage(argv);
}
- kill(-pgrp, SIGKILL);
+ argv++;
+ if (sscanf(*argv++, "%lf", &opt->timeout_secs) != 1) {
+ DIE("timeout_secs is not a real number.\n");
+ }
+ if (sscanf(*argv++, "%lf", &opt->kill_delay_secs) != 1) {
+ DIE("kill_delay_secs is not a real number.\n");
+ }
+ opt->stdout_path = *argv++;
+ opt->stderr_path = *argv++;
+ opt->args = argv;
}
-// Called when timeout or Signal occurs.
-static void OnSignal(int sig) {
+// Called when timeout or signal occurs.
+void OnSignal(int sig) {
global_signal = sig;
+
+ // Nothing to do if we received a signal before spawning the child.
+ if (global_child_pid == -1) {
+ return;
+ }
+
if (sig == SIGALRM) {
// SIGALRM represents a timeout, so we should give the process a bit of
// time to die gracefully if it needs it.
- KillEverything(global_pid);
+ KillEverything(global_child_pid, true, global_kill_delay);
} else {
// Signals should kill the process quickly, as it's typically blocking
// the return of the prompt after a user hits "Ctrl-C".
- kill(-global_pid, SIGKILL);
- }
-}
-
-// Set up a signal handler which kills all subprocesses when the
-// given signal is triggered.
-static void InstallSignalHandler(int sig) {
- struct sigaction sa = {};
-
- sa.sa_handler = OnSignal;
- sigemptyset(&sa.sa_mask);
- CHECK_CALL(sigaction(sig, &sa, NULL));
-}
-
-// Revert signal handler to default.
-static void UnHandle(int sig) {
- struct sigaction sa = {};
- sa.sa_handler = SIG_DFL;
- sigemptyset(&sa.sa_mask);
- CHECK_CALL(sigaction(sig, &sa, NULL));
-}
-
-// Enable the given timeout, or no-op if the timeout is non-positive.
-static void EnableAlarm(double timeout) {
- if (timeout <= 0) return;
-
- struct itimerval timer = {};
- timer.it_interval.tv_sec = 0;
- timer.it_interval.tv_usec = 0;
-
- double int_val, fraction_val;
- fraction_val = modf(timeout, &int_val);
- timer.it_value.tv_sec = (long) int_val;
- timer.it_value.tv_usec = (long) (fraction_val * 1e6);
- CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL));
-}
-
-static void ClearSignalMask() {
- // Use an empty signal mask and default signal handlers in the
- // subprocess.
- sigset_t sset;
- sigemptyset(&sset);
- sigprocmask(SIG_SETMASK, &sset, NULL);
- for (int i = 1; i < NSIG; ++i) {
- if (i == SIGKILL || i == SIGSTOP) continue;
-
- struct sigaction sa = {};
- sa.sa_handler = SIG_DFL;
- sigemptyset(&sa.sa_mask);
- sigaction(i, &sa, NULL);
+ KillEverything(global_child_pid, false, global_kill_delay);
}
}
-static int WaitChild(pid_t pid, const char *name) {
- int err = 0;
- int status = 0;
- do {
- err = waitpid(pid, &status, 0);
- } while (err == -1 && errno == EINTR);
-
- if (err == -1) {
- DIE("wait on %s (pid %d) failed", name, pid);
- }
- return status;
-}
-
-// Usage: process-wrapper
-// <timeout_sec> <kill_delay_sec> <stdout file> <stderr file>
-// [cmdline]
-int main(int argc, char *argv[]) {
- if (argc <= 5) {
- DIE("Not enough cmd line arguments to process-wrapper");
- }
-
- int uid = getuid();
- int euid = geteuid();
- if (uid != euid) {
- // Switch completely to the target uid.
- // Some programs (notably, bash) ignore the euid and just use the uid. This
- // limits the ability for us to use process-wrapper as a setuid binary for
- // security/user-isolation.
- if (setreuid(euid, euid) != 0) {
- DIE("changing uid failed: setreuid");
- }
- }
-
- int gid = getgid();
- int egid = getegid();
- if (gid != egid) {
- // Switch completely to the target gid.
- if (setregid(egid, egid) != 0) {
- DIE("changing gid failed: setregid");
- }
- }
-
- // Parse the cmdline args to get the timeout and redirect files.
- argv++;
- double timeout;
- if (sscanf(*argv++, "%lf", &timeout) != 1) {
- DIE("timeout_sec is not a real number.");
- }
- if (sscanf(*argv++, "%lf", &global_kill_delay) != 1) {
- DIE("kill_delay_sec is not a real number.");
- }
- char *stdout_path = *argv++;
- char *stderr_path = *argv++;
-
- if (strcmp(stdout_path, "-")) {
- // Redirect stdout and stderr.
- int fd_out = open(stdout_path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if (fd_out == -1) {
- DIE("Could not open %s for stdout", stdout_path);
- }
- if (dup2(fd_out, STDOUT_FILENO) == -1) {
- DIE("dup2 failed for stdout");
- }
- CHECK_CALL(close(fd_out));
- }
-
- if (strcmp(stderr_path, "-")) {
- int fd_err = open(stderr_path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if (fd_err == -1) {
- DIE("Could not open %s for stderr", stderr_path);
- }
- if (dup2(fd_err, STDERR_FILENO) == -1) {
- DIE("dup2 failed for stderr");
- }
- CHECK_CALL(close(fd_err));
- }
-
- global_pid = fork();
- if (global_pid < 0) {
- DIE("Fork failed");
- } else if (global_pid == 0) {
+// Run the command specified by the argv array and kill it after timeout
+// seconds.
+static void SpawnCommand(char *const *argv, double timeout_secs) {
+ CHECK_CALL(global_child_pid = fork());
+ if (global_child_pid == 0) {
// In child.
- if (setsid() == -1) {
- DIE("Could not setsid from child");
- }
+ CHECK_CALL(setsid());
ClearSignalMask();
+
// Force umask to include read and execute for everyone, to make
// output permissions predictable.
umask(022);
- execvp(argv[0], argv); // Does not return.
- DIE("execvpe %s failed", argv[0]);
+ // Does not return unless something went wrong.
+ CHECK_CALL(execvp(argv[0], argv));
} else {
// In parent.
- InstallSignalHandler(SIGALRM);
- InstallSignalHandler(SIGTERM);
- InstallSignalHandler(SIGINT);
- EnableAlarm(timeout);
- int status = WaitChild(global_pid, argv[0]);
+ // Set up a signal handler which kills all subprocesses when the given
+ // signal is triggered.
+ HandleSignal(SIGALRM, OnSignal);
+ HandleSignal(SIGTERM, OnSignal);
+ HandleSignal(SIGINT, OnSignal);
+ SetTimeout(timeout_secs);
+
+ int status = WaitChild(global_child_pid, argv[0]);
+
+ // The child is done for, but may have grandchildren that we still have to
+ // kill.
+ kill(-global_child_pid, SIGKILL);
- // The child is done, but may have grandchildren.
- kill(-global_pid, SIGKILL);
if (global_signal > 0) {
// Don't trust the exit code if we got a timeout or signal.
UnHandle(global_signal);
@@ -249,3 +146,21 @@ int main(int argc, char *argv[]) {
}
}
}
+
+int main(int argc, char *argv[]) {
+ struct Options opt;
+ memset(&opt, 0, sizeof(opt));
+
+ ParseCommandLine(argc, argv, &opt);
+ global_kill_delay = opt.kill_delay_secs;
+
+ SwitchToEuid();
+ SwitchToEgid();
+
+ RedirectStdout(opt.stdout_path);
+ RedirectStderr(opt.stderr_path);
+
+ SpawnCommand(opt.args, opt.timeout_secs);
+
+ return 0;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java
index 0e4edbc882..7a91f21620 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java
@@ -29,6 +29,7 @@ public class TestConstants {
public static final ImmutableList<String> EMBEDDED_TOOLS = ImmutableList.of(
"build-runfiles",
"process-wrapper",
+ "namespace-sandbox",
"build_interface_so");
/**
diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD
index e3a9c62780..8bc52c8bba 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -50,6 +50,8 @@ filegroup(
"//src/java_tools/buildjar:JavaBuilder_deploy.jar",
"//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/genclass:GenClass_deploy.jar",
"//src/java_tools/singlejar:SingleJar_deploy.jar",
+ "//src/main/tools:namespace-sandbox",
+ "//src/main/tools:process-wrapper",
"//src/test/shell:bashunit",
"//third_party:srcs",
"//third_party/ijar",
@@ -180,10 +182,21 @@ sh_test(
)
sh_test(
+ name = "process_wrapper_test",
+ srcs = ["process-wrapper_test.sh"],
+ data = [":test-deps"],
+)
+
+sh_test(
+ name = "namespace_runner_test",
+ srcs = ["namespace-runner_test.sh"],
+ data = [":test-deps"],
+)
+
+sh_test(
name = "bazel_sandboxing_test",
srcs = ["bazel_sandboxing_test.sh"],
data = [":test-deps"],
- tags = ["manual"], # Test is still flaky.
)
sh_test(
diff --git a/src/test/shell/bazel/bazel_sandboxing_test.sh b/src/test/shell/bazel/bazel_sandboxing_test.sh
index d21269e861..24f6baa8f1 100755
--- a/src/test/shell/bazel/bazel_sandboxing_test.sh
+++ b/src/test/shell/bazel/bazel_sandboxing_test.sh
@@ -61,19 +61,24 @@ EOF
}
function set_up {
- mkdir -p examples/genrule
- cat << 'EOF' > examples/genrule/a.txt
+ mkdir -p examples/genrule
+ cat << 'EOF' > examples/genrule/a.txt
foo bar bz
EOF
- cat << 'EOF' > examples/genrule/b.txt
+ cat << 'EOF' > examples/genrule/b.txt
apples oranges bananas
EOF
- cat << 'EOF' > examples/genrule/BUILD
+
+ # Create cyclic symbolic links to check whether the strategy catches that.
+ ln -sf cyclic2 examples/genrule/cyclic1
+ ln -sf cyclic1 examples/genrule/cyclic2
+
+ cat << 'EOF' > examples/genrule/BUILD
genrule(
name = "works",
srcs = [ "a.txt" ],
outs = [ "works.txt" ],
- cmd = "wc a.txt > $@",
+ cmd = "wc $(location :a.txt) > $@",
)
sh_binary(
@@ -117,6 +122,13 @@ genrule(
#
cmd = "ls /home > $@",
)
+
+genrule(
+ name = "breaks3",
+ srcs = [ "cyclic1", "cyclic2" ],
+ outs = [ "breaks3.txt" ],
+ cmd = "wc $(location :cyclic1) > $@",
+)
EOF
cat << 'EOF' >> examples/genrule/datafile
this is a datafile
@@ -125,14 +137,14 @@ EOF
#!/bin/sh
set -e
-cp examples/genrule/datafile $1
+cp $(dirname $0)/tool.runfiles/examples/genrule/datafile $1
echo "Tools work!"
EOF
-chmod +x examples/genrule/tool.sh
+ chmod +x examples/genrule/tool.sh
}
function test_sandboxed_genrule() {
- bazel build --genrule_strategy=sandboxed --verbose_failures \
+ bazel build --genrule_strategy=sandboxed \
examples/genrule:works \
|| fail "Hermetic genrule failed: examples/genrule:works"
[ -f "${BAZEL_GENFILES_DIR}/examples/genrule/works.txt" ] \
@@ -140,7 +152,7 @@ function test_sandboxed_genrule() {
}
function test_sandboxed_tooldir() {
- bazel build --genrule_strategy=sandboxed --verbose_failures \
+ bazel build --genrule_strategy=sandboxed \
examples/genrule:tooldir \
|| fail "Hermetic genrule failed: examples/genrule:tooldir"
[ -f "${BAZEL_GENFILES_DIR}/examples/genrule/tooldir.txt" ] \
@@ -150,7 +162,7 @@ function test_sandboxed_tooldir() {
}
function test_sandboxed_genrule_with_tools() {
- bazel build --genrule_strategy=sandboxed --verbose_failures \
+ bazel build --genrule_strategy=sandboxed \
examples/genrule:tools_work \
|| fail "Hermetic genrule failed: examples/genrule:tools_work"
[ -f "${BAZEL_GENFILES_DIR}/examples/genrule/tools.txt" ] \
@@ -158,7 +170,7 @@ function test_sandboxed_genrule_with_tools() {
}
function test_sandbox_undeclared_deps() {
- bazel build --genrule_strategy=sandboxed --verbose_failures \
+ bazel build --genrule_strategy=sandboxed \
examples/genrule:breaks1 \
&& fail "Non-hermetic genrule succeeded: examples/genrule:breaks1" || true
[ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks1.txt" ] || {
@@ -168,7 +180,7 @@ function test_sandbox_undeclared_deps() {
}
function test_sandbox_block_filesystem() {
- bazel build --genrule_strategy=sandboxed --verbose_failures \
+ bazel build --genrule_strategy=sandboxed \
examples/genrule:breaks2 \
&& fail "Non-hermetic genrule succeeded: examples/genrule:breaks2" || true
[ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks2.txt" ] || {
@@ -177,6 +189,16 @@ function test_sandbox_block_filesystem() {
}
}
+function test_sandbox_cyclic_symlink_in_inputs() {
+ bazel build --genrule_strategy=sandboxed \
+ examples/genrule:breaks3 \
+ && fail "Genrule with cyclic symlinks succeeded: examples/genrule:breaks3" || true
+ [ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks3.txt" ] || {
+ output=$(cat "${BAZEL_GENFILES_DIR}/examples/genrule/breaks3.txt")
+ fail "Genrule with cyclic symlinks breaks3 suceeded with following output: $(output)"
+ }
+}
+
check_kernel_version
check_sandbox_allowed || exit 0
run_suite "sandbox"
diff --git a/src/test/shell/bazel/namespace-runner_test.sh b/src/test/shell/bazel/namespace-runner_test.sh
new file mode 100755
index 0000000000..af21543bee
--- /dev/null
+++ b/src/test/shell/bazel/namespace-runner_test.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+#
+# Copyright 2015 Google Inc. 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.
+#
+# Test sandboxing spawn strategy
+#
+
+# Load test environment
+source $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/test-setup.sh \
+ || { echo "test-setup.sh not found!" >&2; exit 1; }
+
+readonly WRAPPER="${bazel_data}/src/main/tools/namespace-sandbox"
+readonly OUT_DIR="${TEST_TMPDIR}/out"
+readonly OUT="${OUT_DIR}/outfile"
+readonly ERR="${OUT_DIR}/errfile"
+readonly SANDBOX_DIR="${OUT_DIR}/sandbox"
+
+WRAPPER_DEFAULT_OPTS="-S $SANDBOX_DIR"
+for dir in /bin* /lib* /usr/bin* /usr/lib*; do
+ WRAPPER_DEFAULT_OPTS="$WRAPPER_DEFAULT_OPTS -M $dir -m ${SANDBOX_DIR}${dir}"
+done
+
+# namespaces which are used by the sandbox were introduced in 3.8, so
+# test won't run on earlier kernels
+function check_kernel_version {
+ if [ "${PLATFORM-}" = "darwin" ]; then
+ echo "Test will skip: sandbox is not yet supported on Darwin."
+ exit 0
+ fi
+ MAJOR=$(uname -r | sed 's/^\([0-9]*\)\.\([0-9]*\)\..*/\1/')
+ MINOR=$(uname -r | sed 's/^\([0-9]*\)\.\([0-9]*\)\..*/\2/')
+ if [ $MAJOR -lt 3 ]; then
+ echo "Test will skip: sandbox requires kernel >= 3.8; got $(uname -r)"
+ exit 0
+ fi
+ if [ $MAJOR -eq 3 ] && [ $MINOR -lt 8 ]; then
+ echo "Test will skip: sandbox requires kernel >= 3.8; got $(uname -r)"
+ exit 0
+ fi
+}
+
+# Some CI systems might deactivate sandboxing
+function check_sandbox_allowed {
+ mkdir -p test
+ # Create a program that check if unshare(2) is allowed.
+ cat <<'EOF' > test/test.c
+#define _GNU_SOURCE
+#include <sched.h>
+int main() {
+ return unshare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER);
+}
+EOF
+ cat <<'EOF' >test/BUILD
+cc_test(name = "sandbox_enabled", srcs = ["test.c"], copts = ["-std=c99"])
+EOF
+ bazel test //test:sandbox_enabled || {
+ echo "Sandboxing disabled, skipping..."
+ return false
+ }
+}
+
+function set_up {
+ rm -rf $OUT_DIR
+ rm -rf $SANDBOX_DIR
+
+ mkdir -p $OUT_DIR
+ mkdir $SANDBOX_DIR
+}
+
+function assert_stdout() {
+ assert_equals "$1" "$(cat $OUT)"
+}
+
+function assert_output() {
+ assert_equals "$1" "$(cat $OUT)"
+ assert_equals "$2" "$(cat $ERR)"
+}
+
+function test_basic_functionality() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/echo hi there || fail
+ assert_output "hi there" ""
+}
+
+function test_to_stderr() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "/bin/echo hi there >&2" || fail
+ assert_output "" "hi there"
+}
+
+function test_exit_code() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "exit 71" || code=$?
+ assert_equals 71 "$code"
+}
+
+function test_signal_death() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c 'kill -ABRT $$' || code=$?
+ assert_equals 134 "$code" # SIGNAL_BASE + SIGABRT = 128 + 6
+}
+
+function test_signal_catcher() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \
+ 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
+ assert_stdout "later"
+}
+
+function test_basic_timeout() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -T 3 -t 3 -l $OUT -L $ERR -- /bin/bash -c "echo before; sleep 1000; echo after" && fail
+ assert_output "before" ""
+}
+
+function test_timeout_grace() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \
+ 'trap "echo -n before; sleep 1; echo -n after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
+ assert_stdout "beforeafter"
+}
+
+function test_timeout_kill() {
+ $WRAPPER $WRAPPER_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \
+ 'trap "echo before; sleep 1000; echo after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
+ assert_stdout "before"
+}
+
+check_kernel_version
+check_sandbox_allowed || exit 0
+run_suite "namespace-runner"
diff --git a/src/test/shell/bazel/process-wrapper_test.sh b/src/test/shell/bazel/process-wrapper_test.sh
new file mode 100755
index 0000000000..df96daab5f
--- /dev/null
+++ b/src/test/shell/bazel/process-wrapper_test.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+# Copyright 2015 Google Inc. 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.
+#
+# Test sandboxing spawn strategy
+#
+
+# Load test environment
+source $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/test-setup.sh \
+ || { echo "test-setup.sh not found!" >&2; exit 1; }
+
+readonly WRAPPER="${bazel_data}/src/main/tools/process-wrapper"
+readonly OUT_DIR="${TEST_TMPDIR}/out"
+readonly OUT="${OUT_DIR}/outfile"
+readonly ERR="${OUT_DIR}/errfile"
+
+function set_up() {
+ rm -rf $OUT_DIR
+ mkdir -p $OUT_DIR
+}
+
+function assert_stdout() {
+ assert_equals "$1" "$(cat $OUT)"
+}
+
+function assert_output() {
+ assert_equals "$1" "$(cat $OUT)"
+ assert_equals "$2" "$(cat $ERR)"
+}
+
+function test_basic_functionality() {
+ $WRAPPER -1 0 $OUT $ERR /bin/echo hi there || fail
+ assert_output "hi there" ""
+}
+
+function test_to_stderr() {
+ $WRAPPER -1 0 $OUT $ERR /bin/bash -c "/bin/echo hi there >&2" || fail
+ assert_output "" "hi there"
+}
+
+function test_exit_code() {
+ $WRAPPER -1 0 $OUT $ERR /bin/bash -c "exit 71" || code=$?
+ assert_equals 71 "$code"
+}
+
+function test_signal_death() {
+ $WRAPPER -1 0 $OUT $ERR /bin/bash -c 'kill -ABRT $$' || code=$?
+ assert_equals 134 "$code" # SIGNAL_BASE + SIGABRT = 128 + 6
+}
+
+function test_signal_catcher() {
+ $WRAPPER 2 3 $OUT $ERR /bin/bash -c \
+ 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
+ assert_stdout "later"
+}
+
+function test_basic_timeout() {
+ $WRAPPER 3 3 $OUT $ERR /bin/bash -c "echo before; sleep 1000; echo after" && fail
+ assert_stdout "before"
+}
+
+function test_timeout_grace() {
+ $WRAPPER 2 3 $OUT $ERR /bin/bash -c \
+ 'trap "echo -n before; sleep 1; echo -n after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
+ assert_stdout "beforeafter"
+}
+
+function test_timeout_kill() {
+ $WRAPPER 2 3 $OUT $ERR /bin/bash -c \
+ 'trap "echo before; sleep 1000; echo after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
+ assert_stdout "before"
+}
+
+run_suite "process-wrapper"