// 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 com.google.devtools.build.lib.sandbox.SandboxfsProcess.Mapping; 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.ArrayList; import java.util.Collection; 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; /** * 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 arguments; /** Environment variables to pass to the spawn. */ private final Map environment; /** Collection of input files to be made available to the spawn in read-only mode. */ private final Map inputs; /** Collection of output files to expect from the spawn. */ private final Collection outputs; /** Collection of directories where the spawn can write files to relative to {@link #execRoot}. */ private final Set writableDirs; /** * 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; /** * Path to the working directory of the command, seen as an absolute path that starts at * the sandboxfs's mount point. */ private final PathFragment innerExecRoot; /** * Constructs a new sandboxfs-based spawn runner. * * @param process sandboxfs instance to use for this spawn * @param outerDir 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 */ SandboxfsSandboxedSpawn( SandboxfsProcess process, Path outerDir, List arguments, Map environment, Map inputs, Collection outputs, Set writableDirs) { this.process = process; this.arguments = arguments; this.environment = environment; this.inputs = inputs; for (PathFragment path : outputs) { 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.sandboxScratchDir = outerDir.getRelative("scratch"); int id = lastId.getAndIncrement(); this.execRoot = process.getMountPoint().getRelative("" + id); this.innerExecRoot = PathFragment.create("/" + id); } @Override public Path getSandboxExecRoot() { return execRoot; } @Override public List getArguments() { return arguments; } @Override public Map getEnvironment() { return environment; } @Override public void createFileSystem() throws IOException { sandboxScratchDir.createDirectory(); reconfigure(inputs, writableDirs, outputs); } @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. SandboxedSpawn.moveOutputs(outputs, sandboxScratchDir, targetExecRoot); } @Override public void delete() { try { process.unmap(innerExecRoot); } 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 " + innerExecRoot + ": " + e); } } /** * Creates a new set of mappings to sandbox the given inputs. * * @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. * @return the collection of mappings to use for reconfiguration * @throws IOException if we fail to resolve symbolic links */ private List createMappings(Map inputs) throws IOException { List mappings = new ArrayList<>(); mappings.add(Mapping.builder() .setPath(innerExecRoot) .setTarget(sandboxScratchDir.asFragment()) .setWritable(true) .build()); // 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. Path emptyFile = null; for (Map.Entry entry : inputs.entrySet()) { PathFragment target; if (entry.getValue() == null) { if (emptyFile == null) { emptyFile = sandboxScratchDir.getRelative("empty"); FileSystemUtils.createEmptyFile(emptyFile); } target = emptyFile.asFragment(); } else if (entry.getValue().isSymbolicLink()) { // If an input is a symlink, we don't necessarily have its target as an input as well. To // ensure the target is reachable within the sandbox, we have two choices: we can either // expose the target in the sandbox and respect the symlink, or we can resolve what the // actual target is and point the mapping there. The former has higher fidelity, as the // sandbox will respect the file's type as a symlink. The latter is easier to implement // and is slightly faster, as we avoid having to resolve symlinks later via sandboxfs. // Therefore, do the latter until proven insufficient. target = entry.getValue().resolveSymbolicLinks().asFragment(); } else { target = entry.getValue().asFragment(); } mappings.add(Mapping.builder() .setPath(innerExecRoot.getRelative(entry.getKey())) .setTarget(target) .setWritable(false) .build()); } return mappings; } /** * Pushes a new configuration to sandboxfs and waits for acceptance. * * @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 * file is mapped. * @param writableDirs collection of writable paths to create within the read-write portion of * the sandbox * @param outputs collection of outputs to expect within the read-write portion of the sandbox * @throws IOException if reconfiguration fails */ private void reconfigure(Map inputs, Set writableDirs, Collection outputs) throws IOException { List mappings = createMappings(inputs); Set dirsToCreate = new HashSet<>(writableDirs); for (PathFragment output : outputs) { dirsToCreate.add(output.getParentDirectory()); } for (PathFragment input : inputs.keySet()) { // We must pre-create the directory layout for input files as well as for output files. // The reason is that we map the root directory as writable within the sandbox and later // map read-only files on top of it. We want the intermediate components on those paths // to remain writable within the top-level root directory we have mounted, but that's // only possible if we pre-create those. Otherwise, sandboxfs will see that those components // do not exist when mapping the read-only file and will generate fake, in-memory, read-only // components for them. dirsToCreate.add(input.getParentDirectory()); } for (PathFragment dir : dirsToCreate) { sandboxScratchDir.getRelative(dir).createDirectoryAndParents(); } process.map(mappings); } }