aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/sandbox
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/sandbox')
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java261
-rw-r--r--src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java128
3 files changed, 391 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
index 9d849a8fc1..daaf92a5e7 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD
@@ -30,6 +30,8 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/standalone",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/common/options",
+ "//third_party:auto_value",
"//third_party:guava",
+ "//third_party:jsr305",
],
)
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java b/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
new file mode 100644
index 0000000000..70a355f378
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/RealSandboxfsProcess.java
@@ -0,0 +1,261 @@
+// 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.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.shell.Subprocess;
+import com.google.devtools.build.lib.shell.SubprocessBuilder;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.List;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/** A sandboxfs implementation that uses an external sandboxfs binary to manage the mount point. */
+final class RealSandboxfsProcess implements SandboxfsProcess {
+ private static final Logger log = Logger.getLogger(RealSandboxfsProcess.class.getName());
+
+ /** Directory on which the sandboxfs is serving. */
+ private final Path mountPoint;
+
+ /**
+ * Process handle to the sandboxfs instance. Null only after {@link #destroy()} has been invoked.
+ */
+ private @Nullable Subprocess process;
+
+ /**
+ * Writer with which to send data to the sandboxfs instance. Null only after {@link #destroy()}
+ * has been invoked.
+ */
+ private @Nullable BufferedWriter processStdIn;
+
+ /**
+ * Reader with which to receive data from the sandboxfs instance. Null only after
+ * {@link #destroy()} has been invoked.
+ */
+ private @Nullable BufferedReader processStdOut;
+
+ /**
+ * Shutdown hook to stop the sandboxfs instance on abrupt termination. Null only after
+ * {@link #destroy()} has been invoked.
+ */
+ private @Nullable Thread shutdownHook;
+
+ /**
+ * Initializes a new sandboxfs process instance.
+ *
+ * @param process process handle for the already-running sandboxfs instance
+ */
+ private RealSandboxfsProcess(Path mountPoint, Subprocess process) {
+ this.mountPoint = mountPoint;
+
+ this.process = process;
+ this.processStdIn = new BufferedWriter(
+ new OutputStreamWriter(process.getOutputStream(), UTF_8));
+ this.processStdOut = new BufferedReader(
+ new InputStreamReader(process.getInputStream(), UTF_8));
+
+ this.shutdownHook =
+ new Thread(
+ () -> {
+ try {
+ this.destroy();
+ } catch (Exception e) {
+ log.warning("Failed to destroy running sandboxfs instance; mount point may have "
+ + "been left behind: " + e);
+ }
+ });
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+ /**
+ * Mounts a new sandboxfs instance.
+ *
+ * <p>The root of the file system instance is left unmapped which means that it remains as
+ * read-only throughout the lifetime of this instance. Writable subdirectories can later be
+ * mapped via {@link #map(List)}.
+ *
+ * @param binary path to the sandboxfs binary
+ * @param mountPoint directory on which to mount the sandboxfs instance
+ * @param logFile path to the file that will receive all sandboxfs logging output
+ * @return a new handle that represents the running process
+ * @throws IOException if there is a problem starting the process
+ */
+ static SandboxfsProcess mount(Path binary, Path mountPoint, Path logFile) throws IOException {
+ log.info("Mounting sandboxfs (" + binary + ") onto " + mountPoint);
+
+ // TODO(jmmv): Before starting a sandboxfs serving instance, we must query the current version
+ // of sandboxfs and check if we support its communication protocol.
+
+ ImmutableList.Builder<String> argvBuilder = ImmutableList.builder();
+
+ argvBuilder.add(binary.getPathString());
+
+ // On macOS, we need to allow users other than self to access the sandboxfs instance. This is
+ // necessary because macOS's amfid, which runs as root, has to have access to the binaries
+ // within the sandbox in order to validate signatures. See:
+ // http://julio.meroh.net/2017/10/fighting-execs-sandboxfs-macos.html
+ argvBuilder.add(OS.getCurrent() == OS.DARWIN ? "--allow=other" : "--allow=self");
+
+ // TODO(jmmv): Pass flags to enable sandboxfs' debugging support (--listen_address and --debug)
+ // when requested by the user via --sandbox_debug. Tricky because we have to figure out how to
+ // deal with port numbers (which sandboxfs can autoassign, but doesn't currently promise a way
+ // to tell us back what it picked).
+
+ argvBuilder.add(mountPoint.getPathString());
+
+ SubprocessBuilder processBuilder = new SubprocessBuilder();
+ processBuilder.setArgv(argvBuilder.build());
+ processBuilder.setStderr(logFile.getPathFile());
+ processBuilder.setEnv(ImmutableMap.of(
+ // sandboxfs may need to locate fusermount depending on the FUSE implementation so pass the
+ // PATH to the subprocess (which we assume is sufficient).
+ "PATH", System.getenv("PATH")));
+
+ Subprocess process = processBuilder.start();
+ RealSandboxfsProcess sandboxfs = new RealSandboxfsProcess(mountPoint, process);
+ // TODO(jmmv): We should have a better mechanism to wait for sandboxfs to start successfully but
+ // sandboxfs currently provides no interface to do so. Just try to push an empty configuration
+ // and see if it works.
+ try {
+ sandboxfs.reconfigure("[]\n\n");
+ } catch (IOException e) {
+ destroyProcess(process);
+ throw new IOException("sandboxfs failed to start", e);
+ }
+ return sandboxfs;
+ }
+
+ @Override
+ public Path getMountPoint() {
+ return mountPoint;
+ }
+
+ @Override
+ public boolean isAlive() {
+ return process != null && !process.finished();
+ }
+
+ /**
+ * Destroys a process and waits for it to exit.
+ *
+ * @param process the process to destroy.
+ */
+ // TODO(jmmv): This is adapted from Worker.java. Should probably replace both with a new variant
+ // of Uninterruptibles.callUninterruptibly that takes a lambda instead of a callable.
+ private static void destroyProcess(Subprocess process) {
+ process.destroy();
+
+ boolean interrupted = false;
+ try {
+ while (true) {
+ try {
+ process.waitFor();
+ return;
+ } catch (InterruptedException ie) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void destroy() {
+ if (shutdownHook != null) {
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ shutdownHook = null;
+ }
+
+ if (processStdIn != null) {
+ try {
+ processStdIn.close();
+ } catch (IOException e) {
+ log.warning("Failed to close sandboxfs's stdin pipe: " + e);
+ }
+ processStdIn = null;
+ }
+
+ if (processStdOut != null) {
+ try {
+ processStdOut.close();
+ } catch (IOException e) {
+ log.warning("Failed to close sandboxfs's stdout pipe: " + e);
+ }
+ processStdOut = null;
+ }
+
+ if (process != null) {
+ destroyProcess(process);
+ process = null;
+ }
+ }
+
+ /**
+ * Pushes a new configuration to sandboxfs and waits for acceptance.
+ *
+ * @param config the configuration chunk to push to sandboxfs
+ * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
+ * configuration or because we failed to communicate with the subprocess
+ */
+ private synchronized void reconfigure(String config) throws IOException {
+ checkNotNull(processStdIn, "sandboxfs already has been destroyed");
+ processStdIn.write(config);
+ processStdIn.flush();
+
+ checkNotNull(processStdOut, "sandboxfs has already been destroyed");
+ String done = processStdOut.readLine();
+ if (done == null) {
+ throw new IOException("premature end of output from sandboxfs");
+ }
+ if (!done.equals("Done")) {
+ throw new IOException("received unknown string from sandboxfs: " + done + "; expected Done");
+ }
+ }
+
+ @Override
+ public void map(List<Mapping> mappings) throws IOException {
+ Function<Mapping, String> formatMapping =
+ (mapping) -> String.format(
+ "{\"Map\": {\"Mapping\": \"%s\", \"Target\": \"%s\", \"Writable\": %s}}",
+ mapping.path(), mapping.target(), mapping.writable() ? "true" : "false");
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("[\n");
+ sb.append(mappings.stream().map(formatMapping).collect(Collectors.joining(",\n")));
+ sb.append("]\n\n");
+ reconfigure(sb.toString());
+ }
+
+ @Override
+ public void unmap(PathFragment mapping) throws IOException {
+ reconfigure(String.format("[{\"Unmap\": \"%s\"}]\n\n", mapping));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java
new file mode 100644
index 0000000000..4b56ed3265
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsProcess.java
@@ -0,0 +1,128 @@
+// 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.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.List;
+
+/** Interface to interact with a sandboxfs instance. */
+interface SandboxfsProcess {
+
+ /** Represents a single mapping within a sandboxfs file system. */
+ @AutoValue
+ abstract class Mapping {
+ /**
+ * Path within the sandbox. This looks like an absolute path but is treated as relative to the
+ * sandbox's root.
+ */
+ abstract PathFragment path();
+
+ /** Absolute path from the host's file system to map into the sandbox. */
+ abstract PathFragment target();
+
+ /** Whether the mapped path is writable or not. */
+ abstract boolean writable();
+
+ /** Constructs a new mapping builder. */
+ static Builder builder() {
+ return new AutoValue_SandboxfsProcess_Mapping.Builder();
+ }
+
+ /** Builder for a single mapping within a sandboxfs file system. */
+ @AutoValue.Builder
+ abstract static class Builder {
+ /**
+ * Sets the path within the sandbox on which this mapping will appear. This looks like an
+ * absolute path but is treated as relative to the sandbox's root.
+ *
+ * @param path absolute path rooted at the sandbox's mount point
+ * @return the builder instance
+ */
+ abstract Builder setPath(PathFragment path);
+
+ /**
+ * Sets the path to which this mapping refers. This is an absolute path into the host's
+ * file system.
+ *
+ * @param target absolute path into the host's file system
+ * @return the builder instance
+ */
+ abstract Builder setTarget(PathFragment target);
+
+ /**
+ * Sets whether this mapping is writable or not when accessed via the sandbox.
+ *
+ * @param writable whether the mapping is writable or not
+ * @return the builder instance
+ */
+ abstract Builder setWritable(boolean writable);
+
+ abstract Mapping autoBuild();
+
+ /**
+ * Constructs the mapping and validates field invariants.
+ *
+ * @return the constructed mapping.
+ */
+ public Mapping build() {
+ Mapping mapping = autoBuild();
+ checkState(mapping.path().isAbsolute(), "Mapping specifications are supposed to be "
+ + "absolute but %s is not", mapping.path());
+ checkState(mapping.target().isAbsolute(), "Mapping targets are supposed to be "
+ + "absolute but %s is not", mapping.target());
+ return mapping;
+ }
+ }
+ }
+
+ /** Returns the path to the sandboxfs's mount point. */
+ Path getMountPoint();
+
+ /** Returns true if the sandboxfs process is still alive. */
+ boolean isAlive();
+
+ /**
+ * Unmounts and stops the sandboxfs process.
+ *
+ * <p>This function must be idempotent because there can be a race between explicit calls during
+ * regular execution and calls from shutdown hooks.
+ */
+ void destroy();
+
+ /**
+ * Adds new mappings to the sandboxfs instance.
+ *
+ * @param mappings the collection of mappings to add, which must not have yet been previously
+ * mapped
+ * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
+ * configuration or because we failed to communicate with the subprocess
+ */
+ void map(List<Mapping> mappings) throws IOException;
+
+ /**
+ * Removes a mapping from the sandboxfs instance.
+ *
+ * @param mapping the mapping to remove, which must have been previously mapped. This looks like
+ * an absolute path but is treated as relative to the sandbox's root.
+ * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
+ * configuration or because we failed to communicate with the subprocess
+ */
+ void unmap(PathFragment mapping) throws IOException;
+}