aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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.