| // 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; | |
| @Nullable private final SandboxfsProcess sandboxfsProcess; | |
| private final boolean sandboxfsMapSymlinkTargets; | |
| 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 | |
| * @param sandboxfsProcess instance of the sandboxfs process to use; may be null for none, in | |
| * which case the runner uses a symlinked sandbox | |
| * @param sandboxfsMapSymlinkTargets map the targets of symlinks within the sandbox if true | |
| */ | |
| DarwinSandboxedSpawnRunner( | |
| SandboxHelpers helpers, | |
| CommandEnvironment cmdEnv, | |
| Path sandboxBase, | |
| @Nullable SandboxfsProcess sandboxfsProcess, | |
| boolean sandboxfsMapSymlinkTargets, | |
| 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.sandboxfsProcess = sandboxfsProcess; | |
| this.sandboxfsMapSymlinkTargets = sandboxfsMapSymlinkTargets; | |
| 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), | |
| 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; | |
| if (getSandboxOptions().collectLocalSandboxExecutionStatistics) { | |
| statisticsPath = sandboxPath.getRelative("stats.out"); | |
| processWrapperCommandLineBuilder.setStatisticsPath(statisticsPath); | |
| } else { | |
| statisticsPath = null; | |
| } | |
| 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); | |
| if (sandboxfsProcess != null) { | |
| return new SandboxfsSandboxedSpawn( | |
| sandboxfsProcess, | |
| sandboxPath, | |
| workspaceName, | |
| commandLine, | |
| environment, | |
| inputs, | |
| outputs, | |
| ImmutableSet.of(), | |
| sandboxfsMapSymlinkTargets, | |
| treeDeleter, | |
| spawn.getMnemonic(), | |
| statisticsPath) { | |
| @Override | |
| public void createFileSystem() throws IOException { | |
| super.createFileSystem(); | |
| // The set of writable dirs includes the path to the execroot in the sandbox tree, but not | |
| // the path to the sibling sandboxfs hierarchy. We must explicitly grant access to this to | |
| // let builds work when the output tree is not under the default path hanging from tmp. | |
| writableDirs.add(getSandboxExecRoot()); | |
| writeConfig( | |
| sandboxConfigPath, | |
| writableDirs, | |
| getInaccessiblePaths(), | |
| allowNetworkForThisSpawn, | |
| statisticsPath); | |
| } | |
| }; | |
| } else { | |
| return new SymlinkedSandboxedSpawn( | |
| sandboxPath, | |
| sandboxExecRoot, | |
| commandLine, | |
| environment, | |
| inputs, | |
| outputs, | |
| writableDirs, | |
| treeDeleter, | |
| statisticsPath, | |
| 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, | |
| 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"; | |
| } | |
| } |