diff options
24 files changed, 1562 insertions, 1871 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java index bae4ec9d45..06c84de165 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java @@ -15,7 +15,6 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.common.io.Files; import com.google.devtools.build.lib.actions.ExecException; @@ -27,6 +26,7 @@ import com.google.devtools.build.lib.shell.CommandException; import com.google.devtools.build.lib.shell.TerminationStatus; import com.google.devtools.build.lib.util.CommandFailureUtils; import com.google.devtools.build.lib.util.OsUtils; +import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.FileOutErr; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; @@ -36,7 +36,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; /** * Helper class for running the Linux sandbox. This runner prepares environment inside the sandbox, @@ -44,34 +48,27 @@ import java.util.List; */ public class LinuxSandboxRunner { private static final String LINUX_SANDBOX = "linux-sandbox" + OsUtils.executableExtension(); - private static final String SANDBOX_TIP = - "\n\nSandboxed execution failed, which may be legitimate (e.g. a compiler error), " - + "or due to missing dependencies. To enter the sandbox environment for easier debugging," - + " run the following command in parentheses. On command failure, " - + "a bash shell running inside the sandbox will then automatically be spawned:\n\n"; private final Path execRoot; - private final Path sandboxPath; private final Path sandboxExecRoot; private final Path argumentsFilePath; - private final ImmutableMap<Path, Path> mounts; - private final ImmutableSet<Path> createDirs; + private final Set<Path> writablePaths; + private final List<Path> inaccessiblePaths; private final boolean verboseFailures; private final boolean sandboxDebug; - public LinuxSandboxRunner( + LinuxSandboxRunner( Path execRoot, - Path sandboxPath, - ImmutableMap<Path, Path> mounts, - ImmutableSet<Path> createDirs, + Path sandboxExecRoot, + Set<Path> writablePaths, + List<Path> inaccessiblePaths, boolean verboseFailures, boolean sandboxDebug) { this.execRoot = execRoot; - this.sandboxPath = sandboxPath; - this.sandboxExecRoot = sandboxPath.getRelative(execRoot.asFragment().relativeTo("/")); + this.sandboxExecRoot = sandboxExecRoot; this.argumentsFilePath = - sandboxPath.getParentDirectory().getRelative(sandboxPath.getBaseName() + ".params"); - this.mounts = mounts; - this.createDirs = createDirs; + sandboxExecRoot.getParentDirectory().getRelative(sandboxExecRoot.getBaseName() + ".params"); + this.writablePaths = writablePaths; + this.inaccessiblePaths = inaccessiblePaths; this.verboseFailures = verboseFailures; this.sandboxDebug = sandboxDebug; } @@ -114,21 +111,20 @@ public class LinuxSandboxRunner { * * @param spawnArguments - arguments of spawn to run inside the sandbox * @param env - environment to run sandbox in - * @param cwd - current working directory * @param outErr - error output to capture sandbox's and command's stderr - * @param outputs - files to extract from the sandbox, paths are relative to the exec root - * @throws ExecException + * @param outputs - files to extract from the sandbox, paths are relative to the exec root @throws + * ExecException */ public void run( List<String> spawnArguments, - ImmutableMap<String, String> env, - File cwd, + Map<String, String> env, FileOutErr outErr, + Map<PathFragment, Path> inputs, Collection<PathFragment> outputs, int timeout, boolean blockNetwork) throws IOException, ExecException { - createFileSystem(outputs); + createFileSystem(inputs, outputs); List<String> fileArgs = new ArrayList<>(); List<String> commandLineArgs = new ArrayList<>(); @@ -139,13 +135,9 @@ public class LinuxSandboxRunner { fileArgs.add("-D"); } - // Sandbox directory. - fileArgs.add("-S"); - fileArgs.add(sandboxPath.getPathString()); - // Working directory of the spawn. fileArgs.add("-W"); - fileArgs.add(cwd.toString()); + fileArgs.add(sandboxExecRoot.toString()); // Kill the process after a timeout. if (timeout != -1) { @@ -154,26 +146,22 @@ public class LinuxSandboxRunner { } // Create all needed directories. - for (Path createDir : createDirs) { - fileArgs.add("-d"); - fileArgs.add(createDir.getPathString()); + for (Path writablePath : writablePaths) { + fileArgs.add("-w"); + fileArgs.add(writablePath.getPathString()); + if (writablePath.startsWith(sandboxExecRoot)) { + FileSystemUtils.createDirectoryAndParents(writablePath); + } } - if (blockNetwork) { - // Block network access out of the namespace. - fileArgs.add("-n"); + for (Path inaccessiblePath : inaccessiblePaths) { + fileArgs.add("-i"); + fileArgs.add(inaccessiblePath.getPathString()); } - // Mount all the inputs. - for (ImmutableMap.Entry<Path, Path> mount : mounts.entrySet()) { - fileArgs.add("-M"); - fileArgs.add(mount.getValue().getPathString()); - - // The file is mounted in a custom location inside the sandbox. - if (!mount.getValue().equals(mount.getKey())) { - fileArgs.add("-m"); - fileArgs.add(mount.getKey().getPathString()); - } + if (blockNetwork) { + // Block network access out of the namespace. + fileArgs.add("-N"); } FileSystemUtils.writeLinesAs(argumentsFilePath, StandardCharsets.ISO_8859_1, fileArgs); @@ -182,7 +170,8 @@ public class LinuxSandboxRunner { commandLineArgs.add("--"); commandLineArgs.addAll(spawnArguments); - Command cmd = new Command(commandLineArgs.toArray(new String[0]), env, cwd); + Command cmd = + new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile()); try { cmd.execute( @@ -200,40 +189,77 @@ public class LinuxSandboxRunner { } String message = CommandFailureUtils.describeCommandFailure( - verboseFailures, commandLineArgs, env, cwd.getPath()); - String finalMsg = (sandboxDebug && verboseFailures) ? SANDBOX_TIP + message : message; - throw new UserExecException(finalMsg, e, timedOut); + verboseFailures, commandLineArgs, env, sandboxExecRoot.getPathString()); + throw new UserExecException(message, e, timedOut); } finally { copyOutputs(outputs); } } - private void createFileSystem(Collection<PathFragment> outputs) throws IOException { - FileSystemUtils.createDirectoryAndParents(sandboxPath); + private void createFileSystem(Map<PathFragment, Path> inputs, Collection<PathFragment> outputs) + throws IOException { + Set<Path> createdDirs = new HashSet<>(); + FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot); + createParentDirectoriesForInputs(createdDirs, inputs.keySet()); + createSymlinksForInputs(inputs); + createDirectoriesForOutputs(createdDirs, outputs); + } + + /** + * No input can be a child of another input, because otherwise we might try to create a symlink + * below another symlink we created earlier - which means we'd actually end up writing somewhere + * in the workspace. + * + * <p>If all inputs were regular files, this situation could naturally not happen - but + * unfortunately, we might get the occasional action that has directories in its inputs. + * + * <p>Creating all parent directories first ensures that we can safely create symlinks to + * directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested + * once we start creating the symlinks for all inputs. + */ + private void createParentDirectoriesForInputs(Set<Path> createdDirs, Set<PathFragment> inputs) + throws IOException { + for (PathFragment inputPath : inputs) { + Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory(); + Preconditions.checkArgument(dir.startsWith(sandboxExecRoot)); + FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir); + } + } + + private void createSymlinksForInputs(Map<PathFragment, Path> inputs) throws IOException { + // All input files are relative to the execroot. + for (Entry<PathFragment, Path> entry : inputs.entrySet()) { + Path key = sandboxExecRoot.getRelative(entry.getKey()); + key.createSymbolicLink(entry.getValue()); + } + } - // Prepare the output directories in the sandbox. + /** Prepare the output directories in the sandbox. */ + private void createDirectoriesForOutputs(Set<Path> createdDirs, Collection<PathFragment> outputs) + throws IOException { for (PathFragment output : outputs) { - FileSystemUtils.createDirectoryAndParents( - sandboxExecRoot.getRelative(output.getParentDirectory())); + FileSystemUtils.createDirectoryAndParentsWithCache( + createdDirs, sandboxExecRoot.getRelative(output.getParentDirectory())); + FileSystemUtils.createDirectoryAndParentsWithCache( + createdDirs, execRoot.getRelative(output.getParentDirectory())); } } private void copyOutputs(Collection<PathFragment> outputs) throws IOException { for (PathFragment output : outputs) { Path source = sandboxExecRoot.getRelative(output); - Path target = execRoot.getRelative(output); - FileSystemUtils.createDirectoryAndParents(target.getParentDirectory()); if (source.isFile() || source.isSymbolicLink()) { + Path target = execRoot.getRelative(output); Files.move(source.getPathFile(), target.getPathFile()); } } } public void cleanup() throws IOException { - if (sandboxPath.exists()) { - FileSystemUtils.deleteTree(sandboxPath); + if (sandboxExecRoot.exists()) { + FileSystemUtils.deleteTree(sandboxExecRoot); } - if (!sandboxDebug && argumentsFilePath.exists()) { + if (argumentsFilePath.exists()) { argumentsFilePath.delete(); } } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java index 9a04ed870c..4ee34e891e 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java @@ -13,10 +13,7 @@ // limitations under the License. package com.google.devtools.build.lib.sandbox; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; import com.google.devtools.build.lib.actions.ActionExecutionContext; @@ -33,28 +30,22 @@ import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.analysis.AnalysisUtils; import com.google.devtools.build.lib.analysis.BlazeDirectories; -import com.google.devtools.build.lib.analysis.config.RunUnder; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.rules.cpp.CppCompileAction; import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; -import com.google.devtools.build.lib.rules.test.TestRunnerAction; +import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy; -import com.google.devtools.build.lib.unix.NativePosixFiles; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.FileOutErr; -import com.google.devtools.build.lib.vfs.FileStatus; import com.google.devtools.build.lib.vfs.FileSystem; -import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; -import com.google.devtools.build.lib.vfs.SearchPath; -import com.google.devtools.build.lib.vfs.Symlinks; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; @@ -67,10 +58,18 @@ import java.util.concurrent.atomic.AtomicInteger; contextType = SpawnActionContext.class ) public class LinuxSandboxedStrategy implements SpawnActionContext { + private static Boolean sandboxingSupported = null; + + public static boolean isSupported(CommandEnvironment env) { + if (sandboxingSupported == null) { + sandboxingSupported = LinuxSandboxRunner.isSupported(env); + } + return sandboxingSupported.booleanValue(); + } + private final ExecutorService backgroundWorkers; private final SandboxOptions sandboxOptions; - private final ImmutableMap<String, String> clientEnv; private final BlazeDirectories blazeDirs; private final Path execRoot; private final boolean verboseFailures; @@ -79,16 +78,14 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { private final AtomicInteger execCounter = new AtomicInteger(); private final String productName; - public LinuxSandboxedStrategy( + LinuxSandboxedStrategy( SandboxOptions options, - Map<String, String> clientEnv, BlazeDirectories blazeDirs, ExecutorService backgroundWorkers, boolean verboseFailures, boolean unblockNetwork, String productName) { this.sandboxOptions = options; - this.clientEnv = ImmutableMap.copyOf(clientEnv); this.blazeDirs = blazeDirs; this.execRoot = blazeDirs.getExecRoot(); this.backgroundWorkers = Preconditions.checkNotNull(backgroundWorkers); @@ -129,18 +126,21 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { String execId = uuid + "-" + execCounter.getAndIncrement(); // Each invocation of "exec" gets its own sandbox. - Path sandboxPath = - execRoot.getRelative(productName + "-sandbox").getRelative(execId); + Path sandboxExecRoot = + blazeDirs.getOutputBase().getRelative(productName + "-sandbox").getRelative(execId); // Gather all necessary mounts for the sandbox. - ImmutableMap<Path, Path> mounts; + Map<PathFragment, Path> mounts; try { mounts = getMounts(spawn, actionExecutionContext); } catch (IllegalArgumentException | IOException e) { throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e); } - ImmutableSet<Path> createDirs = createImportantDirs(spawn.getEnvironment()); + Map<String, String> env = new HashMap<>(spawn.getEnvironment()); + + ImmutableSet<Path> writablePaths = getWritablePaths(sandboxExecRoot, env); + ImmutableList<Path> inaccessiblePaths = getInaccessiblePaths(); int timeout = getTimeout(spawn); @@ -157,17 +157,17 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { final LinuxSandboxRunner runner = new LinuxSandboxRunner( execRoot, - sandboxPath, - mounts, - createDirs, + sandboxExecRoot, + writablePaths, + inaccessiblePaths, verboseFailures, sandboxOptions.sandboxDebug); try { runner.run( spawn.getArguments(), - spawn.getEnvironment(), - execRoot.getPathFile(), + env, outErr, + mounts, outputFiles.build(), timeout, !this.unblockNetwork && !spawn.getExecutionInfo().containsKey("requires-network")); @@ -213,191 +213,86 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { return -1; } - /** - * Most programs expect certain directories to be present, e.g. /tmp. Make sure they are. - * - * <p>Note that $HOME is handled by linux-sandbox.c, because it changes user to nobody and the - * home directory of that user is not known by us. - */ - private ImmutableSet<Path> createImportantDirs(Map<String, String> env) { - ImmutableSet.Builder<Path> dirs = ImmutableSet.builder(); - FileSystem fs = blazeDirs.getFileSystem(); + /** Gets the list of directories that the spawn will assume to be writable. */ + private ImmutableSet<Path> getWritablePaths(Path sandboxExecRoot, Map<String, String> env) { + ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder(); + // We have to make the TEST_TMPDIR directory writable if it is specified. if (env.containsKey("TEST_TMPDIR")) { - PathFragment testTmpDir = new PathFragment(env.get("TEST_TMPDIR")); - if (testTmpDir.isAbsolute()) { - dirs.add(fs.getPath(testTmpDir)); - } else { - dirs.add(execRoot.getRelative(testTmpDir)); - } - } - dirs.add(fs.getPath("/tmp")); - return dirs.build(); - } - - private ImmutableMap<Path, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext) - throws IOException, ExecException { - ImmutableMap.Builder<Path, Path> result = new ImmutableMap.Builder<>(); - result.putAll(mountUsualUnixDirs()); - result.putAll(mountUserDefinedPath()); - - MountMap mounts = new MountMap(); - mounts.putAll(setupBlazeUtils()); - mounts.putAll(mountRunfilesFromManifests(spawn)); - mounts.putAll(mountRunfilesFromSuppliers(spawn)); - mounts.putAll(mountFilesFromFilesetManifests(spawn, executionContext)); - mounts.putAll(mountInputs(spawn, executionContext)); - mounts.putAll(mountRunUnderCommand(spawn)); - result.putAll(finalizeMounts(mounts)); - return result.build(); - } - - /** - * Helper method of {@link #finalizeMounts}. This method handles adding a single path - * to the output map, including making sure it exists and adding the target of a - * symbolic link if necessary. - * - * @param finalizedMounts the map to add the mapping(s) to - * @param target the key to add to the map - * @param source the value to add to the map - * @param stat information about source (passed in to avoid fetching it twice) - */ - private static void finalizeMountPath( - MountMap finalizedMounts, Path target, Path source, FileStatus stat) throws IOException { - // The source must exist. - Preconditions.checkArgument(stat != null, "%s does not exist", source.toString()); - finalizedMounts.put(target, source); - - if (stat.isSymbolicLink()) { - Path symlinkTarget = source.resolveSymbolicLinks(); - Preconditions.checkArgument( - symlinkTarget.exists(), "%s does not exist", symlinkTarget.toString()); - finalizedMounts.put(symlinkTarget, symlinkTarget); + Path testTmpDir = sandboxExecRoot.getRelative(env.get("TEST_TMPDIR")); + writablePaths.add(testTmpDir); + env.put("TEST_TMPDIR", testTmpDir.getPathString()); } + return writablePaths.build(); } - /** - * Performs various checks on each mounted file which require stating each one. - * Contained in one function to allow minimizing the number of syscalls involved. - * - * Checks for each mount if the source refers to a symbolic link and if yes, adds another mount - * for the target of that symlink to ensure that it keeps working inside the sandbox. - * - * Checks for each mount if the source refers to a directory and if yes, replaces that mount with - * mounts of all files inside that directory. - * - * Validates all mounts against a set of criteria and throws an exception on error. - * - * @return a new mounts multimap with all mounts and the added mounts. - */ - @VisibleForTesting - static MountMap finalizeMounts(Map<Path, Path> mounts) throws IOException { - MountMap finalizedMounts = new MountMap(); - for (Entry<Path, Path> mount : mounts.entrySet()) { - Path target = mount.getKey(); - Path source = mount.getValue(); - - FileStatus stat = source.statNullable(Symlinks.NOFOLLOW); - - if (stat != null && stat.isDirectory()) { - for (Path subSource : FileSystemUtils.traverseTree(source, Predicates.alwaysTrue())) { - Path subTarget = target.getRelative(subSource.relativeTo(source)); - finalizeMountPath( - finalizedMounts, subTarget, subSource, subSource.statNullable(Symlinks.NOFOLLOW)); - } - } else { - finalizeMountPath(finalizedMounts, target, source, stat); - } - } - return finalizedMounts; - } - - /** - * Mount a certain set of unix directories to make the usual tools and libraries available to the - * spawn that runs. - * - * Throws an exception if any of them do not exist. - */ - private MountMap mountUsualUnixDirs() throws IOException { - MountMap mounts = new MountMap(); - FileSystem fs = blazeDirs.getFileSystem(); - mounts.put(fs.getPath("/bin"), fs.getPath("/bin")); - mounts.put(fs.getPath("/sbin"), fs.getPath("/sbin")); - mounts.put(fs.getPath("/etc"), fs.getPath("/etc")); - - // Check if /etc/resolv.conf is a symlink and mount its target - // Fix #738 - Path resolv = fs.getPath("/etc/resolv.conf"); - if (resolv.exists() && resolv.isSymbolicLink()) { - mounts.put(resolv.resolveSymbolicLinks(), resolv.resolveSymbolicLinks()); - } - - for (String entry : NativePosixFiles.readdir("/")) { - if (entry.startsWith("lib")) { - Path libDir = fs.getRootDirectory().getRelative(entry); - mounts.put(libDir, libDir); - } - } - for (String entry : NativePosixFiles.readdir("/usr")) { - if (!entry.equals("local")) { - Path usrDir = fs.getPath("/usr").getRelative(entry); - mounts.put(usrDir, usrDir); - } - } - for (Path path : mounts.values()) { - Preconditions.checkArgument(path.exists(), "%s does not exist", path.toString()); + private ImmutableList<Path> getInaccessiblePaths() { + ImmutableList.Builder<Path> inaccessiblePaths = ImmutableList.builder(); + for (String path : sandboxOptions.sandboxBlockPath) { + inaccessiblePaths.add(blazeDirs.getFileSystem().getPath(path)); } - return mounts; + return inaccessiblePaths.build(); } - /** - * Mount the embedded tools. - */ - private MountMap setupBlazeUtils() { - MountMap mounts = new MountMap(); - Path mount = blazeDirs.getEmbeddedBinariesRoot().getRelative("build-runfiles"); - mounts.put(mount, mount); + private Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext) + throws IOException, ExecException { + Map<PathFragment, Path> mounts = new HashMap<>(); + mountRunfilesFromManifests(mounts, spawn); + mountRunfilesFromSuppliers(mounts, spawn); + mountFilesFromFilesetManifests(mounts, spawn, executionContext); + mountInputs(mounts, spawn, executionContext); return mounts; } - /** - * Mount all runfiles that the spawn needs as specified in its runfiles manifests. - */ - private MountMap mountRunfilesFromManifests(Spawn spawn) throws IOException, ExecException { - MountMap mounts = new MountMap(); - for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) { + /** Mount all runfiles that the spawn needs as specified in its runfiles manifests. */ + private void mountRunfilesFromManifests(Map<PathFragment, Path> mounts, Spawn spawn) + throws IOException, ExecException { + for (Map.Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) { String manifestFilePath = manifest.getValue().getPath().getPathString(); Preconditions.checkState(!manifest.getKey().isAbsolute()); - Path targetDirectory = execRoot.getRelative(manifest.getKey()); - - mounts.putAll(parseManifestFile(targetDirectory, new File(manifestFilePath), false, "")); + PathFragment targetDirectory = manifest.getKey(); + + parseManifestFile( + blazeDirs.getFileSystem(), + mounts, + targetDirectory, + new File(manifestFilePath), + false, + ""); } - return mounts; } - /** - * Mount all files that the spawn needs as specified in its fileset manifests. - */ - private MountMap mountFilesFromFilesetManifests( - Spawn spawn, ActionExecutionContext executionContext) throws IOException, ExecException { + /** Mount all files that the spawn needs as specified in its fileset manifests. */ + private void mountFilesFromFilesetManifests( + Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext executionContext) + throws IOException, ExecException { final FilesetActionContext filesetContext = executionContext.getExecutor().getContext(FilesetActionContext.class); - MountMap mounts = new MountMap(); for (Artifact fileset : spawn.getFilesetManifests()) { - Path manifest = - execRoot.getRelative(AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath())); - Path targetDirectory = execRoot.getRelative(fileset.getExecPathString()); - - mounts.putAll( - parseManifestFile( - targetDirectory, manifest.getPathFile(), true, filesetContext.getWorkspaceName())); + File manifestFile = + new File( + execRoot.getPathString(), + AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()).getPathString()); + PathFragment targetDirectory = fileset.getExecPath(); + + parseManifestFile( + blazeDirs.getFileSystem(), + mounts, + targetDirectory, + manifestFile, + true, + filesetContext.getWorkspaceName()); } - return mounts; } - static MountMap parseManifestFile( - Path targetDirectory, File manifestFile, boolean isFilesetManifest, String workspaceName) + /** A parser for the MANIFEST files used by Filesets and runfiles. */ + static void parseManifestFile( + FileSystem fs, + Map<PathFragment, Path> mounts, + PathFragment targetDirectory, + File manifestFile, + boolean isFilesetManifest, + String workspaceName) throws IOException, ExecException { - MountMap mounts = new MountMap(); int lineNum = 0; for (String line : Files.readLines(manifestFile, StandardCharsets.UTF_8)) { if (isFilesetManifest && (++lineNum % 2 == 0)) { @@ -409,7 +304,14 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { String[] fields = line.trim().split(" "); - Path targetPath; + // The "target" field is always a relative path that is to be interpreted in this way: + // (1) If this is a fileset manifest and our workspace name is not empty, the first segment + // of each "target" path must be the workspace name, which is then stripped before further + // processing. + // (2) The "target" path is then appended to the "targetDirectory", which is a path relative + // to the execRoot. Together, this results in the full path in the execRoot in which place a + // symlink referring to "source" has to be created (see below). + PathFragment targetPath; if (isFilesetManifest) { PathFragment targetPathFragment = new PathFragment(fields[0]); if (!workspaceName.isEmpty()) { @@ -424,13 +326,15 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { targetPath = targetDirectory.getRelative(fields[0]); } + // The "source" field, if it exists, is always an absolute path and may point to any file in + // the filesystem (it is not limited to files in the workspace or execroot). Path source; switch (fields.length) { case 1: - source = targetDirectory.getFileSystem().getPath("/dev/null"); + source = fs.getPath("/dev/null"); break; case 2: - source = targetDirectory.getFileSystem().getPath(fields[1]); + source = fs.getPath(fields[1]); break; default: throw new IllegalStateException("'" + line + "' splits into more than 2 parts"); @@ -438,38 +342,34 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { mounts.put(targetPath, source); } - return mounts; } - /** - * Mount all runfiles that the spawn needs as specified via its runfiles suppliers. - */ - private MountMap mountRunfilesFromSuppliers(Spawn spawn) throws IOException { - MountMap mounts = new MountMap(); - FileSystem fs = blazeDirs.getFileSystem(); + /** Mount all runfiles that the spawn needs as specified via its runfiles suppliers. */ + private void mountRunfilesFromSuppliers(Map<PathFragment, Path> mounts, Spawn spawn) + throws IOException { Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings = spawn.getRunfilesSupplier().getMappings(); - for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : + for (Map.Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : rootsAndMappings.entrySet()) { - Path root = fs.getRootDirectory().getRelative(rootAndMappings.getKey()); - for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { + PathFragment root = rootAndMappings.getKey(); + if (root.isAbsolute()) { + root = root.relativeTo(execRoot.asFragment()); + } + for (Map.Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { Artifact sourceArtifact = mapping.getValue(); - Path source = (sourceArtifact != null) ? sourceArtifact.getPath() : fs.getPath("/dev/null"); + PathFragment source = + (sourceArtifact != null) ? sourceArtifact.getExecPath() : new PathFragment("/dev/null"); Preconditions.checkArgument(!mapping.getKey().isAbsolute()); - Path target = root.getRelative(mapping.getKey()); - mounts.put(target, source); + PathFragment target = root.getRelative(mapping.getKey()); + mounts.put(target, execRoot.getRelative(source)); } } - return mounts; } - /** - * Mount all inputs of the spawn. - */ - private MountMap mountInputs(Spawn spawn, ActionExecutionContext actionExecutionContext) { - MountMap mounts = new MountMap(); - + /** Mount all inputs of the spawn. */ + private void mountInputs( + Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext actionExecutionContext) { List<ActionInput> inputs = ActionInputHelper.expandArtifacts( spawn.getInputFiles(), actionExecutionContext.getArtifactExpander()); @@ -485,124 +385,9 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { if (input.getExecPathString().contains("internal/_middlemen/")) { continue; } - Path mount = execRoot.getRelative(input.getExecPathString()); - mounts.put(mount, mount); - } - return mounts; - } - - /** - * If a --run_under= option is set and refers to a command via its path (as opposed to via its - * label), we have to mount this. Note that this is best effort and works fine for shell scripts - * and small binaries, but we can't track any further dependencies of this command. - * - * <p>If --run_under= refers to a label, it is automatically provided in the spawn's input files, - * so mountInputs() will catch that case. - */ - private MountMap mountRunUnderCommand(Spawn spawn) { - MountMap mounts = new MountMap(); - - if (spawn.getResourceOwner() instanceof TestRunnerAction) { - TestRunnerAction testRunnerAction = ((TestRunnerAction) spawn.getResourceOwner()); - RunUnder runUnder = testRunnerAction.getExecutionSettings().getRunUnder(); - if (runUnder != null && runUnder.getCommand() != null) { - PathFragment sourceFragment = new PathFragment(runUnder.getCommand()); - Path mount; - if (sourceFragment.isAbsolute()) { - mount = blazeDirs.getFileSystem().getPath(sourceFragment); - } else if (blazeDirs.getExecRoot().getRelative(sourceFragment).exists()) { - mount = blazeDirs.getExecRoot().getRelative(sourceFragment); - } else { - List<Path> searchPath = - SearchPath.parse(blazeDirs.getFileSystem(), clientEnv.get("PATH")); - mount = SearchPath.which(searchPath, runUnder.getCommand()); - } - if (mount != null) { - mounts.put(mount, mount); - } - } - } - return mounts; - } - - /** - * Mount all user defined path in --sandbox_add_path. - */ - private MountMap mountUserDefinedPath() throws IOException { - MountMap mounts = new MountMap(); - FileSystem fs = blazeDirs.getFileSystem(); - - ImmutableList<Path> exclude = - ImmutableList.of(blazeDirs.getWorkspace(), blazeDirs.getOutputBase()); - - for (String pathStr : sandboxOptions.sandboxAddPath) { - Path path = fs.getPath(pathStr); - - // Check if path is in {workspace, outputBase} - for (Path exc : exclude) { - if (path.startsWith(exc)) { - throw new IllegalArgumentException( - "Mounting subdirectory of WORKSPACE or OUTPUTBASE to sandbox is not allowed."); - } - } - - // Check if path is ancestor of {workspace, outputBase} - // Mount subdirectory of path except {workspace, outputBase} - mounts.putAll(mountChildDirExclude(path, exclude)); + PathFragment mount = new PathFragment(input.getExecPathString()); + mounts.put(mount, execRoot.getRelative(mount)); } - - return mounts; - } - - /** - * Mount all subdirectories recursively except some paths - */ - private MountMap mountDirExclude(Path path, List<Path> exclude) throws IOException { - MountMap mounts = new MountMap(); - - if (!path.isDirectory(Symlinks.NOFOLLOW)) { - if (!exclude.contains(path)) { - mounts.put(path, path); - } - return mounts; - } - - try { - for (Path child : path.getDirectoryEntries()) { - // Ignore broken symlink - if (!child.exists()) { - continue; - } - - mounts.putAll(mountChildDirExclude(child, exclude)); - } - } catch (IOException e) { - throw new IOException("Illegal additional path for mount", e); - } - - return mounts; - } - - /** - * Helper function of mountDirExclude and mountUserDefinedPath - */ - private MountMap mountChildDirExclude(Path child, List<Path> exclude) throws IOException { - MountMap mounts = new MountMap(); - - boolean startsWithFlag = false; - for (Path exc : exclude) { - if (exc.startsWith(child)) { - startsWithFlag = true; - break; - } - } - if (!startsWithFlag) { - mounts.put(child, child); - } else if (!exclude.contains(child)) { - mounts.putAll(mountDirExclude(child, exclude)); - } - - return mounts; } @Override diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java b/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java deleted file mode 100644 index 5051d27df3..0000000000 --- a/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2015 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.collect.ForwardingSortedMap; -import com.google.devtools.build.lib.vfs.Path; - -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -/** - * A map that throws an exception when trying to replace a key (i.e. once a key gets a value, - * any additional attempt of putting a value on the same key will throw an exception). - * - * <p>Returns entries sorted by path depth (shorter paths first) and in lexicographical order. - */ -final class MountMap extends ForwardingSortedMap<Path, Path> { - final TreeMap<Path, Path> delegate = new TreeMap<>(); - - @Override - protected SortedMap<Path, Path> delegate() { - return delegate; - } - - @Override - public Path put(Path key, Path value) { - Path previousValue = get(key); - if (previousValue == null) { - return super.put(key, value); - } else if (previousValue.equals(value)) { - return value; - } else { - throw new IllegalArgumentException( - String.format("Cannot mount both '%s' and '%s' onto '%s'", previousValue, value, key)); - } - } - - @Override - public void putAll(Map<? extends Path, ? extends Path> map) { - for (Entry<? extends Path, ? extends Path> entry : map.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } -} diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java index 2d6e28e562..7b4e30218a 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java @@ -15,12 +15,11 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.ImmutableMultimap.Builder; import com.google.common.collect.Multimap; import com.google.devtools.build.lib.actions.ActionContextConsumer; import com.google.devtools.build.lib.actions.Executor.ActionContext; import com.google.devtools.build.lib.actions.SpawnActionContext; -import com.google.devtools.build.lib.util.OS; +import com.google.devtools.build.lib.runtime.CommandEnvironment; /** * {@link ActionContextConsumer} that requests the action contexts necessary for sandboxed @@ -28,6 +27,19 @@ import com.google.devtools.build.lib.util.OS; */ public class SandboxActionContextConsumer implements ActionContextConsumer { + private final ImmutableMultimap<Class<? extends ActionContext>, String> contexts; + + public SandboxActionContextConsumer(CommandEnvironment env) { + ImmutableMultimap.Builder<Class<? extends ActionContext>, String> contexts = + ImmutableMultimap.builder(); + + if (LinuxSandboxedStrategy.isSupported(env)) { + contexts.put(SpawnActionContext.class, "sandboxed"); + } + + this.contexts = contexts.build(); + } + @Override public ImmutableMap<String, String> getSpawnActionContexts() { return ImmutableMap.of(); @@ -35,13 +47,6 @@ public class SandboxActionContextConsumer implements ActionContextConsumer { @Override public Multimap<Class<? extends ActionContext>, String> getActionContexts() { - Builder<Class<? extends ActionContext>, String> contexts = ImmutableMultimap.builder(); - - if (OS.getCurrent() == OS.LINUX) { - contexts.put(SpawnActionContext.class, "sandboxed"); - } - - return contexts.build(); + return contexts; } - } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java index 6bf355610e..7bf38c901b 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java @@ -14,11 +14,11 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableList.Builder; import com.google.devtools.build.lib.actions.ActionContextProvider; import com.google.devtools.build.lib.actions.Executor.ActionContext; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.buildtool.BuildRequest; +import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.exec.ExecutionOptions; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.util.OS; @@ -30,11 +30,16 @@ import java.util.concurrent.ExecutorService; */ public class SandboxActionContextProvider extends ActionContextProvider { + public static final String SANDBOX_NOT_SUPPORTED_MESSAGE = + "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot " + + "be guaranteed. See http://bazel.io/docs/bazel-user-manual.html#sandboxing for more " + + "information. You can turn off this warning via --ignore_unsupported_sandboxing"; + @SuppressWarnings("unchecked") - private final ImmutableList<ActionContext> strategies; + private final ImmutableList<ActionContext> contexts; - private SandboxActionContextProvider(ImmutableList<ActionContext> strategies) { - this.strategies = strategies; + private SandboxActionContextProvider(ImmutableList<ActionContext> contexts) { + this.contexts = contexts; } public static SandboxActionContextProvider create( @@ -46,36 +51,45 @@ public class SandboxActionContextProvider extends ActionContextProvider { .getOptions(BuildConfiguration.Options.class) .testArguments .contains("--wrapper_script_flag=--debug"); - Builder<ActionContext> strategies = ImmutableList.builder(); + ImmutableList.Builder<ActionContext> contexts = ImmutableList.builder(); - if (OS.getCurrent() == OS.LINUX) { - strategies.add( - new LinuxSandboxedStrategy( - buildRequest.getOptions(SandboxOptions.class), - env.getClientEnv(), - env.getDirectories(), - backgroundWorkers, - verboseFailures, - unblockNetwork, - env.getRuntime().getProductName())); - } else if (OS.getCurrent() == OS.DARWIN) { - strategies.add( - DarwinSandboxedStrategy.create( - buildRequest.getOptions(SandboxOptions.class), - env.getClientEnv(), - env.getDirectories(), - backgroundWorkers, - verboseFailures, - unblockNetwork, - env.getRuntime().getProductName())); + switch (OS.getCurrent()) { + case LINUX: + if (LinuxSandboxedStrategy.isSupported(env)) { + contexts.add( + new LinuxSandboxedStrategy( + buildRequest.getOptions(SandboxOptions.class), + env.getDirectories(), + backgroundWorkers, + verboseFailures, + unblockNetwork, + env.getRuntime().getProductName())); + } else if (!buildRequest.getOptions(SandboxOptions.class).ignoreUnsupportedSandboxing) { + env.getReporter().handle(Event.warn(SANDBOX_NOT_SUPPORTED_MESSAGE)); + } + break; + case DARWIN: + if (DarwinSandboxRunner.isSupported()) { + contexts.add( + DarwinSandboxedStrategy.create( + buildRequest.getOptions(SandboxOptions.class), + env.getClientEnv(), + env.getDirectories(), + backgroundWorkers, + verboseFailures, + unblockNetwork, + env.getRuntime().getProductName())); + } + break; + default: + // No sandboxing available. } - return new SandboxActionContextProvider(strategies.build()); + return new SandboxActionContextProvider(contexts.build()); } @Override public Iterable<ActionContext> getActionContexts() { - return strategies; + return contexts; } - } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java index 5b9ca58931..8a20cb4f16 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java @@ -20,11 +20,9 @@ import com.google.devtools.build.lib.actions.ActionContextProvider; import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent; import com.google.devtools.build.lib.concurrent.ExecutorUtil; -import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.runtime.BlazeModule; import com.google.devtools.build.lib.runtime.Command; import com.google.devtools.build.lib.runtime.CommandEnvironment; -import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.common.options.OptionsBase; import java.io.IOException; @@ -35,66 +33,30 @@ import java.util.concurrent.Executors; * This module provides the Sandbox spawn strategy. */ public class SandboxModule extends BlazeModule { - public static final String SANDBOX_NOT_SUPPORTED_MESSAGE = - "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot " - + "be guaranteed. See http://bazel.io/docs/bazel-user-manual.html#sandboxing for more " - + "information. You can turn off this warning via --ignore_unsupported_sandboxing"; - // Per-server state private ExecutorService backgroundWorkers; - private Boolean sandboxingSupported = null; // Per-command state private CommandEnvironment env; private BuildRequest buildRequest; - private synchronized boolean isSandboxingSupported(CommandEnvironment env) { - switch (OS.getCurrent()) { - case LINUX: - sandboxingSupported = LinuxSandboxRunner.isSupported(env); - break; - case DARWIN: - sandboxingSupported = DarwinSandboxRunner.isSupported(); - break; - default: - sandboxingSupported = false; - } - return sandboxingSupported.booleanValue(); - } - @Override public Iterable<ActionContextProvider> getActionContextProviders() { - Preconditions.checkNotNull(buildRequest); Preconditions.checkNotNull(env); - if (isSandboxingSupported(env)) { - Iterable<ActionContextProvider> ret; - try { - ret = - ImmutableList.<ActionContextProvider>of( - SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers)); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - return ret; - } - - // For now, sandboxing is only supported on Linux and there's not much point in showing a scary - // warning to the user if they can't do anything about it. - if (!buildRequest.getOptions(SandboxOptions.class).ignoreUnsupportedSandboxing - && OS.getCurrent() == OS.LINUX) { - env.getReporter().handle(Event.warn(SANDBOX_NOT_SUPPORTED_MESSAGE)); + Preconditions.checkNotNull(buildRequest); + Preconditions.checkNotNull(backgroundWorkers); + try { + return ImmutableList.<ActionContextProvider>of( + SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers)); + } catch (IOException e) { + throw new IllegalStateException(e); } - - return ImmutableList.of(); } @Override public Iterable<ActionContextConsumer> getActionContextConsumers() { Preconditions.checkNotNull(env); - if (isSandboxingSupported(env)) { - return ImmutableList.<ActionContextConsumer>of(new SandboxActionContextConsumer()); - } - return ImmutableList.of(); + return ImmutableList.<ActionContextConsumer>of(new SandboxActionContextConsumer(env)); } @Override diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java index f1291d2cbb..f18792f606 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java @@ -15,7 +15,6 @@ package com.google.devtools.build.lib.sandbox; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionsBase; - import java.util.List; /** @@ -42,11 +41,11 @@ public class SandboxOptions extends OptionsBase { public boolean sandboxDebug; @Option( - name = "sandbox_add_path", + name = "sandbox_block_path", allowMultiple = true, defaultValue = "", category = "config", - help = "Add additional path to mount to sandbox. Path including workspace is not allowed." + help = "For sandboxed actions, disallow access to this path." ) - public List<String> sandboxAddPath; + public List<String> sandboxBlockPath; } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java index 8c864547ff..cf12b50f21 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java @@ -22,7 +22,6 @@ import com.google.common.io.ByteStreams; import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.util.Preconditions; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -32,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Set; /** * Helper functions that implement often-used complex operations on file @@ -582,10 +582,36 @@ public class FileSystemUtils { */ @ThreadSafe public static boolean createDirectoryAndParents(Path dir) throws IOException { + return createDirectoryAndParentsWithCache(null, dir); + } + + /** + * Attempts to create a directory with the name of the given path, creating ancestors as + * necessary. Only creates directories or their parents if they are not contained in the set + * {@code createdDirs} and instead assumes that they already exist. This saves a round-trip to the + * kernel, but is only safe when no one deletes directories that have been created by this method. + * + * <p>Postcondition: completes normally iff {@code dir} denotes an existing directory (not + * necessarily canonical); completes abruptly otherwise. + * + * @return true if the directory was successfully created anew, false if it already existed + * (including the case where {@code dir} denotes a symlink to an existing directory) + * @throws IOException if the directory could not be created + */ + @ThreadSafe + public static boolean createDirectoryAndParentsWithCache(Set<Path> createdDirs, Path dir) + throws IOException { // Optimised for minimal number of I/O calls. // Don't attempt to create the root directory. - if (dir.getParentDirectory() == null) { return false; } + if (dir.getParentDirectory() == null) { + return false; + } + + // We already created that directory. + if (createdDirs != null && createdDirs.contains(dir)) { + return false; + } FileSystem filesystem = dir.getFileSystem(); if (filesystem instanceof UnionFileSystem) { @@ -596,12 +622,23 @@ public class FileSystemUtils { } try { - return dir.createDirectory(); + boolean result = dir.createDirectory(); + if (createdDirs != null) { + createdDirs.add(dir); + } + return result; } catch (IOException e) { if (e.getMessage().endsWith(" (No such file or directory)")) { // ENOENT - createDirectoryAndParents(dir.getParentDirectory()); - return dir.createDirectory(); + createDirectoryAndParentsWithCache(createdDirs, dir.getParentDirectory()); + boolean result = dir.createDirectory(); + if (createdDirs != null) { + createdDirs.add(dir); + } + return result; } else if (e.getMessage().endsWith(" (File exists)") && dir.isDirectory()) { // EEXIST + if (createdDirs != null) { + createdDirs.add(dir); + } return false; } else { throw e; // some other error (e.g. ENOTDIR, EACCES, etc.) diff --git a/src/main/tools/BUILD b/src/main/tools/BUILD index ead41685ae..23a4e319d1 100644 --- a/src/main/tools/BUILD +++ b/src/main/tools/BUILD @@ -1,26 +1,14 @@ package(default_visibility = ["//src:__subpackages__"]) -cc_library( - name = "network-tools", - srcs = ["network-tools.c"], - hdrs = ["network-tools.h"], - copts = ["-std=c99"], - deps = [":process-tools"], -) - -cc_library( - name = "process-tools", - srcs = ["process-tools.c"], - hdrs = ["process-tools.h"], - copts = ["-std=c99"], -) - cc_binary( name = "process-wrapper", - srcs = ["process-wrapper.c"], + srcs = [ + "process-tools.c", + "process-tools.h", + "process-wrapper.c", + ], copts = ["-std=c99"], linkopts = ["-lm"], - deps = [":process-tools"], ) cc_binary( @@ -35,20 +23,17 @@ cc_binary( "//src:darwin_x86_64": ["dummy-sandbox.c"], "//src:freebsd": ["dummy-sandbox.c"], "//src:windows": ["dummy-sandbox.c"], - "//conditions:default": ["linux-sandbox.c"], - }), - copts = ["-std=c99"], - linkopts = ["-lm"], - deps = select({ - "//src:darwin": [], - "//src:darwin_x86_64": [], - "//src:freebsd": [], - "//src:windows": [], "//conditions:default": [ - ":process-tools", - ":network-tools", + "linux-sandbox.cc", + "linux-sandbox.h", + "linux-sandbox-options.cc", + "linux-sandbox-options.h", + "linux-sandbox-pid1.cc", + "linux-sandbox-pid1.h", + "linux-sandbox-utils.h", ], }), + linkopts = ["-lm"], ) filegroup( diff --git a/src/main/tools/linux-sandbox-options.cc b/src/main/tools/linux-sandbox-options.cc new file mode 100644 index 0000000000..0b43a2cfb4 --- /dev/null +++ b/src/main/tools/linux-sandbox-options.cc @@ -0,0 +1,269 @@ +// Copyright 2016 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. + +#include "linux-sandbox-options.h" +#include "linux-sandbox-utils.h" + +#define DIE(args...) \ + { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \ + fprintf(stderr, "\": "); \ + perror(NULL); \ + exit(EXIT_FAILURE); \ + } + +#include <errno.h> +#include <sched.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <fstream> +#include <iostream> +#include <memory> +#include <string> +#include <vector> + +using std::ifstream; +using std::unique_ptr; +using std::vector; + +struct Options opt; + +// Print out a usage error. argc and argv are the argument counter and vector, +// fmt is a format, string for the error message to print. +static void Usage(char *program_name, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\nUsage: %s -- command arg1 @args\n", program_name); + fprintf(stderr, + "\nPossible arguments:\n" + " -W <working-dir> working directory (uses current directory if " + "not specified)\n" + " -T <timeout> timeout after which the child process will be " + "terminated with SIGTERM\n" + " -t <timeout> in case timeout occurs, how long to wait before " + "killing the child with SIGKILL\n" + " -l <file> redirect stdout to a file\n" + " -L <file> redirect stderr to a file\n" + " -w <file> make a file or directory writable for the sandboxed " + "process\n" + " -i <file> make a file or directory inaccessible for the " + "sandboxed process\n" + " -e <dir> mount an empty tmpfs on a directory\n" + " -N if set, a new network namespace will be created\n" + " -R if set, make the uid/gid be root, otherwise use nobody\n" + " -D if set, debug info will be printed\n" + " @FILE read newline-separated arguments from FILE\n" + " -- command to run inside sandbox, followed by arguments\n"); + exit(EXIT_FAILURE); +} + +// Child function used by CheckNamespacesSupported() in call to clone(). +static int CheckNamespacesSupportedChild(void *arg) { return 0; } + +// Check whether the required namespaces are supported. +static int CheckNamespacesSupported() { + const int kStackSize = 1024 * 1024; + vector<char> child_stack(kStackSize); + + pid_t pid = clone(CheckNamespacesSupportedChild, &child_stack.back(), + CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | + CLONE_NEWNET | CLONE_NEWPID | SIGCHLD, + NULL); + if (pid < 0) { + DIE("pid"); + } + + int err; + do { + err = waitpid(pid, NULL, 0); + } while (err < 0 && errno == EINTR); + + if (err < 0) { + DIE("waitpid"); + } + + return EXIT_SUCCESS; +} + +// Parses command line flags from an argv array and puts the results into an +// Options structure passed in as an argument. +static void ParseCommandLine(unique_ptr<vector<char *>> args) { + extern char *optarg; + extern int optind, optopt; + int c; + + while ((c = getopt(args->size(), args->data(), ":CS:W:T:t:l:L:w:i:e:NRD")) != + -1) { + switch (c) { + case 'C': + // Shortcut for the "does this system support sandboxing" check. + exit(CheckNamespacesSupported()); + break; + case 'W': + if (opt.working_dir == NULL) { + if (optarg[0] != '/') { + Usage(args->front(), + "The -W option must be used with absolute paths only."); + } + opt.working_dir = strdup(optarg); + } else { + Usage(args->front(), + "Multiple working directories (-W) specified, expected one."); + } + break; + case 'T': + if (sscanf(optarg, "%d", &opt.timeout_secs) != 1 || + opt.timeout_secs < 0) { + Usage(args->front(), "Invalid timeout (-T) value: %s", optarg); + } + break; + case 't': + if (sscanf(optarg, "%d", &opt.kill_delay_secs) != 1 || + opt.kill_delay_secs < 0) { + Usage(args->front(), "Invalid kill delay (-t) value: %s", optarg); + } + break; + case 'l': + if (opt.stdout_path == NULL) { + opt.stdout_path = optarg; + } else { + Usage(args->front(), + "Cannot redirect stdout to more than one destination."); + } + break; + case 'L': + if (opt.stderr_path == NULL) { + opt.stderr_path = optarg; + } else { + Usage(args->front(), + "Cannot redirect stderr to more than one destination."); + } + break; + case 'w': + if (optarg[0] != '/') { + Usage(args->front(), + "The -w option must be used with absolute paths only."); + } + opt.writable_files.push_back(strdup(optarg)); + break; + case 'i': + if (optarg[0] != '/') { + Usage(args->front(), + "The -i option must be used with absolute paths only."); + } + opt.inaccessible_files.push_back(strdup(optarg)); + break; + case 'e': + if (optarg[0] != '/') { + Usage(args->front(), + "The -e option must be used with absolute paths only."); + } + opt.tmpfs_dirs.push_back(strdup(optarg)); + break; + case 'N': + opt.create_netns = true; + break; + case 'R': + opt.fake_root = true; + break; + case 'D': + opt.debug = true; + break; + case '?': + Usage(args->front(), "Unrecognized argument: -%c (%d)", optopt, optind); + break; + case ':': + Usage(args->front(), "Flag -%c requires an argument", optopt); + break; + } + } + + if (optind < static_cast<int>(args->size())) { + if (opt.args.empty()) { + opt.args.assign(args->begin() + optind, args->end()); + } else { + Usage(args->front(), "Merging commands not supported."); + } + } +} + +// Expands a single argument, expanding options @filename to read in the content +// of the file and add it to the list of processed arguments. +unique_ptr<vector<char *>> ExpandArgument(unique_ptr<vector<char *>> expanded, + char *arg) { + if (arg[0] == '@') { + const char *filename = arg + 1; // strip off the '@'. + ifstream f(filename); + + if (!f.is_open()) { + DIE("opening argument file %s failed", filename); + } + + for (std::string line; std::getline(f, line);) { + if (line.length() > 0) { + expanded = ExpandArgument(std::move(expanded), strdup(line.c_str())); + } + } + + if (f.bad()) { + DIE("error while reading from argument file %s", filename); + } + } else { + expanded->push_back(arg); + } + + return expanded; +} + +// Pre-processes an argument list, expanding options @filename to read in the +// content of the file and add it to the list of arguments. Stops expanding +// arguments once it encounters "--". +unique_ptr<vector<char *>> ExpandArguments(const vector<char *> &args) { + unique_ptr<vector<char *>> expanded(new vector<char *>()); + expanded->reserve(args.size()); + for (auto arg = args.begin(); arg != args.end(); ++arg) { + if (strcmp(*arg, "--") != 0) { + expanded = ExpandArgument(std::move(expanded), *arg); + } else { + expanded->insert(expanded->end(), arg, args.end()); + break; + } + } + return expanded; +} + +// Handles parsing all command line flags and populates the global opt struct. +void ParseOptions(int argc, char *argv[]) { + vector<char *> args(argv, argv + argc); + ParseCommandLine(ExpandArguments(args)); + + if (opt.args.empty()) { + Usage(args.front(), "No command specified."); + } + + opt.tmpfs_dirs.push_back("/tmp"); + + if (opt.working_dir == NULL) { + opt.working_dir = getcwd(NULL, 0); + } +} diff --git a/src/main/tools/linux-sandbox-options.h b/src/main/tools/linux-sandbox-options.h new file mode 100644 index 0000000000..57004a8206 --- /dev/null +++ b/src/main/tools/linux-sandbox-options.h @@ -0,0 +1,55 @@ +// Copyright 2016 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. + +#ifndef LINUX_SANDBOX_OPTIONS_H__ +#define LINUX_SANDBOX_OPTIONS_H__ + +#include <stdbool.h> +#include <stddef.h> + +#include <vector> + +// Options parsing result. +struct Options { + // Working directory (-W) + const char *working_dir; + // How long to wait before killing the child (-T) + int timeout_secs; + // How long to wait before sending SIGKILL in case of timeout (-t) + int kill_delay_secs; + // Where to redirect stdout (-l) + const char *stdout_path; + // Where to redirect stderr (-L) + const char *stderr_path; + // Files to make writable for the sandboxed process (-w) + std::vector<const char *> writable_files; + // Files to make inaccessible for the sandboxed process (-i) + std::vector<const char *> inaccessible_files; + // Directories where to mount an empty tmpfs (-e) + std::vector<const char *> tmpfs_dirs; + // Create a new network namespace (-N) + bool create_netns; + // Pretend to be root inside the namespace (-R) + bool fake_root; + // Print debugging messages (-D) + bool debug; + // Command to run (--) + std::vector<char *> args; +}; + +extern struct Options opt; + +void ParseOptions(int argc, char *argv[]); + +#endif diff --git a/src/main/tools/linux-sandbox-pid1.cc b/src/main/tools/linux-sandbox-pid1.cc new file mode 100644 index 0000000000..1dd049a1f4 --- /dev/null +++ b/src/main/tools/linux-sandbox-pid1.cc @@ -0,0 +1,506 @@ +// Copyright 2016 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. + +/** + * This is PID 1 inside the sandbox environment and runs in a separate user, + * mount, UTS, IPC and PID namespace. + */ + +#include "linux-sandbox-options.h" +#include "linux-sandbox-utils.h" +#include "linux-sandbox.h" + +// Note that we define DIE() here and not in a shared header, because we want to +// use _exit() in the +// pid1 child, but exit() in the parent. +#define DIE(args...) \ + { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \ + fprintf(stderr, "\": "); \ + perror(NULL); \ + _exit(EXIT_FAILURE); \ + } + +#include <errno.h> +#include <fcntl.h> +#include <libgen.h> +#include <math.h> +#include <mntent.h> +#include <net/if.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +static int global_child_pid; +static char global_inaccessible_directory[] = "/tmp/empty.XXXXXX"; +static char global_inaccessible_file[] = "/tmp/empty.XXXXXX"; + +static void SetupSelfDestruction(int *sync_pipe) { + // We could also poll() on the pipe fd to find out when the parent goes away, + // and rely on SIGCHLD interrupting that otherwise. That might require us to + // install some trivial handler for SIGCHLD. Using O_ASYNC to turn the pipe + // close into SIGIO may also work. Another option is signalfd, although that's + // almost as obscure as this prctl. + if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) { + DIE("prctl"); + } + + // Verify that the parent still lives. + char buf = 0; + if (close(sync_pipe[0]) < 0) { + DIE("close"); + } + if (write(sync_pipe[1], &buf, 1) < 0) { + DIE("write"); + } + if (close(sync_pipe[1]) < 0) { + DIE("close"); + } +} + +static void SetupMountNamespace() { + // Fully isolate our mount namespace private from outside events, so that + // mounts in the outside environment do not affect our sandbox. + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) { + DIE("mount"); + } +} + +static void WriteFile(const char *filename, const char *fmt, ...) { + FILE *stream = fopen(filename, "w"); + if (stream == NULL) { + DIE("fopen(%s)", filename); + } + + va_list ap; + va_start(ap, fmt); + int r = vfprintf(stream, fmt, ap); + va_end(ap); + + if (r < 0) { + DIE("vfprintf"); + } + + if (fclose(stream) != 0) { + DIE("fclose(%s)", filename); + } +} + +static void SetupUserNamespace() { + // Disable needs for CAP_SETGID. + struct stat sb; + if (stat("/proc/self/setgroups", &sb) == 0) { + WriteFile("/proc/self/setgroups", "deny"); + } else { + // Ignore ENOENT, because older Linux versions do not have this file (but + // also do not require writing to it). + if (errno != ENOENT) { + DIE("stat(/proc/self/setgroups"); + } + } + + int inner_uid = 0, inner_gid = 0; + if (!opt.fake_root) { + struct passwd *pwd = getpwnam("nobody"); + if (pwd == NULL) { + DIE("unable to find passwd entry for user nobody") + } + + inner_uid = pwd->pw_uid; + inner_gid = pwd->pw_gid; + } + + WriteFile("/proc/self/uid_map", "%d %d 1\n", inner_uid, global_outer_uid); + WriteFile("/proc/self/gid_map", "%d %d 1\n", inner_gid, global_outer_gid); +} + +static void SetupUtsNamespace() { + if (sethostname("sandbox", 7) < 0) { + DIE("sethostname"); + } + + if (setdomainname("sandbox", 7) < 0) { + DIE("setdomainname"); + } +} + +static void SetupHelperFiles() { + if (mkdtemp(global_inaccessible_directory) == NULL) { + DIE("mkdtemp(%s)", global_inaccessible_directory); + } + if (chmod(global_inaccessible_directory, 0) < 0) { + DIE("chmod(%s, 0)", global_inaccessible_directory); + } + + int handle = mkstemp(global_inaccessible_file); + if (handle < 0) { + DIE("mkstemp(%s)", global_inaccessible_file); + } + if (fchmod(handle, 0)) { + DIE("fchmod(%s, 0)", global_inaccessible_file); + } + if (close(handle) < 0) { + DIE("close(%s)", global_inaccessible_file); + } +} + +static void MountFilesystems() { + if (mount("/", global_sandbox_root, NULL, MS_BIND | MS_REC, NULL) < 0) { + DIE("mount(/, %s, NULL, MS_BIND | MS_REC, NULL)", global_sandbox_root); + } + + if (chdir(global_sandbox_root) < 0) { + DIE("chdir(%s)", global_sandbox_root); + } + + for (const char *tmpfs_dir : opt.tmpfs_dirs) { + PRINT_DEBUG("tmpfs: %s", tmpfs_dir); + if (mount("tmpfs", tmpfs_dir + 1, "tmpfs", + MS_NOSUID | MS_NODEV | MS_NOATIME, NULL) < 0) { + DIE("mount(tmpfs, %s, tmpfs, MS_NOSUID | MS_NODEV | MS_NOATIME, NULL)", + tmpfs_dir + 1); + } + } + + // Make sure that our working directory is a mount point. The easiest way to + // do this is by bind-mounting it upon itself. + if (mount(opt.working_dir, opt.working_dir + 1, NULL, MS_BIND, NULL) < 0) { + DIE("mount(%s, %s, NULL, MS_BIND, NULL)", opt.working_dir, + opt.working_dir + 1); + } + + for (const char *writable_file : opt.writable_files) { + PRINT_DEBUG("writable: %s", writable_file); + if (mount(writable_file, writable_file + 1, NULL, MS_BIND, NULL) < 0) { + DIE("mount(%s, %s, NULL, MS_BIND, NULL)", writable_file, + writable_file + 1); + } + } + + SetupHelperFiles(); + + for (const char *inaccessible_file : opt.inaccessible_files) { + struct stat sb; + if (stat(inaccessible_file, &sb) < 0) { + DIE("stat(%s)", inaccessible_file); + } + + if (S_ISDIR(sb.st_mode)) { + PRINT_DEBUG("inaccessible dir: %s", inaccessible_file); + if (mount(global_inaccessible_directory, inaccessible_file + 1, NULL, + MS_BIND, NULL) < 0) { + DIE("mount(%s, %s, NULL, MS_BIND, NULL)", global_inaccessible_directory, + inaccessible_file + 1); + } + } else { + PRINT_DEBUG("inaccessible file: %s", inaccessible_file); + if (mount(global_inaccessible_file, inaccessible_file + 1, NULL, MS_BIND, + NULL) < 0) { + DIE("mount(%s, %s, NULL, MS_BIND, NULL", global_inaccessible_file, + inaccessible_file + 1); + } + } + } +} + +// We later remount everything read-only, except the paths for which this method +// returns true. +static bool ShouldBeWritable(char *mnt_dir) { + mnt_dir += strlen(global_sandbox_root); + + if (strcmp(mnt_dir, opt.working_dir) == 0) { + return true; + } + + for (const char *writable_file : opt.writable_files) { + if (strcmp(mnt_dir, writable_file) == 0) { + return true; + } + } + + for (const char *tmpfs_dir : opt.tmpfs_dirs) { + if (strcmp(mnt_dir, tmpfs_dir) == 0) { + return true; + } + } + + return false; +} + +// Makes the whole filesystem read-only, except for the paths for which +// ShouldBeWritable returns true. +static void MakeFilesystemMostlyReadOnly() { + FILE *mounts = setmntent("/proc/self/mounts", "r"); + if (mounts == NULL) { + DIE("setmntent"); + } + + struct mntent *ent; + while ((ent = getmntent(mounts)) != NULL) { + // Skip mounts that do not belong to our sandbox. + if (strstr(ent->mnt_dir, global_sandbox_root) != ent->mnt_dir) { + continue; + } + + int mountFlags = MS_BIND | MS_REMOUNT; + + // MS_REMOUNT does not allow us to change certain flags. This means, we have + // to first read them out and then pass them in back again. There seems to + // be no better way than this (an API for just getting the mount flags of a + // mount entry as a bitmask would be great). + if (hasmntopt(ent, "nodev") != NULL) { + mountFlags |= MS_NODEV; + } + if (hasmntopt(ent, "noexec") != NULL) { + mountFlags |= MS_NOEXEC; + } + if (hasmntopt(ent, "nosuid") != NULL) { + mountFlags |= MS_NOSUID; + } + if (hasmntopt(ent, "noatime") != NULL) { + mountFlags |= MS_NOATIME; + } + if (hasmntopt(ent, "nodiratime") != NULL) { + mountFlags |= MS_NODIRATIME; + } + if (hasmntopt(ent, "relatime") != NULL) { + mountFlags |= MS_RELATIME; + } + + if (!ShouldBeWritable(ent->mnt_dir)) { + mountFlags |= MS_RDONLY; + } + + PRINT_DEBUG("remount %s: %s", (mountFlags & MS_RDONLY) ? "ro" : "rw", + ent->mnt_dir); + if (mount(NULL, ent->mnt_dir, NULL, mountFlags, NULL) < 0) { + // If we get EACCES, this might be a mount-point for which we don't have + // read access. Not much we can do about this, but it also won't do any + // harm, so let's go on. The same goes for EINVAL, which is fired in case + // a later mount overlaps an earlier mount, e.g. consider the case of + // /proc, /proc/sys/fs/binfmt_misc and /proc, with the latter /proc being + // the one that an outer sandbox has mounted on top of its parent /proc. + // In that case, we're not allowed to remount /proc/sys/fs/binfmt_misc, + // because it is hidden. + if (errno != EACCES && errno != EINVAL) { + DIE("remount(NULL, %s, NULL, %d, NULL)", ent->mnt_dir, mountFlags); + } + } + } + + endmntent(mounts); +} + +static void MountProc() { + // Mount a new proc on top of the old one, because the old one still refers to + // our parent PID namespace. + if (mount("proc", "proc", "proc", MS_NODEV | MS_NOEXEC | MS_NOSUID, NULL) < + 0) { + DIE("mount"); + } +} + +static void SetupNetworking() { + // When running in a separate network namespace, enable the loopback interface + // because some application may want to use it. + if (opt.create_netns) { + int fd; + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + DIE("socket"); + } + + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, "lo", IF_NAMESIZE); + + // Verify that name is valid. + if (if_nametoindex(ifr.ifr_name) == 0) { + DIE("if_nametoindex"); + } + + // Enable the interface. + ifr.ifr_flags |= IFF_UP; + if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) { + DIE("ioctl"); + } + + if (close(fd) < 0) { + DIE("close"); + } + } +} + +static void EnterSandbox() { + // Move the real root to old_root, then detach it. + char old_root[] = "tmp/old-root-XXXXXX"; + if (mkdtemp(old_root) == NULL) { + DIE("mkdtemp(%s)", old_root); + } + + // pivot_root has no wrapper in libc, so we need syscall() + if (syscall(SYS_pivot_root, ".", old_root) < 0) { + DIE("pivot_root(., %s)", old_root); + } + + if (chroot(".") < 0) { + DIE("chroot(.)"); + } + + if (umount2(old_root, MNT_DETACH) < 0) { + DIE("umount2(%s, MNT_DETACH)", old_root); + } + + if (rmdir(old_root) < 0) { + DIE("rmdir(%s)", old_root); + } + + if (chdir(opt.working_dir) < 0) { + DIE("chdir(%s)", opt.working_dir); + } +} + +static void InstallSignalHandler(int signum, void (*handler)(int)) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + if (sigemptyset(&sa.sa_mask) < 0) { + DIE("sigemptyset"); + } + if (sigaction(signum, &sa, NULL) < 0) { + DIE("sigaction"); + } +} + +static void IgnoreSignal(int signum) { InstallSignalHandler(signum, SIG_IGN); } + +static void InstallDefaultSignalHandler(int signum) { + InstallSignalHandler(signum, SIG_DFL); +} + +static void SpawnChild() { + // Ignore SIGTTIN / SIGTTOU in PID 1, as we're about to hand off the terminal + // to the child. A big thanks to @krallin for figuring out the intricacies of + // dealing with these signals and documenting it on + // http://curiousthing.org/sigttin-sigttou-deep-dive-linux. + IgnoreSignal(SIGTTIN); + IgnoreSignal(SIGTTOU); + + global_child_pid = fork(); + + if (global_child_pid < 0) { + DIE("fork()"); + } else if (global_child_pid == 0) { + // Put the child into its own process group. + if (setpgid(0, 0) < 0) { + DIE("setpgid"); + } + + // Try to assign our terminal to the child process. + if (tcsetpgrp(STDIN_FILENO, getpgrp()) < 0 && errno != ENOTTY) { + DIE("tcsetpgrp") + } + + // Restore handlers for SIGTTIN / SIGTTOU. + InstallDefaultSignalHandler(SIGTTIN); + InstallDefaultSignalHandler(SIGTTOU); + + // Force umask to include read and execute for everyone, to make output + // permissions predictable. + umask(022); + + // argv[] passed to execve() must be a null-terminated array. + opt.args.push_back(nullptr); + + if (execvp(opt.args[0], opt.args.data()) < 0) { + DIE("execvp(%p, %p)", opt.args[0], opt.args.data()); + } + } +} + +static void HandleSignal(int signum) { + if (signum == SIGCHLD) { + // Our child process or one of its children died. + int status; + pid_t killed_pid; + while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) { + if (killed_pid == global_child_pid) { + // If the child process we spawned earlier terminated, we'll also + // terminate. We can simply _exit() here, because the Linux kernel will + // kindly SIGKILL all remaining processes in our PID namespace once we + // exit. + if (WIFSIGNALED(status)) { + _exit(128 + WTERMSIG(status)); + } else { + _exit(WEXITSTATUS(status)); + } + } + } + } else { + kill(-global_child_pid, signum); + } +} + +static void WaitForChild() { + sigset_t all_signals; + if (sigfillset(&all_signals) < 0) { + DIE("sigfillset"); + } + if (sigdelset(&all_signals, SIGTTIN) < 0) { + DIE("sigdelset"); + } + if (sigdelset(&all_signals, SIGTTOU) < 0) { + DIE("sigdelset"); + } + if (sigprocmask(SIG_BLOCK, &all_signals, NULL) < 0) { + DIE("sigprocmask"); + } + + while (1) { + int signum; + sigwait(&all_signals, &signum); + HandleSignal(signum); + } +} + +int Pid1Main(void *sync_pipe_param) { + if (getpid() != 1) { + DIE("Using PID namespaces, but we are not PID 1"); + } + + SetupSelfDestruction(reinterpret_cast<int *>(sync_pipe_param)); + SetupMountNamespace(); + SetupUserNamespace(); + SetupUtsNamespace(); + MountFilesystems(); + MakeFilesystemMostlyReadOnly(); + MountProc(); + SetupNetworking(); + EnterSandbox(); + SpawnChild(); + WaitForChild(); + _exit(EXIT_FAILURE); +} diff --git a/src/main/tools/network-tools.h b/src/main/tools/linux-sandbox-pid1.h index 9c90aabb25..507739a08e 100644 --- a/src/main/tools/network-tools.h +++ b/src/main/tools/linux-sandbox-pid1.h @@ -1,4 +1,4 @@ -// Copyright 2015 The Bazel Authors. All rights reserved. +// Copyright 2016 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. @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef NETWORK_TOOLS_H__ -#define NETWORK_TOOLS_H__ +#ifndef LINUX_SANDBOX_PID1_H__ +#define LINUX_SANDBOX_PID1_H__ -// Bring up the given network interface like "lo". -void BringupInterface(const char *name); +int Pid1Main(void *sync_pipe_param); -#endif // NETWORK_TOOLS_H__ +#endif diff --git a/src/main/tools/linux-sandbox-utils.h b/src/main/tools/linux-sandbox-utils.h new file mode 100644 index 0000000000..3a4a1bff6c --- /dev/null +++ b/src/main/tools/linux-sandbox-utils.h @@ -0,0 +1,30 @@ +// Copyright 2016 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. + +#ifndef LINUX_SANDBOX_UTILS_H__ +#define LINUX_SANDBOX_UTILS_H__ + +#define S(x) #x +#define S_(x) S(x) +#define S__LINE__ S_(__LINE__) + +#define PRINT_DEBUG(...) \ + do { \ + if (opt.debug) { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } \ + } while (0) + +#endif diff --git a/src/main/tools/linux-sandbox.c b/src/main/tools/linux-sandbox.c deleted file mode 100644 index ff75f057ba..0000000000 --- a/src/main/tools/linux-sandbox.c +++ /dev/null @@ -1,815 +0,0 @@ -// 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. - -#define _GNU_SOURCE - -#include <errno.h> -#include <fcntl.h> -#include <ftw.h> -#include <libgen.h> -#include <limits.h> -#include <pwd.h> -#include <sched.h> -#include <signal.h> -#include <stdarg.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/mount.h> -#include <sys/stat.h> -#include <sys/syscall.h> -#include <sys/types.h> -#include <sys/wait.h> -#include <unistd.h> - -#include "network-tools.h" -#include "process-tools.h" - -#define PRINT_DEBUG(...) \ - do { \ - if (opt.debug) { \ - fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \ - } \ - } while (0) - -// The username of 'nobody'. -static const char *kNobodyUsername = "nobody"; - -// Options parsing result. -struct Options { - const char *sandbox_root; // Sandbox root (-S) - const char *working_dir; // Working directory (-W) - double timeout_secs; // How long to wait before killing the child (-T) - double kill_delay_secs; // How long to wait before sending SIGKILL in case of - // timeout (-t) - - char **create_dirs; // empty dirs to create (-d) - size_t create_dirs_size; // How many elements in create_dirs - int num_create_dirs; // How many empty dirs to create were specified - - char **mount_sources; // Map of directories to mount, from (-M) - char **mount_targets; // sources -> targets (-m) - size_t mount_map_sizes; // How many elements in mount_{sources,targets} - int num_mounts; // How many mounts were specified - - int create_netns; // If 1, create a new network namespace (-n) - int fake_root; // Pretend to be root inside the namespace (-r) - bool debug; // Whether to print debugging messages (-D) - const char *stdout_path; // Where to redirect stdout (-l) - const char *stderr_path; // Where to redirect stderr (-L) - char *const *args; // Command to run (--) -}; - -static int global_child_pid; -static volatile sig_atomic_t global_signal; - -static struct Options opt; - -// Child function used by CheckNamespacesSupported() in call to clone(). -static int CheckNamespacesSupportedChild(void *arg) { return 0; } - -// Check whether the required namespaces are supported. -static int CheckNamespacesSupported() { - const int stackSize = 1024 * 1024; - char *stack; - char *stackTop; - pid_t pid; - - // Allocate stack for child. - stack = malloc(stackSize); - if (stack == NULL) { - DIE("malloc failed\n"); - } - - // Assume stack grows downward. - stackTop = stack + stackSize; - - // Create child with own namespaces. We use clone() instead of unshare() here - // because of the kernel bug (ref. CreateNamespaces) that lets unshare fail - // sometimes. As this check has to run as fast as possible, we can't afford to - // spend time sleeping and retrying here until it eventually works (or not). - CHECK_CALL(pid = clone(CheckNamespacesSupportedChild, stackTop, - CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | - CLONE_NEWIPC | CLONE_NEWNET | SIGCHLD, - NULL)); - CHECK_CALL(waitpid(pid, NULL, 0)); - - return EXIT_SUCCESS; -} - -// Print out a usage error. argc and argv are the argument counter and vector, -// fmt is a format, -// string for the error message to print. -static void Usage(int argc, char *const *argv, const char *fmt, ...) { - int i; - va_list ap; - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - - fprintf(stderr, "\nUsage: %s [-S sandbox-root] -- command arg1\n", argv[0]); - fprintf(stderr, " provided:"); - for (i = 0; i < argc; i++) { - fprintf(stderr, " %s", argv[i]); - } - fprintf( - stderr, - "\nMandatory arguments:\n" - " -S <sandbox-root> directory which will become the root of the " - "sandbox\n" - " -- command to run inside sandbox, followed by arguments\n" - "\n" - "Optional arguments:\n" - " -W <working-dir> working directory\n" - " -T <timeout> timeout after which the child process will be " - "terminated with SIGTERM\n" - " -t <timeout> in case timeout occurs, how long to wait before killing " - "the child with SIGKILL\n" - " -d <dir> create an empty directory in the sandbox\n" - " -M/-m <source/target> system directory to mount inside the sandbox\n" - " Multiple directories can be specified and each of them will be " - "mounted readonly.\n" - " The -M option specifies which directory to mount, the -m option " - "specifies where to\n" - " mount it in the sandbox.\n" - " -n if set, a new network namespace will be created\n" - " -r if set, make the uid/gid be root, otherwise use nobody\n" - " -D if set, debug info will be printed\n" - " -l <file> redirect stdout to a file\n" - " -L <file> redirect stderr to a file\n" - " @FILE read newline-separated arguments from FILE\n"); - exit(EXIT_FAILURE); -} - -// Deals with an unfinished (source but no target) mapping in opt. -// Also adds a new unfinished mapping if source is not NULL. -static void AddMountSource(char *source) { - // The last -M flag wasn't followed by an -m flag, so assume that the source - // should be mounted in the sandbox in the same path as outside. - if (opt.mount_sources[opt.num_mounts] != NULL) { - opt.mount_targets[opt.num_mounts] = opt.mount_sources[opt.num_mounts]; - opt.num_mounts++; - } - - if (source != NULL) { - if (opt.num_mounts >= opt.mount_map_sizes - 1) { - opt.mount_sources = - realloc(opt.mount_sources, opt.mount_map_sizes * sizeof(char *) * 2); - if (opt.mount_sources == NULL) { - DIE("realloc failed\n"); - } - memset(opt.mount_sources + opt.mount_map_sizes, 0, - opt.mount_map_sizes * sizeof(char *)); - opt.mount_targets = - realloc(opt.mount_targets, opt.mount_map_sizes * sizeof(char *) * 2); - if (opt.mount_targets == NULL) { - DIE("realloc failed\n"); - } - memset(opt.mount_targets + opt.mount_map_sizes, 0, - opt.mount_map_sizes * sizeof(char *)); - opt.mount_map_sizes *= 2; - } - opt.mount_sources[opt.num_mounts] = source; - } -} - -static void AddCreateDir(char *create_dir) { - if (opt.num_create_dirs > opt.create_dirs_size - 1) { - opt.create_dirs = - realloc(opt.create_dirs, opt.create_dirs_size * sizeof(char *) * 2); - if (opt.create_dirs == NULL) { - DIE("realloc failed\n"); - } - memset(opt.create_dirs + opt.create_dirs_size, 0, - opt.create_dirs_size * sizeof(char *)); - opt.create_dirs_size *= 2; - } - - opt.create_dirs[opt.num_create_dirs++] = create_dir; -} - -static void ParseCommandLine(int argc, char *const *argv); - -// Parses command line flags from a file named filename. -// Expects optind to be initialized to 0 before being called. -static void ParseOptionsFile(const char *filename) { - FILE *const options_file = fopen(filename, "rb"); - if (options_file == NULL) { - DIE("opening argument file %s failed\n", filename); - } - size_t sub_argv_size = 20; - char **sub_argv = malloc(sizeof(char *) * sub_argv_size); - sub_argv[0] = ""; - int sub_argc = 1; - - bool done = false; - while (!done) { - // This buffer determines the maximum size of arguments we can handle out of - // the file. We DIE down below if it's ever too short. - // 4096 is a common value for PATH_MAX. However, many filesystems support - // arbitrarily long pathnames, so this might not be long enough to handle an - // arbitrary filename no matter what. Twice the usual PATH_MAX seems - // reasonable for now. - char argument[8192]; - if (fgets(argument, sizeof(argument), options_file) == NULL) { - if (feof(options_file)) { - done = true; - continue; - } else { - DIE("reading from argument file %s failed\n", filename); - } - } - const size_t length = strlen(argument); - if (length == 0) continue; - if (length == sizeof(argument)) { - DIE("argument from file %s is too long (> %zu)\n", filename, - sizeof(argument)); - } - if (argument[length - 1] == '\n') { - argument[length - 1] = '\0'; - } else { - done = true; - } - if (sub_argv_size == sub_argc + 1) { - sub_argv_size *= 2; - sub_argv = realloc(sub_argv, sizeof(char *) * sub_argv_size); - } - sub_argv[sub_argc++] = strdup(argument); - } - if (fclose(options_file) != 0) { - DIE("closing options file %s failed\n", filename); - } - sub_argv[sub_argc] = NULL; - - ParseCommandLine(sub_argc, sub_argv); -} - -// Parses command line flags from an argv array and puts the results into an -// Options structure -// passed in as an argument. -static void ParseCommandLine(int argc, char *const *argv) { - extern char *optarg; - extern int optind, optopt; - int c; - - while ((c = getopt(argc, argv, ":CS:W:T:t:d:M:m:nrDl:L:")) != -1) { - switch (c) { - case 'C': - // Shortcut for the "does this system support sandboxing" check. - exit(CheckNamespacesSupported()); - break; - case 'S': - if (opt.sandbox_root == NULL) { - char *sandbox_root = strdup(optarg); - - // Make sure that the sandbox_root path has no trailing slash. - if (sandbox_root[strlen(sandbox_root) - 1] == '/') { - sandbox_root[strlen(sandbox_root) - 1] = 0; - } - - opt.sandbox_root = sandbox_root; - } else { - Usage(argc, argv, - "Multiple sandbox roots (-S) specified, expected one."); - } - break; - case 'W': - if (opt.working_dir == NULL) { - opt.working_dir = strdup(optarg); - } else { - Usage(argc, argv, - "Multiple working directories (-W) specified, expected one."); - } - break; - case 'T': - if (sscanf(optarg, "%lf", &opt.timeout_secs) != 1 || - opt.timeout_secs < 0) { - Usage(argc, argv, "Invalid timeout (-T) value: %lf", - opt.timeout_secs); - } - break; - case 't': - if (sscanf(optarg, "%lf", &opt.kill_delay_secs) != 1 || - opt.kill_delay_secs < 0) { - Usage(argc, argv, "Invalid kill delay (-t) value: %lf", - opt.kill_delay_secs); - } - break; - case 'd': - if (optarg[0] != '/') { - Usage(argc, argv, - "The -d option must be used with absolute paths only."); - } - AddCreateDir(optarg); - break; - case 'M': - if (optarg[0] != '/') { - Usage(argc, argv, - "The -M option must be used with absolute paths only."); - } - AddMountSource(optarg); - break; - case 'm': - if (optarg[0] != '/') { - Usage(argc, argv, - "The -m option must be used with absolute paths only."); - } - if (opt.mount_sources[opt.num_mounts] == NULL) { - Usage(argc, argv, "The -m option must be preceded by an -M option."); - } - opt.mount_targets[opt.num_mounts++] = optarg; - break; - case 'n': - opt.create_netns = 1; - break; - case 'r': - opt.fake_root = 1; - break; - case 'D': - opt.debug = true; - break; - case 'l': - if (opt.stdout_path == NULL) { - opt.stdout_path = optarg; - } else { - Usage(argc, argv, - "Cannot redirect stdout to more than one destination."); - } - break; - case 'L': - if (opt.stderr_path == NULL) { - opt.stderr_path = optarg; - } else { - Usage(argc, argv, - "Cannot redirect stderr to more than one destination."); - } - break; - case '?': - Usage(argc, argv, "Unrecognized argument: -%c (%d)", optopt, optind); - break; - case ':': - Usage(argc, argv, "Flag -%c requires an argument", optopt); - break; - } - } - - AddMountSource(NULL); - - while (optind < argc && argv[optind][0] == '@') { - const char *filename = argv[optind] + 1; - const int old_optind = optind; - optind = 0; - ParseOptionsFile(filename); - optind = old_optind + 1; - } - - if (argc > optind) { - if (opt.args == NULL) { - opt.args = argv + optind; - } else { - Usage(argc, argv, "Merging commands not supported."); - } - } -} - -// Handles parsing all command line flags and populates the global opt struct. -static void ParseOptions(int argc, char *const argv[]) { - memset(&opt, 0, sizeof(opt)); - // 16 elements is a sane default, will be realloc'd as needed anyway. - opt.mount_sources = calloc(16, sizeof(char *)); - opt.mount_targets = calloc(16, sizeof(char *)); - opt.mount_map_sizes = 16; - // We'll need at least two slots for homedir_from_env and homedir. - opt.create_dirs = calloc(2, sizeof(char *)); - opt.create_dirs_size = 2; - - ParseCommandLine(argc, argv); - - if (opt.args == NULL) { - Usage(argc, argv, "No command specified."); - } - - if (opt.sandbox_root == NULL) { - Usage(argc, argv, "Sandbox root (-S) must be specified"); - } -} - -static void CreateNamespaces(int create_netns) { - // This weird workaround is necessary due to unshare seldomly failing with - // EINVAL due to a race condition in the Linux kernel (see - // https://lkml.org/lkml/2015/7/28/833). An alternative would be to use - // clone/waitpid instead. - int delay = 1; - int tries = 0; - const int max_tries = 100; - while (tries++ < max_tries) { - if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | - (create_netns ? CLONE_NEWNET : 0)) == 0) { - PRINT_DEBUG("unshare succeeded after %d tries\n", tries); - return; - } else { - if (errno != EINVAL) { - perror("unshare"); - exit(EXIT_FAILURE); - } - } - - // Exponential back-off, but sleep at most 250ms. - usleep(delay); - if (delay < 250000) { - delay *= 2; - } - } - fprintf(stderr, - "unshare failed with EINVAL even after %d tries, giving up.\n", - tries); - exit(EXIT_FAILURE); -} - -static void CreateFile(const char *path) { - int handle; - CHECK_CALL(handle = open(path, O_CREAT | O_WRONLY | O_EXCL, 0666)); - CHECK_CALL(close(handle)); -} - -// Creates an empty file at 'path' by hard linking it from a known empty file. -// This is over two times faster than creating empty files via open() on -// certain filesystems (e.g. XFS). -static void LinkFile(const char *path) { - CHECK_CALL(link("tmp/empty_file", path)); -} - -// Recursively creates the file or directory specified in "path" and its parent -// directories. -static int CreateTarget(const char *path, bool is_directory) { - if (path == NULL) { - errno = EINVAL; - return -1; - } - - struct stat sb; - // If the path already exists... - if (stat(path, &sb) == 0) { - if (is_directory && S_ISDIR(sb.st_mode)) { - // and it's a directory and supposed to be a directory, we're done here. - return 0; - } else if (!is_directory && S_ISREG(sb.st_mode)) { - // and it's a regular file and supposed to be one, we're done here. - return 0; - } else { - // otherwise something is really wrong. - errno = is_directory ? ENOTDIR : EEXIST; - return -1; - } - } else { - // If stat failed because of any error other than "the path does not exist", - // this is an error. - if (errno != ENOENT) { - return -1; - } - } - - // Create the parent directory. - CHECK_CALL(CreateTarget(dirname(strdupa(path)), true)); - - if (is_directory) { - CHECK_CALL(mkdir(path, 0755)); - } else { - LinkFile(path); - } - - return 0; -} - -static void SetupDevices() { - CHECK_CALL(CreateTarget("dev", true)); - const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero", - NULL}; - for (int i = 0; devs[i] != NULL; i++) { - LinkFile(devs[i] + 1); - CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL)); - } - - CHECK_CALL(symlink("/proc/self/fd", "dev/fd")); -} - -static int rmrf(const char *fpath, const struct stat *sb, int typeflag, - struct FTW *ftwbuf) { - if (typeflag == FTW_DP) { - return rmdir(fpath); - } else { - return unlink(fpath); - } -} - -static void SetupDirectories() { - // If in sandbox_debug mode and debugging, create the sandbox root dir first - if (opt.debug && isatty(fileno(stdin))) { - // Enter sandbox_debug mode a second time, delete old sandbox - struct stat sb; - int err = stat(opt.sandbox_root, &sb); - if (err == 0) { - CHECK_CALL(nftw(opt.sandbox_root, *rmrf, sysconf(_SC_OPEN_MAX), - FTW_DEPTH | FTW_PHYS)); - } else if (errno != ENOENT) { - CHECK_CALL(err); - } - - CHECK_CALL(mkdir(opt.sandbox_root, 0755)); - } - - // Mount the sandbox and go there. - CHECK_CALL(mount(opt.sandbox_root, opt.sandbox_root, NULL, - MS_BIND | MS_NOSUID, NULL)); - CHECK_CALL(chdir(opt.sandbox_root)); - - // This is used as the base for hardlinking the input files. - CHECK_CALL(CreateTarget("tmp", true)); - CreateFile("tmp/empty_file"); - - // Setup /dev. - SetupDevices(); - - CHECK_CALL(CreateTarget("proc", true)); - CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL)); - - // Make sure the home directory exists, too. - char *homedir_from_env = getenv("HOME"); - if (homedir_from_env != NULL) { - if (homedir_from_env[0] != '/') { - DIE("Home directory specified in $HOME must be an absolute path, but is " - "%s", - homedir_from_env); - } - if (strcmp(homedir_from_env, "/") != 0) { - AddCreateDir(homedir_from_env); - } - } - - errno = 0; - struct passwd *uid_passwd = getpwuid(getuid()); - if (uid_passwd == NULL) { - if (errno != 0) { - perror("getpwuid(getuid())"); - exit(EXIT_FAILURE); - } else { - DIE("UID %d not found in passwd file\n", (int)getuid()); - } - } - char *homedir = uid_passwd->pw_dir; - if (homedir != NULL && - (homedir_from_env == NULL || strcmp(homedir_from_env, homedir) != 0)) { - if (homedir[0] != '/') { - DIE("Home directory of user nobody must be an absolute path, but is %s", - homedir); - } - if (strcmp(homedir, "/") != 0) { - AddCreateDir(homedir); - } - } - - // Create needed directories. - for (int i = 0; i < opt.num_create_dirs; i++) { - if (opt.debug) { - PRINT_DEBUG("createdir: %s\n", opt.create_dirs[i]); - } - CHECK_CALL(CreateTarget(opt.create_dirs[i] + 1, true)); - } - - // Mount all mounts. - for (int i = 0; i < opt.num_mounts; i++) { - struct stat sb; - stat(opt.mount_sources[i], &sb); - - if (opt.debug) { - if (strcmp(opt.mount_sources[i], opt.mount_targets[i]) == 0) { - // The file is mounted to the same path inside the sandbox, as outside - // (e.g. /home/user -> <sandbox>/home/user), so we'll just show a - // simplified version of the mount command. - PRINT_DEBUG("mount: %s\n", opt.mount_sources[i]); - } else { - // The file is mounted to a custom location inside the sandbox. - // Create a user-friendly string for the sandboxed path and show it. - char *user_friendly_mount_target = - malloc(strlen("<sandbox>") + strlen(opt.mount_targets[i]) + 1); - strcpy(user_friendly_mount_target, "<sandbox>"); - strcat(user_friendly_mount_target, opt.mount_targets[i]); - PRINT_DEBUG("mount: %s -> %s\n", opt.mount_sources[i], - user_friendly_mount_target); - free(user_friendly_mount_target); - } - } - - char *full_sandbox_path = - malloc(strlen(opt.sandbox_root) + strlen(opt.mount_targets[i]) + 1); - strcpy(full_sandbox_path, opt.sandbox_root); - strcat(full_sandbox_path, opt.mount_targets[i]); - CHECK_CALL(CreateTarget(full_sandbox_path, S_ISDIR(sb.st_mode))); - CHECK_CALL(mount(opt.mount_sources[i], full_sandbox_path, NULL, - MS_REC | MS_BIND | MS_RDONLY, NULL)); - free(full_sandbox_path); - } -} - -// Write the file "filename" using a format string specified by "fmt". Returns -// -1 on failure. -static int WriteFile(const char *filename, const char *fmt, ...) { - int r; - va_list ap; - FILE *stream = fopen(filename, "w"); - if (stream == NULL) { - return -1; - } - va_start(ap, fmt); - r = vfprintf(stream, fmt, ap); - va_end(ap); - if (r >= 0) { - r = fclose(stream); - } - return r; -} - -static void SetupUserNamespace(int uid, int gid, int new_uid, int new_gid) { - // Disable needs for CAP_SETGID - int r = WriteFile("/proc/self/setgroups", "deny"); - if (r < 0 && errno != ENOENT) { - // Writing to /proc/self/setgroups might fail on earlier - // version of linux because setgroups does not exist, ignore. - perror("WriteFile(\"/proc/self/setgroups\", \"deny\")"); - exit(EXIT_FAILURE); - } - - // Set group and user mapping from outer namespace to inner: - // No changes in the parent, be nobody in the child. - // - // We can't be root in the child, because some code may assume that running as - // root grants it certain capabilities that it doesn't in fact have. It's - // safer to let the child think that it is just a normal user. - CHECK_CALL(WriteFile("/proc/self/uid_map", "%d %d 1\n", new_uid, uid)); - CHECK_CALL(WriteFile("/proc/self/gid_map", "%d %d 1\n", new_gid, gid)); - - CHECK_CALL(setresuid(new_uid, new_uid, new_uid)); - CHECK_CALL(setresgid(new_gid, new_gid, new_gid)); -} - -static void SetupUserNamespaceForNobody(int uid, int gid) { - struct passwd *pwd = getpwnam(kNobodyUsername); - - if (pwd == NULL) { - perror("Unable to find passwd entry for user nobody."); - exit(EXIT_FAILURE); - } - - SetupUserNamespace(uid, gid, pwd->pw_uid, pwd->pw_gid); -} - -static void ChangeRoot() { - // move the real root to old_root, then detach it - char old_root[16] = "old-root-XXXXXX"; - if (mkdtemp(old_root) == NULL) { - perror("mkdtemp"); - DIE("mkdtemp returned NULL\n"); - } - - // pivot_root has no wrapper in libc, so we need syscall() - CHECK_CALL(syscall(SYS_pivot_root, ".", old_root)); - CHECK_CALL(chroot(".")); - CHECK_CALL(umount2(old_root, MNT_DETACH)); - CHECK_CALL(rmdir(old_root)); - - if (opt.working_dir != NULL) { - CHECK_CALL(chdir(opt.working_dir)); - } -} - -// Called when timeout or signal occurs. -void OnSignal(int sig) { - global_signal = sig; - - // Nothing to do if we received a signal before spawning the child. - if (global_child_pid == -1) { - return; - } - - if (sig == SIGALRM) { - // SIGALRM represents a timeout, so we should give the process a bit of - // time to die gracefully if it needs it. - KillEverything(global_child_pid, true, opt.kill_delay_secs); - } else { - // Signals should kill the process quickly, as it's typically blocking - // the return of the prompt after a user hits "Ctrl-C". - KillEverything(global_child_pid, false, opt.kill_delay_secs); - } -} - -// Run the command specified by the argv array and kill it after timeout -// seconds. -static void SpawnCommand(char *const *argv, double timeout_secs, - bool isFallback) { - for (int i = 0; argv[i] != NULL; i++) { - PRINT_DEBUG("arg: %s\n", argv[i]); - } - CHECK_CALL(global_child_pid = fork()); - if (global_child_pid == 0) { - // In child. - CHECK_CALL(setsid()); - ClearSignalMask(); - - // Force umask to include read and execute for everyone, to make - // output permissions predictable. - umask(022); - - // Does not return unless something went wrong. - CHECK_CALL(execvp(argv[0], argv)); - } else { - // In parent. - - // Set up a signal handler which kills all subprocesses when the given - // signal is triggered. - HandleSignal(SIGALRM, OnSignal); - HandleSignal(SIGTERM, OnSignal); - HandleSignal(SIGINT, OnSignal); - SetTimeout(timeout_secs); - - int status = WaitChild(global_child_pid, argv[0]); - - // The child is done for, but may have grandchildren that we still have to - // kill. - kill(-global_child_pid, SIGKILL); - - if (global_signal > 0) { - // Don't trust the exit code if we got a timeout or signal. - UnHandle(global_signal); - raise(global_signal); - } else if (WIFEXITED(status)) { - if (opt.debug && !isFallback && isatty(fileno(stdin)) && - WEXITSTATUS(status) > 0) { - char **cmdList = calloc(2, sizeof(char *)); - cmdList[0] = "/bin/bash"; - cmdList[1] = NULL; - SpawnCommand(cmdList, 0, true); - } - exit(WEXITSTATUS(status)); - } else { - int sig = WTERMSIG(status); - UnHandle(sig); - raise(sig); - } - } -} - -int main(int argc, char *const argv[]) { - ParseOptions(argc, argv); - - int uid = SwitchToEuid(); - int gid = SwitchToEgid(); - - RedirectStdout(opt.stdout_path); - RedirectStderr(opt.stderr_path); - - PRINT_DEBUG("sandbox root is %s\n", opt.sandbox_root); - PRINT_DEBUG("working dir is %s\n", - (opt.working_dir != NULL) ? opt.working_dir : "/ (default)"); - - CreateNamespaces(opt.create_netns); - - if (opt.create_netns) { - // Enable the loopback interface because some application may want - // to use it. - BringupInterface("lo"); - } - - // Make our mount namespace private, so that further mounts do not affect the - // outside environment. - CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL)); - - if (opt.fake_root) { - SetupUserNamespace(uid, gid, 0, 0); - } else { - SetupUserNamespaceForNobody(uid, gid); - } - - SetupDirectories(); - - ChangeRoot(); - - SpawnCommand(opt.args, opt.timeout_secs, false); - - free(opt.create_dirs); - free(opt.mount_sources); - free(opt.mount_targets); - - return 0; -} diff --git a/src/main/tools/linux-sandbox.cc b/src/main/tools/linux-sandbox.cc new file mode 100644 index 0000000000..8eeef841e6 --- /dev/null +++ b/src/main/tools/linux-sandbox.cc @@ -0,0 +1,289 @@ +// Copyright 2016 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. + +/** + * linux-sandbox runs commands in a restricted environment where they are + * subject to a few rules: + * + * - The entire filesystem is made read-only. + * - The working directory (-W) will be made read-write, though. + * - Individual files or directories can be made writable (but not deletable) + * (-w). + * - Individual files or directories can be made inaccessible / unreadable + * (-i). + * - tmpfs will be mounted on /tmp. + * - tmpfs can be mounted on top of existing directories (-e), too. + * - If the process takes longer than the timeout (-T), it will be killed with + * SIGTERM. If it does not exit within the grace period (-t), it all of its + * children will be killed with SIGKILL. + * - If linux-sandbox itself gets killed, the process and all of its children + * will be killed. + * - If linux-sandbox's parent dies, it will kill itself, the process and all + * the children. + * - Network access is allowed, but can be disabled via -N. + * - The process runs as user "nobody", unless fakeroot is enabled (-R). + * - The hostname and domainname will be set to "sandbox". + * - The process runs in its own PID namespace, so other processes on the + * system are invisible. + */ + +#include "linux-sandbox-options.h" +#include "linux-sandbox-pid1.h" +#include "linux-sandbox-utils.h" + +#define DIE(args...) \ + { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \ + fprintf(stderr, "\": "); \ + perror(NULL); \ + exit(EXIT_FAILURE); \ + } + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <math.h> +#include <sched.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <vector> + +int global_outer_uid; +int global_outer_gid; +char global_sandbox_root[] = "/tmp/sandbox.XXXXXX"; + +static int global_child_pid; + +// The signal that will be sent to the child when a timeout occurs. +static volatile sig_atomic_t global_next_timeout_signal = SIGTERM; + +// The signal that caused us to kill the child (e.g. on timeout). +static volatile sig_atomic_t global_signal; + +static void CloseFds() { + DIR *fds = opendir("/proc/self/fd"); + if (fds == NULL) { + DIE("opendir"); + } + + while (1) { + errno = 0; + struct dirent *dent = readdir(fds); + + if (dent == NULL) { + if (errno != 0) { + DIE("readdir"); + } + break; + } + + if (isdigit(dent->d_name[0])) { + errno = 0; + int fd = strtol(dent->d_name, nullptr, 10); + + // (1) Skip unparseable entries. + // (2) Close everything except stdin, stdout and stderr. + // (3) Do not accidentally close our directory handle. + if (errno == 0 && fd > STDERR_FILENO && fd != dirfd(fds)) { + if (close(fd) < 0) { + DIE("close"); + } + } + } + } + + if (closedir(fds) < 0) { + DIE("closedir"); + } +} + +static void SetupSandboxRoot() { + if (mkdtemp(global_sandbox_root) == NULL) { + DIE("mkdtemp(%s)", global_sandbox_root); + } +} + +static void RemoveSandboxRoot() { + if (rmdir(global_sandbox_root) < 0) { + DIE("rmdir(%s)", global_sandbox_root); + } +} + +static void HandleSignal(int signum, void (*handler)(int)) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + if (sigemptyset(&sa.sa_mask) < 0) { + DIE("sigemptyset"); + } + if (sigaction(signum, &sa, NULL) < 0) { + DIE("sigaction"); + } +} + +static void OnTimeout(int sig) { + global_signal = sig; + kill(global_child_pid, global_next_timeout_signal); + if (global_next_timeout_signal == SIGTERM && opt.kill_delay_secs > 0) { + global_next_timeout_signal = SIGKILL; + alarm(opt.kill_delay_secs); + } +} + +static void SpawnPid1() { + const int kStackSize = 1024 * 1024; + std::vector<char> child_stack(kStackSize); + + int sync_pipe[2]; + if (pipe(sync_pipe) < 0) { + DIE("pipe"); + } + + int clone_flags = CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | + CLONE_NEWPID | SIGCHLD; + if (opt.create_netns) { + clone_flags |= CLONE_NEWNET; + } + + // We use clone instead of unshare, because unshare sometimes fails with + // EINVAL due to a race condition in the Linux kernel (see + // https://lkml.org/lkml/2015/7/28/833). + global_child_pid = + clone(Pid1Main, child_stack.data() + kStackSize, clone_flags, sync_pipe); + if (global_child_pid < 0) { + DIE("clone"); + } + + // We close the write end of the sync pipe, read a byte and then close the + // pipe. This proves to the linux-sandbox-pid1 process that we still existed + // after it ran prctl(PR_SET_PDEATHSIG, SIGKILL), thus preventing a race + // condition where the parent is killed before that call was made. + char buf; + if (close(sync_pipe[1]) < 0) { + DIE("close"); + } + if (read(sync_pipe[0], &buf, 1) < 0) { + DIE("read"); + } + if (close(sync_pipe[0]) < 0) { + DIE("close"); + } +} + +static int WaitForPid1() { + int err, status; + do { + err = waitpid(global_child_pid, &status, 0); + } while (err < 0 && errno == EINTR); + + if (err < 0) { + DIE("waitpid"); + } + + if (global_signal > 0) { + // The child exited because we killed it due to receiving a signal + // ourselves. Do not trust the exitcode in this case, just calculate it from + // the signal. + PRINT_DEBUG("child exited due to us catching signal: %s", + strsignal(global_signal)); + return 128 + global_signal; + } else if (WIFSIGNALED(status)) { + PRINT_DEBUG("child exited due to receiving signal: %s", + strsignal(WTERMSIG(status))); + return 128 + WTERMSIG(status); + } else { + PRINT_DEBUG("child exited normally with exitcode %d", WEXITSTATUS(status)); + return WEXITSTATUS(status); + } +} + +static void Redirect(const char *target_path, int fd, const char *name) { + if (target_path != NULL && strcmp(target_path, "-") != 0) { + const int flags = O_WRONLY | O_CREAT | O_TRUNC | O_APPEND; + int fd_out = open(target_path, flags, 0666); + if (fd_out < 0) { + DIE("open(%s)", target_path); + } + // If we were launched with less than 3 fds (stdin, stdout, stderr) open, + // but redirection is still requested via a command-line flag, something is + // wacky and the following code would not do what we intend to do, so let's + // bail. + if (fd_out < 3) { + DIE("open(%s) returned a handle that is reserved for stdin / stdout / " + "stderr", + target_path); + } + if (dup2(fd_out, fd) < 0) { + DIE("dup2()"); + } + if (close(fd_out) < 0) { + DIE("close()"); + } + } +} + +static void RedirectStdout(const char *stdout_path) { + Redirect(stdout_path, STDOUT_FILENO, "stdout"); +} + +static void RedirectStderr(const char *stderr_path) { + Redirect(stderr_path, STDERR_FILENO, "stderr"); +} + +int main(int argc, char *argv[]) { + // Ask the kernel to kill us with SIGKILL if our parent dies. + if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) { + DIE("prctl"); + } + + ParseOptions(argc, argv); + + RedirectStdout(opt.stdout_path); + RedirectStderr(opt.stderr_path); + + // This should never be called as a setuid binary, drop privileges just in + // case. We don't need to be root, because we use user namespaces anyway. + if (setuid(getuid()) < 0) { + DIE("setuid"); + } + + global_outer_uid = getuid(); + global_outer_gid = getgid(); + + // Make sure the sandboxed process does not inherit any accidentally left open + // file handles from our parent. + CloseFds(); + + SetupSandboxRoot(); + atexit(RemoveSandboxRoot); + + HandleSignal(SIGALRM, OnTimeout); + if (opt.timeout_secs > 0) { + alarm(opt.timeout_secs); + } + + SpawnPid1(); + return WaitForPid1(); +} diff --git a/src/main/tools/linux-sandbox.h b/src/main/tools/linux-sandbox.h new file mode 100644 index 0000000000..5ff778da38 --- /dev/null +++ b/src/main/tools/linux-sandbox.h @@ -0,0 +1,22 @@ +// Copyright 2016 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. + +#ifndef LINUX_SANDBOX_H__ +#define LINUX_SANDBOX_H__ + +extern int global_outer_uid; +extern int global_outer_gid; +extern char global_sandbox_root[]; + +#endif diff --git a/src/main/tools/network-tools.c b/src/main/tools/network-tools.c deleted file mode 100644 index 16241cd64c..0000000000 --- a/src/main/tools/network-tools.c +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2015 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. - -#define _GNU_SOURCE - -#include <net/if.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/ioctl.h> -#include <sys/socket.h> -#include <sys/types.h> -#include <unistd.h> - -#include "process-tools.h" -#include "network-tools.h" - -void BringupInterface(const char *name) { - int fd; - - struct ifreq ifr; - - CHECK_CALL(fd = socket(AF_INET, SOCK_DGRAM, 0)); - - memset(&ifr, 0, sizeof(ifr)); - strncpy(ifr.ifr_name, name, IF_NAMESIZE); - - // Verify that name is valid. - CHECK_CALL(if_nametoindex(ifr.ifr_name)); - - // Enable the interface - ifr.ifr_flags |= IFF_UP; - CHECK_CALL(ioctl(fd, SIOCSIFFLAGS, &ifr)); - - CHECK_CALL(close(fd)); -} diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java index b2fe0e6720..182a62dc1e 100644 --- a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java +++ b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTest.java @@ -14,220 +14,27 @@ package com.google.devtools.build.lib.sandbox; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; - +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - /** * Tests for {@code LinuxSandboxedStrategy}. - * - * <p>The general idea for each test is to provide a file tree consisting of symlinks, directories - * and empty files and then handing that together with an arbitrary number of input files (what - * would be specified in the "srcs" attribute, for example) to the LinuxSandboxedStrategy. - * - * <p>The algorithm that processes the mounts must then always find (and thus mount) the expected - * tree of files given only the set of input files. */ @RunWith(JUnit4.class) public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase { - /** - * Strips the working directory (which can be very long) from the file names in the input map, to - * make assertion failures easier to read. - */ - private ImmutableMap<String, String> userFriendlyMap(Map<Path, Path> input) { - ImmutableMap.Builder<String, String> userFriendlyMap = ImmutableMap.builder(); - for (Entry<Path, Path> entry : input.entrySet()) { - String key = entry.getKey().getPathString().replace(workspaceDir.getPathString(), ""); - String value = entry.getValue().getPathString().replace(workspaceDir.getPathString(), ""); - userFriendlyMap.put(key, value); - } - return userFriendlyMap.build(); - } - - /** - * Takes a map of file specifications, creates the necessary files / symlinks / dirs, - * mounts files listed in customMount at their canonical location in the sandbox and returns the - * output of {@code LinuxSandboxedStrategy#fixMounts} for it. - */ - private ImmutableMap<String, String> userFriendlyMounts( - Map<String, String> linksAndFiles, List<String> customMounts) throws Exception { - return userFriendlyMap(mounts(linksAndFiles, customMounts)); - } - - private ImmutableMap<Path, Path> mounts( - Map<String, String> linksAndFiles, List<String> customMounts) throws Exception { - createTreeStructure(linksAndFiles); - - ImmutableMap.Builder<Path, Path> mounts = ImmutableMap.builder(); - for (String customMount : customMounts) { - Path customMountPath = workspaceDir.getRelative(customMount); - mounts.put(customMountPath, customMountPath); - } - return ImmutableMap.copyOf(LinuxSandboxedStrategy.finalizeMounts(mounts.build())); - } - - /** - * Takes a map of file specifications, creates the necessary files / symlinks / dirs, - * mounts the first file of the specification at its canonical location in the sandbox and returns - * the output of {@code LinuxSandboxedStrategy#fixMounts} for it. - */ - private Map<String, String> userFriendlyMounts(Map<String, String> linksAndFiles) - throws Exception { - return userFriendlyMap(mounts(linksAndFiles)); - } - - private Map<Path, Path> mounts(Map<String, String> linksAndFiles) throws Exception { - return mounts( - linksAndFiles, ImmutableList.of(Iterables.getFirst(linksAndFiles.keySet(), null))); - } - - /** - * Returns a map of mount entries for a list files, which can be used to assert that all - * expected mounts have been made by the LinuxSandboxedStrategy. - */ - private ImmutableMap<String, String> userFriendlyAsserts(List<String> asserts) { - return userFriendlyMap(asserts(asserts)); - } - - private ImmutableMap<Path, Path> asserts(List<String> asserts) { - ImmutableMap.Builder<Path, Path> pathifiedAsserts = ImmutableMap.builder(); - for (String fileName : asserts) { - Path inputPath = workspaceDir.getRelative(fileName); - pathifiedAsserts.put(inputPath, inputPath); - } - return pathifiedAsserts.build(); - } - - private void createTreeStructure(Map<String, String> linksAndFiles) throws Exception { - for (Entry<String, String> entry : linksAndFiles.entrySet()) { - Path filePath = workspaceDir.getRelative(entry.getKey()); - String linkTarget = entry.getValue(); - - FileSystemUtils.createDirectoryAndParents(filePath.getParentDirectory()); - - if (!linkTarget.isEmpty()) { - filePath.createSymbolicLink(new PathFragment(linkTarget)); - } else if (filePath.getPathString().endsWith("/")) { - filePath.createDirectory(); - } else { - FileSystemUtils.createEmptyFile(filePath); - } - } - } - - @Test - public void testResolvesRelativeFileToFileSymlinkInSameDir() throws Exception { - Map<String, String> testFiles = new LinkedHashMap<>(); - testFiles.put("symlink.txt", "goal.txt"); - testFiles.put("goal.txt", ""); - - List<String> assertMounts = new ArrayList<>(); - assertMounts.add("symlink.txt"); - assertMounts.add("goal.txt"); - - assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts)); - } - - @Test - public void testResolvesRelativeFileToFileSymlinkInSubDir() throws Exception { - Map<String, String> testFiles = - ImmutableMap.of( - "symlink.txt", "x/goal.txt", - "x/goal.txt", ""); - - List<String> assertMounts = ImmutableList.of("symlink.txt", "x/goal.txt"); - assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts)); - } - - @Test - public void testResolvesRelativeFileToFileSymlinkInParentDir() throws Exception { - Map<String, String> testFiles = - ImmutableMap.of( - "x/symlink.txt", "../goal.txt", - "goal.txt", ""); - - List<String> assertMounts = ImmutableList.of("x/symlink.txt", "goal.txt"); - - assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts)); - } - - @Test - public void testRecursesSubDirs() throws Exception { - ImmutableList<String> inputFile = ImmutableList.of("a/b"); - - Map<String, String> testFiles = - ImmutableMap.of( - "a/b/x.txt", "", - "a/b/y.txt", "z.txt", - "a/b/z.txt", ""); - - List<String> assertMounts = ImmutableList.of("a/b/x.txt", "a/b/y.txt", "a/b/z.txt"); - - assertThat(userFriendlyMounts(testFiles, inputFile)) - .isEqualTo(userFriendlyAsserts(assertMounts)); - } - - /** - * Test that the algorithm correctly identifies and refuses symlink loops. - */ - @Test - public void testCatchesSymlinkLoop() throws Exception { - try { - mounts( - ImmutableMap.of( - "a", "b", - "b", "a")); - fail(); - } catch (IOException e) { - assertThat(e) - .hasMessage( - String.format( - "%s (Too many levels of symbolic links)", - workspaceDir.getRelative("a").getPathString())); - } - } - - /** - * Test that the algorithm correctly detects and refuses symlinks whose subcomponents are not all - * directories (e.g. "a -> dir/file/file"). - */ - @Test - public void testCatchesIllegalSymlink() throws Exception { - try { - mounts( - ImmutableMap.of( - "b", "a/c", - "a", "")); - fail(); - } catch (IOException e) { - assertThat(e) - .hasMessage( - String.format( - "%s (Not a directory)", workspaceDir.getRelative("a/c").getPathString())); - } - } - @Test public void testParseManifestFile() throws Exception { - Path targetDir = workspaceDir.getRelative("runfiles"); - targetDir.createDirectory(); + PathFragment targetDir = new PathFragment("runfiles"); Path testFile = workspaceDir.getRelative("testfile"); FileSystemUtils.createEmptyFile(testFile); @@ -238,23 +45,22 @@ public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase { Charset.defaultCharset(), String.format("x/testfile %s\nx/emptyfile \n", testFile.getPathString())); - Map mounts = - LinuxSandboxedStrategy.parseManifestFile(targetDir, manifestFile.getPathFile(), false, ""); + Map<PathFragment, Path> mounts = new TreeMap<>(); + LinuxSandboxedStrategy.parseManifestFile( + fileSystem, mounts, targetDir, manifestFile.getPathFile(), false, ""); - assertThat(userFriendlyMap(mounts)) + assertThat(mounts) .isEqualTo( - userFriendlyMap( - ImmutableMap.of( - fileSystem.getPath("/runfiles/x/testfile"), - testFile, - fileSystem.getPath("/runfiles/x/emptyfile"), - fileSystem.getPath("/dev/null")))); + ImmutableMap.of( + new PathFragment("runfiles/x/testfile"), + testFile, + new PathFragment("runfiles/x/emptyfile"), + fileSystem.getPath("/dev/null"))); } @Test public void testParseFilesetManifestFile() throws Exception { - Path targetDir = workspaceDir.getRelative("fileset"); - targetDir.createDirectory(); + PathFragment targetDir = new PathFragment("fileset"); Path testFile = workspaceDir.getRelative("testfile"); FileSystemUtils.createEmptyFile(testFile); @@ -265,12 +71,10 @@ public class LinuxSandboxedStrategyTest extends LinuxSandboxedStrategyTestCase { Charset.defaultCharset(), String.format("workspace/x/testfile %s\n0\n", testFile.getPathString())); - Map mounts = - LinuxSandboxedStrategy.parseManifestFile( - targetDir, manifestFile.getPathFile(), true, "workspace"); + Map<PathFragment, Path> mounts = new HashMap<>(); + LinuxSandboxedStrategy.parseManifestFile( + fileSystem, mounts, targetDir, manifestFile.getPathFile(), true, "workspace"); - assertThat(userFriendlyMap(mounts)) - .isEqualTo( - userFriendlyMap(ImmutableMap.of(fileSystem.getPath("/fileset/x/testfile"), testFile))); + assertThat(mounts).isEqualTo(ImmutableMap.of(new PathFragment("fileset/x/testfile"), testFile)); } } diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java index b97e396c53..a80074a9fa 100644 --- a/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java +++ b/src/test/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategyTestCase.java @@ -36,10 +36,8 @@ import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.util.FileSystems; import com.google.devtools.common.options.OptionsParser; - -import org.junit.Before; - import java.io.IOException; +import org.junit.Before; /** * Common parts of all {@link LinuxSandboxedStrategy} tests. @@ -50,7 +48,6 @@ public class LinuxSandboxedStrategyTestCase { protected FileSystem fileSystem; protected Path workspaceDir; - protected Path fakeSandboxDir; protected BlazeExecutor executor; protected BlazeDirectories blazeDirs; @@ -75,9 +72,6 @@ public class LinuxSandboxedStrategyTestCase { outputBase = testRoot.getRelative("outputBase"); outputBase.createDirectory(); - fakeSandboxDir = testRoot.getRelative("sandbox"); - fakeSandboxDir.createDirectory(); - blazeDirs = new BlazeDirectories(outputBase, outputBase, workspaceDir, "mock-product-name"); BlazeTestUtils.getIntegrationBinTools(blazeDirs); @@ -101,7 +95,6 @@ public class LinuxSandboxedStrategyTestCase { "", new LinuxSandboxedStrategy( optionsParser.getOptions(SandboxOptions.class), - ImmutableMap.<String, String>of(), blazeDirs, MoreExecutors.newDirectExecutorService(), true, diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java deleted file mode 100644 index 060eb6f264..0000000000 --- a/src/test/java/com/google/devtools/build/lib/sandbox/MountMapTest.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2015 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.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import com.google.common.collect.ImmutableMap; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.io.IOException; - -/** - * Tests for {@code MountMap}. - */ -@RunWith(JUnit4.class) -public class MountMapTest extends LinuxSandboxedStrategyTestCase { - @Test - public void testMountMapWithNormalMounts() throws IOException { - // Allowed: Just two normal mounts (a -> sandbox/a, b -> sandbox/b) - MountMap mounts = new MountMap(); - mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("a")); - mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("b")); - assertThat(mounts) - .isEqualTo( - ImmutableMap.of( - fileSystem.getPath("/a"), workspaceDir.getRelative("a"), - fileSystem.getPath("/b"), workspaceDir.getRelative("b"))); - } - - @Test - public void testMountMapWithSameMountTwice() throws IOException { - // Allowed: Mount same thing twice (a -> sandbox/a, a -> sandbox/a, b -> sandbox/b) - MountMap mounts = new MountMap(); - mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("a")); - mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("a")); - mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("b")); - assertThat(mounts) - .isEqualTo( - ImmutableMap.of( - fileSystem.getPath("/a"), workspaceDir.getRelative("a"), - fileSystem.getPath("/b"), workspaceDir.getRelative("b"))); - } - - @Test - public void testMountMapWithOneThingTwoTargets() throws IOException { - // Allowed: Mount one thing in two targets (x -> sandbox/a, x -> sandbox/b) - MountMap mounts = new MountMap(); - mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("x")); - mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("x")); - assertThat(mounts) - .isEqualTo( - ImmutableMap.of( - fileSystem.getPath("/a"), workspaceDir.getRelative("x"), - fileSystem.getPath("/b"), workspaceDir.getRelative("x"))); - } - - @Test - public void testMountMapWithTwoThingsOneTarget() throws IOException { - // Forbidden: Mount two things onto the same target (x -> sandbox/a, y -> sandbox/a) - try { - MountMap mounts = new MountMap(); - mounts.put(fileSystem.getPath("/x"), workspaceDir.getRelative("a")); - mounts.put(fileSystem.getPath("/x"), workspaceDir.getRelative("b")); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e) - .hasMessage( - String.format( - "Cannot mount both '%s' and '%s' onto '%s'", - workspaceDir.getRelative("a"), - workspaceDir.getRelative("b"), - fileSystem.getPath("/x"))); - } - } - - @Test - public void testMountMapGuaranteesOrdering() { - MountMap mounts = new MountMap(); - mounts.put(fileSystem.getPath("/a/c"), workspaceDir.getRelative("x")); - mounts.put(fileSystem.getPath("/b"), workspaceDir.getRelative("x")); - mounts.put(fileSystem.getPath("/a/b"), workspaceDir.getRelative("x")); - mounts.put(fileSystem.getPath("/a"), workspaceDir.getRelative("x")); - - assertThat(mounts.entrySet()) - .containsExactlyElementsIn( - ImmutableMap.builder() - .put(fileSystem.getPath("/a"), workspaceDir.getRelative("x")) - .put(fileSystem.getPath("/a/b"), workspaceDir.getRelative("x")) - .put(fileSystem.getPath("/a/c"), workspaceDir.getRelative("x")) - .put(fileSystem.getPath("/b"), workspaceDir.getRelative("x")) - .build() - .entrySet()) - .inOrder(); - } -} diff --git a/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java b/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java index 46522a7459..962022017b 100644 --- a/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java +++ b/src/test/java/com/google/devtools/build/lib/unix/NativePosixFilesTest.java @@ -23,15 +23,14 @@ import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.UnixFileSystem; - +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.io.File; -import java.io.FileNotFoundException; - /** * This class tests the FilesystemUtils class. */ @@ -104,9 +103,12 @@ public class NativePosixFilesTest { File foo = new File("/bin"); try { NativePosixFiles.setWritable(foo); - fail("Expected FilePermissionException, but wasn't thrown."); + fail("Expected FilePermissionException or IOException, but wasn't thrown."); } catch (FilePermissionException e) { assertThat(e).hasMessage(foo + " (Operation not permitted)"); + } catch (IOException e) { + // When running in a sandbox, /bin might actually be a read-only file system. + assertThat(e).hasMessage(foo + " (Read-only file system)"); } } } diff --git a/src/test/shell/bazel/bazel_sandboxing_test.sh b/src/test/shell/bazel/bazel_sandboxing_test.sh index 5238c09f77..0f911f3f7d 100755 --- a/src/test/shell/bazel/bazel_sandboxing_test.sh +++ b/src/test/shell/bazel/bazel_sandboxing_test.sh @@ -222,7 +222,7 @@ function test_sandbox_cleanup() { bazel build examples/genrule:tools_work &> $TEST_log \ || fail "Hermetic genrule failed: examples/genrule:tools_work" bazel shutdown &> $TEST_log || fail "bazel shutdown failed" - ls -la "$(bazel info execution_root)/bazel-sandbox" + ls -la "$(bazel info output_base)/bazel-sandbox" if [[ "$(ls -A "$(bazel info execution_root)"/bazel-sandbox)" ]]; then fail "Build left files around afterwards" fi @@ -301,7 +301,7 @@ function test_sandbox_undeclared_deps_skylark_with_local_tag() { function test_sandbox_block_filesystem() { output_file="${BAZEL_GENFILES_DIR}/examples/genrule/breaks2.txt" - bazel build examples/genrule:breaks2 &> $TEST_log \ + bazel build --sandbox_block_path=/var/log examples/genrule:breaks2 &> $TEST_log \ && fail "Non-hermetic genrule succeeded: examples/genrule:breaks2" || true [ -f "$output_file" ] || @@ -311,7 +311,7 @@ function test_sandbox_block_filesystem() { fail "Output contained more than one line: $output_file" fi - fgrep "No such file or directory" $output_file || + fgrep "Permission denied" $output_file || fail "Output did not contain expected error message: $output_file" } @@ -379,62 +379,9 @@ EOF kill_nc } -function test_sandbox_add_path_valid_path() { - output_file="${BAZEL_GENFILES_DIR}/examples/genrule/breaks2.txt" - - bazel build --sandbox_add_path=/var/log examples/genrule:breaks2 &> $TEST_log \ - || fail "Non-hermetic genrule failed: examples/genrule:breaks2 (with additional path)" - - [ -f "$output_file" ] || - fail "Action did not produce output: $output_file" - - if [ $(wc -l < $output_file) -le 1 ]; then - fail "Output contained less than or equal to one line: $output_file" - fi -} - -function test_sandbox_add_path_workspace_parent() { - output_file="${BAZEL_GENFILES_DIR}/examples/genrule/check_sandbox_contain_WORKSPACE.txt" - parent_path="$(dirname "$(pwd)")" - - bazel build --sandbox_add_path=$parent_path examples/genrule:check_sandbox_contain_WORKSPACE &> $TEST_log \ - || fail "Non-hermetic genrule succeeded: examples/genrule:works (with additional path)" - [ -f "$output_file" ] \ - || fail "Genrule did not produce output: examples/genrule:check_sandbox_contain_WORKSPACE (with additional path: WORKSPACE/..)" - cat $output_file &> $TEST_log - - # file and directory inside workspace (except project) should not be mounted - egrep "\bWORKSPACE\b" $output_file \ - && fail "WORKSPACE file should not be mounted." || true -} - -function test_sandbox_add_path_workspace_child() { - child_path="$(pwd)/examples" - output_file="${BAZEL_GENFILES_DIR}/examples/genrule/works.txt" - - bazel build --sandbox_add_path=$child_path examples/genrule:works &> $TEST_log \ - && fail "Non-hermetic genrule succeeded: examples/genrule:works (with additional path: WORKSPACE:/examples)" || true - - expect_log "Mounting subdirectory of WORKSPACE or OUTPUTBASE to sandbox is not allowed" -} - -function test_sandbox_fail_command() { - mkdir -p "javatests/orange" - echo "java_test(name = 'Orange', srcs = ['Orange.java'])" > javatests/orange/BUILD - cat > javatests/orange/Orange.java <<EOF -package orange; -import junit.framework.TestCase; -public class Orange extends TestCase { - public void testFails() { fail("juice"); } -} -EOF - bazel test --sandbox_debug --verbose_failures //javatests/orange:Orange >& $TEST_log \ - && fail "Expected failure" || true - - expect_log "Sandboxed execution failed, which may be legitimate" -} - -function test_sandbox_different_nobody_uid() { +# TODO(philwo) - this doesn't work on Ubuntu 14.04 due to "unshare" being too +# old and not understanding the --user flag. +function DISABLED_test_sandbox_different_nobody_uid() { cat /etc/passwd | sed 's/\(^nobody:[^:]*:\)[0-9]*:[0-9]*/\15000:16000/g' > \ "${TEST_TMPDIR}/passwd" unshare --user --mount --map-root-user -- bash - \ diff --git a/src/test/shell/bazel/linux-sandbox_test.sh b/src/test/shell/bazel/linux-sandbox_test.sh index a737578a29..7b533af5be 100755 --- a/src/test/shell/bazel/linux-sandbox_test.sh +++ b/src/test/shell/bazel/linux-sandbox_test.sh @@ -29,97 +29,87 @@ readonly OUT="${OUT_DIR}/outfile" readonly ERR="${OUT_DIR}/errfile" readonly SANDBOX_DIR="${OUT_DIR}/sandbox" -SANDBOX_DEFAULT_OPTS="-S $SANDBOX_DIR" -for dir in /bin* /lib* /usr/bin* /usr/lib*; do - SANDBOX_DEFAULT_OPTS="$SANDBOX_DEFAULT_OPTS -M $dir" -done +SANDBOX_DEFAULT_OPTS="-W $SANDBOX_DIR" function set_up { rm -rf $OUT_DIR mkdir -p $SANDBOX_DIR } -function assert_stdout() { - assert_equals "$1" "$(cat $OUT)" -} - -function assert_output() { - assert_equals "$1" "$(cat $OUT)" - assert_equals "$2" "$(cat $ERR)" -} - function test_basic_functionality() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/echo hi there || fail - assert_output "hi there" "" + $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /bin/echo hi there &> $TEST_log || fail + expect_log "hi there" } function test_default_user_is_nobody() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /usr/bin/id || fail - assert_output "uid=65534 gid=65534 groups=65534" "" + $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /usr/bin/id &> $TEST_log || fail + expect_log "uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)" } function test_user_switched_to_root() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -r -l $OUT -L $ERR -- /usr/bin/id || fail - assert_contains "uid=0 gid=0" "$OUT" + $linux_sandbox $SANDBOX_DEFAULT_OPTS -R -- /usr/bin/id &> $TEST_log || fail + expect_log "uid=0(root) gid=0(root)" } function test_network_namespace() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -n -l $OUT -L $ERR -- /bin/ip link ls || fail - assert_contains "LOOPBACK,UP" "$OUT" + $linux_sandbox $SANDBOX_DEFAULT_OPTS -N -- /bin/ip link ls &> $TEST_log || fail + expect_log "LOOPBACK,UP" } function test_ping_loopback() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -n -r -- \ + $linux_sandbox $SANDBOX_DEFAULT_OPTS -N -R -- \ /bin/sh -c 'ping6 -c 1 ::1 || ping -c 1 127.0.0.1' &>$TEST_log || fail expect_log "1 received" } -function test_to_stderr() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "/bin/echo hi there >&2" || fail - assert_output "" "hi there" -} - function test_exit_code() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "exit 71" || code=$? + $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /bin/bash -c "exit 71" &> $TEST_log || code=$? assert_equals 71 "$code" } function test_signal_death() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c 'kill -ABRT $$' || code=$? + $linux_sandbox $SANDBOX_DEFAULT_OPTS -- /bin/bash -c 'kill -ABRT $$' &> $TEST_log || code=$? assert_equals 134 "$code" # SIGNAL_BASE + SIGABRT = 128 + 6 } +# Tests that even when the child catches SIGTERM and exits with code 0, that the sandbox exits with +# code 142 (telling us about the expired timeout). function test_signal_catcher() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \ - 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -- /bin/bash -c \ + 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' &> $TEST_log || code=$? assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 - assert_stdout "later" + expect_log "^later$" } function test_basic_timeout() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 3 -t 3 -l $OUT -L $ERR -- /bin/bash -c "echo before; sleep 1000; echo after" && fail - assert_output "before" "" + $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 3 -t 3 -- /bin/bash -c "echo before; sleep 1000; echo after" &> $TEST_log && fail + expect_log "^before$" "" } function test_timeout_grace() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \ - 'trap "echo -n before; sleep 1; echo -n after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -- /bin/bash -c \ + 'trap "echo -n before; sleep 1; echo -n after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' &> $TEST_log || code=$? assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 - assert_stdout "beforeafter" + expect_log "^beforeafter$" } function test_timeout_kill() { - $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \ - 'trap "echo before; sleep 1000; echo after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + $linux_sandbox $SANDBOX_DEFAULT_OPTS -T 2 -t 3 -- /bin/bash -c \ + 'trap "echo before; sleep 1000; echo after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' &> $TEST_log || code=$? assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 - assert_stdout "before" + expect_log "^before$" } function test_debug_logging() { touch ${TEST_TMPDIR}/testfile - $linux_sandbox $SANDBOX_DEFAULT_OPTS -D -M ${TEST_TMPDIR}/testfile -m /tmp/sandboxed_testfile -l $OUT -L $ERR -- /bin/true || code=$? - assert_contains "mount: /usr/bin\$" "$ERR" - assert_contains "mount: ${TEST_TMPDIR}/testfile -> <sandbox>/tmp/sandboxed_testfile\$" "$ERR" + $linux_sandbox $SANDBOX_DEFAULT_OPTS -D -- /bin/true &> $TEST_log || code=$? + expect_log "child exited normally with exitcode 0" +} + +function test_redirect_output() { + $linux_sandbox $SANDBOX_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "echo out; echo err >&2" &> $TEST_log || code=$? + assert_equals "out" "$(cat $OUT)" + assert_equals "err" "$(cat $ERR)" } # The test shouldn't fail if the environment doesn't support running it. |