blob: 3ad9c32531e0f01f02c573c321cc010c7ad6c67c [file] [log] [blame]
// Copyright 2014 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 java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
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.exec.TreeDeleter;
import com.google.devtools.build.lib.exec.local.LocalEnvProvider;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.ProcessWrapper;
import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
import com.google.devtools.build.lib.shell.Command;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.CommandResult;
import com.google.devtools.build.lib.util.OS;
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.Root;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/** Spawn runner that uses Darwin (macOS) sandboxing to execute a process. */
final class DarwinSandboxedSpawnRunner extends AbstractSandboxSpawnRunner {
/** Path to the {@code getconf} system tool to use. */
@VisibleForTesting
static String getconfBinary = "/usr/bin/getconf";
/** Path to the {@code sandbox-exec} system tool to use. */
@VisibleForTesting
static String sandboxExecBinary = "/usr/bin/sandbox-exec";
// Since checking if sandbox is supported is expensive, we remember what we've checked.
private static Boolean isSupported = null;
/**
* Returns whether the darwin sandbox is supported on the local machine by running a small command
* in it.
*/
public static boolean isSupported(CommandEnvironment cmdEnv) throws InterruptedException {
if (OS.getCurrent() != OS.DARWIN) {
return false;
}
if (ProcessWrapper.fromCommandEnvironment(cmdEnv) == null) {
return false;
}
if (isSupported == null) {
isSupported = computeIsSupported();
}
return isSupported;
}
private static boolean computeIsSupported() throws InterruptedException {
List<String> args = new ArrayList<>();
args.add(sandboxExecBinary);
args.add("-p");
args.add("(version 1) (allow default)");
args.add("/usr/bin/true");
ImmutableMap<String, String> env = ImmutableMap.of();
File cwd = new File("/usr/bin");
Command cmd = new Command(args.toArray(new String[0]), env, cwd);
try {
cmd.execute(ByteStreams.nullOutputStream(), ByteStreams.nullOutputStream());
} catch (CommandException e) {
return false;
}
return true;
}
private final SandboxHelpers helpers;
private final Path execRoot;
private final ImmutableList<Root> packageRoots;
private final boolean allowNetwork;
private final ProcessWrapper processWrapper;
private final Path sandboxBase;
private final TreeDeleter treeDeleter;
/**
* The set of directories that always should be writable, independent of the Spawn itself.
*
* <p>We cache this, because creating it involves executing {@code getconf}, which is expensive.
*/
private final ImmutableSet<Path> alwaysWritableDirs;
private final LocalEnvProvider localEnvProvider;
/**
* Creates a sandboxed spawn runner that uses the {@code process-wrapper} tool and the MacOS
* {@code sandbox-exec} binary.
*
* @param helpers common tools and state across all spawns during sandboxed execution
* @param cmdEnv the command environment to use
* @param sandboxBase path to the sandbox base directory
*/
DarwinSandboxedSpawnRunner(
SandboxHelpers helpers,
CommandEnvironment cmdEnv,
Path sandboxBase,
TreeDeleter treeDeleter)
throws IOException, InterruptedException {
super(cmdEnv);
this.helpers = helpers;
this.execRoot = cmdEnv.getExecRoot();
this.packageRoots = cmdEnv.getPackageLocator().getPathEntries();
this.allowNetwork = helpers.shouldAllowNetwork(cmdEnv.getOptions());
this.alwaysWritableDirs = getAlwaysWritableDirs(cmdEnv.getRuntime().getFileSystem());
this.processWrapper = ProcessWrapper.fromCommandEnvironment(cmdEnv);
this.localEnvProvider = LocalEnvProvider.forCurrentOs(cmdEnv.getClientEnv());
this.sandboxBase = sandboxBase;
this.treeDeleter = treeDeleter;
}
private static void addPathToSetIfExists(FileSystem fs, Set<Path> paths, String path)
throws IOException {
if (path != null) {
addPathToSetIfExists(paths, fs.getPath(path));
}
}
private static void addPathToSetIfExists(Set<Path> paths, Path path) throws IOException {
if (path.exists()) {
paths.add(path.resolveSymbolicLinks());
}
}
private static ImmutableSet<Path> getAlwaysWritableDirs(FileSystem fs)
throws IOException, InterruptedException {
HashSet<Path> writableDirs = new HashSet<>();
addPathToSetIfExists(fs, writableDirs, "/dev");
addPathToSetIfExists(fs, writableDirs, "/tmp");
addPathToSetIfExists(fs, writableDirs, "/private/tmp");
addPathToSetIfExists(fs, writableDirs, "/private/var/tmp");
// On macOS, processes may write to not only $TMPDIR but also to two other temporary
// directories. We have to get their location by calling "getconf".
addPathToSetIfExists(fs, writableDirs, getConfStr("DARWIN_USER_TEMP_DIR"));
addPathToSetIfExists(fs, writableDirs, getConfStr("DARWIN_USER_CACHE_DIR"));
// We don't add any value for $TMPDIR here, instead we compute its value later in
// {@link #actuallyExec} and add it as a writable directory in
// {@link AbstractSandboxSpawnRunner#getWritableDirs}.
// ~/Library/Caches and ~/Library/Logs need to be writable (cf. issue #2231).
Path homeDir = fs.getPath(System.getProperty("user.home"));
addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Caches"));
addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Logs"));
// Certain Xcode tools expect to be able to write to this path.
addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Developer"));
return ImmutableSet.copyOf(writableDirs);
}
/** Returns the value of a POSIX or X/Open system configuration variable. */
private static String getConfStr(String confVar) throws IOException, InterruptedException {
String[] commandArr = new String[2];
commandArr[0] = getconfBinary;
commandArr[1] = confVar;
Command cmd = new Command(commandArr);
CommandResult res;
try {
res = cmd.execute();
} catch (CommandException e) {
throw new IOException("getconf failed", e);
}
return new String(res.getStdout(), UTF_8).trim();
}
@Override
protected SandboxedSpawn prepareSpawn(Spawn spawn, SpawnExecutionContext context)
throws IOException, ForbiddenActionInputException, InterruptedException {
// Each invocation of "exec" gets its own sandbox base.
// Note that the value returned by context.getId() is only unique inside one given SpawnRunner,
// so we have to prefix our name to turn it into a globally unique value.
Path sandboxPath =
sandboxBase.getRelative(getName()).getRelative(Integer.toString(context.getId()));
sandboxPath.getParentDirectory().createDirectory();
sandboxPath.createDirectory();
// b/64689608: The execroot of the sandboxed process must end with the workspace name, just like
// the normal execroot does.
String workspaceName = execRoot.getBaseName();
Path sandboxExecRoot = sandboxPath.getRelative("execroot").getRelative(workspaceName);
sandboxExecRoot.getParentDirectory().createDirectory();
sandboxExecRoot.createDirectory();
ImmutableMap<String, String> environment =
localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), binTools, "/tmp");
final HashSet<Path> writableDirs = new HashSet<>(alwaysWritableDirs);
ImmutableSet<Path> extraWritableDirs =
getWritableDirs(sandboxExecRoot, sandboxExecRoot, environment);
writableDirs.addAll(extraWritableDirs);
SandboxInputs inputs =
helpers.processInputFiles(
context.getInputMapping(PathFragment.EMPTY_FRAGMENT, /* willAccessRepeatedly= */ true),
context.getInputMetadataProvider(),
execRoot,
execRoot,
packageRoots,
null);
SandboxOutputs outputs = helpers.getOutputs(spawn);
final Path sandboxConfigPath = sandboxPath.getRelative("sandbox.sb");
Duration timeout = context.getTimeout();
ProcessWrapper.CommandLineBuilder processWrapperCommandLineBuilder =
processWrapper
.commandLineBuilder(spawn.getArguments())
.addExecutionInfo(spawn.getExecutionInfo())
.setTimeout(timeout);
final Path statisticsPath = sandboxPath.getRelative("stats.out");
processWrapperCommandLineBuilder.setStatisticsPath(statisticsPath);
ImmutableList<String> commandLine =
ImmutableList.<String>builder()
.add(sandboxExecBinary)
.add("-f")
.add(sandboxConfigPath.getPathString())
.addAll(processWrapperCommandLineBuilder.build())
.build();
boolean allowNetworkForThisSpawn =
allowNetwork
|| Spawns.requiresNetwork(spawn, getSandboxOptions().defaultSandboxAllowNetwork);
return new SymlinkedSandboxedSpawn(
sandboxPath,
sandboxExecRoot,
commandLine,
environment,
inputs,
outputs,
writableDirs,
treeDeleter,
/* sandboxDebugPath= */ null,
statisticsPath,
/* interactiveDebugArguments= */ null,
spawn.getMnemonic()) {
@Override
public void createFileSystem() throws IOException, InterruptedException {
super.createFileSystem();
writeConfig(
sandboxConfigPath,
writableDirs,
getInaccessiblePaths(),
allowNetworkForThisSpawn,
statisticsPath);
}
};
}
private void writeConfig(
Path sandboxConfigPath,
Set<Path> writableDirs,
Set<Path> inaccessiblePaths,
boolean allowNetwork,
@Nullable Path statisticsPath)
throws IOException {
try (PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(sandboxConfigPath.getOutputStream(), UTF_8)))) {
// Note: In Apple's sandbox configuration language, the *last* matching rule wins.
out.println("(version 1)");
out.println("(debug deny)");
out.println("(allow default)");
out.println("(allow process-exec (with no-sandbox) (literal \"/bin/ps\"))");
if (!allowNetwork) {
out.println("(deny network*)");
out.println("(allow network-inbound (local ip \"localhost:*\"))");
out.println("(allow network* (remote ip \"localhost:*\"))");
out.println("(allow network* (remote unix-socket))");
}
// By default, everything is read-only.
out.println("(deny file-write*)");
out.println("(allow file-write*");
for (Path path : writableDirs) {
out.println(" (subpath \"" + path.getPathString() + "\")");
}
if (statisticsPath != null) {
out.println(" (literal \"" + statisticsPath.getPathString() + "\")");
}
out.println(")");
if (!inaccessiblePaths.isEmpty()) {
out.println("(deny file-read*");
// The sandbox configuration file is not part of a cache key and sandbox-exec doesn't care
// about ordering of paths in expressions, so it's fine if the iteration order is random.
for (Path inaccessiblePath : inaccessiblePaths) {
out.println(" (subpath \"" + inaccessiblePath + "\")");
}
out.println(")");
}
}
}
@Override
public String getName() {
return "darwin-sandbox";
}
}