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 f56bbf1e9f6a78..facf851ca15b08 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 @@ -21,31 +21,38 @@ /** * 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 ctime alone is insufficient, and we add node id, + * 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. + * + *

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 implements Serializable { private final long ctime; + private final long mtime; private final long nodeId; /** * Visible for serialization / deserialization. Do not use this method, but call {@link #create} * instead. */ - public 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()); } /** Visible for serialization / deserialization. Do not use this method; use {@link #equals}. */ @@ -53,6 +60,11 @@ public long getCTime() { return ctime; } + /** Visible for serialization / deserialization. Do not use this method; use {@link #equals}. */ + public long getMTime() { + return mtime; + } + /** Visible for serialization / deserialization. Do not use this method; use {@link #equals}. */ public long getNodeId() { return nodeId; @@ -72,6 +84,20 @@ public boolean equals(Object other) { return ctime == that.ctime && 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); 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 3d14c227e912bc..199ec2e635846c 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 @@ -123,6 +123,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")) { @@ -140,6 +143,12 @@ 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 { + return; + } 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..e184079a036320 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/sandbox/HardlinkedSandboxedSpawn.java @@ -0,0 +1,95 @@ +// 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 sandbox_debug = false; + public HardlinkedSandboxedSpawn( + Path sandboxPath, + Path sandboxExecRoot, + List arguments, + Map environment, + SandboxInputs inputs, + SandboxOutputs outputs, + Set writableDirs, + TreeDeleter treeDeleter, + @Nullable Path statisticsPath, + boolean sandbox_debug) { + + super( + sandboxPath, + sandboxExecRoot, + arguments, + environment, + inputs, + outputs, + writableDirs, + treeDeleter, + statisticsPath); + this.sandbox_debug = sandbox_debug; + } + + @Override + protected void copyFile(Path source, Path target) throws IOException { + hardLinkRecursive(source, target); + } + + 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 (sandbox_debug) { + logger.atInfo().log("File " + source + " could not be hardlinked, file will be copied instead."); + } + 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); + } + } + } +} \ No newline at end of file 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 473232b869dd62..cfb350941fe247 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 @@ -56,7 +56,7 @@ public static CommandLineBuilder commandLineBuilder( public static class CommandLineBuilder { private final Path linuxSandboxPath; private final List commandArguments; - + private Path sandboxPath; private Path workingDirectory; private Duration timeout; private Duration killDelay; @@ -71,6 +71,7 @@ public static class CommandLineBuilder { private boolean useFakeRoot = false; private boolean useFakeUsername = false; private boolean useDebugMode = false; + private boolean useHermetic = false; private boolean sigintSendsSigterm = false; private CommandLineBuilder(Path linuxSandboxPath, List commandArguments) { @@ -78,6 +79,13 @@ private CommandLineBuilder(Path linuxSandboxPath, List commandArguments) this.commandArguments = commandArguments; } + /** Sets the sandbox path, required for the hermetic linux sandbox to figure out + where the working directory is. */ + public CommandLineBuilder setSandboxPath(Path sandboxPath) { + this.sandboxPath = sandboxPath; + return this; + } + /** Sets the working directory to use, if any. */ public CommandLineBuilder setWorkingDirectory(Path workingDirectory) { this.workingDirectory = workingDirectory; @@ -169,6 +177,12 @@ public CommandLineBuilder setUseDebugMode(boolean useDebugMode) { return this; } + /** Sets whether to use the hermetic version of the linux-sandbox. */ + public CommandLineBuilder setUseHermetic(boolean useHermetic) { + this.useHermetic = useHermetic; + return this; + } + /** Incorporates settings from a spawn's execution info. */ public CommandLineBuilder addExecutionInfo(Map executionInfo) { if (executionInfo.containsKey(ExecutionRequirements.GRACEFUL_TERMINATION)) { @@ -220,6 +234,9 @@ public ImmutableList build() { if (statisticsPath != null) { commandLineBuilder.add("-S", statisticsPath.getPathString()); } + if (sandboxPath != null) { + commandLineBuilder.add("-s", sandboxPath.getPathString()); + } if (useFakeHostname) { commandLineBuilder.add("-H"); } @@ -238,6 +255,9 @@ public ImmutableList build() { if (sigintSendsSigterm) { commandLineBuilder.add("-i"); } + if (useHermetic) { + commandLineBuilder.add("-h"); + } commandLineBuilder.add("--"); commandLineBuilder.addAll(commandArguments); 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 96158eda8f8276..15fe6fc213af13 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,11 +19,8 @@ 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.ExecException; -import com.google.devtools.build.lib.actions.ExecutionRequirements; -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.*; +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; @@ -37,17 +34,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 { @@ -228,6 +227,20 @@ spawn, getSandboxOptions().defaultSandboxAllowNetwork))) sandboxfsMapSymlinkTargets, treeDeleter, statisticsPath); + } else if (getSandboxOptions().useHermetic) { + commandLineBuilder.setUseHermetic(true) + .setSandboxPath(sandboxPath); + return new HardlinkedSandboxedSpawn( + sandboxPath, + sandboxExecRoot, + commandLineBuilder.build(), + environment, + inputs, + outputs, + writableDirs, + treeDeleter, + statisticsPath, + getSandboxOptions().sandboxDebug); } else { return new SymlinkedSandboxedSpawn( sandboxPath, @@ -359,6 +372,40 @@ private void validateBindMounts(SortedMap bindMounts) throws UserExe } } } + @Override + public void verifyPostCondition( + Spawn originalSpawn, SandboxedSpawn sandbox, SpawnExecutionContext context) throws IOException { + if(getSandboxOptions().useHermetic){ + checkForConcurrentModifications(context); + } + } + + private void checkForConcurrentModifications(SpawnExecutionContext context) throws IOException { + for (ActionInput input : context.getInputMapping().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."); + } + } + } + + 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 57d78dcc309ef7..29a64e0ee55cee 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 @@ -363,6 +363,19 @@ public ImmutableSet getInaccessiblePaths(FileSystem fs) { + " use --strategy or --spawn_strategy to configure fallbacks 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..f57ecd622f0eb3 100644 --- a/src/main/tools/linux-sandbox-options.cc +++ b/src/main/tools/linux-sandbox-options.cc @@ -73,6 +73,8 @@ 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, use whitelisting for improved hermeticity\n" + " -s The sandbox root directory\n" " @FILE read newline-separated arguments from FILE\n" " -- command to run inside sandbox, followed by arguments\n"); exit(EXIT_FAILURE); @@ -94,7 +96,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:s:HNRUDh")) != -1) { if (c != 'M' && c != 'm') source_specified = false; switch (c) { case 'W': @@ -170,6 +172,19 @@ static void ParseCommandLine(unique_ptr> args) { "Cannot write stats to more than one destination."); } break; + case 's': + 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() == '/') { + opt.sandbox_root.assign(sandbox_root, 0, sandbox_root.length() - 1); + } 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; @@ -195,6 +210,9 @@ static void ParseCommandLine(unique_ptr> args) { case 'D': opt.debug = true; break; + case 'h': + opt.hermetic = true; + break; case '?': Usage(args->front(), "Unrecognized argument: -%c (%d)", optopt, optind); break; 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 5c9c53cb9cd2aa..dbfb7e47c29594 100644 --- a/src/main/tools/linux-sandbox-pid1.cc +++ b/src/main/tools/linux-sandbox-pid1.cc @@ -56,6 +56,82 @@ static int global_child_pid; +// Helper methods + +static bool IsDirectory(const std::string path) { + struct stat sb; + if (stat(path.c_str(), &sb) < 0) { + return false; + } + return S_ISDIR(sb.st_mode); +} + +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)) { + 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"); + } +} + +// 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. + if (CreateTarget(dirname(strdupa(path)), true) < 0) { + DIE("CreateTarget"); + } + + 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 @@ -310,11 +386,21 @@ 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"); + if(!opt.hermetic){ + // 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"); + } + } else { + // Create a bind mount to /proc + if (CreateTarget("proc", true) < 0) { + DIE("CreateTarget"); + } + if (mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL) < 0) { + DIE("mount"); + } } } @@ -349,8 +435,13 @@ static void SetupNetworking() { } static void EnterSandbox() { - if (chdir(opt.working_dir.c_str()) < 0) { - DIE("chdir(%s)", opt.working_dir.c_str()); + 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()); } } @@ -430,6 +521,112 @@ 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. + CreateTarget("tmp", true); + 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]); + + if (CreateTarget(full_sandbox_path.c_str(), IsDirectory(opt.bind_mount_sources[i])) < 0) { + DIE("CreateTarget"); + } + 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"); @@ -448,9 +645,20 @@ 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(); diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD index 5a490331ba3c27..8e18626ec5c8a4 100644 --- a/src/test/shell/bazel/BUILD +++ b/src/test/shell/bazel/BUILD @@ -931,6 +931,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..fdf64fd94c7f27 --- /dev/null +++ b/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh @@ -0,0 +1,129 @@ +#!/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=/usr:/usr +build --sandbox_add_mount_pair=/usr/lib64:/lib64 +build --sandbox_add_mount_pair=/usr/bin:/bin +build --sandbox_add_mount_pair=/etc +build --sandbox_fake_username +EOF + + +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=$TEST_TMPDIR/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", +) + +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: cannot access .*/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() { + 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() { + 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'" +} + +# The test shouldn't fail if the environment doesn't support running it. +check_sandbox_allowed || exit 0 + +run_suite "hermetic_sandbox"