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..46b28e119ed7d1 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 + * 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. + *

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. + */ + 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 8a55c4057099c6..dce2cd5bd51b21 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,11 @@ 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 f0d710539ebe0a..6f1487b52fc7fa 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD +++ b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD @@ -18,6 +18,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib:runtime", "//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:file_metadata", "//src/main/java/com/google/devtools/build/lib/actions:execution_requirements", "//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity", "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories", 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..54e492d9b64dbb --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedSandboxedSpawn.java @@ -0,0 +1,100 @@ +// 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 javax.annotation.Nullable; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 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 @param source path, in @param 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..3e0b5b61bb787b 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,13 @@ 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 +228,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 071426fe8eb8e1..086e20b6a814b2 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,17 +42,19 @@ 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; import com.google.devtools.build.lib.vfs.Symlinks; + +import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; -import javax.annotation.Nullable; /** Spawn runner that uses linux sandboxing APIs to execute a local subprocess. */ final class LinuxSandboxedSpawnRunner extends AbstractSandboxSpawnRunner { @@ -229,6 +235,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, @@ -352,6 +371,42 @@ 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 { 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 75b937ba209f0a..16bdb8be646196 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 @@ -361,6 +361,19 @@ public ImmutableSet getInaccessiblePaths(FileSystem fs) { + " instead.") public boolean legacyLocalFallback; + @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..2181c687b50053 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,24 @@ 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 has trailing too many 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 +225,11 @@ 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 5e7c64a7ac7b34..4e6f4626dc3d1a 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); } @@ -323,12 +394,12 @@ static void MakeFilesystemMostlyReadOnly() { } 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, - nullptr) < 0) { - DIE("mount"); - } + // 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, + nullptr) < 0) { + DIE("mount"); + } } static void SetupNetworking() { @@ -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()); } } @@ -386,7 +462,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. @@ -443,6 +519,119 @@ 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"); @@ -461,11 +650,22 @@ 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 4cc47312c8b1e7..7bcef5e5bbbce3 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 @@ -573,7 +573,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 bde16516aa6bde..c23e962ebfd084 100644 --- a/src/test/shell/bazel/BUILD +++ b/src/test/shell/bazel/BUILD @@ -952,6 +952,19 @@ 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..e729c409c4afbe --- /dev/null +++ b/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh @@ -0,0 +1,184 @@ +#!/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_add_mount_pair=/etc/passwd +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"