blob: a69ed2f387c892904ce8b4d23d059829b8f3ea95 [file] [log] [blame]
// Copyright 2018 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.sandbox;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Joiner;
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.PathFragment;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Creates an execRoot for a Spawn that contains all required input files by mounting a sandboxfs
* FUSE filesystem on the provided path.
*/
class SandboxfsSandboxedSpawn implements SandboxedSpawn {
private static final Logger log = Logger.getLogger(SandboxfsSandboxedSpawn.class.getName());
/** Sequence number to assign a unique subtree to each action within the mount point. */
private static final AtomicInteger lastId = new AtomicInteger();
/** The sandboxfs instance to use for this spawn. */
private final SandboxfsProcess process;
/** Arguments to pass to the spawn, including the binary name. */
private final List<String> arguments;
/** Environment variables to pass to the spawn. */
private final Map<String, String> environment;
/** Collection of input files to be made available to the spawn in read-only mode. */
private final SandboxInputs inputs;
/** Collection of output files to expect from the spawn. */
private final SandboxOutputs outputs;
/** Collection of directories where the spawn can write files to relative to {@link #execRoot}. */
private final Set<PathFragment> writableDirs;
/** Map the targets of symlinks within the sandbox if true. */
private final boolean mapSymlinkTargets;
/** Scheduler for tree deletions. */
private final TreeDeleter treeDeleter;
/**
* Writable directory where the spawn runner keeps control files and the execroot outside of the
* sandboxfs instance.
*/
private final Path sandboxPath;
/**
* Writable directory to support the writes performed by the command. This acts as the target
* of all writable mappings in the sandboxfs instance.
*/
private final Path sandboxScratchDir;
/** Path to the working directory of the command. */
private final Path execRoot;
/**
* Name of the sandbox within the sandboxfs mount point, which is just the basename of the
* top-level directory where all execroot paths start.
*/
private final String sandboxName;
/** Path to the execroot within the sandbox. */
private final PathFragment rootFragment;
/** Flag to track whether the sandbox needs to be unmapped. */
private boolean sandboxIsMapped;
@Nullable private final Path statisticsPath;
/**
* Constructs a new sandboxfs-based spawn runner.
*
* @param process sandboxfs instance to use for this spawn
* @param sandboxPath writable directory where the spawn runner keeps control files
* @param arguments arguments to pass to the spawn, including the binary name
* @param environment environment variables to pass to the spawn
* @param inputs input files to be made available to the spawn in read-only mode
* @param outputs output files to expect from the spawn
* @param writableDirs directories where the spawn can write files to, relative to the sandbox's
* dynamically-allocated execroot
* @param mapSymlinkTargets map the targets of symlinks within the sandbox if true
* @param treeDeleter scheduler for tree deletions
*/
SandboxfsSandboxedSpawn(
SandboxfsProcess process,
Path sandboxPath,
String workspaceName,
List<String> arguments,
Map<String, String> environment,
SandboxInputs inputs,
SandboxOutputs outputs,
Set<PathFragment> writableDirs,
boolean mapSymlinkTargets,
TreeDeleter treeDeleter,
@Nullable Path statisticsPath) {
this.process = process;
this.arguments = arguments;
this.environment = environment;
this.inputs = inputs;
for (PathFragment path : outputs.files()) {
checkArgument(!path.isAbsolute(), "outputs %s must be relative", path);
}
for (PathFragment path : outputs.dirs()) {
checkArgument(!path.isAbsolute(), "outputs %s must be relative", path);
}
this.outputs = outputs;
for (PathFragment path : writableDirs) {
checkArgument(!path.isAbsolute(), "writable directory %s must be relative", path);
}
this.writableDirs = writableDirs;
this.mapSymlinkTargets = mapSymlinkTargets;
this.treeDeleter = treeDeleter;
this.sandboxPath = sandboxPath;
this.sandboxScratchDir = sandboxPath.getRelative("scratch");
int id = lastId.getAndIncrement();
this.sandboxName = "" + id;
this.sandboxIsMapped = false;
this.statisticsPath = statisticsPath;
// b/64689608: The execroot of the sandboxed process must end with the workspace name, just
// like the normal execroot does. Some tools walk their path hierarchy looking for this
// component and misbehave if they don't find it.
this.execRoot =
process.getMountPoint().getRelative(this.sandboxName).getRelative(workspaceName);
this.rootFragment = PathFragment.create("/" + workspaceName);
}
@Override
public Path getSandboxExecRoot() {
return execRoot;
}
@Override
public List<String> getArguments() {
return arguments;
}
@Override
public Map<String, String> getEnvironment() {
return environment;
}
@Override
public Path getStatisticsPath() {
return statisticsPath;
}
@Override
public void createFileSystem() throws IOException {
sandboxScratchDir.createDirectory();
Set<PathFragment> dirsToCreate = new HashSet<>(writableDirs);
for (PathFragment output : outputs.files()) {
dirsToCreate.add(output.getParentDirectory());
}
dirsToCreate.addAll(outputs.dirs());
for (PathFragment dir : dirsToCreate) {
sandboxScratchDir.getRelative(dir).createDirectoryAndParents();
}
createSandbox(process, sandboxName, rootFragment, sandboxScratchDir, inputs, mapSymlinkTargets);
sandboxIsMapped = true;
}
@Override
public void copyOutputs(Path targetExecRoot) throws IOException {
// TODO(jmmv): If we knew the targetExecRoot when setting up the spawn, we may be able to
// configure sandboxfs so that the output files are written directly to their target locations.
// This would avoid having to move them after-the-fact.
AbstractContainerizingSandboxedSpawn.moveOutputs(outputs, sandboxScratchDir, targetExecRoot);
}
@Override
public void delete() {
// We can only ask sandboxfs to unmap a sandbox if we successfully finished creating it.
// Otherwise, the request may fail, or we may fail our own sanity-checks that validate the
// lifecycle of the sandboxes.
if (sandboxIsMapped) {
try {
process.destroySandbox(sandboxName);
} catch (IOException e) {
// We use independent subdirectories for each action, so a failure to unmap one, while
// annoying, is not a big deal. The sandboxfs instance will be unmounted anyway after
// the build, which will cause these to go away anyway.
log.warning("Cannot unmap " + sandboxName + ": " + e);
}
sandboxIsMapped = false;
}
try {
treeDeleter.deleteTree(sandboxPath);
} catch (IOException e) {
// This usually means that the Spawn itself exited but still has children running that
// we couldn't wait for, which now block deletion of the sandbox directory. (Those processes
// may be creating new files in the directories we are trying to delete, preventing the
// deletion.) On Linux this should never happen: we use PID namespaces when available and the
// subreaper feature when not to make sure all children have been reliably killed before
// returning, but on other OSes this might not always work. The SandboxModule will try to
// delete them again when the build is all done, at which point it hopefully works... so let's
// just go on here.
}
}
/**
* Maps the targets of relative symlinks into the sandbox.
*
* <p>Symlinks with relative targets are tricky business. Consider this simple case: the source
* tree contains {@code dir/file.h} and {@code dir/symlink.h} where {@code dir/symlink.h}'s target
* is {@code ./file.h}. If {@code dir/symlink.h} is supplied as an input, we must preserve its
* target "as is" to avoid confusing any tooling: for example, the C compiler will understand that
* both {@code dir/file.h} and {@code dir/symlink.h} are the same entity and handle them
* appropriately. (We did encounter a case where the compiler complained about duplicate symbols
* because we exposed symlinks as regular files.)
*
* <p>However, there is no guarantee that the target of the symlink is mapped in the sandbox. You
* may think that this is a bug in the rules, and you would probably be right, but until those
* rules are fixed, we must supply a workaround. Therefore, we must handle these two cases: if the
* target is explicitly mapped, we do nothing. If it isn't, we have to compute where the target
* lives within the sandbox and map that as well. Oh, and we have to do this recursively.
*
* @param path path to expose within the sandbox
* @param symlink path to the target of the mapping specified by {@code path}
* @param mappings mutable collection of mappings to extend with the new symlink entries. Note
* that the entries added to this map may correspond to explicitly-mapped entries, so the
* caller must check this to avoid duplicate mappings
* @throws IOException if we fail to resolve symbolic links
*/
private static void computeSymlinkMappings(
PathFragment path, Path symlink, Map<PathFragment, PathFragment> mappings)
throws IOException {
for (; ; ) {
PathFragment symlinkTarget = symlink.readSymbolicLinkUnchecked();
if (!symlinkTarget.isAbsolute()) {
PathFragment keyParent = path.getParentDirectory();
if (keyParent == null) {
throw new IOException("Cannot resolve " + symlinkTarget + " relative to " + path);
}
PathFragment key = keyParent.getRelative(symlinkTarget);
Path valueParent = symlink.getParentDirectory();
if (valueParent == null) {
throw new IOException("Cannot resolve " + symlinkTarget + " relative to " + symlink);
}
Path value = valueParent.getRelative(symlinkTarget);
mappings.put(key, value.asFragment());
if (value.isSymbolicLink()) {
path = key;
symlink = value;
continue;
}
}
break;
}
}
/**
* Creates a new set of mappings to sandbox the given inputs.
*
* @param process the sandboxfs instance on which to create the sandbox
* @param sandboxName the name of the sandbox to pass to sandboxfs
* @param rootFragment path within the sandbox to the execroot to create
* @param scratchDir writable used as the target for all writable mappings
* @param inputs collection of paths to expose within the sandbox as read-only mappings, given as
* a map of mapped path to target path. The target path may be null, in which case an empty
* read-only file is mapped.
* @param sandboxfsMapSymlinkTargets map the targets of symlinks within the sandbox if true
* @return the collection of mappings to use for reconfiguration
* @throws IOException if we fail to resolve symbolic links
*/
private static void createSandbox(
SandboxfsProcess process,
String sandboxName,
PathFragment rootFragment,
Path scratchDir,
SandboxInputs inputs,
boolean sandboxfsMapSymlinkTargets)
throws IOException {
// Path to the empty file used as the target of mappings that don't provide one. This is
// lazily created and initialized only when we need such a mapping. It's safe to share the
// same empty file across all such mappings because this file is exposed as read-only.
//
// We cannot use /dev/null, as we used to do in the past, because exposing devices via a
// FUSE file system (which sandboxfs is) requires root privileges.
PathFragment emptyFile = null;
// Collection of extra mappings needed to represent the targets of relative symlinks. Lazily
// created once we encounter the first symlink in the list of inputs.
Map<PathFragment, PathFragment> symlinks = null;
for (Map.Entry<PathFragment, Path> entry : inputs.getFiles().entrySet()) {
if (entry.getValue() == null) {
if (emptyFile == null) {
Path emptyFilePath = scratchDir.getRelative("empty");
FileSystemUtils.createEmptyFile(emptyFilePath);
emptyFile = emptyFilePath.asFragment();
}
} else {
if (sandboxfsMapSymlinkTargets && entry.getValue().isSymbolicLink()) {
if (symlinks == null) {
symlinks = new HashMap<>();
}
computeSymlinkMappings(entry.getKey(), entry.getValue(), symlinks);
}
}
}
// IMPORTANT: Keep the code in the lambda passed to createSandbox() free from any operations
// that may block. This includes doing any kind of I/O. We used to include the loop above in
// this call and doing so cost 2-3% of the total build time measured on an iOS build with many
// actions that have thousands of inputs each.
@Nullable final PathFragment finalEmptyFile = emptyFile;
@Nullable final Map<PathFragment, PathFragment> finalSymlinks = symlinks;
process.createSandbox(
sandboxName,
(mapper) -> {
mapper.map(rootFragment, scratchDir.asFragment(), true);
for (Map.Entry<PathFragment, Path> entry : inputs.getFiles().entrySet()) {
PathFragment target;
if (entry.getValue() == null) {
checkNotNull(finalEmptyFile, "Must have been initialized above by matching logic");
target = finalEmptyFile;
} else {
target = entry.getValue().asFragment();
}
mapper.map(rootFragment.getRelative(entry.getKey()), target, false);
}
if (finalSymlinks != null) {
for (Map.Entry<PathFragment, PathFragment> entry : finalSymlinks.entrySet()) {
if (!inputs.getFiles().containsKey(entry.getKey())) {
mapper.map(rootFragment.getRelative(entry.getKey()), entry.getValue(), false);
}
}
}
});
// sandboxfs probably doesn't support symlinks.
// TODO(jmmv): This claim is simply not true. Figure out why this code snippet was added and
// address the real problem.
if (!inputs.getSymlinks().isEmpty()) {
throw new IOException(
"sandboxfs sandbox does not support unresolved symlinks "
+ Joiner.on(", ").join(inputs.getSymlinks().keySet()));
}
}
}