| // 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 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 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.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, environment); |
| writableDirs.addAll(extraWritableDirs); |
| |
| SandboxInputs inputs = |
| helpers.processInputFiles( |
| context.getInputMapping(PathFragment.EMPTY_FRAGMENT, /* willAccessRepeatedly= */ true), |
| execRoot); |
| 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(), |
| spawn.getTargetLabel()) { |
| @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"; |
| } |
| } |