| // 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 com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.flogger.GoogleLogger; |
| 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.versioning.GnuVersionParser; |
| import com.google.devtools.build.lib.versioning.ParseException; |
| import com.google.devtools.build.lib.versioning.SemVer; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.io.IOException; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A sandboxfs implementation that uses an external sandboxfs binary to manage the mount point. |
| * |
| * <p>This class implements common code to generalize the interactions with sandboxfs, but delegates |
| * to its subclassess once the version of sandboxfs in use has been determined. The subclasses |
| * implement logic specific to each version to provide compatibility with the different versions of |
| * sandboxfs that the user might have installed. |
| */ |
| abstract class RealSandboxfsProcess implements SandboxfsProcess { |
| |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); |
| |
| /** |
| * Contains the {@code --allow} flag to pass to sandboxfs. |
| * |
| * <p>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://jmmv.dev/2017/10/fighting-execs-sandboxfs-macos.html |
| */ |
| @VisibleForTesting |
| static final String ALLOW_FLAG = OS.getCurrent() == OS.DARWIN ? "--allow=other" : "--allow=self"; |
| |
| /** 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. |
| */ |
| @Nullable private Subprocess process; |
| |
| /** |
| * Shutdown hook to stop the sandboxfs instance on abrupt termination. Null only after {@link |
| * #destroy()} has been invoked. |
| */ |
| @Nullable private Thread shutdownHook; |
| |
| /** |
| * Initializes a new sandboxfs process instance. |
| * |
| * @param process process handle for the already-running sandboxfs instance |
| */ |
| RealSandboxfsProcess(Path mountPoint, Subprocess process) { |
| this.mountPoint = mountPoint; |
| this.process = process; |
| |
| this.shutdownHook = |
| new Thread( |
| () -> { |
| try { |
| this.destroy(); |
| } catch (Exception e) { |
| logger.atWarning().withCause(e).log( |
| "Failed to destroy running sandboxfs instance; mount point may have " |
| + "been left behind"); |
| } |
| }); |
| 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 #createSandbox}. |
| * |
| * @param binary path to the sandboxfs binary. This is a {@link PathFragment} and not a {@link |
| * Path} because we want to support "bare" (non-absolute) names for the location of the |
| * sandboxfs binary; such names are automatically looked for in the {@code PATH}. |
| * @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(PathFragment binary, Path mountPoint, Path logFile) |
| throws IOException { |
| logger.atInfo().log("Mounting sandboxfs (%s) onto %s", binary, mountPoint); |
| |
| GnuVersionParser<SemVer> parser = new GnuVersionParser<>("sandboxfs", SemVer::parse); |
| SemVer version; |
| try { |
| version = parser.fromProgram(binary); |
| } catch (IOException | ParseException e) { |
| throw new IOException("Failed to get sandboxfs version from " + binary, e); |
| } |
| |
| ImmutableList.Builder<String> argvBuilder = ImmutableList.builder(); |
| argvBuilder.add(binary.getPathString()); |
| argvBuilder.add(ALLOW_FLAG); |
| |
| // 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; |
| if (version.compareTo(SemVer.from(0, 2)) >= 0) { |
| sandboxfs = new RealSandboxfs02Process(mountPoint, process); |
| } else { |
| sandboxfs = new RealSandboxfs01Process(mountPoint, process); |
| } |
| try { |
| // Create an empty sandbox to ensure sandboxfs is successfully serving. |
| sandboxfs.createSandbox("empty", (mapper) -> {}); |
| } catch (IOException e) { |
| process.destroyAndWait(); |
| 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(); |
| } |
| |
| @Override |
| public synchronized void destroy() { |
| if (shutdownHook != null) { |
| Runtime.getRuntime().removeShutdownHook(shutdownHook); |
| shutdownHook = null; |
| } |
| |
| if (process != null) { |
| try { |
| process.getOutputStream().close(); |
| } catch (IOException e) { |
| logger.atWarning().withCause(e).log("Failed to close sandboxfs's stdin pipe"); |
| } |
| |
| try { |
| process.getInputStream().close(); |
| } catch (IOException e) { |
| logger.atWarning().withCause(e).log("Failed to close sandboxfs's stdout pipe"); |
| } |
| |
| process.destroyAndWait(); |
| process = null; |
| } |
| } |
| } |