aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/sandbox
diff options
context:
space:
mode:
authorGravatar jmmv <jmmv@google.com>2018-03-08 10:02:54 -0800
committerGravatar Copybara-Service <copybara-piper@google.com>2018-03-08 10:05:09 -0800
commit55ccf58f9da9847269a42c15e05317aab993d78c (patch)
treedb2dd738554c7b395da04fde33112ee4a116c680 /src/main/java/com/google/devtools/build/lib/sandbox
parent2838dd9f7f8556247f480b1e2ce0ced0e349e474 (diff)
Add an interface to interact with sandboxfs.
The new SandboxfsProcess interface allows interacting with sandboxfs. There are two implementations: RealSandboxfsProcess, which spawns the sandboxfs binary, and FakeSandboxfsProcess, which mimics what sandboxfs does but using symlinks and is intended for testing purposes only. The RealSandboxfsProcess implementation works but still carries many TODOs. The most "painful" one may be that the test requires manual invocation because we do not yet have an easy way to integrate with sandboxfs. That will be solved later on; for now this is sufficient for initial testing. RELNOTES: None. PiperOrigin-RevId: 188347393
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;
+}