blob: 7cc54c31a759475035d7682a1d180c943530f879 [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.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ExecutionStrategy;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnActionContext;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.exec.apple.XCodeLocalEnvProvider;
import com.google.devtools.build.lib.exec.local.LocalEnvProvider;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
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 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.Map;
import java.util.Set;
/** Spawn runner that uses Darwin (macOS) sandboxing to execute a process. */
@ExecutionStrategy(
name = {"sandboxed", "darwin-sandbox"},
contextType = SpawnActionContext.class
)
final class DarwinSandboxedSpawnRunner extends AbstractSandboxSpawnRunner {
private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec";
public static boolean isSupported(CommandEnvironment cmdEnv) {
if (OS.getCurrent() != OS.DARWIN) {
return false;
}
if (!ProcessWrapperRunner.isSupported(cmdEnv)) {
return false;
}
List<String> args = new ArrayList<>();
args.add(SANDBOX_EXEC);
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 Path execRoot;
private final boolean allowNetwork;
private final String productName;
private final Path processWrapper;
private final int timeoutGraceSeconds;
/**
* 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;
DarwinSandboxedSpawnRunner(
CommandEnvironment cmdEnv,
Path sandboxBase,
String productName,
int timeoutGraceSeconds)
throws IOException {
super(cmdEnv, sandboxBase);
this.execRoot = cmdEnv.getExecRoot();
this.allowNetwork = SandboxHelpers.shouldAllowNetwork(cmdEnv.getOptions());
this.productName = productName;
this.alwaysWritableDirs = getAlwaysWritableDirs(cmdEnv.getRuntime().getFileSystem());
this.processWrapper = ProcessWrapperRunner.getProcessWrapper(cmdEnv);
this.localEnvProvider = new XCodeLocalEnvProvider();
this.timeoutGraceSeconds = timeoutGraceSeconds;
}
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 {
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/Cache and ~/Library/Logs need to be writable (cf. issue #2231).
Path homeDir = fs.getPath(System.getProperty("user.home"));
addPathToSetIfExists(writableDirs, homeDir.getRelative("Library/Cache"));
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 {
String[] commandArr = new String[2];
commandArr[0] = "/usr/bin/getconf";
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 SpawnResult actuallyExec(Spawn spawn, SpawnExecutionPolicy policy)
throws ExecException, IOException, InterruptedException {
// Each invocation of "exec" gets its own sandbox.
Path sandboxPath = getSandboxRoot();
Path sandboxExecRoot = sandboxPath.getRelative("execroot").getRelative(execRoot.getBaseName());
// Each sandboxed action runs in its own execroot, so we don't need to make the temp directory's
// name unique (like we have to with standalone execution strategy).
Path tmpDir = sandboxExecRoot.getRelative("tmp");
Map<String, String> spawnEnvironment =
localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, tmpDir, productName);
final HashSet<Path> writableDirs = new HashSet<>(alwaysWritableDirs);
ImmutableSet<Path> extraWritableDirs =
getWritableDirs(sandboxExecRoot, spawnEnvironment, tmpDir);
writableDirs.addAll(extraWritableDirs);
ImmutableSet<PathFragment> outputs = SandboxHelpers.getOutputFiles(spawn);
final Path sandboxConfigPath = sandboxPath.getRelative("sandbox.sb");
Duration timeout = policy.getTimeout();
List<String> arguments =
computeCommandLine(spawn, timeout, sandboxConfigPath, timeoutGraceSeconds);
Map<String, String> environment =
localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, tmpDir, productName);
boolean allowNetworkForThisSpawn = allowNetwork || SandboxHelpers.shouldAllowNetwork(spawn);
SandboxedSpawn sandbox = new SymlinkedSandboxedSpawn(
sandboxPath,
sandboxExecRoot,
arguments,
environment,
SandboxHelpers.getInputFiles(spawn, policy, execRoot),
outputs,
writableDirs) {
@Override
public void createFileSystem() throws IOException {
super.createFileSystem();
writeConfig(
sandboxConfigPath, writableDirs, getInaccessiblePaths(), allowNetworkForThisSpawn);
}
};
return runSpawn(spawn, sandbox, policy, execRoot, tmpDir, timeout);
}
private List<String> computeCommandLine(
Spawn spawn, Duration timeout, Path sandboxConfigPath, int timeoutGraceSeconds) {
List<String> commandLineArgs = new ArrayList<>();
commandLineArgs.add(SANDBOX_EXEC);
commandLineArgs.add("-f");
commandLineArgs.add(sandboxConfigPath.getPathString());
commandLineArgs.addAll(
ProcessWrapperRunner.getCommandLine(
processWrapper, spawn.getArguments(), timeout, timeoutGraceSeconds));
return commandLineArgs;
}
private void writeConfig(
Path sandboxConfigPath,
Set<Path> writableDirs,
Set<Path> inaccessiblePaths,
boolean allowNetwork) 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)");
if (!allowNetwork) {
out.println("(deny network*)");
out.println("(allow network* (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() + "\")");
}
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
protected String getName() {
return "darwin-sandbox";
}
}