aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Philipp Wollermann <philwo@google.com>2016-08-18 14:39:37 +0000
committerGravatar Philipp Wollermann <philwo@google.com>2016-08-18 17:25:38 +0000
commite219a24a30960ab5c686dd09197b5e22626e1fe0 (patch)
tree07a2fa50d965d1e5af5a8b7140812f69e80e7096
parent88de40ee8bf2248fbf2264a7725170e0a83ee84c (diff)
Implement the first stage of Bazel's "Sandbox 2.0" for Linux.
This has the following improvements upon the older one: - Uses PID namespaces, PR_SET_PDEATHSIG and a number of other tricks for further process isolation and 100% reliable killing of child processes. - Uses clone() instead of unshare() to work around a Linux kernel bug that made creating a sandbox unreliable. - Instead of mounting a hardcoded list of paths + whatever you add with --sandbox_add_path, this sandbox instead mounts all of /, except for what you make inaccessible via --sandbox_block_path. This should solve the majority of "Sandboxing breaks my build, because my compiler is installed in /opt or /usr/local" issues that users have seen. - Instead of doing magic with bind mounts, we create a separate execroot for each process containing symlinks to the input files. This is simpler and gives more predictable performance. - Actually makes everything except the working directory read-only (fixes #1364). This means that a running process can no longer accidentally modify your source code (yay!). - Prevents a number of additional "attacks" or leaks, like accidentally inheriting file handles from the parent. - Simpler command-line interface. - We can provide the same semantics in a Mac OS X sandbox, which will come in a separate code review from yueg@. It has the following caveats / known issues: - The "fallback to /bin/bash on error" feature is gone, but now that the sandbox mounts everything by default, the main use-case for this is no longer needed. The following improvements are planned: - Use a FUSE filesystem if possible for the new execroot, instead of creating symlinks. - Mount a base image instead of "/". FAQ: Q: Why is mounting all of "/" okay, doesn't this make the whole sandbox useless? A: This is still a reasonable behavior, because the sandbox never tried to isolate your build from the operating system it runs in. Instead it is supposed to protect your data from a test running "rm -rf $HOME" and to make it difficult / impossible for actions to use input files that are not declared dependencies. For even more isolation the sandbox will support mounting a base image as its root in a future version (similar to Docker images). Q: Let's say my process-specific execroot contains a symlink to an input file "good.h", can't the process just resolve the symlink, strip off the file name and then look around in the workspace? A: Yes. Unfortunately we could not find any way on Linux to make a file appear in a different directory with *all* of the semantics we would like. The options investigated were: 1) Copying input files, which is much too slow. 2) Hard linking input files, which is fast, but doesn't work cross- filesystems and it's also not possible to make them read-only. 3) Bind mounts, which don't scale once you're up in the thousands of input files (across all actions) - it seems like the kernel has some non-linear performance behavior when the mount table grows too much, resulting in the mount syscall taking more time the more mounts you have. 4) FUSE filesystem, good in theory, but wasn't ready for the first iteration. RELNOTES: New sandboxing implementation for Linux in which all actions run in a separate execroot that contains input files as symlinks back to the originals in the workspace. The running action now has read-write access to its execroot and /tmp only and can no longer write in arbitrary other places in the file system. -- Change-Id: Ic91386fc92f8eef727ed6d22e6bd0f357d145063 Reviewed-on: https://bazel-review.googlesource.com/#/c/4053 MOS_MIGRATED_REVID=130638204
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java144
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java439
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java56
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java25
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java70
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java54
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java7
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java47
-rw-r--r--src/main/tools/BUILD41
-rw-r--r--src/main/tools/linux-sandbox-options.cc269
-rw-r--r--src/main/tools/linux-sandbox-options.h55
-rw-r--r--src/main/tools/linux-sandbox-pid1.cc506
-rw-r--r--src/main/tools/linux-sandbox-pid1.h (renamed from src/main/tools/network-tools.h)11
-rw-r--r--src/main/tools/linux-sandbox-utils.h30
-rw-r--r--src/main/tools/linux-sandbox.c815
-rw-r--r--src/main/tools/linux-sandbox.cc289
-rw-r--r--src/main/tools/linux-sandbox.h22
-rw-r--r--src/main/tools/network-tools.c47
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java234
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java9
-rw-r--r--src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java110
-rw-r--r--src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java12
-rwxr-xr-xsrc/test/shell/bazel/bazel_sandboxing_test.sh65
-rwxr-xr-xsrc/test/shell/bazel/linux-sandbox_test.sh76
24 files changed, 1562 insertions, 1871 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java
index bae4ec9d45..06c84de165 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java
@@ -15,7 +15,6 @@
package com.google.devtools.build.lib.sandbox;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.devtools.build.lib.actions.ExecException;
@@ -27,6 +26,7 @@ import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.TerminationStatus;
import com.google.devtools.build.lib.util.CommandFailureUtils;
import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
@@ -36,7 +36,11 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
/**
* Helper class for running the Linux sandbox. This runner prepares environment inside the sandbox,
@@ -44,34 +48,27 @@ import java.util.List;
*/
public class LinuxSandboxRunner {
private static final String LINUX_SANDBOX = "linux-sandbox" + OsUtils.executableExtension();
- private static final String SANDBOX_TIP =
- "\n\nSandboxed execution failed, which may be legitimate (e.g. a compiler error), "
- + "or due to missing dependencies. To enter the sandbox environment for easier debugging,"
- + " run the following command in parentheses. On command failure, "
- + "a bash shell running inside the sandbox will then automatically be spawned:\n\n";
private final Path execRoot;
- private final Path sandboxPath;
private final Path sandboxExecRoot;
private final Path argumentsFilePath;
- private final ImmutableMap<Path, Path> mounts;
- private final ImmutableSet<Path> createDirs;
+ private final Set<Path> writablePaths;
+ private final List<Path> inaccessiblePaths;
private final boolean verboseFailures;
private final boolean sandboxDebug;
- public LinuxSandboxRunner(
+ LinuxSandboxRunner(
Path execRoot,
- Path sandboxPath,
- ImmutableMap<Path, Path> mounts,
- ImmutableSet<Path> createDirs,
+ Path sandboxExecRoot,
+ Set<Path> writablePaths,
+ List<Path> inaccessiblePaths,
boolean verboseFailures,
boolean sandboxDebug) {
this.execRoot = execRoot;
- this.sandboxPath = sandboxPath;
- this.sandboxExecRoot = sandboxPath.getRelative(execRoot.asFragment().relativeTo("/"));
+ this.sandboxExecRoot = sandboxExecRoot;
this.argumentsFilePath =
- sandboxPath.getParentDirectory().getRelative(sandboxPath.getBaseName() + ".params");
- this.mounts = mounts;
- this.createDirs = createDirs;
+ sandboxExecRoot.getParentDirectory().getRelative(sandboxExecRoot.getBaseName() + ".params");
+ this.writablePaths = writablePaths;
+ this.inaccessiblePaths = inaccessiblePaths;
this.verboseFailures = verboseFailures;
this.sandboxDebug = sandboxDebug;
}
@@ -114,21 +111,20 @@ public class LinuxSandboxRunner {
*
* @param spawnArguments - arguments of spawn to run inside the sandbox
* @param env - environment to run sandbox in
- * @param cwd - current working directory
* @param outErr - error output to capture sandbox's and command's stderr
- * @param outputs - files to extract from the sandbox, paths are relative to the exec root
- * @throws ExecException
+ * @param outputs - files to extract from the sandbox, paths are relative to the exec root @throws
+ * ExecException
*/
public void run(
List<String> spawnArguments,
- ImmutableMap<String, String> env,
- File cwd,
+ Map<String, String> env,
FileOutErr outErr,
+ Map<PathFragment, Path> inputs,
Collection<PathFragment> outputs,
int timeout,
boolean blockNetwork)
throws IOException, ExecException {
- createFileSystem(outputs);
+ createFileSystem(inputs, outputs);
List<String> fileArgs = new ArrayList<>();
List<String> commandLineArgs = new ArrayList<>();
@@ -139,13 +135,9 @@ public class LinuxSandboxRunner {
fileArgs.add("-D");
}
- // Sandbox directory.
- fileArgs.add("-S");
- fileArgs.add(sandboxPath.getPathString());
-
// Working directory of the spawn.
fileArgs.add("-W");
- fileArgs.add(cwd.toString());
+ fileArgs.add(sandboxExecRoot.toString());
// Kill the process after a timeout.
if (timeout != -1) {
@@ -154,26 +146,22 @@ public class LinuxSandboxRunner {
}
// Create all needed directories.
- for (Path createDir : createDirs) {
- fileArgs.add("-d");
- fileArgs.add(createDir.getPathString());
+ for (Path writablePath : writablePaths) {
+ fileArgs.add("-w");
+ fileArgs.add(writablePath.getPathString());
+ if (writablePath.startsWith(sandboxExecRoot)) {
+ FileSystemUtils.createDirectoryAndParents(writablePath);
+ }
}
- if (blockNetwork) {
- // Block network access out of the namespace.
- fileArgs.add("-n");
+ for (Path inaccessiblePath : inaccessiblePaths) {
+ fileArgs.add("-i");
+ fileArgs.add(inaccessiblePath.getPathString());
}
- // Mount all the inputs.
- for (ImmutableMap.Entry<Path, Path> mount : mounts.entrySet()) {
- fileArgs.add("-M");
- fileArgs.add(mount.getValue().getPathString());
-
- // The file is mounted in a custom location inside the sandbox.
- if (!mount.getValue().equals(mount.getKey())) {
- fileArgs.add("-m");
- fileArgs.add(mount.getKey().getPathString());
- }
+ if (blockNetwork) {
+ // Block network access out of the namespace.
+ fileArgs.add("-N");
}
FileSystemUtils.writeLinesAs(argumentsFilePath, StandardCharsets.ISO_8859_1, fileArgs);
@@ -182,7 +170,8 @@ public class LinuxSandboxRunner {
commandLineArgs.add("--");
commandLineArgs.addAll(spawnArguments);
- Command cmd = new Command(commandLineArgs.toArray(new String[0]), env, cwd);
+ Command cmd =
+ new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile());
try {
cmd.execute(
@@ -200,40 +189,77 @@ public class LinuxSandboxRunner {
}
String message =
CommandFailureUtils.describeCommandFailure(
- verboseFailures, commandLineArgs, env, cwd.getPath());
- String finalMsg = (sandboxDebug && verboseFailures) ? SANDBOX_TIP + message : message;
- throw new UserExecException(finalMsg, e, timedOut);
+ verboseFailures, commandLineArgs, env, sandboxExecRoot.getPathString());
+ throw new UserExecException(message, e, timedOut);
} finally {
copyOutputs(outputs);
}
}
- private void createFileSystem(Collection<PathFragment> outputs) throws IOException {
- FileSystemUtils.createDirectoryAndParents(sandboxPath);
+ private void createFileSystem(Map<PathFragment, Path> inputs, Collection<PathFragment> outputs)
+ throws IOException {
+ Set<Path> createdDirs = new HashSet<>();
+ FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot);
+ createParentDirectoriesForInputs(createdDirs, inputs.keySet());
+ createSymlinksForInputs(inputs);
+ createDirectoriesForOutputs(createdDirs, outputs);
+ }
+
+ /**
+ * No input can be a child of another input, because otherwise we might try to create a symlink
+ * below another symlink we created earlier - which means we'd actually end up writing somewhere
+ * in the workspace.
+ *
+ * <p>If all inputs were regular files, this situation could naturally not happen - but
+ * unfortunately, we might get the occasional action that has directories in its inputs.
+ *
+ * <p>Creating all parent directories first ensures that we can safely create symlinks to
+ * directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested
+ * once we start creating the symlinks for all inputs.
+ */
+ private void createParentDirectoriesForInputs(Set<Path> createdDirs, Set<PathFragment> inputs)
+ throws IOException {
+ for (PathFragment inputPath : inputs) {
+ Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory();
+ Preconditions.checkArgument(dir.startsWith(sandboxExecRoot));
+ FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir);
+ }
+ }
+
+ private void createSymlinksForInputs(Map<PathFragment, Path> inputs) throws IOException {
+ // All input files are relative to the execroot.
+ for (Entry<PathFragment, Path> entry : inputs.entrySet()) {
+ Path key = sandboxExecRoot.getRelative(entry.getKey());
+ key.createSymbolicLink(entry.getValue());
+ }
+ }
- // Prepare the output directories in the sandbox.
+ /** Prepare the output directories in the sandbox. */
+ private void createDirectoriesForOutputs(Set<Path> createdDirs, Collection<PathFragment> outputs)
+ throws IOException {
for (PathFragment output : outputs) {
- FileSystemUtils.createDirectoryAndParents(
- sandboxExecRoot.getRelative(output.getParentDirectory()));
+ FileSystemUtils.createDirectoryAndParentsWithCache(
+ createdDirs, sandboxExecRoot.getRelative(output.getParentDirectory()));
+ FileSystemUtils.createDirectoryAndParentsWithCache(
+ createdDirs, execRoot.getRelative(output.getParentDirectory()));
}
}
private void copyOutputs(Collection<PathFragment> outputs) throws IOException {
for (PathFragment output : outputs) {
Path source = sandboxExecRoot.getRelative(output);
- Path target = execRoot.getRelative(output);
- FileSystemUtils.createDirectoryAndParents(target.getParentDirectory());
if (source.isFile() || source.isSymbolicLink()) {
+ Path target = execRoot.getRelative(output);
Files.move(source.getPathFile(), target.getPathFile());
}
}
}
public void cleanup() throws IOException {
- if (sandboxPath.exists()) {
- FileSystemUtils.deleteTree(sandboxPath);
+ if (sandboxExecRoot.exists()) {
+ FileSystemUtils.deleteTree(sandboxExecRoot);
}
- if (!sandboxDebug && argumentsFilePath.exists()) {
+ if (argumentsFilePath.exists()) {
argumentsFilePath.delete();
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java
index 9a04ed870c..4ee34e891e 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,10 +13,7 @@
// limitations under the License.
package com.google.devtools.build.lib.sandbox;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Files;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
@@ -33,28 +30,22 @@ import com.google.devtools.build.lib.actions.SpawnActionContext;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.analysis.AnalysisUtils;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
-import com.google.devtools.build.lib.analysis.config.RunUnder;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
-import com.google.devtools.build.lib.rules.test.TestRunnerAction;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
-import com.google.devtools.build.lib.unix.NativePosixFiles;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.io.FileOutErr;
-import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
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 com.google.devtools.build.lib.vfs.Symlinks;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
@@ -67,10 +58,18 @@ import java.util.concurrent.atomic.AtomicInteger;
contextType = SpawnActionContext.class
)
public class LinuxSandboxedStrategy implements SpawnActionContext {
+ private static Boolean sandboxingSupported = null;
+
+ public static boolean isSupported(CommandEnvironment env) {
+ if (sandboxingSupported == null) {
+ sandboxingSupported = LinuxSandboxRunner.isSupported(env);
+ }
+ return sandboxingSupported.booleanValue();
+ }
+
private final ExecutorService backgroundWorkers;
private final SandboxOptions sandboxOptions;
- private final ImmutableMap<String, String> clientEnv;
private final BlazeDirectories blazeDirs;
private final Path execRoot;
private final boolean verboseFailures;
@@ -79,16 +78,14 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
private final AtomicInteger execCounter = new AtomicInteger();
private final String productName;
- public LinuxSandboxedStrategy(
+ LinuxSandboxedStrategy(
SandboxOptions options,
- Map<String, String> clientEnv,
BlazeDirectories blazeDirs,
ExecutorService backgroundWorkers,
boolean verboseFailures,
boolean unblockNetwork,
String productName) {
this.sandboxOptions = options;
- this.clientEnv = ImmutableMap.copyOf(clientEnv);
this.blazeDirs = blazeDirs;
this.execRoot = blazeDirs.getExecRoot();
this.backgroundWorkers = Preconditions.checkNotNull(backgroundWorkers);
@@ -129,18 +126,21 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
String execId = uuid + "-" + execCounter.getAndIncrement();
// Each invocation of "exec" gets its own sandbox.
- Path sandboxPath =
- execRoot.getRelative(productName + "-sandbox").getRelative(execId);
+ Path sandboxExecRoot =
+ blazeDirs.getOutputBase().getRelative(productName + "-sandbox").getRelative(execId);
// Gather all necessary mounts for the sandbox.
- ImmutableMap<Path, Path> mounts;
+ Map<PathFragment, Path> mounts;
try {
mounts = getMounts(spawn, actionExecutionContext);
} catch (IllegalArgumentException | IOException e) {
throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e);
}
- ImmutableSet<Path> createDirs = createImportantDirs(spawn.getEnvironment());
+ Map<String, String> env = new HashMap<>(spawn.getEnvironment());
+
+ ImmutableSet<Path> writablePaths = getWritablePaths(sandboxExecRoot, env);
+ ImmutableList<Path> inaccessiblePaths = getInaccessiblePaths();
int timeout = getTimeout(spawn);
@@ -157,17 +157,17 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
final LinuxSandboxRunner runner =
new LinuxSandboxRunner(
execRoot,
- sandboxPath,
- mounts,
- createDirs,
+ sandboxExecRoot,
+ writablePaths,
+ inaccessiblePaths,
verboseFailures,
sandboxOptions.sandboxDebug);
try {
runner.run(
spawn.getArguments(),
- spawn.getEnvironment(),
- execRoot.getPathFile(),
+ env,
outErr,
+ mounts,
outputFiles.build(),
timeout,
!this.unblockNetwork && !spawn.getExecutionInfo().containsKey("requires-network"));
@@ -213,191 +213,86 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
return -1;
}
- /**
- * Most programs expect certain directories to be present, e.g. /tmp. Make sure they are.
- *
- * <p>Note that $HOME is handled by linux-sandbox.c, because it changes user to nobody and the
- * home directory of that user is not known by us.
- */
- private ImmutableSet<Path> createImportantDirs(Map<String, String> env) {
- ImmutableSet.Builder<Path> dirs = ImmutableSet.builder();
- FileSystem fs = blazeDirs.getFileSystem();
+ /** Gets the list of directories that the spawn will assume to be writable. */
+ private ImmutableSet<Path> getWritablePaths(Path sandboxExecRoot, Map<String, String> env) {
+ ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder();
+ // We have to make the TEST_TMPDIR directory writable if it is specified.
if (env.containsKey("TEST_TMPDIR")) {
- PathFragment testTmpDir = new PathFragment(env.get("TEST_TMPDIR"));
- if (testTmpDir.isAbsolute()) {
- dirs.add(fs.getPath(testTmpDir));
- } else {
- dirs.add(execRoot.getRelative(testTmpDir));
- }
- }
- dirs.add(fs.getPath("/tmp"));
- return dirs.build();
- }
-
- private ImmutableMap<Path, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext)
- throws IOException, ExecException {
- ImmutableMap.Builder<Path, Path> result = new ImmutableMap.Builder<>();
- result.putAll(mountUsualUnixDirs());
- result.putAll(mountUserDefinedPath());
-
- MountMap mounts = new MountMap();
- mounts.putAll(setupBlazeUtils());
- mounts.putAll(mountRunfilesFromManifests(spawn));
- mounts.putAll(mountRunfilesFromSuppliers(spawn));
- mounts.putAll(mountFilesFromFilesetManifests(spawn, executionContext));
- mounts.putAll(mountInputs(spawn, executionContext));
- mounts.putAll(mountRunUnderCommand(spawn));
- result.putAll(finalizeMounts(mounts));
- return result.build();
- }
-
- /**
- * Helper method of {@link #finalizeMounts}. This method handles adding a single path
- * to the output map, including making sure it exists and adding the target of a
- * symbolic link if necessary.
- *
- * @param finalizedMounts the map to add the mapping(s) to
- * @param target the key to add to the map
- * @param source the value to add to the map
- * @param stat information about source (passed in to avoid fetching it twice)
- */
- private static void finalizeMountPath(
- MountMap finalizedMounts, Path target, Path source, FileStatus stat) throws IOException {
- // The source must exist.
- Preconditions.checkArgument(stat != null, "%s does not exist", source.toString());
- finalizedMounts.put(target, source);
-
- if (stat.isSymbolicLink()) {
- Path symlinkTarget = source.resolveSymbolicLinks();
- Preconditions.checkArgument(
- symlinkTarget.exists(), "%s does not exist", symlinkTarget.toString());
- finalizedMounts.put(symlinkTarget, symlinkTarget);
+ Path testTmpDir = sandboxExecRoot.getRelative(env.get("TEST_TMPDIR"));
+ writablePaths.add(testTmpDir);
+ env.put("TEST_TMPDIR", testTmpDir.getPathString());
}
+ return writablePaths.build();
}
- /**
- * Performs various checks on each mounted file which require stating each one.
- * Contained in one function to allow minimizing the number of syscalls involved.
- *
- * Checks for each mount if the source refers to a symbolic link and if yes, adds another mount
- * for the target of that symlink to ensure that it keeps working inside the sandbox.
- *
- * Checks for each mount if the source refers to a directory and if yes, replaces that mount with
- * mounts of all files inside that directory.
- *
- * Validates all mounts against a set of criteria and throws an exception on error.
- *
- * @return a new mounts multimap with all mounts and the added mounts.
- */
- @VisibleForTesting
- static MountMap finalizeMounts(Map<Path, Path> mounts) throws IOException {
- MountMap finalizedMounts = new MountMap();
- for (Entry<Path, Path> mount : mounts.entrySet()) {
- Path target = mount.getKey();
- Path source = mount.getValue();
-
- FileStatus stat = source.statNullable(Symlinks.NOFOLLOW);
-
- if (stat != null && stat.isDirectory()) {
- for (Path subSource : FileSystemUtils.traverseTree(source, Predicates.alwaysTrue())) {
- Path subTarget = target.getRelative(subSource.relativeTo(source));
- finalizeMountPath(
- finalizedMounts, subTarget, subSource, subSource.statNullable(Symlinks.NOFOLLOW));
- }
- } else {
- finalizeMountPath(finalizedMounts, target, source, stat);
- }
- }
- return finalizedMounts;
- }
-
- /**
- * Mount a certain set of unix directories to make the usual tools and libraries available to the
- * spawn that runs.
- *
- * Throws an exception if any of them do not exist.
- */
- private MountMap mountUsualUnixDirs() throws IOException {
- MountMap mounts = new MountMap();
- FileSystem fs = blazeDirs.getFileSystem();
- mounts.put(fs.getPath("/bin"), fs.getPath("/bin"));
- mounts.put(fs.getPath("/sbin"), fs.getPath("/sbin"));
- mounts.put(fs.getPath("/etc"), fs.getPath("/etc"));
-
- // Check if /etc/resolv.conf is a symlink and mount its target
- // Fix #738
- Path resolv = fs.getPath("/etc/resolv.conf");
- if (resolv.exists() && resolv.isSymbolicLink()) {
- mounts.put(resolv.resolveSymbolicLinks(), resolv.resolveSymbolicLinks());
- }
-
- for (String entry : NativePosixFiles.readdir("/")) {
- if (entry.startsWith("lib")) {
- Path libDir = fs.getRootDirectory().getRelative(entry);
- mounts.put(libDir, libDir);
- }
- }
- for (String entry : NativePosixFiles.readdir("/usr")) {
- if (!entry.equals("local")) {
- Path usrDir = fs.getPath("/usr").getRelative(entry);
- mounts.put(usrDir, usrDir);
- }
- }
- for (Path path : mounts.values()) {
- Preconditions.checkArgument(path.exists(), "%s does not exist", path.toString());
+ private ImmutableList<Path> getInaccessiblePaths() {
+ ImmutableList.Builder<Path> inaccessiblePaths = ImmutableList.builder();
+ for (String path : sandboxOptions.sandboxBlockPath) {
+ inaccessiblePaths.add(blazeDirs.getFileSystem().getPath(path));
}
- return mounts;
+ return inaccessiblePaths.build();
}
- /**
- * Mount the embedded tools.
- */
- private MountMap setupBlazeUtils() {
- MountMap mounts = new MountMap();
- Path mount = blazeDirs.getEmbeddedBinariesRoot().getRelative("build-runfiles");
- mounts.put(mount, mount);
+ private Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext)
+ throws IOException, ExecException {
+ Map<PathFragment, Path> mounts = new HashMap<>();
+ mountRunfilesFromManifests(mounts, spawn);
+ mountRunfilesFromSuppliers(mounts, spawn);
+ mountFilesFromFilesetManifests(mounts, spawn, executionContext);
+ mountInputs(mounts, spawn, executionContext);
return mounts;
}
- /**
- * Mount all runfiles that the spawn needs as specified in its runfiles manifests.
- */
- private MountMap mountRunfilesFromManifests(Spawn spawn) throws IOException, ExecException {
- MountMap mounts = new MountMap();
- for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
+ /** Mount all runfiles that the spawn needs as specified in its runfiles manifests. */
+ private void mountRunfilesFromManifests(Map<PathFragment, Path> mounts, Spawn spawn)
+ throws IOException, ExecException {
+ for (Map.Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) {
String manifestFilePath = manifest.getValue().getPath().getPathString();
Preconditions.checkState(!manifest.getKey().isAbsolute());
- Path targetDirectory = execRoot.getRelative(manifest.getKey());
-
- mounts.putAll(parseManifestFile(targetDirectory, new File(manifestFilePath), false, ""));
+ PathFragment targetDirectory = manifest.getKey();
+
+ parseManifestFile(
+ blazeDirs.getFileSystem(),
+ mounts,
+ targetDirectory,
+ new File(manifestFilePath),
+ false,
+ "");
}
- return mounts;
}
- /**
- * Mount all files that the spawn needs as specified in its fileset manifests.
- */
- private MountMap mountFilesFromFilesetManifests(
- Spawn spawn, ActionExecutionContext executionContext) throws IOException, ExecException {
+ /** Mount all files that the spawn needs as specified in its fileset manifests. */
+ private void mountFilesFromFilesetManifests(
+ Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext executionContext)
+ throws IOException, ExecException {
final FilesetActionContext filesetContext =
executionContext.getExecutor().getContext(FilesetActionContext.class);
- MountMap mounts = new MountMap();
for (Artifact fileset : spawn.getFilesetManifests()) {
- Path manifest =
- execRoot.getRelative(AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()));
- Path targetDirectory = execRoot.getRelative(fileset.getExecPathString());
-
- mounts.putAll(
- parseManifestFile(
- targetDirectory, manifest.getPathFile(), true, filesetContext.getWorkspaceName()));
+ File manifestFile =
+ new File(
+ execRoot.getPathString(),
+ AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()).getPathString());
+ PathFragment targetDirectory = fileset.getExecPath();
+
+ parseManifestFile(
+ blazeDirs.getFileSystem(),
+ mounts,
+ targetDirectory,
+ manifestFile,
+ true,
+ filesetContext.getWorkspaceName());
}
- return mounts;
}
- static MountMap parseManifestFile(
- Path targetDirectory, File manifestFile, boolean isFilesetManifest, String workspaceName)
+ /** A parser for the MANIFEST files used by Filesets and runfiles. */
+ static void parseManifestFile(
+ FileSystem fs,
+ Map<PathFragment, Path> mounts,
+ PathFragment targetDirectory,
+ File manifestFile,
+ boolean isFilesetManifest,
+ String workspaceName)
throws IOException, ExecException {
- MountMap mounts = new MountMap();
int lineNum = 0;
for (String line : Files.readLines(manifestFile, StandardCharsets.UTF_8)) {
if (isFilesetManifest && (++lineNum % 2 == 0)) {
@@ -409,7 +304,14 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
String[] fields = line.trim().split(" ");
- Path targetPath;
+ // The "target" field is always a relative path that is to be interpreted in this way:
+ // (1) If this is a fileset manifest and our workspace name is not empty, the first segment
+ // of each "target" path must be the workspace name, which is then stripped before further
+ // processing.
+ // (2) The "target" path is then appended to the "targetDirectory", which is a path relative
+ // to the execRoot. Together, this results in the full path in the execRoot in which place a
+ // symlink referring to "source" has to be created (see below).
+ PathFragment targetPath;
if (isFilesetManifest) {
PathFragment targetPathFragment = new PathFragment(fields[0]);
if (!workspaceName.isEmpty()) {
@@ -424,13 +326,15 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
targetPath = targetDirectory.getRelative(fields[0]);
}
+ // The "source" field, if it exists, is always an absolute path and may point to any file in
+ // the filesystem (it is not limited to files in the workspace or execroot).
Path source;
switch (fields.length) {
case 1:
- source = targetDirectory.getFileSystem().getPath("/dev/null");
+ source = fs.getPath("/dev/null");
break;
case 2:
- source = targetDirectory.getFileSystem().getPath(fields[1]);
+ source = fs.getPath(fields[1]);
break;
default:
throw new IllegalStateException("'" + line + "' splits into more than 2 parts");
@@ -438,38 +342,34 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
mounts.put(targetPath, source);
}
- return mounts;
}
- /**
- * Mount all runfiles that the spawn needs as specified via its runfiles suppliers.
- */
- private MountMap mountRunfilesFromSuppliers(Spawn spawn) throws IOException {
- MountMap mounts = new MountMap();
- FileSystem fs = blazeDirs.getFileSystem();
+ /** Mount all runfiles that the spawn needs as specified via its runfiles suppliers. */
+ private void mountRunfilesFromSuppliers(Map<PathFragment, Path> mounts, Spawn spawn)
+ throws IOException {
Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings =
spawn.getRunfilesSupplier().getMappings();
- for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
+ for (Map.Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings :
rootsAndMappings.entrySet()) {
- Path root = fs.getRootDirectory().getRelative(rootAndMappings.getKey());
- for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
+ PathFragment root = rootAndMappings.getKey();
+ if (root.isAbsolute()) {
+ root = root.relativeTo(execRoot.asFragment());
+ }
+ for (Map.Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) {
Artifact sourceArtifact = mapping.getValue();
- Path source = (sourceArtifact != null) ? sourceArtifact.getPath() : fs.getPath("/dev/null");
+ PathFragment source =
+ (sourceArtifact != null) ? sourceArtifact.getExecPath() : new PathFragment("/dev/null");
Preconditions.checkArgument(!mapping.getKey().isAbsolute());
- Path target = root.getRelative(mapping.getKey());
- mounts.put(target, source);
+ PathFragment target = root.getRelative(mapping.getKey());
+ mounts.put(target, execRoot.getRelative(source));
}
}
- return mounts;
}
- /**
- * Mount all inputs of the spawn.
- */
- private MountMap mountInputs(Spawn spawn, ActionExecutionContext actionExecutionContext) {
- MountMap mounts = new MountMap();
-
+ /** Mount all inputs of the spawn. */
+ private void mountInputs(
+ Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext actionExecutionContext) {
List<ActionInput> inputs =
ActionInputHelper.expandArtifacts(
spawn.getInputFiles(), actionExecutionContext.getArtifactExpander());
@@ -485,124 +385,9 @@ public class LinuxSandboxedStrategy implements SpawnActionContext {
if (input.getExecPathString().contains("internal/_middlemen/")) {
continue;
}
- Path mount = execRoot.getRelative(input.getExecPathString());
- mounts.put(mount, mount);
- }
- return mounts;
- }
-
- /**
- * If a --run_under= option is set and refers to a command via its path (as opposed to via its
- * label), we have to mount this. Note that this is best effort and works fine for shell scripts
- * 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 MountMap mountRunUnderCommand(Spawn spawn) {
- MountMap mounts = new MountMap();
-
- 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 mount;
- if (sourceFragment.isAbsolute()) {
- mount = blazeDirs.getFileSystem().getPath(sourceFragment);
- } else if (blazeDirs.getExecRoot().getRelative(sourceFragment).exists()) {
- mount = blazeDirs.getExecRoot().getRelative(sourceFragment);
- } else {
- List<Path> searchPath =
- SearchPath.parse(blazeDirs.getFileSystem(), clientEnv.get("PATH"));
- mount = SearchPath.which(searchPath, runUnder.getCommand());
- }
- if (mount != null) {
- mounts.put(mount, mount);
- }
- }
- }
- return mounts;
- }
-
- /**
- * Mount all user defined path in --sandbox_add_path.
- */
- private MountMap mountUserDefinedPath() throws IOException {
- MountMap mounts = new MountMap();
- FileSystem fs = blazeDirs.getFileSystem();
-
- ImmutableList<Path> exclude =
- ImmutableList.of(blazeDirs.getWorkspace(), blazeDirs.getOutputBase());
-
- for (String pathStr : sandboxOptions.sandboxAddPath) {
- Path path = fs.getPath(pathStr);
-
- // Check if path is in {workspace, outputBase}
- for (Path exc : exclude) {
- if (path.startsWith(exc)) {
- throw new IllegalArgumentException(
- "Mounting subdirectory of WORKSPACE or OUTPUTBASE to sandbox is not allowed.");
- }
- }
-
- // Check if path is ancestor of {workspace, outputBase}
- // Mount subdirectory of path except {workspace, outputBase}
- mounts.putAll(mountChildDirExclude(path, exclude));
+ PathFragment mount = new PathFragment(input.getExecPathString());
+ mounts.put(mount, execRoot.getRelative(mount));
}
-
- return mounts;
- }
-
- /**
- * Mount all subdirectories recursively except some paths
- */
- private MountMap mountDirExclude(Path path, List<Path> exclude) throws IOException {
- MountMap mounts = new MountMap();
-
- if (!path.isDirectory(Symlinks.NOFOLLOW)) {
- if (!exclude.contains(path)) {
- mounts.put(path, path);
- }
- return mounts;
- }
-
- try {
- for (Path child : path.getDirectoryEntries()) {
- // Ignore broken symlink
- if (!child.exists()) {
- continue;
- }
-
- mounts.putAll(mountChildDirExclude(child, exclude));
- }
- } catch (IOException e) {
- throw new IOException("Illegal additional path for mount", e);
- }
-
- return mounts;
- }
-
- /**
- * Helper function of mountDirExclude and mountUserDefinedPath
- */
- private MountMap mountChildDirExclude(Path child, List<Path> exclude) throws IOException {
- MountMap mounts = new MountMap();
-
- boolean startsWithFlag = false;
- for (Path exc : exclude) {
- if (exc.startsWith(child)) {
- startsWithFlag = true;
- break;
- }
- }
- if (!startsWithFlag) {
- mounts.put(child, child);
- } else if (!exclude.contains(child)) {
- mounts.putAll(mountDirExclude(child, exclude));
- }
-
- return mounts;
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java b/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java
deleted file mode 100644
index 5051d27df3..0000000000
--- a/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2015 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.sandbox;
-
-import com.google.common.collect.ForwardingSortedMap;
-import com.google.devtools.build.lib.vfs.Path;
-
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * A map that throws an exception when trying to replace a key (i.e. once a key gets a value,
- * any additional attempt of putting a value on the same key will throw an exception).
- *
- * <p>Returns entries sorted by path depth (shorter paths first) and in lexicographical order.
- */
-final class MountMap extends ForwardingSortedMap<Path, Path> {
- final TreeMap<Path, Path> delegate = new TreeMap<>();
-
- @Override
- protected SortedMap<Path, Path> delegate() {
- return delegate;
- }
-
- @Override
- public Path put(Path key, Path value) {
- Path previousValue = get(key);
- if (previousValue == null) {
- return super.put(key, value);
- } else if (previousValue.equals(value)) {
- return value;
- } else {
- throw new IllegalArgumentException(
- String.format("Cannot mount both '%s' and '%s' onto '%s'", previousValue, value, key));
- }
- }
-
- @Override
- public void putAll(Map<? extends Path, ? extends Path> map) {
- for (Entry<? extends Path, ? extends Path> entry : map.entrySet()) {
- put(entry.getKey(), entry.getValue());
- }
- }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java
index 2d6e28e562..7b4e30218a 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java
@@ -15,12 +15,11 @@ package com.google.devtools.build.lib.sandbox;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.ImmutableMultimap.Builder;
import com.google.common.collect.Multimap;
import com.google.devtools.build.lib.actions.ActionContextConsumer;
import com.google.devtools.build.lib.actions.Executor.ActionContext;
import com.google.devtools.build.lib.actions.SpawnActionContext;
-import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
/**
* {@link ActionContextConsumer} that requests the action contexts necessary for sandboxed
@@ -28,6 +27,19 @@ import com.google.devtools.build.lib.util.OS;
*/
public class SandboxActionContextConsumer implements ActionContextConsumer {
+ private final ImmutableMultimap<Class<? extends ActionContext>, String> contexts;
+
+ public SandboxActionContextConsumer(CommandEnvironment env) {
+ ImmutableMultimap.Builder<Class<? extends ActionContext>, String> contexts =
+ ImmutableMultimap.builder();
+
+ if (LinuxSandboxedStrategy.isSupported(env)) {
+ contexts.put(SpawnActionContext.class, "sandboxed");
+ }
+
+ this.contexts = contexts.build();
+ }
+
@Override
public ImmutableMap<String, String> getSpawnActionContexts() {
return ImmutableMap.of();
@@ -35,13 +47,6 @@ public class SandboxActionContextConsumer implements ActionContextConsumer {
@Override
public Multimap<Class<? extends ActionContext>, String> getActionContexts() {
- Builder<Class<? extends ActionContext>, String> contexts = ImmutableMultimap.builder();
-
- if (OS.getCurrent() == OS.LINUX) {
- contexts.put(SpawnActionContext.class, "sandboxed");
- }
-
- return contexts.build();
+ return contexts;
}
-
}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
index 6bf355610e..7bf38c901b 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
@@ -14,11 +14,11 @@
package com.google.devtools.build.lib.sandbox;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableList.Builder;
import com.google.devtools.build.lib.actions.ActionContextProvider;
import com.google.devtools.build.lib.actions.Executor.ActionContext;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.util.OS;
@@ -30,11 +30,16 @@ import java.util.concurrent.ExecutorService;
*/
public class SandboxActionContextProvider extends ActionContextProvider {
+ public static final String SANDBOX_NOT_SUPPORTED_MESSAGE =
+ "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot "
+ + "be guaranteed. See http://bazel.io/docs/bazel-user-manual.html#sandboxing for more "
+ + "information. You can turn off this warning via --ignore_unsupported_sandboxing";
+
@SuppressWarnings("unchecked")
- private final ImmutableList<ActionContext> strategies;
+ private final ImmutableList<ActionContext> contexts;
- private SandboxActionContextProvider(ImmutableList<ActionContext> strategies) {
- this.strategies = strategies;
+ private SandboxActionContextProvider(ImmutableList<ActionContext> contexts) {
+ this.contexts = contexts;
}
public static SandboxActionContextProvider create(
@@ -46,36 +51,45 @@ public class SandboxActionContextProvider extends ActionContextProvider {
.getOptions(BuildConfiguration.Options.class)
.testArguments
.contains("--wrapper_script_flag=--debug");
- Builder<ActionContext> strategies = ImmutableList.builder();
+ ImmutableList.Builder<ActionContext> contexts = ImmutableList.builder();
- if (OS.getCurrent() == OS.LINUX) {
- strategies.add(
- new LinuxSandboxedStrategy(
- buildRequest.getOptions(SandboxOptions.class),
- env.getClientEnv(),
- env.getDirectories(),
- backgroundWorkers,
- verboseFailures,
- unblockNetwork,
- env.getRuntime().getProductName()));
- } else if (OS.getCurrent() == OS.DARWIN) {
- strategies.add(
- DarwinSandboxedStrategy.create(
- buildRequest.getOptions(SandboxOptions.class),
- env.getClientEnv(),
- env.getDirectories(),
- backgroundWorkers,
- verboseFailures,
- unblockNetwork,
- env.getRuntime().getProductName()));
+ switch (OS.getCurrent()) {
+ case LINUX:
+ if (LinuxSandboxedStrategy.isSupported(env)) {
+ contexts.add(
+ new LinuxSandboxedStrategy(
+ buildRequest.getOptions(SandboxOptions.class),
+ env.getDirectories(),
+ backgroundWorkers,
+ verboseFailures,
+ unblockNetwork,
+ env.getRuntime().getProductName()));
+ } else if (!buildRequest.getOptions(SandboxOptions.class).ignoreUnsupportedSandboxing) {
+ env.getReporter().handle(Event.warn(SANDBOX_NOT_SUPPORTED_MESSAGE));
+ }
+ break;
+ case DARWIN:
+ if (DarwinSandboxRunner.isSupported()) {
+ contexts.add(
+ DarwinSandboxedStrategy.create(
+ buildRequest.getOptions(SandboxOptions.class),
+ env.getClientEnv(),
+ env.getDirectories(),
+ backgroundWorkers,
+ verboseFailures,
+ unblockNetwork,
+ env.getRuntime().getProductName()));
+ }
+ break;
+ default:
+ // No sandboxing available.
}
- return new SandboxActionContextProvider(strategies.build());
+ return new SandboxActionContextProvider(contexts.build());
}
@Override
public Iterable<ActionContext> getActionContexts() {
- return strategies;
+ return contexts;
}
-
}
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 5b9ca58931..8a20cb4f16 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
@@ -20,11 +20,9 @@ import com.google.devtools.build.lib.actions.ActionContextProvider;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
-import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
-import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.common.options.OptionsBase;
import java.io.IOException;
@@ -35,66 +33,30 @@ import java.util.concurrent.Executors;
* This module provides the Sandbox spawn strategy.
*/
public class SandboxModule extends BlazeModule {
- public static final String SANDBOX_NOT_SUPPORTED_MESSAGE =
- "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot "
- + "be guaranteed. See http://bazel.io/docs/bazel-user-manual.html#sandboxing for more "
- + "information. You can turn off this warning via --ignore_unsupported_sandboxing";
-
// Per-server state
private ExecutorService backgroundWorkers;
- private Boolean sandboxingSupported = null;
// Per-command state
private CommandEnvironment env;
private BuildRequest buildRequest;
- private synchronized boolean isSandboxingSupported(CommandEnvironment env) {
- switch (OS.getCurrent()) {
- case LINUX:
- sandboxingSupported = LinuxSandboxRunner.isSupported(env);
- break;
- case DARWIN:
- sandboxingSupported = DarwinSandboxRunner.isSupported();
- break;
- default:
- sandboxingSupported = false;
- }
- return sandboxingSupported.booleanValue();
- }
-
@Override
public Iterable<ActionContextProvider> getActionContextProviders() {
- Preconditions.checkNotNull(buildRequest);
Preconditions.checkNotNull(env);
- if (isSandboxingSupported(env)) {
- Iterable<ActionContextProvider> ret;
- try {
- ret =
- ImmutableList.<ActionContextProvider>of(
- SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers));
- } catch (IOException e) {
- throw new IllegalArgumentException(e);
- }
- return ret;
- }
-
- // For now, sandboxing is only supported on Linux and there's not much point in showing a scary
- // warning to the user if they can't do anything about it.
- if (!buildRequest.getOptions(SandboxOptions.class).ignoreUnsupportedSandboxing
- && OS.getCurrent() == OS.LINUX) {
- env.getReporter().handle(Event.warn(SANDBOX_NOT_SUPPORTED_MESSAGE));
+ Preconditions.checkNotNull(buildRequest);
+ Preconditions.checkNotNull(backgroundWorkers);
+ try {
+ return ImmutableList.<ActionContextProvider>of(
+ SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
}
-
- return ImmutableList.of();
}
@Override
public Iterable<ActionContextConsumer> getActionContextConsumers() {
Preconditions.checkNotNull(env);
- if (isSandboxingSupported(env)) {
- return ImmutableList.<ActionContextConsumer>of(new SandboxActionContextConsumer());
- }
- return ImmutableList.of();
+ return ImmutableList.<ActionContextConsumer>of(new SandboxActionContextConsumer(env));
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
index f1291d2cbb..f18792f606 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java
@@ -15,7 +15,6 @@ package com.google.devtools.build.lib.sandbox;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionsBase;
-
import java.util.List;
/**
@@ -42,11 +41,11 @@ public class SandboxOptions extends OptionsBase {
public boolean sandboxDebug;
@Option(
- name = "sandbox_add_path",
+ name = "sandbox_block_path",
allowMultiple = true,
defaultValue = "",
category = "config",
- help = "Add additional path to mount to sandbox. Path including workspace is not allowed."
+ help = "For sandboxed actions, disallow access to this path."
)
- public List<String> sandboxAddPath;
+ public List<String> sandboxBlockPath;
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
index 8c864547ff..cf12b50f21 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
@@ -22,7 +22,6 @@ import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.util.Preconditions;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -32,6 +31,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
/**
* Helper functions that implement often-used complex operations on file
@@ -582,10 +582,36 @@ public class FileSystemUtils {
*/
@ThreadSafe
public static boolean createDirectoryAndParents(Path dir) throws IOException {
+ return createDirectoryAndParentsWithCache(null, dir);
+ }
+
+ /**
+ * Attempts to create a directory with the name of the given path, creating ancestors as
+ * necessary. Only creates directories or their parents if they are not contained in the set
+ * {@code createdDirs} and instead assumes that they already exist. This saves a round-trip to the
+ * kernel, but is only safe when no one deletes directories that have been created by this method.
+ *
+ * <p>Postcondition: completes normally iff {@code dir} denotes an existing directory (not
+ * necessarily canonical); completes abruptly otherwise.
+ *
+ * @return true if the directory was successfully created anew, false if it already existed
+ * (including the case where {@code dir} denotes a symlink to an existing directory)
+ * @throws IOException if the directory could not be created
+ */
+ @ThreadSafe
+ public static boolean createDirectoryAndParentsWithCache(Set<Path> createdDirs, Path dir)
+ throws IOException {
// Optimised for minimal number of I/O calls.
// Don't attempt to create the root directory.
- if (dir.getParentDirectory() == null) { return false; }
+ if (dir.getParentDirectory() == null) {
+ return false;
+ }
+
+ // We already created that directory.
+ if (createdDirs != null && createdDirs.contains(dir)) {
+ return false;
+ }
FileSystem filesystem = dir.getFileSystem();
if (filesystem instanceof UnionFileSystem) {
@@ -596,12 +622,23 @@ public class FileSystemUtils {
}
try {
- return dir.createDirectory();
+ boolean result = dir.createDirectory();
+ if (createdDirs != null) {
+ createdDirs.add(dir);
+ }
+ return result;
} catch (IOException e) {
if (e.getMessage().endsWith(" (No such file or directory)")) { // ENOENT
- createDirectoryAndParents(dir.getParentDirectory());
- return dir.createDirectory();
+ createDirectoryAndParentsWithCache(createdDirs, dir.getParentDirectory());
+ boolean result = dir.createDirectory();
+ if (createdDirs != null) {
+ createdDirs.add(dir);
+ }
+ return result;
} else if (e.getMessage().endsWith(" (File exists)") && dir.isDirectory()) { // EEXIST
+ if (createdDirs != null) {
+ createdDirs.add(dir);
+ }
return false;
} else {
throw e; // some other error (e.g. ENOTDIR, EACCES, etc.)
diff --git a/src/main/tools/BUILD b/src/main/tools/BUILD
index ead41685ae..23a4e319d1 100644
--- a/src/main/tools/BUILD
+++ b/src/main/tools/BUILD
@@ -1,26 +1,14 @@
package(default_visibility = ["//src:__subpackages__"])
-cc_library(
- name = "network-tools",
- srcs = ["network-tools.c"],
- hdrs = ["network-tools.h"],
- copts = ["-std=c99"],
- deps = [":process-tools"],
-)
-
-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"],
+ srcs = [
+ "process-tools.c",
+ "process-tools.h",
+ "process-wrapper.c",
+ ],
copts = ["-std=c99"],
linkopts = ["-lm"],
- deps = [":process-tools"],
)
cc_binary(
@@ -35,20 +23,17 @@ cc_binary(
"//src:darwin_x86_64": ["dummy-sandbox.c"],
"//src:freebsd": ["dummy-sandbox.c"],
"//src:windows": ["dummy-sandbox.c"],
- "//conditions:default": ["linux-sandbox.c"],
- }),
- copts = ["-std=c99"],
- linkopts = ["-lm"],
- deps = select({
- "//src:darwin": [],
- "//src:darwin_x86_64": [],
- "//src:freebsd": [],
- "//src:windows": [],
"//conditions:default": [
- ":process-tools",
- ":network-tools",
+ "linux-sandbox.cc",
+ "linux-sandbox.h",
+ "linux-sandbox-options.cc",
+ "linux-sandbox-options.h",
+ "linux-sandbox-pid1.cc",
+ "linux-sandbox-pid1.h",
+ "linux-sandbox-utils.h",
],
}),
+ linkopts = ["-lm"],
)
filegroup(
diff --git a/src/main/tools/linux-sandbox-options.cc b/src/main/tools/linux-sandbox-options.cc
new file mode 100644
index 0000000000..0b43a2cfb4
--- /dev/null
+++ b/src/main/tools/linux-sandbox-options.cc
@@ -0,0 +1,269 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "linux-sandbox-options.h"
+#include "linux-sandbox-utils.h"
+
+#define DIE(args...) \
+ { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \
+ fprintf(stderr, "\": "); \
+ perror(NULL); \
+ exit(EXIT_FAILURE); \
+ }
+
+#include <errno.h>
+#include <sched.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <vector>
+
+using std::ifstream;
+using std::unique_ptr;
+using std::vector;
+
+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(char *program_name, const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ fprintf(stderr, "\nUsage: %s -- command arg1 @args\n", program_name);
+ fprintf(stderr,
+ "\nPossible arguments:\n"
+ " -W <working-dir> working directory (uses current directory if "
+ "not specified)\n"
+ " -T <timeout> timeout after which the child process will be "
+ "terminated with SIGTERM\n"
+ " -t <timeout> in case timeout occurs, how long to wait before "
+ "killing the child with SIGKILL\n"
+ " -l <file> redirect stdout to a file\n"
+ " -L <file> redirect stderr to a file\n"
+ " -w <file> make a file or directory writable for the sandboxed "
+ "process\n"
+ " -i <file> make a file or directory inaccessible for the "
+ "sandboxed process\n"
+ " -e <dir> mount an empty tmpfs on a directory\n"
+ " -N if set, a new network namespace will be created\n"
+ " -R if set, make the uid/gid be root, otherwise use nobody\n"
+ " -D if set, debug info will be printed\n"
+ " @FILE read newline-separated arguments from FILE\n"
+ " -- command to run inside sandbox, followed by arguments\n");
+ exit(EXIT_FAILURE);
+}
+
+// Child function used by CheckNamespacesSupported() in call to clone().
+static int CheckNamespacesSupportedChild(void *arg) { return 0; }
+
+// Check whether the required namespaces are supported.
+static int CheckNamespacesSupported() {
+ const int kStackSize = 1024 * 1024;
+ vector<char> child_stack(kStackSize);
+
+ pid_t pid = clone(CheckNamespacesSupportedChild, &child_stack.back(),
+ CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
+ CLONE_NEWNET | CLONE_NEWPID | SIGCHLD,
+ NULL);
+ if (pid < 0) {
+ DIE("pid");
+ }
+
+ int err;
+ do {
+ err = waitpid(pid, NULL, 0);
+ } while (err < 0 && errno == EINTR);
+
+ if (err < 0) {
+ DIE("waitpid");
+ }
+
+ return EXIT_SUCCESS;
+}
+
+// Parses command line flags from an argv array and puts the results into an
+// Options structure passed in as an argument.
+static void ParseCommandLine(unique_ptr<vector<char *>> args) {
+ extern char *optarg;
+ extern int optind, optopt;
+ int c;
+
+ while ((c = getopt(args->size(), args->data(), ":CS:W:T:t:l:L:w:i:e:NRD")) !=
+ -1) {
+ switch (c) {
+ case 'C':
+ // Shortcut for the "does this system support sandboxing" check.
+ exit(CheckNamespacesSupported());
+ break;
+ case 'W':
+ if (opt.working_dir == NULL) {
+ if (optarg[0] != '/') {
+ Usage(args->front(),
+ "The -W option must be used with absolute paths only.");
+ }
+ opt.working_dir = strdup(optarg);
+ } else {
+ Usage(args->front(),
+ "Multiple working directories (-W) specified, expected one.");
+ }
+ break;
+ case 'T':
+ if (sscanf(optarg, "%d", &opt.timeout_secs) != 1 ||
+ opt.timeout_secs < 0) {
+ Usage(args->front(), "Invalid timeout (-T) value: %s", optarg);
+ }
+ break;
+ case 't':
+ if (sscanf(optarg, "%d", &opt.kill_delay_secs) != 1 ||
+ opt.kill_delay_secs < 0) {
+ Usage(args->front(), "Invalid kill delay (-t) value: %s", optarg);
+ }
+ break;
+ case 'l':
+ if (opt.stdout_path == NULL) {
+ opt.stdout_path = optarg;
+ } else {
+ Usage(args->front(),
+ "Cannot redirect stdout to more than one destination.");
+ }
+ break;
+ case 'L':
+ if (opt.stderr_path == NULL) {
+ opt.stderr_path = optarg;
+ } else {
+ Usage(args->front(),
+ "Cannot redirect stderr to more than one destination.");
+ }
+ break;
+ case 'w':
+ if (optarg[0] != '/') {
+ Usage(args->front(),
+ "The -w option must be used with absolute paths only.");
+ }
+ opt.writable_files.push_back(strdup(optarg));
+ break;
+ case 'i':
+ if (optarg[0] != '/') {
+ Usage(args->front(),
+ "The -i option must be used with absolute paths only.");
+ }
+ opt.inaccessible_files.push_back(strdup(optarg));
+ break;
+ case 'e':
+ if (optarg[0] != '/') {
+ Usage(args->front(),
+ "The -e option must be used with absolute paths only.");
+ }
+ opt.tmpfs_dirs.push_back(strdup(optarg));
+ break;
+ case 'N':
+ opt.create_netns = true;
+ break;
+ case 'R':
+ opt.fake_root = true;
+ break;
+ case 'D':
+ opt.debug = true;
+ break;
+ case '?':
+ Usage(args->front(), "Unrecognized argument: -%c (%d)", optopt, optind);
+ break;
+ case ':':
+ Usage(args->front(), "Flag -%c requires an argument", optopt);
+ break;
+ }
+ }
+
+ if (optind < static_cast<int>(args->size())) {
+ if (opt.args.empty()) {
+ opt.args.assign(args->begin() + optind, args->end());
+ } else {
+ Usage(args->front(), "Merging commands not supported.");
+ }
+ }
+}
+
+// Expands a single argument, expanding options @filename to read in the content
+// of the file and add it to the list of processed arguments.
+unique_ptr<vector<char *>> ExpandArgument(unique_ptr<vector<char *>> expanded,
+ char *arg) {
+ if (arg[0] == '@') {
+ const char *filename = arg + 1; // strip off the '@'.
+ ifstream f(filename);
+
+ if (!f.is_open()) {
+ DIE("opening argument file %s failed", filename);
+ }
+
+ for (std::string line; std::getline(f, line);) {
+ if (line.length() > 0) {
+ expanded = ExpandArgument(std::move(expanded), strdup(line.c_str()));
+ }
+ }
+
+ if (f.bad()) {
+ DIE("error while reading from argument file %s", filename);
+ }
+ } else {
+ expanded->push_back(arg);
+ }
+
+ return expanded;
+}
+
+// Pre-processes an argument list, expanding options @filename to read in the
+// content of the file and add it to the list of arguments. Stops expanding
+// arguments once it encounters "--".
+unique_ptr<vector<char *>> ExpandArguments(const vector<char *> &args) {
+ unique_ptr<vector<char *>> expanded(new vector<char *>());
+ expanded->reserve(args.size());
+ for (auto arg = args.begin(); arg != args.end(); ++arg) {
+ if (strcmp(*arg, "--") != 0) {
+ expanded = ExpandArgument(std::move(expanded), *arg);
+ } else {
+ expanded->insert(expanded->end(), arg, args.end());
+ break;
+ }
+ }
+ return expanded;
+}
+
+// Handles parsing all command line flags and populates the global opt struct.
+void ParseOptions(int argc, char *argv[]) {
+ vector<char *> args(argv, argv + argc);
+ ParseCommandLine(ExpandArguments(args));
+
+ if (opt.args.empty()) {
+ Usage(args.front(), "No command specified.");
+ }
+
+ opt.tmpfs_dirs.push_back("/tmp");
+
+ if (opt.working_dir == NULL) {
+ opt.working_dir = getcwd(NULL, 0);
+ }
+}
diff --git a/src/main/tools/linux-sandbox-options.h b/src/main/tools/linux-sandbox-options.h
new file mode 100644
index 0000000000..57004a8206
--- /dev/null
+++ b/src/main/tools/linux-sandbox-options.h
@@ -0,0 +1,55 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef LINUX_SANDBOX_OPTIONS_H__
+#define LINUX_SANDBOX_OPTIONS_H__
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include <vector>
+
+// Options parsing result.
+struct Options {
+ // Working directory (-W)
+ const char *working_dir;
+ // How long to wait before killing the child (-T)
+ int timeout_secs;
+ // How long to wait before sending SIGKILL in case of timeout (-t)
+ int kill_delay_secs;
+ // Where to redirect stdout (-l)
+ const char *stdout_path;
+ // Where to redirect stderr (-L)
+ const char *stderr_path;
+ // Files to make writable for the sandboxed process (-w)
+ std::vector<const char *> writable_files;
+ // Files to make inaccessible for the sandboxed process (-i)
+ std::vector<const char *> inaccessible_files;
+ // Directories where to mount an empty tmpfs (-e)
+ std::vector<const char *> tmpfs_dirs;
+ // Create a new network namespace (-N)
+ bool create_netns;
+ // Pretend to be root inside the namespace (-R)
+ bool fake_root;
+ // Print debugging messages (-D)
+ bool debug;
+ // Command to run (--)
+ std::vector<char *> args;
+};
+
+extern struct Options opt;
+
+void ParseOptions(int argc, char *argv[]);
+
+#endif
diff --git a/src/main/tools/linux-sandbox-pid1.cc b/src/main/tools/linux-sandbox-pid1.cc
new file mode 100644
index 0000000000..1dd049a1f4
--- /dev/null
+++ b/src/main/tools/linux-sandbox-pid1.cc
@@ -0,0 +1,506 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * This is PID 1 inside the sandbox environment and runs in a separate user,
+ * mount, UTS, IPC and PID namespace.
+ */
+
+#include "linux-sandbox-options.h"
+#include "linux-sandbox-utils.h"
+#include "linux-sandbox.h"
+
+// Note that we define DIE() here and not in a shared header, because we want to
+// use _exit() in the
+// pid1 child, but exit() in the parent.
+#define DIE(args...) \
+ { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \
+ fprintf(stderr, "\": "); \
+ perror(NULL); \
+ _exit(EXIT_FAILURE); \
+ }
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <math.h>
+#include <mntent.h>
+#include <net/if.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+static int global_child_pid;
+static char global_inaccessible_directory[] = "/tmp/empty.XXXXXX";
+static char global_inaccessible_file[] = "/tmp/empty.XXXXXX";
+
+static void SetupSelfDestruction(int *sync_pipe) {
+ // We could also poll() on the pipe fd to find out when the parent goes away,
+ // and rely on SIGCHLD interrupting that otherwise. That might require us to
+ // install some trivial handler for SIGCHLD. Using O_ASYNC to turn the pipe
+ // close into SIGIO may also work. Another option is signalfd, although that's
+ // almost as obscure as this prctl.
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) {
+ DIE("prctl");
+ }
+
+ // Verify that the parent still lives.
+ char buf = 0;
+ if (close(sync_pipe[0]) < 0) {
+ DIE("close");
+ }
+ if (write(sync_pipe[1], &buf, 1) < 0) {
+ DIE("write");
+ }
+ if (close(sync_pipe[1]) < 0) {
+ DIE("close");
+ }
+}
+
+static void SetupMountNamespace() {
+ // Fully isolate our mount namespace private from outside events, so that
+ // mounts in the outside environment do not affect our sandbox.
+ if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) {
+ DIE("mount");
+ }
+}
+
+static void WriteFile(const char *filename, const char *fmt, ...) {
+ FILE *stream = fopen(filename, "w");
+ if (stream == NULL) {
+ DIE("fopen(%s)", filename);
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ int r = vfprintf(stream, fmt, ap);
+ va_end(ap);
+
+ if (r < 0) {
+ DIE("vfprintf");
+ }
+
+ if (fclose(stream) != 0) {
+ DIE("fclose(%s)", filename);
+ }
+}
+
+static void SetupUserNamespace() {
+ // Disable needs for CAP_SETGID.
+ struct stat sb;
+ if (stat("/proc/self/setgroups", &sb) == 0) {
+ WriteFile("/proc/self/setgroups", "deny");
+ } else {
+ // Ignore ENOENT, because older Linux versions do not have this file (but
+ // also do not require writing to it).
+ if (errno != ENOENT) {
+ DIE("stat(/proc/self/setgroups");
+ }
+ }
+
+ int inner_uid = 0, inner_gid = 0;
+ if (!opt.fake_root) {
+ struct passwd *pwd = getpwnam("nobody");
+ if (pwd == NULL) {
+ DIE("unable to find passwd entry for user nobody")
+ }
+
+ inner_uid = pwd->pw_uid;
+ inner_gid = pwd->pw_gid;
+ }
+
+ WriteFile("/proc/self/uid_map", "%d %d 1\n", inner_uid, global_outer_uid);
+ WriteFile("/proc/self/gid_map", "%d %d 1\n", inner_gid, global_outer_gid);
+}
+
+static void SetupUtsNamespace() {
+ if (sethostname("sandbox", 7) < 0) {
+ DIE("sethostname");
+ }
+
+ if (setdomainname("sandbox", 7) < 0) {
+ DIE("setdomainname");
+ }
+}
+
+static void SetupHelperFiles() {
+ if (mkdtemp(global_inaccessible_directory) == NULL) {
+ DIE("mkdtemp(%s)", global_inaccessible_directory);
+ }
+ if (chmod(global_inaccessible_directory, 0) < 0) {
+ DIE("chmod(%s, 0)", global_inaccessible_directory);
+ }
+
+ int handle = mkstemp(global_inaccessible_file);
+ if (handle < 0) {
+ DIE("mkstemp(%s)", global_inaccessible_file);
+ }
+ if (fchmod(handle, 0)) {
+ DIE("fchmod(%s, 0)", global_inaccessible_file);
+ }
+ if (close(handle) < 0) {
+ DIE("close(%s)", global_inaccessible_file);
+ }
+}
+
+static void MountFilesystems() {
+ if (mount("/", global_sandbox_root, NULL, MS_BIND | MS_REC, NULL) < 0) {
+ DIE("mount(/, %s, NULL, MS_BIND | MS_REC, NULL)", global_sandbox_root);
+ }
+
+ if (chdir(global_sandbox_root) < 0) {
+ DIE("chdir(%s)", global_sandbox_root);
+ }
+
+ for (const char *tmpfs_dir : opt.tmpfs_dirs) {
+ PRINT_DEBUG("tmpfs: %s", tmpfs_dir);
+ if (mount("tmpfs", tmpfs_dir + 1, "tmpfs",
+ MS_NOSUID | MS_NODEV | MS_NOATIME, NULL) < 0) {
+ DIE("mount(tmpfs, %s, tmpfs, MS_NOSUID | MS_NODEV | MS_NOATIME, NULL)",
+ tmpfs_dir + 1);
+ }
+ }
+
+ // Make sure that our working directory is a mount point. The easiest way to
+ // do this is by bind-mounting it upon itself.
+ if (mount(opt.working_dir, opt.working_dir + 1, NULL, MS_BIND, NULL) < 0) {
+ DIE("mount(%s, %s, NULL, MS_BIND, NULL)", opt.working_dir,
+ opt.working_dir + 1);
+ }
+
+ for (const char *writable_file : opt.writable_files) {
+ PRINT_DEBUG("writable: %s", writable_file);
+ if (mount(writable_file, writable_file + 1, NULL, MS_BIND, NULL) < 0) {
+ DIE("mount(%s, %s, NULL, MS_BIND, NULL)", writable_file,
+ writable_file + 1);
+ }
+ }
+
+ SetupHelperFiles();
+
+ for (const char *inaccessible_file : opt.inaccessible_files) {
+ struct stat sb;
+ if (stat(inaccessible_file, &sb) < 0) {
+ DIE("stat(%s)", inaccessible_file);
+ }
+
+ if (S_ISDIR(sb.st_mode)) {
+ PRINT_DEBUG("inaccessible dir: %s", inaccessible_file);
+ if (mount(global_inaccessible_directory, inaccessible_file + 1, NULL,
+ MS_BIND, NULL) < 0) {
+ DIE("mount(%s, %s, NULL, MS_BIND, NULL)", global_inaccessible_directory,
+ inaccessible_file + 1);
+ }
+ } else {
+ PRINT_DEBUG("inaccessible file: %s", inaccessible_file);
+ if (mount(global_inaccessible_file, inaccessible_file + 1, NULL, MS_BIND,
+ NULL) < 0) {
+ DIE("mount(%s, %s, NULL, MS_BIND, NULL", global_inaccessible_file,
+ inaccessible_file + 1);
+ }
+ }
+ }
+}
+
+// We later remount everything read-only, except the paths for which this method
+// returns true.
+static bool ShouldBeWritable(char *mnt_dir) {
+ mnt_dir += strlen(global_sandbox_root);
+
+ if (strcmp(mnt_dir, opt.working_dir) == 0) {
+ return true;
+ }
+
+ for (const char *writable_file : opt.writable_files) {
+ if (strcmp(mnt_dir, writable_file) == 0) {
+ return true;
+ }
+ }
+
+ for (const char *tmpfs_dir : opt.tmpfs_dirs) {
+ if (strcmp(mnt_dir, tmpfs_dir) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Makes the whole filesystem read-only, except for the paths for which
+// ShouldBeWritable returns true.
+static void MakeFilesystemMostlyReadOnly() {
+ FILE *mounts = setmntent("/proc/self/mounts", "r");
+ if (mounts == NULL) {
+ DIE("setmntent");
+ }
+
+ struct mntent *ent;
+ while ((ent = getmntent(mounts)) != NULL) {
+ // Skip mounts that do not belong to our sandbox.
+ if (strstr(ent->mnt_dir, global_sandbox_root) != ent->mnt_dir) {
+ continue;
+ }
+
+ int mountFlags = MS_BIND | MS_REMOUNT;
+
+ // MS_REMOUNT does not allow us to change certain flags. This means, we have
+ // to first read them out and then pass them in back again. There seems to
+ // be no better way than this (an API for just getting the mount flags of a
+ // mount entry as a bitmask would be great).
+ if (hasmntopt(ent, "nodev") != NULL) {
+ mountFlags |= MS_NODEV;
+ }
+ if (hasmntopt(ent, "noexec") != NULL) {
+ mountFlags |= MS_NOEXEC;
+ }
+ if (hasmntopt(ent, "nosuid") != NULL) {
+ mountFlags |= MS_NOSUID;
+ }
+ if (hasmntopt(ent, "noatime") != NULL) {
+ mountFlags |= MS_NOATIME;
+ }
+ if (hasmntopt(ent, "nodiratime") != NULL) {
+ mountFlags |= MS_NODIRATIME;
+ }
+ if (hasmntopt(ent, "relatime") != NULL) {
+ mountFlags |= MS_RELATIME;
+ }
+
+ if (!ShouldBeWritable(ent->mnt_dir)) {
+ mountFlags |= MS_RDONLY;
+ }
+
+ PRINT_DEBUG("remount %s: %s", (mountFlags & MS_RDONLY) ? "ro" : "rw",
+ ent->mnt_dir);
+ if (mount(NULL, ent->mnt_dir, NULL, mountFlags, NULL) < 0) {
+ // If we get EACCES, this might be a mount-point for which we don't have
+ // read access. Not much we can do about this, but it also won't do any
+ // harm, so let's go on. The same goes for EINVAL, which is fired in case
+ // a later mount overlaps an earlier mount, e.g. consider the case of
+ // /proc, /proc/sys/fs/binfmt_misc and /proc, with the latter /proc being
+ // the one that an outer sandbox has mounted on top of its parent /proc.
+ // In that case, we're not allowed to remount /proc/sys/fs/binfmt_misc,
+ // because it is hidden.
+ if (errno != EACCES && errno != EINVAL) {
+ DIE("remount(NULL, %s, NULL, %d, NULL)", ent->mnt_dir, mountFlags);
+ }
+ }
+ }
+
+ endmntent(mounts);
+}
+
+static void MountProc() {
+ // Mount a new proc on top of the old one, because the old one still refers to
+ // our parent PID namespace.
+ if (mount("proc", "proc", "proc", MS_NODEV | MS_NOEXEC | MS_NOSUID, NULL) <
+ 0) {
+ DIE("mount");
+ }
+}
+
+static void SetupNetworking() {
+ // When running in a separate network namespace, enable the loopback interface
+ // because some application may want to use it.
+ if (opt.create_netns) {
+ int fd;
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ DIE("socket");
+ }
+
+ struct ifreq ifr;
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, "lo", IF_NAMESIZE);
+
+ // Verify that name is valid.
+ if (if_nametoindex(ifr.ifr_name) == 0) {
+ DIE("if_nametoindex");
+ }
+
+ // Enable the interface.
+ ifr.ifr_flags |= IFF_UP;
+ if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) {
+ DIE("ioctl");
+ }
+
+ if (close(fd) < 0) {
+ DIE("close");
+ }
+ }
+}
+
+static void EnterSandbox() {
+ // Move the real root to old_root, then detach it.
+ char old_root[] = "tmp/old-root-XXXXXX";
+ if (mkdtemp(old_root) == NULL) {
+ DIE("mkdtemp(%s)", old_root);
+ }
+
+ // pivot_root has no wrapper in libc, so we need syscall()
+ if (syscall(SYS_pivot_root, ".", old_root) < 0) {
+ DIE("pivot_root(., %s)", old_root);
+ }
+
+ if (chroot(".") < 0) {
+ DIE("chroot(.)");
+ }
+
+ if (umount2(old_root, MNT_DETACH) < 0) {
+ DIE("umount2(%s, MNT_DETACH)", old_root);
+ }
+
+ if (rmdir(old_root) < 0) {
+ DIE("rmdir(%s)", old_root);
+ }
+
+ if (chdir(opt.working_dir) < 0) {
+ DIE("chdir(%s)", opt.working_dir);
+ }
+}
+
+static void InstallSignalHandler(int signum, void (*handler)(int)) {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = handler;
+ if (sigemptyset(&sa.sa_mask) < 0) {
+ DIE("sigemptyset");
+ }
+ if (sigaction(signum, &sa, NULL) < 0) {
+ DIE("sigaction");
+ }
+}
+
+static void IgnoreSignal(int signum) { InstallSignalHandler(signum, SIG_IGN); }
+
+static void InstallDefaultSignalHandler(int signum) {
+ InstallSignalHandler(signum, SIG_DFL);
+}
+
+static void SpawnChild() {
+ // Ignore SIGTTIN / SIGTTOU in PID 1, as we're about to hand off the terminal
+ // to the child. A big thanks to @krallin for figuring out the intricacies of
+ // dealing with these signals and documenting it on
+ // http://curiousthing.org/sigttin-sigttou-deep-dive-linux.
+ IgnoreSignal(SIGTTIN);
+ IgnoreSignal(SIGTTOU);
+
+ global_child_pid = fork();
+
+ if (global_child_pid < 0) {
+ DIE("fork()");
+ } else if (global_child_pid == 0) {
+ // Put the child into its own process group.
+ if (setpgid(0, 0) < 0) {
+ DIE("setpgid");
+ }
+
+ // Try to assign our terminal to the child process.
+ if (tcsetpgrp(STDIN_FILENO, getpgrp()) < 0 && errno != ENOTTY) {
+ DIE("tcsetpgrp")
+ }
+
+ // Restore handlers for SIGTTIN / SIGTTOU.
+ InstallDefaultSignalHandler(SIGTTIN);
+ InstallDefaultSignalHandler(SIGTTOU);
+
+ // Force umask to include read and execute for everyone, to make output
+ // permissions predictable.
+ umask(022);
+
+ // argv[] passed to execve() must be a null-terminated array.
+ opt.args.push_back(nullptr);
+
+ if (execvp(opt.args[0], opt.args.data()) < 0) {
+ DIE("execvp(%p, %p)", opt.args[0], opt.args.data());
+ }
+ }
+}
+
+static void HandleSignal(int signum) {
+ if (signum == SIGCHLD) {
+ // Our child process or one of its children died.
+ int status;
+ pid_t killed_pid;
+ while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) {
+ if (killed_pid == global_child_pid) {
+ // If the child process we spawned earlier terminated, we'll also
+ // terminate. We can simply _exit() here, because the Linux kernel will
+ // kindly SIGKILL all remaining processes in our PID namespace once we
+ // exit.
+ if (WIFSIGNALED(status)) {
+ _exit(128 + WTERMSIG(status));
+ } else {
+ _exit(WEXITSTATUS(status));
+ }
+ }
+ }
+ } else {
+ kill(-global_child_pid, signum);
+ }
+}
+
+static void WaitForChild() {
+ sigset_t all_signals;
+ if (sigfillset(&all_signals) < 0) {
+ DIE("sigfillset");
+ }
+ if (sigdelset(&all_signals, SIGTTIN) < 0) {
+ DIE("sigdelset");
+ }
+ if (sigdelset(&all_signals, SIGTTOU) < 0) {
+ DIE("sigdelset");
+ }
+ if (sigprocmask(SIG_BLOCK, &all_signals, NULL) < 0) {
+ DIE("sigprocmask");
+ }
+
+ while (1) {
+ int signum;
+ sigwait(&all_signals, &signum);
+ HandleSignal(signum);
+ }
+}
+
+int Pid1Main(void *sync_pipe_param) {
+ if (getpid() != 1) {
+ DIE("Using PID namespaces, but we are not PID 1");
+ }
+
+ SetupSelfDestruction(reinterpret_cast<int *>(sync_pipe_param));
+ SetupMountNamespace();
+ SetupUserNamespace();
+ SetupUtsNamespace();
+ MountFilesystems();
+ MakeFilesystemMostlyReadOnly();
+ MountProc();
+ SetupNetworking();
+ EnterSandbox();
+ SpawnChild();
+ WaitForChild();
+ _exit(EXIT_FAILURE);
+}
diff --git a/src/main/tools/network-tools.h b/src/main/tools/linux-sandbox-pid1.h
index 9c90aabb25..507739a08e 100644
--- a/src/main/tools/network-tools.h
+++ b/src/main/tools/linux-sandbox-pid1.h
@@ -1,4 +1,4 @@
-// Copyright 2015 The Bazel Authors. All rights reserved.
+// Copyright 2016 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,10 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-#ifndef NETWORK_TOOLS_H__
-#define NETWORK_TOOLS_H__
+#ifndef LINUX_SANDBOX_PID1_H__
+#define LINUX_SANDBOX_PID1_H__
-// Bring up the given network interface like "lo".
-void BringupInterface(const char *name);
+int Pid1Main(void *sync_pipe_param);
-#endif // NETWORK_TOOLS_H__
+#endif
diff --git a/src/main/tools/linux-sandbox-utils.h b/src/main/tools/linux-sandbox-utils.h
new file mode 100644
index 0000000000..3a4a1bff6c
--- /dev/null
+++ b/src/main/tools/linux-sandbox-utils.h
@@ -0,0 +1,30 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef LINUX_SANDBOX_UTILS_H__
+#define LINUX_SANDBOX_UTILS_H__
+
+#define S(x) #x
+#define S_(x) S(x)
+#define S__LINE__ S_(__LINE__)
+
+#define PRINT_DEBUG(...) \
+ do { \
+ if (opt.debug) { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \
+ fprintf(stderr, "\n"); \
+ } \
+ } while (0)
+
+#endif
diff --git a/src/main/tools/linux-sandbox.c b/src/main/tools/linux-sandbox.c
deleted file mode 100644
index ff75f057ba..0000000000
--- a/src/main/tools/linux-sandbox.c
+++ /dev/null
@@ -1,815 +0,0 @@
-// Copyright 2014 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#define _GNU_SOURCE
-
-#include <errno.h>
-#include <fcntl.h>
-#include <ftw.h>
-#include <libgen.h>
-#include <limits.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/types.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include "network-tools.h"
-#include "process-tools.h"
-
-#define PRINT_DEBUG(...) \
- do { \
- if (opt.debug) { \
- fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \
- } \
- } while (0)
-
-// The username of 'nobody'.
-static const char *kNobodyUsername = "nobody";
-
-// Options parsing result.
-struct Options {
- const char *sandbox_root; // Sandbox root (-S)
- const char *working_dir; // Working directory (-W)
- 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)
-
- char **create_dirs; // empty dirs to create (-d)
- size_t create_dirs_size; // How many elements in create_dirs
- int num_create_dirs; // How many empty dirs to create were specified
-
- char **mount_sources; // Map of directories to mount, from (-M)
- char **mount_targets; // sources -> targets (-m)
- size_t mount_map_sizes; // How many elements in mount_{sources,targets}
- int num_mounts; // How many mounts were specified
-
- int create_netns; // If 1, create a new network namespace (-n)
- int fake_root; // Pretend to be root inside the namespace (-r)
- bool debug; // Whether to print debugging messages (-D)
- const char *stdout_path; // Where to redirect stdout (-l)
- const char *stderr_path; // Where to redirect stderr (-L)
- char *const *args; // Command to run (--)
-};
-
-static int global_child_pid;
-static volatile sig_atomic_t global_signal;
-
-static struct Options opt;
-
-// Child function used by CheckNamespacesSupported() in call to clone().
-static int CheckNamespacesSupportedChild(void *arg) { return 0; }
-
-// Check whether the required namespaces are supported.
-static int CheckNamespacesSupported() {
- const int stackSize = 1024 * 1024;
- char *stack;
- char *stackTop;
- pid_t pid;
-
- // Allocate stack for child.
- stack = malloc(stackSize);
- if (stack == NULL) {
- DIE("malloc failed\n");
- }
-
- // Assume stack grows downward.
- stackTop = stack + stackSize;
-
- // Create child with own namespaces. We use clone() instead of unshare() here
- // because of the kernel bug (ref. CreateNamespaces) that lets unshare fail
- // sometimes. As this check has to run as fast as possible, we can't afford to
- // spend time sleeping and retrying here until it eventually works (or not).
- CHECK_CALL(pid = clone(CheckNamespacesSupportedChild, stackTop,
- CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS |
- CLONE_NEWIPC | CLONE_NEWNET | SIGCHLD,
- NULL));
- CHECK_CALL(waitpid(pid, NULL, 0));
-
- return EXIT_SUCCESS;
-}
-
-// 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);
-
- fprintf(stderr, "\nUsage: %s [-S sandbox-root] -- 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 <sandbox-root> 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-dir> working directory\n"
- " -T <timeout> timeout after which the child process will be "
- "terminated with SIGTERM\n"
- " -t <timeout> in case timeout occurs, how long to wait before killing "
- "the child with SIGKILL\n"
- " -d <dir> create an empty directory in the sandbox\n"
- " -M/-m <source/target> system directory to mount inside the sandbox\n"
- " Multiple directories can be specified and each of them will be "
- "mounted readonly.\n"
- " The -M option specifies which directory to mount, the -m option "
- "specifies where to\n"
- " mount it in the sandbox.\n"
- " -n if set, a new network namespace will be created\n"
- " -r if set, make the uid/gid be root, otherwise use nobody\n"
- " -D if set, debug info will be printed\n"
- " -l <file> redirect stdout to a file\n"
- " -L <file> redirect stderr to a file\n"
- " @FILE read newline-separated arguments from FILE\n");
- exit(EXIT_FAILURE);
-}
-
-// Deals with an unfinished (source but no target) mapping in opt.
-// Also adds a new unfinished mapping if source is not NULL.
-static void AddMountSource(char *source) {
- // The last -M flag wasn't followed by an -m flag, so assume that the source
- // should be mounted in the sandbox in the same path as outside.
- if (opt.mount_sources[opt.num_mounts] != NULL) {
- opt.mount_targets[opt.num_mounts] = opt.mount_sources[opt.num_mounts];
- opt.num_mounts++;
- }
-
- if (source != NULL) {
- if (opt.num_mounts >= opt.mount_map_sizes - 1) {
- opt.mount_sources =
- realloc(opt.mount_sources, opt.mount_map_sizes * sizeof(char *) * 2);
- if (opt.mount_sources == NULL) {
- DIE("realloc failed\n");
- }
- memset(opt.mount_sources + opt.mount_map_sizes, 0,
- opt.mount_map_sizes * sizeof(char *));
- opt.mount_targets =
- realloc(opt.mount_targets, opt.mount_map_sizes * sizeof(char *) * 2);
- if (opt.mount_targets == NULL) {
- DIE("realloc failed\n");
- }
- memset(opt.mount_targets + opt.mount_map_sizes, 0,
- opt.mount_map_sizes * sizeof(char *));
- opt.mount_map_sizes *= 2;
- }
- opt.mount_sources[opt.num_mounts] = source;
- }
-}
-
-static void AddCreateDir(char *create_dir) {
- if (opt.num_create_dirs > opt.create_dirs_size - 1) {
- opt.create_dirs =
- realloc(opt.create_dirs, opt.create_dirs_size * sizeof(char *) * 2);
- if (opt.create_dirs == NULL) {
- DIE("realloc failed\n");
- }
- memset(opt.create_dirs + opt.create_dirs_size, 0,
- opt.create_dirs_size * sizeof(char *));
- opt.create_dirs_size *= 2;
- }
-
- opt.create_dirs[opt.num_create_dirs++] = create_dir;
-}
-
-static void ParseCommandLine(int argc, char *const *argv);
-
-// Parses command line flags from a file named filename.
-// Expects optind to be initialized to 0 before being called.
-static void ParseOptionsFile(const char *filename) {
- FILE *const options_file = fopen(filename, "rb");
- if (options_file == NULL) {
- DIE("opening argument file %s failed\n", filename);
- }
- size_t sub_argv_size = 20;
- char **sub_argv = malloc(sizeof(char *) * sub_argv_size);
- sub_argv[0] = "";
- int sub_argc = 1;
-
- bool done = false;
- while (!done) {
- // This buffer determines the maximum size of arguments we can handle out of
- // the file. We DIE down below if it's ever too short.
- // 4096 is a common value for PATH_MAX. However, many filesystems support
- // arbitrarily long pathnames, so this might not be long enough to handle an
- // arbitrary filename no matter what. Twice the usual PATH_MAX seems
- // reasonable for now.
- char argument[8192];
- if (fgets(argument, sizeof(argument), options_file) == NULL) {
- if (feof(options_file)) {
- done = true;
- continue;
- } else {
- DIE("reading from argument file %s failed\n", filename);
- }
- }
- const size_t length = strlen(argument);
- if (length == 0) continue;
- if (length == sizeof(argument)) {
- DIE("argument from file %s is too long (> %zu)\n", filename,
- sizeof(argument));
- }
- if (argument[length - 1] == '\n') {
- argument[length - 1] = '\0';
- } else {
- done = true;
- }
- if (sub_argv_size == sub_argc + 1) {
- sub_argv_size *= 2;
- sub_argv = realloc(sub_argv, sizeof(char *) * sub_argv_size);
- }
- sub_argv[sub_argc++] = strdup(argument);
- }
- if (fclose(options_file) != 0) {
- DIE("closing options file %s failed\n", filename);
- }
- sub_argv[sub_argc] = NULL;
-
- ParseCommandLine(sub_argc, sub_argv);
-}
-
-// Parses command line flags from an argv array and puts the results into an
-// Options structure
-// passed in as an argument.
-static void ParseCommandLine(int argc, char *const *argv) {
- extern char *optarg;
- extern int optind, optopt;
- int c;
-
- while ((c = getopt(argc, argv, ":CS:W:T:t:d:M:m:nrDl:L:")) != -1) {
- switch (c) {
- case 'C':
- // Shortcut for the "does this system support sandboxing" check.
- exit(CheckNamespacesSupported());
- break;
- case 'S':
- if (opt.sandbox_root == NULL) {
- char *sandbox_root = strdup(optarg);
-
- // Make sure that the sandbox_root path has no trailing slash.
- if (sandbox_root[strlen(sandbox_root) - 1] == '/') {
- sandbox_root[strlen(sandbox_root) - 1] = 0;
- }
-
- opt.sandbox_root = sandbox_root;
- } else {
- Usage(argc, argv,
- "Multiple sandbox roots (-S) specified, expected one.");
- }
- break;
- case 'W':
- if (opt.working_dir == NULL) {
- opt.working_dir = strdup(optarg);
- } else {
- Usage(argc, argv,
- "Multiple working directories (-W) specified, expected one.");
- }
- 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 '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 'd':
- if (optarg[0] != '/') {
- Usage(argc, argv,
- "The -d option must be used with absolute paths only.");
- }
- AddCreateDir(optarg);
- break;
- case 'M':
- if (optarg[0] != '/') {
- Usage(argc, argv,
- "The -M option must be used with absolute paths only.");
- }
- AddMountSource(optarg);
- break;
- case 'm':
- if (optarg[0] != '/') {
- Usage(argc, argv,
- "The -m option must be used with absolute paths only.");
- }
- if (opt.mount_sources[opt.num_mounts] == NULL) {
- Usage(argc, argv, "The -m option must be preceded by an -M option.");
- }
- opt.mount_targets[opt.num_mounts++] = optarg;
- break;
- case 'n':
- opt.create_netns = 1;
- break;
- case 'r':
- opt.fake_root = 1;
- break;
- case 'D':
- opt.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;
- }
- }
-
- AddMountSource(NULL);
-
- while (optind < argc && argv[optind][0] == '@') {
- const char *filename = argv[optind] + 1;
- const int old_optind = optind;
- optind = 0;
- ParseOptionsFile(filename);
- optind = old_optind + 1;
- }
-
- if (argc > optind) {
- if (opt.args == NULL) {
- opt.args = argv + optind;
- } else {
- Usage(argc, argv, "Merging commands not supported.");
- }
- }
-}
-
-// Handles parsing all command line flags and populates the global opt struct.
-static void ParseOptions(int argc, char *const argv[]) {
- memset(&opt, 0, sizeof(opt));
- // 16 elements is a sane default, will be realloc'd as needed anyway.
- opt.mount_sources = calloc(16, sizeof(char *));
- opt.mount_targets = calloc(16, sizeof(char *));
- opt.mount_map_sizes = 16;
- // We'll need at least two slots for homedir_from_env and homedir.
- opt.create_dirs = calloc(2, sizeof(char *));
- opt.create_dirs_size = 2;
-
- ParseCommandLine(argc, argv);
-
- if (opt.args == NULL) {
- Usage(argc, argv, "No command specified.");
- }
-
- if (opt.sandbox_root == NULL) {
- Usage(argc, argv, "Sandbox root (-S) must be specified");
- }
-}
-
-static void CreateNamespaces(int create_netns) {
- // This weird workaround is necessary due to unshare seldomly 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 = 100;
- while (tries++ < max_tries) {
- if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
- (create_netns ? CLONE_NEWNET : 0)) == 0) {
- PRINT_DEBUG("unshare succeeded after %d tries\n", tries);
- return;
- } else {
- if (errno != EINVAL) {
- perror("unshare");
- exit(EXIT_FAILURE);
- }
- }
-
- // Exponential back-off, but sleep at most 250ms.
- usleep(delay);
- if (delay < 250000) {
- delay *= 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));
-}
-
-// Creates an empty file at 'path' by hard linking it from a known empty file.
-// This is over two times faster than creating empty files via open() on
-// certain filesystems (e.g. XFS).
-static void LinkFile(const char *path) {
- CHECK_CALL(link("tmp/empty_file", path));
-}
-
-// 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 {
- LinkFile(path);
- }
-
- return 0;
-}
-
-static void SetupDevices() {
- CHECK_CALL(CreateTarget("dev", true));
- const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero",
- NULL};
- for (int i = 0; devs[i] != NULL; i++) {
- LinkFile(devs[i] + 1);
- CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL));
- }
-
- CHECK_CALL(symlink("/proc/self/fd", "dev/fd"));
-}
-
-static int rmrf(const char *fpath, const struct stat *sb, int typeflag,
- struct FTW *ftwbuf) {
- if (typeflag == FTW_DP) {
- return rmdir(fpath);
- } else {
- return unlink(fpath);
- }
-}
-
-static void SetupDirectories() {
- // If in sandbox_debug mode and debugging, create the sandbox root dir first
- if (opt.debug && isatty(fileno(stdin))) {
- // Enter sandbox_debug mode a second time, delete old sandbox
- struct stat sb;
- int err = stat(opt.sandbox_root, &sb);
- if (err == 0) {
- CHECK_CALL(nftw(opt.sandbox_root, *rmrf, sysconf(_SC_OPEN_MAX),
- FTW_DEPTH | FTW_PHYS));
- } else if (errno != ENOENT) {
- CHECK_CALL(err);
- }
-
- CHECK_CALL(mkdir(opt.sandbox_root, 0755));
- }
-
- // 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));
-
- // This is used as the base for hardlinking the input files.
- CHECK_CALL(CreateTarget("tmp", true));
- CreateFile("tmp/empty_file");
-
- // Setup /dev.
- SetupDevices();
-
- CHECK_CALL(CreateTarget("proc", true));
- CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL));
-
- // Make sure the home directory exists, too.
- char *homedir_from_env = getenv("HOME");
- if (homedir_from_env != NULL) {
- if (homedir_from_env[0] != '/') {
- DIE("Home directory specified in $HOME must be an absolute path, but is "
- "%s",
- homedir_from_env);
- }
- if (strcmp(homedir_from_env, "/") != 0) {
- AddCreateDir(homedir_from_env);
- }
- }
-
- errno = 0;
- struct passwd *uid_passwd = getpwuid(getuid());
- if (uid_passwd == NULL) {
- if (errno != 0) {
- perror("getpwuid(getuid())");
- exit(EXIT_FAILURE);
- } else {
- DIE("UID %d not found in passwd file\n", (int)getuid());
- }
- }
- char *homedir = uid_passwd->pw_dir;
- if (homedir != NULL &&
- (homedir_from_env == NULL || strcmp(homedir_from_env, homedir) != 0)) {
- if (homedir[0] != '/') {
- DIE("Home directory of user nobody must be an absolute path, but is %s",
- homedir);
- }
- if (strcmp(homedir, "/") != 0) {
- AddCreateDir(homedir);
- }
- }
-
- // Create needed directories.
- for (int i = 0; i < opt.num_create_dirs; i++) {
- if (opt.debug) {
- PRINT_DEBUG("createdir: %s\n", opt.create_dirs[i]);
- }
- CHECK_CALL(CreateTarget(opt.create_dirs[i] + 1, true));
- }
-
- // Mount all mounts.
- for (int i = 0; i < opt.num_mounts; i++) {
- struct stat sb;
- stat(opt.mount_sources[i], &sb);
-
- if (opt.debug) {
- if (strcmp(opt.mount_sources[i], opt.mount_targets[i]) == 0) {
- // The file is mounted to the same path inside the sandbox, as outside
- // (e.g. /home/user -> <sandbox>/home/user), so we'll just show a
- // simplified version of the mount command.
- PRINT_DEBUG("mount: %s\n", opt.mount_sources[i]);
- } else {
- // The file is mounted to a custom location inside the sandbox.
- // Create a user-friendly string for the sandboxed path and show it.
- char *user_friendly_mount_target =
- malloc(strlen("<sandbox>") + strlen(opt.mount_targets[i]) + 1);
- strcpy(user_friendly_mount_target, "<sandbox>");
- strcat(user_friendly_mount_target, opt.mount_targets[i]);
- PRINT_DEBUG("mount: %s -> %s\n", opt.mount_sources[i],
- user_friendly_mount_target);
- free(user_friendly_mount_target);
- }
- }
-
- char *full_sandbox_path =
- malloc(strlen(opt.sandbox_root) + strlen(opt.mount_targets[i]) + 1);
- strcpy(full_sandbox_path, opt.sandbox_root);
- strcat(full_sandbox_path, opt.mount_targets[i]);
- CHECK_CALL(CreateTarget(full_sandbox_path, S_ISDIR(sb.st_mode)));
- CHECK_CALL(mount(opt.mount_sources[i], full_sandbox_path, NULL,
- MS_REC | MS_BIND | MS_RDONLY, NULL));
- free(full_sandbox_path);
- }
-}
-
-// Write the file "filename" using a format string specified by "fmt". Returns
-// -1 on failure.
-static int WriteFile(const char *filename, const char *fmt, ...) {
- int r;
- va_list ap;
- FILE *stream = fopen(filename, "w");
- if (stream == NULL) {
- return -1;
- }
- va_start(ap, fmt);
- r = vfprintf(stream, fmt, ap);
- va_end(ap);
- if (r >= 0) {
- r = fclose(stream);
- }
- return r;
-}
-
-static void SetupUserNamespace(int uid, int gid, int new_uid, int new_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(EXIT_FAILURE);
- }
-
- // 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", new_uid, uid));
- CHECK_CALL(WriteFile("/proc/self/gid_map", "%d %d 1\n", new_gid, gid));
-
- CHECK_CALL(setresuid(new_uid, new_uid, new_uid));
- CHECK_CALL(setresgid(new_gid, new_gid, new_gid));
-}
-
-static void SetupUserNamespaceForNobody(int uid, int gid) {
- struct passwd *pwd = getpwnam(kNobodyUsername);
-
- if (pwd == NULL) {
- perror("Unable to find passwd entry for user nobody.");
- exit(EXIT_FAILURE);
- }
-
- SetupUserNamespace(uid, gid, pwd->pw_uid, pwd->pw_gid);
-}
-
-static void ChangeRoot() {
- // move the real root to old_root, then detach it
- char old_root[16] = "old-root-XXXXXX";
- if (mkdtemp(old_root) == NULL) {
- perror("mkdtemp");
- DIE("mkdtemp returned NULL\n");
- }
-
- // pivot_root has no wrapper in libc, so we need syscall()
- CHECK_CALL(syscall(SYS_pivot_root, ".", old_root));
- CHECK_CALL(chroot("."));
- CHECK_CALL(umount2(old_root, MNT_DETACH));
- CHECK_CALL(rmdir(old_root));
-
- if (opt.working_dir != NULL) {
- CHECK_CALL(chdir(opt.working_dir));
- }
-}
-
-// 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_child_pid, true, opt.kill_delay_secs);
- } 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, opt.kill_delay_secs);
- }
-}
-
-// Run the command specified by the argv array and kill it after timeout
-// seconds.
-static void SpawnCommand(char *const *argv, double timeout_secs,
- bool isFallback) {
- 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)) {
- if (opt.debug && !isFallback && isatty(fileno(stdin)) &&
- WEXITSTATUS(status) > 0) {
- char **cmdList = calloc(2, sizeof(char *));
- cmdList[0] = "/bin/bash";
- cmdList[1] = NULL;
- SpawnCommand(cmdList, 0, true);
- }
- exit(WEXITSTATUS(status));
- } else {
- int sig = WTERMSIG(status);
- UnHandle(sig);
- raise(sig);
- }
- }
-}
-
-int main(int argc, char *const argv[]) {
- ParseOptions(argc, argv);
-
- 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(opt.create_netns);
-
- if (opt.create_netns) {
- // Enable the loopback interface because some application may want
- // to use it.
- BringupInterface("lo");
- }
-
- // 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));
-
- if (opt.fake_root) {
- SetupUserNamespace(uid, gid, 0, 0);
- } else {
- SetupUserNamespaceForNobody(uid, gid);
- }
-
- SetupDirectories();
-
- ChangeRoot();
-
- SpawnCommand(opt.args, opt.timeout_secs, false);
-
- free(opt.create_dirs);
- free(opt.mount_sources);
- free(opt.mount_targets);
-
- return 0;
-}
diff --git a/src/main/tools/linux-sandbox.cc b/src/main/tools/linux-sandbox.cc
new file mode 100644
index 0000000000..8eeef841e6
--- /dev/null
+++ b/src/main/tools/linux-sandbox.cc
@@ -0,0 +1,289 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * linux-sandbox runs commands in a restricted environment where they are
+ * subject to a few rules:
+ *
+ * - The entire filesystem is made read-only.
+ * - The working directory (-W) will be made read-write, though.
+ * - Individual files or directories can be made writable (but not deletable)
+ * (-w).
+ * - Individual files or directories can be made inaccessible / unreadable
+ * (-i).
+ * - tmpfs will be mounted on /tmp.
+ * - tmpfs can be mounted on top of existing directories (-e), too.
+ * - If the process takes longer than the timeout (-T), it will be killed with
+ * SIGTERM. If it does not exit within the grace period (-t), it all of its
+ * children will be killed with SIGKILL.
+ * - If linux-sandbox itself gets killed, the process and all of its children
+ * will be killed.
+ * - If linux-sandbox's parent dies, it will kill itself, the process and all
+ * the children.
+ * - Network access is allowed, but can be disabled via -N.
+ * - The process runs as user "nobody", unless fakeroot is enabled (-R).
+ * - The hostname and domainname will be set to "sandbox".
+ * - The process runs in its own PID namespace, so other processes on the
+ * system are invisible.
+ */
+
+#include "linux-sandbox-options.h"
+#include "linux-sandbox-pid1.h"
+#include "linux-sandbox-utils.h"
+
+#define DIE(args...) \
+ { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \
+ fprintf(stderr, "\": "); \
+ perror(NULL); \
+ exit(EXIT_FAILURE); \
+ }
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <vector>
+
+int global_outer_uid;
+int global_outer_gid;
+char global_sandbox_root[] = "/tmp/sandbox.XXXXXX";
+
+static int global_child_pid;
+
+// The signal that will be sent to the child when a timeout occurs.
+static volatile sig_atomic_t global_next_timeout_signal = SIGTERM;
+
+// The signal that caused us to kill the child (e.g. on timeout).
+static volatile sig_atomic_t global_signal;
+
+static void CloseFds() {
+ DIR *fds = opendir("/proc/self/fd");
+ if (fds == NULL) {
+ DIE("opendir");
+ }
+
+ while (1) {
+ errno = 0;
+ struct dirent *dent = readdir(fds);
+
+ if (dent == NULL) {
+ if (errno != 0) {
+ DIE("readdir");
+ }
+ break;
+ }
+
+ if (isdigit(dent->d_name[0])) {
+ errno = 0;
+ int fd = strtol(dent->d_name, nullptr, 10);
+
+ // (1) Skip unparseable entries.
+ // (2) Close everything except stdin, stdout and stderr.
+ // (3) Do not accidentally close our directory handle.
+ if (errno == 0 && fd > STDERR_FILENO && fd != dirfd(fds)) {
+ if (close(fd) < 0) {
+ DIE("close");
+ }
+ }
+ }
+ }
+
+ if (closedir(fds) < 0) {
+ DIE("closedir");
+ }
+}
+
+static void SetupSandboxRoot() {
+ if (mkdtemp(global_sandbox_root) == NULL) {
+ DIE("mkdtemp(%s)", global_sandbox_root);
+ }
+}
+
+static void RemoveSandboxRoot() {
+ if (rmdir(global_sandbox_root) < 0) {
+ DIE("rmdir(%s)", global_sandbox_root);
+ }
+}
+
+static void HandleSignal(int signum, void (*handler)(int)) {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = handler;
+ if (sigemptyset(&sa.sa_mask) < 0) {
+ DIE("sigemptyset");
+ }
+ if (sigaction(signum, &sa, NULL) < 0) {
+ DIE("sigaction");
+ }
+}
+
+static void OnTimeout(int sig) {
+ global_signal = sig;
+ kill(global_child_pid, global_next_timeout_signal);
+ if (global_next_timeout_signal == SIGTERM && opt.kill_delay_secs > 0) {
+ global_next_timeout_signal = SIGKILL;
+ alarm(opt.kill_delay_secs);
+ }
+}
+
+static void SpawnPid1() {
+ const int kStackSize = 1024 * 1024;
+ std::vector<char> child_stack(kStackSize);
+
+ int sync_pipe[2];
+ if (pipe(sync_pipe) < 0) {
+ DIE("pipe");
+ }
+
+ int clone_flags = CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
+ CLONE_NEWPID | SIGCHLD;
+ if (opt.create_netns) {
+ clone_flags |= CLONE_NEWNET;
+ }
+
+ // We use clone instead of unshare, because unshare sometimes fails with
+ // EINVAL due to a race condition in the Linux kernel (see
+ // https://lkml.org/lkml/2015/7/28/833).
+ global_child_pid =
+ clone(Pid1Main, child_stack.data() + kStackSize, clone_flags, sync_pipe);
+ if (global_child_pid < 0) {
+ DIE("clone");
+ }
+
+ // We close the write end of the sync pipe, read a byte and then close the
+ // pipe. This proves to the linux-sandbox-pid1 process that we still existed
+ // after it ran prctl(PR_SET_PDEATHSIG, SIGKILL), thus preventing a race
+ // condition where the parent is killed before that call was made.
+ char buf;
+ if (close(sync_pipe[1]) < 0) {
+ DIE("close");
+ }
+ if (read(sync_pipe[0], &buf, 1) < 0) {
+ DIE("read");
+ }
+ if (close(sync_pipe[0]) < 0) {
+ DIE("close");
+ }
+}
+
+static int WaitForPid1() {
+ int err, status;
+ do {
+ err = waitpid(global_child_pid, &status, 0);
+ } while (err < 0 && errno == EINTR);
+
+ if (err < 0) {
+ DIE("waitpid");
+ }
+
+ if (global_signal > 0) {
+ // The child exited because we killed it due to receiving a signal
+ // ourselves. Do not trust the exitcode in this case, just calculate it from
+ // the signal.
+ PRINT_DEBUG("child exited due to us catching signal: %s",
+ strsignal(global_signal));
+ return 128 + global_signal;
+ } else if (WIFSIGNALED(status)) {
+ PRINT_DEBUG("child exited due to receiving signal: %s",
+ strsignal(WTERMSIG(status)));
+ return 128 + WTERMSIG(status);
+ } else {
+ PRINT_DEBUG("child exited normally with exitcode %d", WEXITSTATUS(status));
+ return WEXITSTATUS(status);
+ }
+}
+
+static void Redirect(const char *target_path, int fd, const char *name) {
+ if (target_path != NULL && strcmp(target_path, "-") != 0) {
+ const int flags = O_WRONLY | O_CREAT | O_TRUNC | O_APPEND;
+ int fd_out = open(target_path, flags, 0666);
+ if (fd_out < 0) {
+ DIE("open(%s)", target_path);
+ }
+ // If we were launched with less than 3 fds (stdin, stdout, stderr) open,
+ // but redirection is still requested via a command-line flag, something is
+ // wacky and the following code would not do what we intend to do, so let's
+ // bail.
+ if (fd_out < 3) {
+ DIE("open(%s) returned a handle that is reserved for stdin / stdout / "
+ "stderr",
+ target_path);
+ }
+ if (dup2(fd_out, fd) < 0) {
+ DIE("dup2()");
+ }
+ if (close(fd_out) < 0) {
+ DIE("close()");
+ }
+ }
+}
+
+static void RedirectStdout(const char *stdout_path) {
+ Redirect(stdout_path, STDOUT_FILENO, "stdout");
+}
+
+static void RedirectStderr(const char *stderr_path) {
+ Redirect(stderr_path, STDERR_FILENO, "stderr");
+}
+
+int main(int argc, char *argv[]) {
+ // Ask the kernel to kill us with SIGKILL if our parent dies.
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) {
+ DIE("prctl");
+ }
+
+ ParseOptions(argc, argv);
+
+ RedirectStdout(opt.stdout_path);
+ RedirectStderr(opt.stderr_path);
+
+ // This should never be called as a setuid binary, drop privileges just in
+ // case. We don't need to be root, because we use user namespaces anyway.
+ if (setuid(getuid()) < 0) {
+ DIE("setuid");
+ }
+
+ global_outer_uid = getuid();
+ global_outer_gid = getgid();
+
+ // Make sure the sandboxed process does not inherit any accidentally left open
+ // file handles from our parent.
+ CloseFds();
+
+ SetupSandboxRoot();
+ atexit(RemoveSandboxRoot);
+
+ HandleSignal(SIGALRM, OnTimeout);
+ if (opt.timeout_secs > 0) {
+ alarm(opt.timeout_secs);
+ }
+
+ SpawnPid1();
+ return WaitForPid1();
+}
diff --git a/src/main/tools/linux-sandbox.h b/src/main/tools/linux-sandbox.h
new file mode 100644
index 0000000000..5ff778da38
--- /dev/null
+++ b/src/main/tools/linux-sandbox.h
@@ -0,0 +1,22 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef LINUX_SANDBOX_H__
+#define LINUX_SANDBOX_H__
+
+extern int global_outer_uid;
+extern int global_outer_gid;
+extern char global_sandbox_root[];
+
+#endif
diff --git a/src/main/tools/network-tools.c b/src/main/tools/network-tools.c
deleted file mode 100644
index 16241cd64c..0000000000
--- a/src/main/tools/network-tools.c
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2015 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#define _GNU_SOURCE
-
-#include <net/if.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include "process-tools.h"
-#include "network-tools.h"
-
-void BringupInterface(const char *name) {
- int fd;
-
- struct ifreq ifr;
-
- CHECK_CALL(fd = socket(AF_INET, SOCK_DGRAM, 0));
-
- memset(&ifr, 0, sizeof(ifr));
- strncpy(ifr.ifr_name, name, IF_NAMESIZE);
-
- // Verify that name is valid.
- CHECK_CALL(if_nametoindex(ifr.ifr_name));
-
- // Enable the interface
- ifr.ifr_flags |= IFF_UP;
- CHECK_CALL(ioctl(fd, SIOCSIFFLAGS, &ifr));
-
- CHECK_CALL(close(fd));
-}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java
index b2fe0e6720..182a62dc1e 100644
--- a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java
@@ -14,220 +14,27 @@
package com.google.devtools.build.lib.sandbox;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
-
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
/**
* Tests for {@code LinuxSandboxedStrategy}.
- *
- * <p>The general idea for each test is to provide a file tree consisting of symlinks, directories
- * and empty files and then handing that together with an arbitrary number of input files (what
- * would be specified in the "srcs" attribute, for example) to the LinuxSandboxedStrategy.
- *
- * <p>The algorithm that processes the mounts must then always find (and thus mount) the expected
- * tree of files given only the set of input files.
*/
@RunWith(JUnit4.class)
public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase {
- /**
- * Strips the working directory (which can be very long) from the file names in the input map, to
- * make assertion failures easier to read.
- */
- private ImmutableMap<String, String> userFriendlyMap(Map<Path, Path> input) {
- ImmutableMap.Builder<String, String> userFriendlyMap = ImmutableMap.builder();
- for (Entry<Path, Path> entry : input.entrySet()) {
- String key = entry.getKey().getPathString().replace(workspaceDir.getPathString(), "");
- String value = entry.getValue().getPathString().replace(workspaceDir.getPathString(), "");
- userFriendlyMap.put(key, value);
- }
- return userFriendlyMap.build();
- }
-
- /**
- * Takes a map of file specifications, creates the necessary files / symlinks / dirs,
- * mounts files listed in customMount at their canonical location in the sandbox and returns the
- * output of {@code LinuxSandboxedStrategy#fixMounts} for it.
- */
- private ImmutableMap<String, String> userFriendlyMounts(
- Map<String, String> linksAndFiles, List<String> customMounts) throws Exception {
- return userFriendlyMap(mounts(linksAndFiles, customMounts));
- }
-
- private ImmutableMap<Path, Path> mounts(
- Map<String, String> linksAndFiles, List<String> customMounts) throws Exception {
- createTreeStructure(linksAndFiles);
-
- ImmutableMap.Builder<Path, Path> mounts = ImmutableMap.builder();
- for (String customMount : customMounts) {
- Path customMountPath = workspaceDir.getRelative(customMount);
- mounts.put(customMountPath, customMountPath);
- }
- return ImmutableMap.copyOf(LinuxSandboxedStrategy.finalizeMounts(mounts.build()));
- }
-
- /**
- * Takes a map of file specifications, creates the necessary files / symlinks / dirs,
- * mounts the first file of the specification at its canonical location in the sandbox and returns
- * the output of {@code LinuxSandboxedStrategy#fixMounts} for it.
- */
- private Map<String, String> userFriendlyMounts(Map<String, String> linksAndFiles)
- throws Exception {
- return userFriendlyMap(mounts(linksAndFiles));
- }
-
- private Map<Path, Path> mounts(Map<String, String> linksAndFiles) throws Exception {
- return mounts(
- linksAndFiles, ImmutableList.of(Iterables.getFirst(linksAndFiles.keySet(), null)));
- }
-
- /**
- * Returns a map of mount entries for a list files, which can be used to assert that all
- * expected mounts have been made by the LinuxSandboxedStrategy.
- */
- private ImmutableMap<String, String> userFriendlyAsserts(List<String> asserts) {
- return userFriendlyMap(asserts(asserts));
- }
-
- private ImmutableMap<Path, Path> asserts(List<String> asserts) {
- ImmutableMap.Builder<Path, Path> pathifiedAsserts = ImmutableMap.builder();
- for (String fileName : asserts) {
- Path inputPath = workspaceDir.getRelative(fileName);
- pathifiedAsserts.put(inputPath, inputPath);
- }
- return pathifiedAsserts.build();
- }
-
- private void createTreeStructure(Map<String, String> linksAndFiles) throws Exception {
- for (Entry<String, String> entry : linksAndFiles.entrySet()) {
- Path filePath = workspaceDir.getRelative(entry.getKey());
- String linkTarget = entry.getValue();
-
- FileSystemUtils.createDirectoryAndParents(filePath.getParentDirectory());
-
- if (!linkTarget.isEmpty()) {
- filePath.createSymbolicLink(new PathFragment(linkTarget));
- } else if (filePath.getPathString().endsWith("/")) {
- filePath.createDirectory();
- } else {
- FileSystemUtils.createEmptyFile(filePath);
- }
- }
- }
-
- @Test
- public void testResolvesRelativeFileToFileSymlinkInSameDir() throws Exception {
- Map<String, String> testFiles = new LinkedHashMap<>();
- testFiles.put("symlink.txt", "goal.txt");
- testFiles.put("goal.txt", "");
-
- List<String> assertMounts = new ArrayList<>();
- assertMounts.add("symlink.txt");
- assertMounts.add("goal.txt");
-
- assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts));
- }
-
- @Test
- public void testResolvesRelativeFileToFileSymlinkInSubDir() throws Exception {
- Map<String, String> testFiles =
- ImmutableMap.of(
- "symlink.txt", "x/goal.txt",
- "x/goal.txt", "");
-
- List<String> assertMounts = ImmutableList.of("symlink.txt", "x/goal.txt");
- assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts));
- }
-
- @Test
- public void testResolvesRelativeFileToFileSymlinkInParentDir() throws Exception {
- Map<String, String> testFiles =
- ImmutableMap.of(
- "x/symlink.txt", "../goal.txt",
- "goal.txt", "");
-
- List<String> assertMounts = ImmutableList.of("x/symlink.txt", "goal.txt");
-
- assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts));
- }
-
- @Test
- public void testRecursesSubDirs() throws Exception {
- ImmutableList<String> inputFile = ImmutableList.of("a/b");
-
- Map<String, String> testFiles =
- ImmutableMap.of(
- "a/b/x.txt", "",
- "a/b/y.txt", "z.txt",
- "a/b/z.txt", "");
-
- List<String> assertMounts = ImmutableList.of("a/b/x.txt", "a/b/y.txt", "a/b/z.txt");
-
- assertThat(userFriendlyMounts(testFiles, inputFile))
- .isEqualTo(userFriendlyAsserts(assertMounts));
- }
-
- /**
- * Test that the algorithm correctly identifies and refuses symlink loops.
- */
- @Test
- public void testCatchesSymlinkLoop() throws Exception {
- try {
- mounts(
- ImmutableMap.of(
- "a", "b",
- "b", "a"));
- fail();
- } catch (IOException e) {
- assertThat(e)
- .hasMessage(
- String.format(
- "%s (Too many levels of symbolic links)",
- workspaceDir.getRelative("a").getPathString()));
- }
- }
-
- /**
- * Test that the algorithm correctly detects and refuses symlinks whose subcomponents are not all
- * directories (e.g. "a -> dir/file/file").
- */
- @Test
- public void testCatchesIllegalSymlink() throws Exception {
- try {
- mounts(
- ImmutableMap.of(
- "b", "a/c",
- "a", ""));
- fail();
- } catch (IOException e) {
- assertThat(e)
- .hasMessage(
- String.format(
- "%s (Not a directory)", workspaceDir.getRelative("a/c").getPathString()));
- }
- }
-
@Test
public void testParseManifestFile() throws Exception {
- Path targetDir = workspaceDir.getRelative("runfiles");
- targetDir.createDirectory();
+ PathFragment targetDir = new PathFragment("runfiles");
Path testFile = workspaceDir.getRelative("testfile");
FileSystemUtils.createEmptyFile(testFile);
@@ -238,23 +45,22 @@ public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase {
Charset.defaultCharset(),
String.format("x/testfile %s\nx/emptyfile \n", testFile.getPathString()));
- Map mounts =
- LinuxSandboxedStrategy.parseManifestFile(targetDir, manifestFile.getPathFile(), false, "");
+ Map<PathFragment, Path> mounts = new TreeMap<>();
+ LinuxSandboxedStrategy.parseManifestFile(
+ fileSystem, mounts, targetDir, manifestFile.getPathFile(), false, "");
- assertThat(userFriendlyMap(mounts))
+ assertThat(mounts)
.isEqualTo(
- userFriendlyMap(
- ImmutableMap.of(
- fileSystem.getPath("/runfiles/x/testfile"),
- testFile,
- fileSystem.getPath("/runfiles/x/emptyfile"),
- fileSystem.getPath("/dev/null"))));
+ ImmutableMap.of(
+ new PathFragment("runfiles/x/testfile"),
+ testFile,
+ new PathFragment("runfiles/x/emptyfile"),
+ fileSystem.getPath("/dev/null")));
}
@Test
public void testParseFilesetManifestFile() throws Exception {
- Path targetDir = workspaceDir.getRelative("fileset");
- targetDir.createDirectory();
+ PathFragment targetDir = new PathFragment("fileset");
Path testFile = workspaceDir.getRelative("testfile");
FileSystemUtils.createEmptyFile(testFile);
@@ -265,12 +71,10 @@ public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase {
Charset.defaultCharset(),
String.format("workspace/x/testfile %s\n0\n", testFile.getPathString()));
- Map mounts =
- LinuxSandboxedStrategy.parseManifestFile(
- targetDir, manifestFile.getPathFile(), true, "workspace");
+ Map<PathFragment, Path> mounts = new HashMap<>();
+ LinuxSandboxedStrategy.parseManifestFile(
+ fileSystem, mounts, targetDir, manifestFile.getPathFile(), true, "workspace");
- assertThat(userFriendlyMap(mounts))
- .isEqualTo(
- userFriendlyMap(ImmutableMap.of(fileSystem.getPath("/fileset/x/testfile"), testFile)));
+ assertThat(mounts).isEqualTo(ImmutableMap.of(new PathFragment("fileset/x/testfile"), testFile));
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java
index b97e396c53..a80074a9fa 100644
--- a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java
@@ -36,10 +36,8 @@ import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.util.FileSystems;
import com.google.devtools.common.options.OptionsParser;
-
-import org.junit.Before;
-
import java.io.IOException;
+import org.junit.Before;
/**
* Common parts of all {@link LinuxSandboxedStrategy} tests.
@@ -50,7 +48,6 @@ public class LinuxSandboxedStrategyTestCase {
protected FileSystem fileSystem;
protected Path workspaceDir;
- protected Path fakeSandboxDir;
protected BlazeExecutor executor;
protected BlazeDirectories blazeDirs;
@@ -75,9 +72,6 @@ public class LinuxSandboxedStrategyTestCase {
outputBase = testRoot.getRelative("outputBase");
outputBase.createDirectory();
- fakeSandboxDir = testRoot.getRelative("sandbox");
- fakeSandboxDir.createDirectory();
-
blazeDirs = new BlazeDirectories(outputBase, outputBase, workspaceDir, "mock-product-name");
BlazeTestUtils.getIntegrationBinTools(blazeDirs);
@@ -101,7 +95,6 @@ public class LinuxSandboxedStrategyTestCase {
"",
new LinuxSandboxedStrategy(
optionsParser.getOptions(SandboxOptions.class),
- ImmutableMap.<String, String>of(),
blazeDirs,
MoreExecutors.newDirectExecutorService(),
true,
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java
deleted file mode 100644
index 060eb6f264..0000000000
--- a/src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright 2015 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.sandbox;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.IOException;
-
-/**
- * Tests for {@code MountMap}.
- */
-@RunWith(JUnit4.class)
-public class MountMapTest extends LinuxSandboxedStrategyTestCase {
- @Test
- public void testMountMapWithNormalMounts() throws IOException {
- // Allowed: Just two normal mounts (a -> sandbox/a, b -> sandbox/b)
- MountMap mounts = new MountMap();
- mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("a"));
- mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("b"));
- assertThat(mounts)
- .isEqualTo(
- ImmutableMap.of(
- fileSystem.getPath("/a"), workspaceDir.getRelative("a"),
- fileSystem.getPath("/b"), workspaceDir.getRelative("b")));
- }
-
- @Test
- public void testMountMapWithSameMountTwice() throws IOException {
- // Allowed: Mount same thing twice (a -> sandbox/a, a -> sandbox/a, b -> sandbox/b)
- MountMap mounts = new MountMap();
- mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("a"));
- mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("a"));
- mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("b"));
- assertThat(mounts)
- .isEqualTo(
- ImmutableMap.of(
- fileSystem.getPath("/a"), workspaceDir.getRelative("a"),
- fileSystem.getPath("/b"), workspaceDir.getRelative("b")));
- }
-
- @Test
- public void testMountMapWithOneThingTwoTargets() throws IOException {
- // Allowed: Mount one thing in two targets (x -> sandbox/a, x -> sandbox/b)
- MountMap mounts = new MountMap();
- mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("x"));
- mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("x"));
- assertThat(mounts)
- .isEqualTo(
- ImmutableMap.of(
- fileSystem.getPath("/a"), workspaceDir.getRelative("x"),
- fileSystem.getPath("/b"), workspaceDir.getRelative("x")));
- }
-
- @Test
- public void testMountMapWithTwoThingsOneTarget() throws IOException {
- // Forbidden: Mount two things onto the same target (x -> sandbox/a, y -> sandbox/a)
- try {
- MountMap mounts = new MountMap();
- mounts.put(fileSystem.getPath("/x"), workspaceDir.getRelative("a"));
- mounts.put(fileSystem.getPath("/x"), workspaceDir.getRelative("b"));
- fail();
- } catch (IllegalArgumentException e) {
- assertThat(e)
- .hasMessage(
- String.format(
- "Cannot mount both '%s' and '%s' onto '%s'",
- workspaceDir.getRelative("a"),
- workspaceDir.getRelative("b"),
- fileSystem.getPath("/x")));
- }
- }
-
- @Test
- public void testMountMapGuaranteesOrdering() {
- MountMap mounts = new MountMap();
- mounts.put(fileSystem.getPath("/a/c"), workspaceDir.getRelative("x"));
- mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("x"));
- mounts.put(fileSystem.getPath("/a/b"), workspaceDir.getRelative("x"));
- mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("x"));
-
- assertThat(mounts.entrySet())
- .containsExactlyElementsIn(
- ImmutableMap.builder()
- .put(fileSystem.getPath("/a"), workspaceDir.getRelative("x"))
- .put(fileSystem.getPath("/a/b"), workspaceDir.getRelative("x"))
- .put(fileSystem.getPath("/a/c"), workspaceDir.getRelative("x"))
- .put(fileSystem.getPath("/b"), workspaceDir.getRelative("x"))
- .build()
- .entrySet())
- .inOrder();
- }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java b/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java
index 46522a7459..962022017b 100644
--- a/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java
@@ -23,15 +23,14 @@ 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.UnixFileSystem;
-
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import java.io.File;
-import java.io.FileNotFoundException;
-
/**
* This class tests the FilesystemUtils class.
*/
@@ -104,9 +103,12 @@ public class NativePosixFilesTest {
File foo = new File("/bin");
try {
NativePosixFiles.setWritable(foo);
- fail("Expected FilePermissionException, but wasn't thrown.");
+ fail("Expected FilePermissionException or IOException, but wasn't thrown.");
} catch (FilePermissionException e) {
assertThat(e).hasMessage(foo + " (Operation not permitted)");
+ } catch (IOException e) {
+ // When running in a sandbox, /bin might actually be a read-only file system.
+ assertThat(e).hasMessage(foo + " (Read-only file system)");
}
}
}
diff --git a/src/test/shell/bazel/bazel_sandboxing_test.sh b/src/test/shell/bazel/bazel_sandboxing_test.sh
index 5238c09f77..0f911f3f7d 100755
--- a/src/test/shell/bazel/bazel_sandboxing_test.sh
+++ b/src/test/shell/bazel/bazel_sandboxing_test.sh
@@ -222,7 +222,7 @@ function test_sandbox_cleanup() {
bazel build examples/genrule:tools_work &> $TEST_log \
|| fail "Hermetic genrule failed: examples/genrule:tools_work"
bazel shutdown &> $TEST_log || fail "bazel shutdown failed"
- ls -la "$(bazel info execution_root)/bazel-sandbox"
+ ls -la "$(bazel info output_base)/bazel-sandbox"
if [[ "$(ls -A "$(bazel info execution_root)"/bazel-sandbox)" ]]; then
fail "Build left files around afterwards"
fi
@@ -301,7 +301,7 @@ function test_sandbox_undeclared_deps_skylark_with_local_tag() {
function test_sandbox_block_filesystem() {
output_file="${BAZEL_GENFILES_DIR}/examples/genrule/breaks2.txt"
- bazel build examples/genrule:breaks2 &> $TEST_log \
+ bazel build --sandbox_block_path=/var/log examples/genrule:breaks2 &> $TEST_log \
&& fail "Non-hermetic genrule succeeded: examples/genrule:breaks2" || true
[ -f "$output_file" ] ||
@@ -311,7 +311,7 @@ function test_sandbox_block_filesystem() {
fail "Output contained more than one line: $output_file"
fi
- fgrep "No such file or directory" $output_file ||
+ fgrep "Permission denied" $output_file ||
fail "Output did not contain expected error message: $output_file"
}
@@ -379,62 +379,9 @@ EOF
kill_nc
}
-function test_sandbox_add_path_valid_path() {
- output_file="${BAZEL_GENFILES_DIR}/examples/genrule/breaks2.txt"
-
- bazel build --sandbox_add_path=/var/log examples/genrule:breaks2 &> $TEST_log \
- || fail "Non-hermetic genrule failed: examples/genrule:breaks2 (with additional path)"
-
- [ -f "$output_file" ] ||
- fail "Action did not produce output: $output_file"
-
- if [ $(wc -l < $output_file) -le 1 ]; then
- fail "Output contained less than or equal to one line: $output_file"
- fi
-}
-
-function test_sandbox_add_path_workspace_parent() {
- output_file="${BAZEL_GENFILES_DIR}/examples/genrule/check_sandbox_contain_WORKSPACE.txt"
- parent_path="$(dirname "$(pwd)")"
-
- bazel build --sandbox_add_path=$parent_path examples/genrule:check_sandbox_contain_WORKSPACE &> $TEST_log \
- || fail "Non-hermetic genrule succeeded: examples/genrule:works (with additional path)"
- [ -f "$output_file" ] \
- || fail "Genrule did not produce output: examples/genrule:check_sandbox_contain_WORKSPACE (with additional path: WORKSPACE/..)"
- cat $output_file &> $TEST_log
-
- # file and directory inside workspace (except project) should not be mounted
- egrep "\bWORKSPACE\b" $output_file \
- && fail "WORKSPACE file should not be mounted." || true
-}
-
-function test_sandbox_add_path_workspace_child() {
- child_path="$(pwd)/examples"
- output_file="${BAZEL_GENFILES_DIR}/examples/genrule/works.txt"
-
- bazel build --sandbox_add_path=$child_path examples/genrule:works &> $TEST_log \
- && fail "Non-hermetic genrule succeeded: examples/genrule:works (with additional path: WORKSPACE:/examples)" || true
-
- expect_log "Mounting subdirectory of WORKSPACE or OUTPUTBASE to sandbox is not allowed"
-}
-
-function test_sandbox_fail_command() {
- mkdir -p "javatests/orange"
- echo "java_test(name = 'Orange', srcs = ['Orange.java'])" > javatests/orange/BUILD
- cat > javatests/orange/Orange.java <<EOF
-package orange;
-import junit.framework.TestCase;
-public class Orange extends TestCase {
- public void testFails() { fail("juice"); }
-}
-EOF
- bazel test --sandbox_debug --verbose_failures //javatests/orange:Orange >& $TEST_log \
- && fail "Expected failure" || true
-
- expect_log "Sandboxed execution failed, which may be legitimate"
-}
-
-function test_sandbox_different_nobody_uid() {
+# TODO(philwo) - this doesn't work on Ubuntu 14.04 due to "unshare" being too
+# old and not understanding the --user flag.
+function DISABLED_test_sandbox_different_nobody_uid() {
cat /etc/passwd | sed 's/\(^nobody:[^:]*:\)[0-9]*:[0-9]*/\15000:16000/g' > \
"${TEST_TMPDIR}/passwd"
unshare --user --mount --map-root-user -- bash - \
diff --git a/src/test/shell/bazel/linux-sandbox_test.sh b/src/test/shell/bazel/linux-sandbox_test.sh
index a737578a29..7b533af5be 100755
--- a/src/test/shell/bazel/linux-sandbox_test.sh
+++ b/src/test/shell/bazel/linux-sandbox_test.sh
@@ -29,97 +29,87 @@ readonly OUT="${OUT_DIR}/outfile"
readonly ERR="${OUT_DIR}/errfile"
readonly SANDBOX_DIR="${OUT_DIR}/sandbox"
-SANDBOX_DEFAULT_OPTS="-S $SANDBOX_DIR"
-for dir in /bin* /lib* /usr/bin* /usr/lib*; do
- SANDBOX_DEFAULT_OPTS="$SANDBOX_DEFAULT_OPTS -M $dir"
-done
+SANDBOX_DEFAULT_OPTS="-W $SANDBOX_DIR"
function set_up {
rm -rf $OUT_DIR
mkdir -p $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() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/echo hi there || fail
- assert_output "hi there" ""
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /bin/echo hi there &> $TEST_log || fail
+ expect_log "hi there"
}
function test_default_user_is_nobody() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /usr/bin/id || fail
- assert_output "uid=65534 gid=65534 groups=65534" ""
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /usr/bin/id &> $TEST_log || fail
+ expect_log "uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)"
}
function test_user_switched_to_root() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -r -l $OUT -L $ERR -- /usr/bin/id || fail
- assert_contains "uid=0 gid=0" "$OUT"
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -R -- /usr/bin/id &> $TEST_log || fail
+ expect_log "uid=0(root) gid=0(root)"
}
function test_network_namespace() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -n -l $OUT -L $ERR -- /bin/ip link ls || fail
- assert_contains "LOOPBACK,UP" "$OUT"
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -N -- /bin/ip link ls &> $TEST_log || fail
+ expect_log "LOOPBACK,UP"
}
function test_ping_loopback() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -n -r -- \
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -N -R -- \
/bin/sh -c 'ping6 -c 1 ::1 || ping -c 1 127.0.0.1' &>$TEST_log || fail
expect_log "1 received"
}
-function test_to_stderr() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "/bin/echo hi there >&2" || fail
- assert_output "" "hi there"
-}
-
function test_exit_code() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "exit 71" || code=$?
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /bin/bash -c "exit 71" &> $TEST_log || code=$?
assert_equals 71 "$code"
}
function test_signal_death() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c 'kill -ABRT $$' || code=$?
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /bin/bash -c 'kill -ABRT $$' &> $TEST_log || code=$?
assert_equals 134 "$code" # SIGNAL_BASE + SIGABRT = 128 + 6
}
+# Tests that even when the child catches SIGTERM and exits with code 0, that the sandbox exits with
+# code 142 (telling us about the expired timeout).
function test_signal_catcher() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \
- 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$?
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -- /bin/bash -c \
+ 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' &> $TEST_log || code=$?
assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
- assert_stdout "later"
+ expect_log "^later$"
}
function test_basic_timeout() {
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 3 -t 3 -l $OUT -L $ERR -- /bin/bash -c "echo before; sleep 1000; echo after" && fail
- assert_output "before" ""
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 3 -t 3 -- /bin/bash -c "echo before; sleep 1000; echo after" &> $TEST_log && fail
+ expect_log "^before$" ""
}
function test_timeout_grace() {
- $linux_sandbox $SANDBOX_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=$?
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -- /bin/bash -c \
+ 'trap "echo -n before; sleep 1; echo -n after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' &> $TEST_log || code=$?
assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
- assert_stdout "beforeafter"
+ expect_log "^beforeafter$"
}
function test_timeout_kill() {
- $linux_sandbox $SANDBOX_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=$?
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -- /bin/bash -c \
+ 'trap "echo before; sleep 1000; echo after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' &> $TEST_log || code=$?
assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14
- assert_stdout "before"
+ expect_log "^before$"
}
function test_debug_logging() {
touch ${TEST_TMPDIR}/testfile
- $linux_sandbox $SANDBOX_DEFAULT_OPTS -D -M ${TEST_TMPDIR}/testfile -m /tmp/sandboxed_testfile -l $OUT -L $ERR -- /bin/true || code=$?
- assert_contains "mount: /usr/bin\$" "$ERR"
- assert_contains "mount: ${TEST_TMPDIR}/testfile -> <sandbox>/tmp/sandboxed_testfile\$" "$ERR"
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -D -- /bin/true &> $TEST_log || code=$?
+ expect_log "child exited normally with exitcode 0"
+}
+
+function test_redirect_output() {
+ $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "echo out; echo err >&2" &> $TEST_log || code=$?
+ assert_equals "out" "$(cat $OUT)"
+ assert_equals "err" "$(cat $ERR)"
}
# The test shouldn't fail if the environment doesn't support running it.