diff --git a/src/main/java/com/google/devtools/build/lib/actions/FileContentsProxy.java b/src/main/java/com/google/devtools/build/lib/actions/FileContentsProxy.java
index f8c9d275553753..08e15127cc41bd 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/FileContentsProxy.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/FileContentsProxy.java
@@ -20,27 +20,33 @@
/**
* In case we can't get a fast digest from the filesystem, we store this metadata as a proxy to the
- * file contents. Currently it is a pair of a relevant timestamp and a "node id". On Linux the
- * former is the ctime and the latter is the inode number. We might want to add the device number in
- * the future.
+ * file contents. Currently it is two timestamps and a "node id". On Linux we use both ctime and
+ * mtime and inode number. We might want to add the device number in the future.
*
- *
For a Linux example of why mtime alone is insufficient, note that 'mv' preserves timestamps.
- * So if files 'a' and 'b' initially have the same timestamp, then we would think 'b' is unchanged
- * after the user executes `mv a b` between two builds.
+ *
For a Linux example of why mtime alone is insufficient, note that 'mv' preserves mtime. So if
+ * files 'a' and 'b' initially have the same timestamp, then we would think 'b' is unchanged after
+ * the user executes `mv a b` between two builds.
+ *
+ *
On Linux we also need mtime for hardlinking sandbox, since updating the inode reference
+ * counter preserves mtime, but updates ctime. isModified() call can be used to compare two
+ * FileContentsProxys of hardlinked files.
*/
public final class FileContentsProxy {
private final long ctime;
+ private final long mtime;
private final long nodeId;
- private FileContentsProxy(long ctime, long nodeId) {
+ public FileContentsProxy(long ctime, long mtime, long nodeId) {
this.ctime = ctime;
+ this.mtime = mtime;
this.nodeId = nodeId;
}
public static FileContentsProxy create(FileStatus stat) throws IOException {
// Note: there are file systems that return mtime for this call instead of ctime, such as the
// WindowsFileSystem.
- return new FileContentsProxy(stat.getLastChangeTime(), stat.getNodeId());
+ return new FileContentsProxy(
+ stat.getLastChangeTime(), stat.getLastModifiedTime(), stat.getNodeId());
}
@Override
@@ -54,16 +60,31 @@ public boolean equals(Object other) {
}
FileContentsProxy that = (FileContentsProxy) other;
- return ctime == that.ctime && nodeId == that.nodeId;
+ return ctime == that.ctime && mtime == that.mtime && nodeId == that.nodeId;
+ }
+
+ /**
+ * Can be used when hardlink reference counter changes should not be considered a file
+ * modification. Is only comparing mtime and not ctime and is therefore not detecting changed
+ * metadata like permission.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public boolean isModified(FileContentsProxy other) {
+ if (other == this) {
+ return false;
+ }
+ // true if nodeId are different or inode has a new mtime
+ return nodeId != other.nodeId || mtime != other.mtime;
}
@Override
public int hashCode() {
- return Objects.hash(ctime, nodeId);
+ return Objects.hash(ctime, mtime, nodeId);
}
void addToFingerprint(Fingerprint fp) {
fp.addLong(ctime);
+ fp.addLong(mtime);
fp.addLong(nodeId);
}
@@ -73,6 +94,6 @@ public String toString() {
}
public String prettyPrint() {
- return String.format("ctime of %d and nodeId of %d", ctime, nodeId);
+ return String.format("ctime of %d and mtime of %d and nodeId of %d", ctime, mtime, nodeId);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
index 9f54a941473a45..7272343e79256b 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
@@ -131,6 +131,9 @@ private SpawnResult runSpawn(
try (SilentCloseable c = Profiler.instance().profile("subprocess.run")) {
result = run(originalSpawn, sandbox, context.getTimeout(), outErr);
}
+ try (SilentCloseable c = Profiler.instance().profile("sandbox.verifyPostCondition")) {
+ verifyPostCondition(originalSpawn, sandbox, context);
+ }
context.lockOutputFiles();
try (SilentCloseable c = Profiler.instance().profile("sandbox.copyOutputs")) {
@@ -148,6 +151,10 @@ private SpawnResult runSpawn(
}
}
}
+ /** Override this method if you need to run a post condition after the action has executed */
+ public void verifyPostCondition(
+ Spawn originalSpawn, SandboxedSpawn sandbox, SpawnExecutionContext context)
+ throws IOException, ForbiddenActionInputException {}
private String makeFailureMessage(Spawn originalSpawn, SandboxedSpawn sandbox) {
if (sandboxOptions.sandboxDebug) {
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
index 0dffde49aacbce..8701c2463f6bc2 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
@@ -19,6 +19,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/actions",
"//src/main/java/com/google/devtools/build/lib/actions:artifacts",
"//src/main/java/com/google/devtools/build/lib/actions:execution_requirements",
+ "//src/main/java/com/google/devtools/build/lib/actions:file_metadata",
"//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity",
"//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories",
"//src/main/java/com/google/devtools/build/lib/analysis:test/test_configuration",
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedSandboxedSpawn.java b/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedSandboxedSpawn.java
new file mode 100644
index 00000000000000..929f41646c5bdc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedSandboxedSpawn.java
@@ -0,0 +1,101 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.sandbox;
+
+import com.google.common.flogger.GoogleLogger;
+import com.google.devtools.build.lib.exec.TreeDeleter;
+import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
+import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * Creates an execRoot for a Spawn that contains input files as hardlinks to their original
+ * destination.
+ */
+public class HardlinkedSandboxedSpawn extends AbstractContainerizingSandboxedSpawn {
+ private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+ private boolean sandboxDebug = false;
+
+ public HardlinkedSandboxedSpawn(
+ Path sandboxPath,
+ Path sandboxExecRoot,
+ List arguments,
+ Map environment,
+ SandboxInputs inputs,
+ SandboxOutputs outputs,
+ Set writableDirs,
+ TreeDeleter treeDeleter,
+ @Nullable Path statisticsPath,
+ boolean sandboxDebug) {
+ super(
+ sandboxPath,
+ sandboxExecRoot,
+ arguments,
+ environment,
+ inputs,
+ outputs,
+ writableDirs,
+ treeDeleter,
+ statisticsPath);
+ this.sandboxDebug = sandboxDebug;
+ }
+
+ @Override
+ protected void copyFile(Path source, Path target) throws IOException {
+ hardLinkRecursive(source, target);
+ }
+
+ /**
+ * Recursively creates hardlinks for all files in {@code source} path, in {@code target} path.
+ * Symlinks are resolved. If files is located on another disk, hardlink will fail and a copy will
+ * be made instead. Throws IllegalArgumentException if source path is a subdirectory of target
+ * path.
+ */
+ private void hardLinkRecursive(Path source, Path target) throws IOException {
+ if (source.isSymbolicLink()) {
+ source = source.resolveSymbolicLinks();
+ }
+
+ if (source.isFile(Symlinks.NOFOLLOW)) {
+ try {
+ source.createHardLink(target);
+ } catch (IOException e) {
+ if (sandboxDebug) {
+ logger.atInfo().log(
+ "File %s could not be hardlinked, file will be copied instead.", source);
+ }
+ FileSystemUtils.copyFile(source, target);
+ }
+ } else if (source.isDirectory()) {
+ if (source.startsWith(target)) {
+ throw new IllegalArgumentException(source + " is a subdirectory of " + target);
+ }
+ target.createDirectory();
+ Collection entries = source.getDirectoryEntries();
+ for (Path entry : entries) {
+ Path toPath = target.getChild(entry.getBaseName());
+ hardLinkRecursive(entry, toPath);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java
index c3209290245c0a..551ff44252c61b 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java
@@ -57,7 +57,7 @@ public static CommandLineBuilder commandLineBuilder(
public static class CommandLineBuilder {
private final Path linuxSandboxPath;
private final List commandArguments;
-
+ private Path hermeticSandboxPath;
private Path workingDirectory;
private Duration timeout;
private Duration killDelay;
@@ -79,6 +79,15 @@ private CommandLineBuilder(Path linuxSandboxPath, List commandArguments)
this.commandArguments = commandArguments;
}
+ /**
+ * Sets the sandbox path to chroot to, required for the hermetic linux sandbox to figure out
+ * where the working directory is.
+ */
+ public CommandLineBuilder setHermeticSandboxPath(Path sandboxPath) {
+ this.hermeticSandboxPath = sandboxPath;
+ return this;
+ }
+
/** Sets the working directory to use, if any. */
public CommandLineBuilder setWorkingDirectory(Path workingDirectory) {
this.workingDirectory = workingDirectory;
@@ -221,6 +230,9 @@ public ImmutableList build() {
if (statisticsPath != null) {
commandLineBuilder.add("-S", statisticsPath.getPathString());
}
+ if (hermeticSandboxPath != null) {
+ commandLineBuilder.add("-h", hermeticSandboxPath.getPathString());
+ }
if (useFakeHostname) {
commandLineBuilder.add("-H");
}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index be23fb0d5eb89e..ffb7a5f3b7fd02 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -19,12 +19,16 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ExecutionRequirements;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
+import com.google.devtools.build.lib.actions.FileContentsProxy;
import com.google.devtools.build.lib.actions.ForbiddenActionInputException;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.Spawns;
import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.exec.TreeDeleter;
import com.google.devtools.build.lib.exec.local.LocalEnvProvider;
@@ -38,6 +42,7 @@
import com.google.devtools.build.lib.shell.Command;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -229,6 +234,19 @@ spawn, getSandboxOptions().defaultSandboxAllowNetwork)))
sandboxfsMapSymlinkTargets,
treeDeleter,
statisticsPath);
+ } else if (getSandboxOptions().useHermetic) {
+ commandLineBuilder.setHermeticSandboxPath(sandboxPath);
+ return new HardlinkedSandboxedSpawn(
+ sandboxPath,
+ sandboxExecRoot,
+ commandLineBuilder.build(),
+ environment,
+ inputs,
+ outputs,
+ writableDirs,
+ treeDeleter,
+ statisticsPath,
+ getSandboxOptions().sandboxDebug);
} else {
return new SymlinkedSandboxedSpawn(
sandboxPath,
@@ -356,6 +374,47 @@ private void validateBindMounts(SortedMap bindMounts) throws UserExe
}
}
+ @Override
+ public void verifyPostCondition(
+ Spawn originalSpawn, SandboxedSpawn sandbox, SpawnExecutionContext context)
+ throws IOException, ForbiddenActionInputException {
+ if (getSandboxOptions().useHermetic) {
+ checkForConcurrentModifications(context);
+ }
+ }
+
+ private void checkForConcurrentModifications(SpawnExecutionContext context)
+ throws IOException, ForbiddenActionInputException {
+ for (ActionInput input : (context.getInputMapping(PathFragment.EMPTY_FRAGMENT).values())) {
+ if (input instanceof VirtualActionInput) {
+ continue;
+ }
+
+ FileArtifactValue metadata = context.getMetadataProvider().getMetadata(input);
+ Path path = execRoot.getRelative(input.getExecPath());
+
+ try {
+ if (wasModifiedSinceDigest(metadata.getContentsProxy(), path)) {
+ throw new IOException("input dependency " + path + " was modified during execution.");
+ }
+ } catch (UnsupportedOperationException e) {
+ throw new IOException(
+ "input dependency "
+ + path
+ + " could not be checked for modifications during execution.",
+ e);
+ }
+ }
+ }
+
+ private boolean wasModifiedSinceDigest(FileContentsProxy proxy, Path path) throws IOException {
+ if (proxy == null) {
+ return false;
+ }
+ FileStatus stat = path.statIfFound(Symlinks.FOLLOW);
+ return stat == null || !stat.isFile() || proxy.isModified(FileContentsProxy.create(stat));
+ }
+
@Override
public void cleanupSandboxBase(Path sandboxBase, TreeDeleter treeDeleter) throws IOException {
// Delete the inaccessible files synchronously, bypassing the treeDeleter. They are only a
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 40e20fb129bdd7..35b4213a87af0c 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
@@ -368,6 +368,19 @@ public ImmutableSet getInaccessiblePaths(FileSystem fs) {
+ " avoid unnecessary setup costs.")
public boolean reuseSandboxDirectories;
+ @Option(
+ name = "experimental_use_hermetic_linux_sandbox",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
+ effectTags = {OptionEffectTag.EXECUTION},
+ help =
+ "If set to true, do not mount root, only mount whats provided with "
+ + "sandbox_add_mount_pair. Input files will be hardlinked to the sandbox instead of "
+ + "symlinked to from the sandbox. "
+ + "If action input files are located on a filesystem different from the sandbox, "
+ + "then the input files will be copied instead.")
+ public boolean useHermetic;
+
/** Converter for the number of threads used for asynchronous tree deletion. */
public static final class AsyncTreeDeletesConverter extends ResourceConverter {
public AsyncTreeDeletesConverter() {
diff --git a/src/main/tools/linux-sandbox-options.cc b/src/main/tools/linux-sandbox-options.cc
index 088f7947c3e1c9..a9a7ac552dc8b1 100644
--- a/src/main/tools/linux-sandbox-options.cc
+++ b/src/main/tools/linux-sandbox-options.cc
@@ -73,6 +73,9 @@ static void Usage(char *program_name, const char *fmt, ...) {
" -R if set, make the uid/gid be root\n"
" -U if set, make the uid/gid be nobody\n"
" -D if set, debug info will be printed\n"
+ " -h if set, chroot to sandbox-dir and only "
+ " mount whats been specified with -M/-m for improved hermeticity. "
+ " The working-dir should be a folder inside the sandbox-dir\n"
" @FILE read newline-separated arguments from FILE\n"
" -- command to run inside sandbox, followed by arguments\n");
exit(EXIT_FAILURE);
@@ -94,7 +97,7 @@ static void ParseCommandLine(unique_ptr> args) {
bool source_specified = false;
while ((c = getopt(args->size(), args->data(),
- ":W:T:t:il:L:w:e:M:m:S:HNRUD")) != -1) {
+ ":W:T:t:il:L:w:e:M:m:S:h:HNRUD")) != -1) {
if (c != 'M' && c != 'm') source_specified = false;
switch (c) {
case 'W':
@@ -170,6 +173,26 @@ static void ParseCommandLine(unique_ptr> args) {
"Cannot write stats to more than one destination.");
}
break;
+ case 'h':
+ opt.hermetic = true;
+ if (opt.sandbox_root.empty()) {
+ std::string sandbox_root(optarg);
+ // Make sure that the sandbox_root path has no trailing slash.
+ if (sandbox_root.back() == '/') {
+ ValidateIsAbsolutePath(optarg, args->front(), static_cast(c));
+ opt.sandbox_root.assign(sandbox_root, 0, sandbox_root.length() - 1);
+ if (opt.sandbox_root.back() == '/') {
+ Usage(args->front(),
+ "Sandbox root path should not have trailing slashes");
+ }
+ } else {
+ opt.sandbox_root.assign(sandbox_root);
+ }
+ } else {
+ Usage(args->front(),
+ "Multiple sandbox roots (-s) specified, expected one.");
+ }
+ break;
case 'H':
opt.fake_hostname = true;
break;
@@ -204,6 +227,13 @@ static void ParseCommandLine(unique_ptr> args) {
}
}
+ if (!opt.working_dir.empty() && !opt.sandbox_root.empty() &&
+ opt.working_dir.find(opt.sandbox_root) == std::string::npos) {
+ Usage(args->front(),
+ "working-dir %s (-W) should be a "
+ "subdirectory of sandbox-dir %s (-h)",
+ opt.working_dir.c_str(), opt.sandbox_root.c_str());
+ }
if (optind < static_cast(args->size())) {
if (opt.args.empty()) {
opt.args.assign(args->begin() + optind, args->end());
diff --git a/src/main/tools/linux-sandbox-options.h b/src/main/tools/linux-sandbox-options.h
index d3b77d45b5384c..2843e9f8e3aa90 100644
--- a/src/main/tools/linux-sandbox-options.h
+++ b/src/main/tools/linux-sandbox-options.h
@@ -54,6 +54,10 @@ struct Options {
bool fake_username;
// Print debugging messages (-D)
bool debug;
+ // Improved hermetic build using whitelisting strategy (-h)
+ bool hermetic;
+ // The sandbox root directory (-s)
+ std::string sandbox_root;
// Command to run (--)
std::vector args;
};
diff --git a/src/main/tools/linux-sandbox-pid1.cc b/src/main/tools/linux-sandbox-pid1.cc
index 33c3543ce50cbb..b6f4f3d665a758 100644
--- a/src/main/tools/linux-sandbox-pid1.cc
+++ b/src/main/tools/linux-sandbox-pid1.cc
@@ -67,8 +67,100 @@
#include "src/main/tools/logging.h"
#include "src/main/tools/process-tools.h"
+static void WriteFile(const std::string &filename, const char *fmt, ...) {
+ FILE *stream = fopen(filename.c_str(), "w");
+ if (stream == nullptr) {
+ DIE("fopen(%s)", filename.c_str());
+ }
+
+ 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.c_str());
+ }
+}
+
static int global_child_pid;
+// Helper methods
+static void CreateFile(const char *path) {
+ int handle = open(path, O_CREAT | O_WRONLY | O_EXCL, 0666);
+ if (handle < 0) {
+ DIE("open");
+ }
+ if (close(handle) < 0) {
+ DIE("close");
+ }
+}
+
+// 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) {
+ if (link("tmp/empty_file", path) < 0) {
+ DIE("link %s", path);
+ }
+}
+
+// Recursively creates the file or directory specified in "path" and its parent
+// directories.
+// Return -1 on failure and sets errno to:
+// EINVAL path is null
+// ENOTDIR path exists and is not a directory
+// EEXIST path exists and is a directory
+// ENOENT stat call with the path failed
+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.
+ if (CreateTarget(dirname(strdupa(path)), true) < 0) {
+ DIE("CreateTarget %s", dirname(strdupa(path)));
+ }
+
+ if (is_directory) {
+ if (mkdir(path, 0755) < 0) {
+ DIE("mkdir");
+ }
+ } else {
+ LinkFile(path);
+ }
+
+ return 0;
+}
+
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
@@ -107,26 +199,6 @@ static void SetupMountNamespace() {
}
}
-static void WriteFile(const std::string &filename, const char *fmt, ...) {
- FILE *stream = fopen(filename.c_str(), "w");
- if (stream == nullptr) {
- DIE("fopen(%s)", filename.c_str());
- }
-
- 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.c_str());
- }
-}
-
static void SetupUserNamespace() {
// Disable needs for CAP_SETGID.
struct stat sb;
@@ -159,7 +231,6 @@ static void SetupUserNamespace() {
inner_uid = global_outer_uid;
inner_gid = global_outer_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);
}
@@ -361,9 +432,14 @@ static void SetupNetworking() {
}
}
-static void EnterSandbox() {
- if (chdir(opt.working_dir.c_str()) < 0) {
- DIE("chdir(%s)", opt.working_dir.c_str());
+static void EnterWorkingDirectory() {
+ std::string path = opt.working_dir;
+ if (opt.hermetic) {
+ path = path.substr(opt.sandbox_root.size() + 1);
+ }
+
+ if (chdir(path.c_str()) < 0) {
+ DIE("chdir(%s)", path.c_str());
}
}
@@ -385,7 +461,7 @@ static void SpawnChild() {
// Try to assign our terminal to the child process.
if (tcsetpgrp(STDIN_FILENO, getpgrp()) < 0 && errno != ENOTTY) {
- DIE("tcsetpgrp")
+ DIE("tcsetpgrp");
}
// Unblock all signals, restore default handlers.
@@ -442,6 +518,125 @@ static int WaitForChild() {
}
}
+static void MountSandboxAndGoThere() {
+ if (mount(opt.sandbox_root.c_str(), opt.sandbox_root.c_str(), nullptr,
+ MS_BIND | MS_NOSUID, nullptr) < 0) {
+ DIE("mount");
+ }
+ if (chdir(opt.sandbox_root.c_str()) < 0) {
+ DIE("chdir(%s)", opt.sandbox_root.c_str());
+ }
+}
+
+static void CreateEmptyFile() {
+ // This is used as the base for bind mounting.
+ if (CreateTarget("tmp", true) < 0) {
+ DIE("CreateTarget tmp")
+ }
+ CreateFile("tmp/empty_file");
+}
+
+static void MountDev() {
+ if (CreateTarget("dev", true) < 0) {
+ DIE("CreateTarget /dev");
+ }
+ const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero",
+ NULL};
+ for (int i = 0; devs[i] != NULL; i++) {
+ LinkFile(devs[i] + 1);
+ if (mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL) < 0) {
+ DIE("mount");
+ }
+ }
+ if (symlink("/proc/self/fd", "dev/fd") < 0) {
+ DIE("symlink");
+ }
+}
+
+static void MountAllMounts() {
+ for (const std::string &tmpfs_dir : opt.tmpfs_dirs) {
+ PRINT_DEBUG("tmpfs: %s", tmpfs_dir.c_str());
+ if (mount("tmpfs", tmpfs_dir.c_str(), "tmpfs",
+ MS_NOSUID | MS_NODEV | MS_NOATIME, nullptr) < 0) {
+ DIE("mount(tmpfs, %s, tmpfs, MS_NOSUID | MS_NODEV | MS_NOATIME, nullptr)",
+ tmpfs_dir.c_str());
+ }
+ }
+
+ // 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.c_str(), opt.working_dir.c_str(), nullptr, MS_BIND,
+ nullptr) < 0) {
+ DIE("mount(%s, %s, nullptr, MS_BIND, nullptr)", opt.working_dir.c_str(),
+ opt.working_dir.c_str());
+ }
+ for (int i = 0; i < (signed)opt.bind_mount_sources.size(); i++) {
+ if (opt.debug) {
+ if (strcmp(opt.bind_mount_sources[i].c_str(),
+ opt.bind_mount_targets[i].c_str()) == 0) {
+ // The file is mounted to the same path inside the sandbox, as outside
+ // (e.g. /home/user -> /home/user), so we'll just show a
+ // simplified version of the mount command.
+ PRINT_DEBUG("mount: %s\n", opt.bind_mount_sources[i].c_str());
+ } else {
+ // The file is mounted to a custom location inside the sandbox.
+ // Create a user-friendly string for the sandboxed path and show it.
+ const std::string user_friendly_mount_target("" +
+ opt.bind_mount_targets[i]);
+ PRINT_DEBUG("mount: %s -> %s\n", opt.bind_mount_sources[i].c_str(),
+ user_friendly_mount_target.c_str());
+ }
+ }
+ const std::string full_sandbox_path(opt.sandbox_root +
+ opt.bind_mount_targets[i]);
+
+ struct stat sb;
+ if (stat(opt.bind_mount_sources[i].c_str(), &sb) < 0) {
+ DIE("stat");
+ }
+ bool IsDirectory = S_ISDIR(sb.st_mode);
+ if (CreateTarget(full_sandbox_path.c_str(), IsDirectory) < 0) {
+ DIE("CreateTarget %s", full_sandbox_path.c_str());
+ }
+ int result =
+ mount(opt.bind_mount_sources[i].c_str(), full_sandbox_path.c_str(),
+ NULL, MS_REC | MS_BIND | MS_RDONLY, NULL);
+ if (result != 0) {
+ DIE("mount");
+ }
+ }
+ for (const std::string &writable_file : opt.writable_files) {
+ PRINT_DEBUG("writable: %s", writable_file.c_str());
+ if (mount(writable_file.c_str(), writable_file.c_str(), nullptr,
+ MS_BIND | MS_REC, nullptr) < 0) {
+ DIE("mount(%s, %s, nullptr, MS_BIND | MS_REC, nullptr)",
+ writable_file.c_str(), writable_file.c_str());
+ }
+ }
+}
+
+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()
+ if (syscall(SYS_pivot_root, ".", old_root) < 0) {
+ DIE("syscall");
+ }
+ if (chroot(".") < 0) {
+ DIE("chroot");
+ }
+ if (umount2(old_root, MNT_DETACH) < 0) {
+ DIE("umount2");
+ }
+ if (rmdir(old_root) < 0) {
+ DIE("rmdir");
+ }
+}
+
int Pid1Main(void *sync_pipe_param) {
PRINT_DEBUG("Pid1Main started");
@@ -460,11 +655,21 @@ int Pid1Main(void *sync_pipe_param) {
if (opt.fake_hostname) {
SetupUtsNamespace();
}
- MountFilesystems();
- MakeFilesystemMostlyReadOnly();
- MountProc();
+
+ if (opt.hermetic) {
+ MountSandboxAndGoThere();
+ CreateEmptyFile();
+ MountDev();
+ MountProc();
+ MountAllMounts();
+ ChangeRoot();
+ } else {
+ MountFilesystems();
+ MakeFilesystemMostlyReadOnly();
+ MountProc();
+ }
SetupNetworking();
- EnterSandbox();
+ EnterWorkingDirectory();
// Ignore terminal signals; we hand off the terminal to the child in
// SpawnChild below.
diff --git a/src/test/java/com/google/devtools/build/lib/actions/FileContentsProxyTest.java b/src/test/java/com/google/devtools/build/lib/actions/FileContentsProxyTest.java
index a405fe79dc563b..f00179f2574698 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/FileContentsProxyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/FileContentsProxyTest.java
@@ -42,8 +42,8 @@ private static final class InjectedStat implements FileStatus {
InjectedStat(long ctime, long nodeId) {
this.ctime = ctime;
+ this.mtime = ctime;
this.nodeId = nodeId;
- this.mtime = 0;
this.size = 0;
}
@@ -108,6 +108,6 @@ public void fingerprint() throws Exception {
Fingerprint fingerprint = new Fingerprint();
p1.addToFingerprint(fingerprint);
assertThat(fingerprint.digestAndReset())
- .isEqualTo(new Fingerprint().addLong(2L).addLong(4L).digestAndReset());
+ .isEqualTo(new Fingerprint().addLong(2L).addLong(1L).addLong(4L).digestAndReset());
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
index 83fac54af163c9..21047ad1222f44 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
@@ -576,7 +576,7 @@ public void testUnreadableFileWithNoFastDigest() throws Exception {
assertThat(value.getDigest()).isNull();
p.setLastModifiedTime(10L);
- assertThat(valueForPath(p)).isEqualTo(value);
+ assertThat(valueForPath(p)).isNotEqualTo(value);
p.setLastModifiedTime(0L);
assertThat(valueForPath(p)).isEqualTo(value);
diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD
index 5fd379339cc9f7..63f7b636c6396d 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -952,6 +952,20 @@ sh_test(
],
)
+sh_test(
+ name = "bazel_hermetic_sandboxing_test",
+ size = "small",
+ srcs = ["bazel_hermetic_sandboxing_test.sh"],
+ data = [
+ ":test-deps",
+ "//src/test/shell:sandboxing_test_utils.sh",
+ ],
+ tags = [
+ "no-sandbox",
+ "no_windows",
+ ],
+)
+
sh_test(
name = "bazel_sandboxing_cpp_test",
srcs = ["bazel_sandboxing_cpp_test.sh"],
diff --git a/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh b/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh
new file mode 100755
index 00000000000000..a67f4be7421ebb
--- /dev/null
+++ b/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh
@@ -0,0 +1,183 @@
+#!/bin/bash
+#
+# 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.
+#
+# Test hermetic Linux sandbox
+#
+
+
+# Load test environment
+# Load the test setup defined in the parent directory
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${CURRENT_DIR}/../integration_test_setup.sh" \
+ || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+source ${CURRENT_DIR}/../sandboxing_test_utils.sh \
+ || { echo "sandboxing_test_utils.sh not found!" >&2; exit 1; }
+
+cat >>$TEST_TMPDIR/bazelrc <<'EOF'
+# Testing the sandboxed strategy requires using the sandboxed strategy. While it is the default,
+# we want to make sure that this explicitly fails when the strategy is not available on the system
+# running the test.
+# The hermetic sandbox requires the Linux sandbox.
+build --spawn_strategy=sandboxed
+build --experimental_use_hermetic_linux_sandbox
+build --sandbox_fake_username
+EOF
+
+# For the test to work we need to bind mount a couple of folders to
+# get access to bash, ls, python etc. Depending on linux distribution
+# these folders may vary. Mount all folders in the root directory '/'
+# except the project directory, the directory containing the bazel
+# workspace under test.
+project_folder=`pwd | cut -d"/" -f 2`
+for folder in /*/
+do
+ if [ -d "$folder" ] && [ "$folder" != "/$project_folder/" ]
+ then
+ if [[ -L $folder ]]
+ then
+ # Get resolved link
+ linked_folder=`readlink -f $folder`
+ echo "build --sandbox_add_mount_pair=/$linked_folder:$folder" >> $TEST_TMPDIR/bazelrc
+ else
+ echo "build --sandbox_add_mount_pair=$folder" >> $TEST_TMPDIR/bazelrc
+ fi
+ fi
+done
+
+function set_up {
+ export BAZEL_GENFILES_DIR=$(bazel info bazel-genfiles 2>/dev/null)
+ export BAZEL_BIN_DIR=$(bazel info bazel-bin 2>/dev/null)
+
+ sed -i.bak '/sandbox_tmpfs_path/d' $TEST_TMPDIR/bazelrc
+
+ mkdir -p examples/hermetic
+
+ cat << 'EOF' > examples/hermetic/unknown_file.txt
+text inside this file
+EOF
+
+ ABSOLUTE_PATH=$CURRENT_DIR/workspace/examples/hermetic/unknown_file.txt
+
+ # In this case the ABSOLUTE_PATH will be expanded
+ # and the absolute path will be written to script_absolute_path.sh
+ cat << EOF > examples/hermetic/script_absolute_path.sh
+#! /bin/sh
+ls ${ABSOLUTE_PATH}
+EOF
+
+ chmod 777 examples/hermetic/script_absolute_path.sh
+
+ cat << 'EOF' > examples/hermetic/script_symbolic_link.sh
+#! /bin/sh
+OUTSIDE_SANDBOX_DIR=$(dirname $(realpath $0))
+cat $OUTSIDE_SANDBOX_DIR/unknown_file.txt
+EOF
+
+ chmod 777 examples/hermetic/script_symbolic_link.sh
+
+ touch examples/hermetic/import_module.py
+
+ cat << 'EOF' > examples/hermetic/py_module_test.py
+import import_module
+EOF
+
+ cat << 'EOF' > examples/hermetic/BUILD
+genrule(
+ name = "absolute_path",
+ srcs = ["script_absolute_path.sh"], # unknown_file.txt not referenced.
+ outs = [ "absolute_path.txt" ],
+ cmd = "./$(location :script_absolute_path.sh) > $@",
+)
+
+genrule(
+ name = "symbolic_link",
+ srcs = ["script_symbolic_link.sh"], # unknown_file.txt not referenced.
+ outs = ["symbolic_link.txt"],
+ cmd = "./$(location :script_symbolic_link.sh) > $@",
+)
+
+py_test(
+ name = "py_module_test",
+ srcs = ["py_module_test.py"], # import_module.py not referenced.
+ size = "small",
+)
+
+genrule(
+ name = "input_file",
+ outs = ["input_file.txt"],
+ cmd = "echo original text input > $@",
+)
+
+genrule(
+ name = "write_input_test",
+ srcs = [":input_file"],
+ outs = ["status.txt"],
+ cmd = "(chmod 777 $(location :input_file) && \
+ (echo overwrite text > $(location :input_file)) && \
+ (echo success > $@)) || (echo fail > $@)",
+)
+EOF
+}
+
+# Test that the build can't escape the sandbox via absolute path.
+function test_absolute_path() {
+ bazel build examples/hermetic:absolute_path &> $TEST_log \
+ && fail "Fail due to non hermetic sandbox: examples/hermetic:absolute_path" || true
+ expect_log "ls:.* '\?.*/examples/hermetic/unknown_file.txt'\?: No such file or directory"
+}
+
+# Test that the build can't escape the sandbox by resolving symbolic link.
+function test_symbolic_link() {
+ [ "$PLATFORM" != "darwin" ] || return 0
+
+ bazel build examples/hermetic:symbolic_link &> $TEST_log \
+ && fail "Fail due to non hermetic sandbox: examples/hermetic:symbolic_link" || true
+ expect_log "cat: \/execroot\/main\/examples\/hermetic\/unknown_file.txt: No such file or directory"
+}
+
+# Test that the sandbox discover if the bazel python rule miss dependencies.
+function test_missing_python_deps() {
+ [ "$PLATFORM" != "darwin" ] || return 0
+
+ bazel test examples/hermetic:py_module_test --test_output=all &> $TEST_TMPDIR/log \
+ && fail "Fail due to non hermetic sandbox: examples/hermetic:py_module_test" || true
+
+ expect_log "No module named '\?import_module'\?"
+}
+
+# Test that the intermediate corrupt input file gets re:evaluated
+function test_writing_input_file() {
+ [ "$PLATFORM" != "darwin" ] || return 0
+ # Write an input file, this should cause the hermetic sandbox to fail with an exception
+ bazel build examples/hermetic:write_input_test &> $TEST_log \
+ && fail "Fail due to non hermetic sandbox: examples/hermetic:write_input_test" || true
+ expect_log "input dependency .*examples/hermetic/input_file.txt was modified during execution."
+ cat "${BAZEL_GENFILES_DIR}/examples/hermetic/input_file.txt" &> $TEST_log
+ expect_log "overwrite text"
+
+ # Build the input file again, this should not use the cache, but instead re:evaluate the file
+ bazel build examples/hermetic:input_file &> $TEST_log \
+ || fail "Fail due to non hermetic sandbox: examples/hermetic:input_file"
+ [ -f "${BAZEL_GENFILES_DIR}/examples/hermetic/input_file.txt" ] \
+ || fail "Genrule did not produce output: examples/hermetic:input_file"
+ cat "${BAZEL_GENFILES_DIR}/examples/hermetic/input_file.txt" &> $TEST_log
+ expect_log "original text input"
+}
+
+# The test shouldn't fail if the environment doesn't support running it.
+check_sandbox_allowed || exit 0
+
+run_suite "hermetic_sandbox"