Execute spawns inside sandboxes to improve hermeticity (spawns can no longer use non-declared inputs) and safety (spawns can no longer affect the host system, e.g. accidentally wipe your home directory). This implementation works on Linux only and uses Linux containers ("namespaces").
The strategy works with all actions that Bazel supports (C++ / Java compilation, genrules, test execution, Skylark-based rules, ...) and in tests, Bazel could successfully bootstrap itself and pass the whole test suite using sandboxed execution.
This is not the default behavior yet, but can be activated explicitly by using:
bazel build --genrule_strategy=sandboxed --spawn_strategy=sandboxed //my:stuff
--
MOS_MIGRATED_REVID=101457297
diff --git a/scripts/bootstrap/compile.sh b/scripts/bootstrap/compile.sh
index cd7a5b3..1715316 100755
--- a/scripts/bootstrap/compile.sh
+++ b/scripts/bootstrap/compile.sh
@@ -308,16 +308,23 @@
log "Compiling build-runfiles..."
# Clang on Linux requires libstdc++
-run_silent "${CXX}" -o ${OUTPUT_DIR}/build-runfiles -std=c++0x -l stdc++ src/main/tools/build-runfiles.cc
+run_silent "${CXX}" -o ${OUTPUT_DIR}/build-runfiles -std=c++0x src/main/tools/build-runfiles.cc -l stdc++
log "Compiling process-wrapper..."
-run_silent "${CC}" -o ${OUTPUT_DIR}/process-wrapper -std=c99 src/main/tools/process-wrapper.c
+run_silent "${CC}" -o ${OUTPUT_DIR}/process-wrapper -std=c99 src/main/tools/process-wrapper.c src/main/tools/process-tools.c -lm
+
+log "Compiling namespace-sandbox..."
+if [[ $PLATFORM == "linux" ]]; then
+ run_silent "${CC}" -o ${OUTPUT_DIR}/namespace-sandbox -std=c99 src/main/tools/namespace-sandbox.c src/main/tools/process-tools.c -lm
+else
+ run_silent "${CC}" -o ${OUTPUT_DIR}/namespace-sandbox -std=c99 src/main/tools/namespace-sandbox-dummy.c -lm
+fi
cp src/main/tools/build_interface_so ${OUTPUT_DIR}/build_interface_so
cp src/main/tools/jdk.* ${OUTPUT_DIR}
log "Creating Bazel self-extracting archive..."
-TO_ZIP="libblaze.jar ${JNILIB} build-runfiles${EXE_EXT} process-wrapper${EXE_EXT} build_interface_so ${MSYS_DLLS} jdk.BUILD"
+TO_ZIP="libblaze.jar ${JNILIB} build-runfiles${EXE_EXT} process-wrapper${EXE_EXT} namespace-sandbox${EXE_EXT} build_interface_so ${MSYS_DLLS} jdk.BUILD"
(cd ${OUTPUT_DIR}/ ; cat client ${TO_ZIP} | ${MD5SUM} | awk '{ print $1; }' > install_base_key)
(cd ${OUTPUT_DIR}/ ; echo "${JAVA_VERSION}" > java.version)
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 a391a30..377e95c 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.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 @@
.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 @@
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 9b549e9..febc5d7 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 @@
"//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 4f7557c..67aebee 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.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 @@
@ExecutionStrategy(name = {"sandboxed"},
contextType = SpawnActionContext.class)
public class LinuxSandboxedStrategy implements SpawnActionContext {
- private final boolean verboseFailures;
- private final BlazeDirectories directories;
+ private final ExecutorService backgroundWorkers;
- public LinuxSandboxedStrategy(BlazeDirectories blazeDirectories, boolean verboseFailures) {
- this.directories = blazeDirectories;
+ private final BlazeRuntime blazeRuntime;
+ private final BlazeDirectories blazeDirs;
+ private final Path execRoot;
+ private final boolean verboseFailures;
+ private final StandaloneSpawnStrategy standaloneStrategy;
+ private final UUID uuid = UUID.randomUUID();
+ private final AtomicInteger execCounter = new AtomicInteger();
+
+ 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 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);
- }
- }
-
- List<? extends ActionInput> expandedInputs =
- ActionInputHelper.expandMiddlemen(spawn.getInputFiles(),
- actionExecutionContext.getMiddlemanExpander());
-
- String cwd = executor.getExecRoot().getPathString();
FileOutErr outErr = actionExecutionContext.getFileOutErr();
+
+ // The execId is a unique ID just for this invocation of "exec".
+ String execId = uuid + "-" + Integer.toString(execCounter.getAndIncrement());
+
+ // Each invocation of "exec" gets its own sandbox.
+ Path sandboxPath =
+ execRoot.getRelative(Constants.PRODUCT_NAME + "-sandbox").getRelative(execId);
+
+ 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);
- }
-
- 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);
- }
- runner.cleanup();
- } catch (CommandException e) {
- String message = CommandFailureUtils.describeCommandFailure(verboseFailures,
- spawn.getArguments(), spawn.getEnvironment(), cwd);
- throw new UserExecException(String.format("%s: %s", message, e));
+ // Gather all necessary mounts for the sandbox.
+ mounts = getMounts(spawn, sandboxPath, actionExecutionContext);
} catch (IOException e) {
- throw new UserExecException(e.getMessage());
+ throw new UserExecException("Could not prepare mounts for sandbox execution", e);
+ }
+
+ int timeout = getTimeout(spawn);
+
+ 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.
+ }
+ }
+ });
+ }
+ } catch (CommandException 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) {
+ 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));
- 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());
+ 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);
- // 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());
+ // 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("/"));
- 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));
- // 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));
- }
- if (spawnArguments.get(i).equals("-iquote") || spawnArguments.get(i).equals("-isystem")) {
- spawnArguments.set(i + 1, setIncludeDirSandboxPath(execPath,
- spawnArguments.get(i + 1), prefix));
- }
- }
- return ImmutableList.copyOf(processedIncludes);
+ return mounts.put(source, target);
}
- 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 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));
+ }
}
- builder.append(argument);
+ for (String entry : FilesystemUtils.readdir("/usr")) {
+ if (!entry.equals("local")) {
+ mounts.put(
+ fs.getPath("/usr").getRelative(entry),
+ sandboxPath.getRelative("usr").getRelative(entry));
+ }
+ }
+ return mounts.build();
+ }
- return builder.toString();
+ /**
+ * 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();
+ }
+
+ /**
+ * 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);
+ }
+ }
+ return mounts.build();
+ }
+
+ /**
+ * 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 7720b78..b68590c 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 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 @@
* @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 {
+ 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);
+
List<String> args = new ArrayList<>();
+
args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString());
- // Only for c++ compilation
- if (includePrefix != null) {
- for (PathFragment include : includeDirectories) {
- args.add("-n");
- args.add(include.getPathString());
- }
-
- args.add("-N");
- args.add(includePrefix.getPathString());
- }
-
if (debug) {
args.add("-D");
}
+ // Sandbox directory.
args.add("-S");
args.add(sandboxPath.getPathString());
- for (String mount : mounts) {
- args.add("-m");
- args.add(mount);
+
+ // 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));
}
- args.add("-C");
+ // 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.getValue().getPathString());
+ }
+
+ 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());
- }
-
-
- public void copyOutputs(Collection<? extends ActionInput> outputs, FileOutErr outErr)
- throws IOException {
+ // Prepare the output directories in the sandbox.
for (ActionInput output : outputs) {
- Path source = this.sandboxPath.getRelative(output.getExecPathString());
- Path target = this.execRoot.getRelative(output.getExecPathString());
+ PathFragment parentDirectory =
+ new PathFragment(output.getExecPathString()).getParentDirectory();
+ FileSystemUtils.createDirectoryAndParents(sandboxExecRoot.getRelative(parentDirectory));
+ }
+ }
+
+ private void copyOutputs(Collection<? extends ActionInput> outputs) throws IOException {
+ for (ActionInput output : outputs) {
+ 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 69b13af..20b64c8 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.runtime.BlazeRuntime;
import com.google.devtools.build.lib.util.OS;
+import java.util.concurrent.ExecutorService;
+
/**
* Provides the sandboxed spawn strategy.
*/
@@ -30,12 +32,13 @@
@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 65d0c15..bc8785b 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.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 @@
@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 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 92f7a3c..cdd2d70 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 @@
// 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 8fe91a5..02f0b8d 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 @@
"//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 5cf6b43..060356d 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,174 +12,343 @@
// 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);
+// 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);
-// 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);
+ 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);
+}
+// 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;
+ 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 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();
+ if (opt->sandbox_root == NULL) {
+ Usage(argc, argv, "Sandbox root (-S) must be specified");
+ }
- // 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();
+ if (opt->mount_sources[opt->num_mounts] != NULL &&
+ opt->mount_sources[opt->num_mounts] == NULL) {
+ Usage(argc, argv, "An -m option is missing.");
+ }
- // Finally call the command
- free(opt.mounts);
- free(opt.includes);
- SpawnCommand(opt.args, opt.timeout);
+ opt->args = argv + optind;
+ if (argc <= optind) {
+ Usage(argc, argv, "No command specified.");
+ }
+}
+
+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);
+}
+
+static void CreateFile(const char *path) {
+ int handle;
+ CHECK_CALL(handle = open(path, O_CREAT | O_WRONLY | O_EXCL, 0666));
+ CHECK_CALL(close(handle));
+}
+
+static void SetupDevices() {
+ CHECK_CALL(mkdir("dev", 0755));
+ const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero",
+ NULL};
+ for (int i = 0; devs[i] != NULL; i++) {
+ CreateFile(devs[i] + 1);
+ CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL));
+ }
+
+ CHECK_CALL(symlink("/proc/self/fd", "dev/fd"));
+}
+
+// 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;
+ }
+
+ 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;
+ }
+ }
+
+ // Create the parent directory.
+ CHECK_CALL(CreateTarget(dirname(strdupa(path)), true));
+
+ if (is_directory) {
+ CHECK_CALL(mkdir(path, 0755));
+ } else {
+ CreateFile(path);
+ }
+
return 0;
}
-void SpawnCommand(char **argv, int timeout) {
- for (int i = 0; argv[i] != NULL; i++) {
- PRINT_DEBUG("arg: %s\n", argv[i]);
+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;
}
- // 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));
- }
- }
+ 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));
}
}
-int WriteFile(const char *filename, const char *fmt, ...) {
+// 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");
@@ -197,221 +364,150 @@
return r;
}
-//
-// Signal handling
-//
-void SignalHandler(int signum, siginfo_t *info, void *uctxt) {
- global_signal_received = signum;
-}
-
-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);
- }
-}
-
-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));
-}
-
-//
-// Sandbox setup
-//
-void SetupSlashDev() {
- CHECK_CALL(mkdir("dev", 0755));
- 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));
- CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL));
- }
-}
-
-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));
- }
-
- // 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));
- }
-
- // 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(".."));
- }
-
- CHECK_CALL(mkdir("proc", 0755));
- CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL));
-}
-
-void SetupUserNamespace(int uid, int gid) {
+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));
-}
-//
-// 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);
-
- 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]);
+ if (opt->working_dir != NULL) {
+ CHECK_CALL(chdir(opt->working_dir));
}
- 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;
+// Called when timeout or signal occurs.
+void OnSignal(int sig) {
+ global_signal = sig;
- 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;
+ // Nothing to do if we received a signal before spawning the child.
+ if (global_child_pid == -1) {
+ return;
+ }
- 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;
+ 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);
+ }
+}
+
+// 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]);
+ }
+
+ 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 0000000..48441e8
--- /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 0000000..ef9fcdc
--- /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 27601bf..d156463 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;
+static double global_kill_delay;
+static int global_child_pid;
+static volatile sig_atomic_t global_signal;
-#define DIE(args...) { \
- fprintf(stderr, args); \
- fprintf(stderr, " --- "); \
- perror(NULL); \
- fprintf(stderr, "\n"); \
- exit(EXIT_FAILURE); \
+// 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);
+ KillEverything(global_child_pid, false, global_kill_delay);
}
}
-// 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);
- }
-}
-
-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);
- // The child is done, but may have grandchildren.
- kill(-global_pid, SIGKILL);
+ 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);
@@ -249,3 +146,21 @@
}
}
}
+
+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 0e4edbc..7a91f21 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 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 e3a9c62..8bc52c8 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -50,6 +50,8 @@
"//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(
+ 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 d21269e..24f6baa 100755
--- a/src/test/shell/bazel/bazel_sandboxing_test.sh
+++ b/src/test/shell/bazel/bazel_sandboxing_test.sh
@@ -61,19 +61,24 @@
}
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 @@
#
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 @@
#!/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_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_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_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_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_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 0000000..af21543
--- /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 0000000..df96daa
--- /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"