diff options
Diffstat (limited to 'src')
17 files changed, 1467 insertions, 839 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java index a391a30869..377e95c00b 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java @@ -24,6 +24,7 @@ import com.google.devtools.build.lib.actions.ResourceManager; import com.google.devtools.build.lib.actions.ResourceSet; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.TestExecException; +import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl; import com.google.devtools.build.lib.analysis.config.BinTools; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.events.Event; @@ -77,7 +78,6 @@ public class StandaloneTestStrategy extends TestStrategy { .getChild(getTmpDirName(action.getExecutionSettings().getExecutable().getExecPath())); Path workingDirectory = runfilesDir.getRelative(action.getRunfilesPrefix()); - TestRunnerAction.ResolvedPaths resolvedPaths = action.resolve(actionExecutionContext.getExecutor().getExecRoot()); Map<String, String> env = getEnv(action, runfilesDir, testTmpDir, resolvedPaths); @@ -95,6 +95,8 @@ public class StandaloneTestStrategy extends TestStrategy { getArgs(TEST_SETUP, "", action), env, info, + new RunfilesSupplierImpl( + runfilesDir.asFragment(), action.getExecutionSettings().getRunfiles()), action, action .getTestProperties() diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD index 9b549e949c..febc5d76ff 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/BUILD +++ b/src/main/java/com/google/devtools/build/lib/sandbox/BUILD @@ -12,10 +12,12 @@ java_library( "//src/main/java:analysis-exec-rules-skyframe", "//src/main/java:buildtool-runtime", "//src/main/java:common", + "//src/main/java:events", "//src/main/java:packages", "//src/main/java:shell", "//src/main/java:unix", "//src/main/java:vfs", + "//src/main/java/com/google/devtools/build/lib/standalone", "//third_party:guava", ], ) 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 4f7557cb7e..67aebeecc8 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,11 +13,17 @@ // limitations under the License. package com.google.devtools.build.lib.sandbox; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.SetMultimap; +import com.google.common.io.Files; +import com.google.devtools.build.lib.Constants; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionInputHelper; -import com.google.devtools.build.lib.actions.Actions; +import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.Executor; @@ -25,21 +31,34 @@ import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.analysis.BlazeDirectories; +import com.google.devtools.build.lib.analysis.config.RunUnder; +import com.google.devtools.build.lib.events.Event; +import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.rules.cpp.CppCompileAction; +import com.google.devtools.build.lib.rules.test.TestRunnerAction; +import com.google.devtools.build.lib.runtime.BlazeRuntime; import com.google.devtools.build.lib.shell.CommandException; +import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy; import com.google.devtools.build.lib.syntax.Label; import com.google.devtools.build.lib.unix.FilesystemUtils; -import com.google.devtools.build.lib.util.CommandFailureUtils; -import com.google.devtools.build.lib.util.DependencySet; import com.google.devtools.build.lib.util.io.FileOutErr; +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 java.io.File; import java.io.IOException; -import java.util.ArrayList; +import java.nio.charset.Charset; +import java.util.HashSet; import java.util.List; -import java.util.TreeSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; /** * Strategy that uses sandboxing to execute a process. @@ -47,12 +66,24 @@ import java.util.TreeSet; @ExecutionStrategy(name = {"sandboxed"}, contextType = SpawnActionContext.class) public class LinuxSandboxedStrategy implements SpawnActionContext { + private final ExecutorService backgroundWorkers; + + private final BlazeRuntime blazeRuntime; + private final BlazeDirectories blazeDirs; + private final Path execRoot; private final boolean verboseFailures; - private final BlazeDirectories directories; + private final StandaloneSpawnStrategy standaloneStrategy; + private final UUID uuid = UUID.randomUUID(); + private final AtomicInteger execCounter = new AtomicInteger(); - public LinuxSandboxedStrategy(BlazeDirectories blazeDirectories, boolean verboseFailures) { - this.directories = blazeDirectories; + public LinuxSandboxedStrategy( + BlazeRuntime blazeRuntime, boolean verboseFailures, ExecutorService backgroundWorkers) { + this.blazeRuntime = blazeRuntime; + this.blazeDirs = blazeRuntime.getDirectories(); + this.execRoot = blazeDirs.getExecRoot(); this.verboseFailures = verboseFailures; + this.backgroundWorkers = backgroundWorkers; + this.standaloneStrategy = new StandaloneSpawnStrategy(blazeDirs.getExecRoot(), verboseFailures); } /** @@ -62,158 +93,331 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { public void exec(Spawn spawn, ActionExecutionContext actionExecutionContext) throws ExecException { Executor executor = actionExecutionContext.getExecutor(); + + // TODO(philwo) - this catches BuildInfo, which can't run in a sandbox. Is there a better way? + // Maybe add an annotation to actions that they can refuse to run under certain strategies? + if (spawn.getOwner().getLabel() == null + || spawn.getArguments().get(0).contains("build-runfiles")) { + standaloneStrategy.exec(spawn, actionExecutionContext); + return; + } + if (executor.reportsSubcommands()) { executor.reportSubcommand( Label.print(spawn.getOwner().getLabel()) + " [" + spawn.getResourceOwner().prettyPrint() + "]", spawn.asShellCommand(executor.getExecRoot())); } - boolean processHeaders = spawn.getResourceOwner() instanceof CppCompileAction; - - Path execPath = this.directories.getExecRoot(); - List<String> spawnArguments = new ArrayList<>(); - for (String arg : spawn.getArguments()) { - if (arg.startsWith(execPath.getPathString())) { - // make all paths relative for the sandbox - spawnArguments.add(arg.substring(execPath.getPathString().length())); - } else { - spawnArguments.add(arg); - } - } + FileOutErr outErr = actionExecutionContext.getFileOutErr(); - List<? extends ActionInput> expandedInputs = - ActionInputHelper.expandMiddlemen(spawn.getInputFiles(), - actionExecutionContext.getMiddlemanExpander()); + // The execId is a unique ID just for this invocation of "exec". + String execId = uuid + "-" + Integer.toString(execCounter.getAndIncrement()); - String cwd = executor.getExecRoot().getPathString(); + // Each invocation of "exec" gets its own sandbox. + Path sandboxPath = + execRoot.getRelative(Constants.PRODUCT_NAME + "-sandbox").getRelative(execId); - FileOutErr outErr = actionExecutionContext.getFileOutErr(); + ImmutableMultimap<Path, Path> mounts; try { - PathFragment includePrefix = null; // null when there's no include mangling to do - List<PathFragment> includeDirectories = ImmutableList.of(); - if (processHeaders) { - CppCompileAction cppAction = (CppCompileAction) spawn.getResourceOwner(); - // headers are mounted in the sandbox in a separate include dir, so their names are mangled - // when running the compilation and will have to be unmangled after it's done in the *.pic.d - includeDirectories = extractIncludeDirs(execPath, cppAction, spawnArguments); - includePrefix = getSandboxIncludeDir(cppAction); - } + // Gather all necessary mounts for the sandbox. + mounts = getMounts(spawn, sandboxPath, actionExecutionContext); + } catch (IOException e) { + throw new UserExecException("Could not prepare mounts for sandbox execution", e); + } + + int timeout = getTimeout(spawn); - NamespaceSandboxRunner runner = new NamespaceSandboxRunner(directories, spawn, includePrefix, - includeDirectories, verboseFailures); - runner.setupSandbox(expandedInputs, spawn.getOutputFiles()); - runner.run(spawnArguments, spawn.getEnvironment(), new File(cwd), outErr); - runner.copyOutputs(spawn.getOutputFiles(), outErr); - if (processHeaders) { - CppCompileAction cppAction = (CppCompileAction) spawn.getResourceOwner(); - unmangleHeaderFiles(cppAction); + try { + final NamespaceSandboxRunner runner = + new NamespaceSandboxRunner(execRoot, sandboxPath, mounts, verboseFailures); + try { + runner.run( + spawn.getArguments(), + spawn.getEnvironment(), + blazeDirs.getExecRoot().getPathFile(), + outErr, + spawn.getOutputFiles(), + timeout); + } finally { + // Due to the Linux kernel behavior, if we try to remove the sandbox too quickly after the + // process has exited, we get "Device busy" errors because some of the mounts have not yet + // been undone. A second later it usually works. We will just clean the old sandboxes up + // using a background worker. + backgroundWorkers.execute( + new Runnable() { + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + try { + runner.cleanup(); + return; + } catch (IOException e2) { + // Sleep & retry. + Thread.sleep(250); + } + } + } catch (InterruptedException e) { + // Exit. + } + } + }); } - runner.cleanup(); } catch (CommandException e) { - String message = CommandFailureUtils.describeCommandFailure(verboseFailures, - spawn.getArguments(), spawn.getEnvironment(), cwd); - throw new UserExecException(String.format("%s: %s", message, e)); + EventHandler handler = actionExecutionContext.getExecutor().getEventHandler(); + handler.handle( + Event.error("Sandboxed execution failed: " + spawn.getOwner().getLabel() + ".")); + throw new UserExecException("Error during execution of spawn", e); } catch (IOException e) { - throw new UserExecException(e.getMessage()); + EventHandler handler = actionExecutionContext.getExecutor().getEventHandler(); + handler.handle( + Event.error( + "I/O error during sandboxed execution:\n" + Throwables.getStackTraceAsString(e))); + throw new UserExecException("Could not execute spawn", e); } } - private void unmangleHeaderFiles(CppCompileAction cppCompileAction) throws IOException { - Path execPath = this.directories.getExecRoot(); - CppCompileAction.DotdFile dotdfile = cppCompileAction.getDotdFile(); - DependencySet depset = new DependencySet(execPath).read(dotdfile.getPath()); - DependencySet unmangled = new DependencySet(execPath); - PathFragment sandboxIncludeDir = getSandboxIncludeDir(cppCompileAction); - PathFragment prefix = sandboxIncludeDir.getRelative(execPath.asFragment().relativeTo("/")); - for (PathFragment dep : depset.getDependencies()) { - if (dep.startsWith(prefix)) { - dep = dep.relativeTo(prefix); + private int getTimeout(Spawn spawn) throws UserExecException { + String timeoutStr = spawn.getExecutionInfo().get("timeout"); + if (timeoutStr != null) { + try { + return Integer.parseInt(timeoutStr); + } catch (NumberFormatException e) { + throw new UserExecException("could not parse timeout: " + e); } - unmangled.addDependency(dep); } - unmangled.write(execPath.getRelative(depset.getOutputFileName()), ".d"); + return -1; } - private PathFragment getSandboxIncludeDir(CppCompileAction cppCompileAction) { - return new PathFragment( - "include-" + Actions.escapedPath(cppCompileAction.getPrimaryOutput().toString())); - } + private ImmutableMultimap<Path, Path> getMounts( + Spawn spawn, Path sandboxPath, ActionExecutionContext actionExecutionContext) + throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + mounts.putAll(mountUsualUnixDirs(sandboxPath)); + mounts.putAll(setupBlazeUtils(sandboxPath)); + mounts.putAll(mountRunfilesFromManifests(spawn, sandboxPath)); + mounts.putAll(mountRunfilesFromSuppliers(spawn, sandboxPath)); + mounts.putAll(mountRunfilesForTests(spawn, sandboxPath)); + mounts.putAll(mountInputs(spawn, sandboxPath, actionExecutionContext)); + mounts.putAll(mountRunUnderCommand(spawn, sandboxPath)); + + SetMultimap<Path, Path> fixedMounts = LinkedHashMultimap.create(); + for (Entry<Path, Path> mount : mounts.build().entries()) { + Path source = mount.getKey(); + Path target = mount.getValue(); + validateAndAddMount(sandboxPath, fixedMounts, source, target); + + // Iteratively resolve symlinks and mount the whole chain. Take care not to run into a cyclic + // symlink - when we already processed the source once, we can exit the loop. Skyframe will + // catch cyclic symlinks for declared inputs, but this won't help if there is one in the parts + // of the host system that we mount. + Set<Path> seenSources = new HashSet<>(); + while (source.isSymbolicLink() && seenSources.add(source)) { + source = source.getParentDirectory().getRelative(source.readSymbolicLink()); + target = sandboxPath.getRelative(source.asFragment().relativeTo("/")); - private ImmutableList<PathFragment> extractIncludeDirs(Path execPath, - CppCompileAction cppCompileAction, List<String> spawnArguments) throws IOException { - List<PathFragment> includes = new ArrayList<>(); - includes.addAll(cppCompileAction.getQuoteIncludeDirs()); - includes.addAll(cppCompileAction.getIncludeDirs()); - includes.addAll(cppCompileAction.getSystemIncludeDirs()); - - // gcc implicitly includes headers in the same dir as .cc file - PathFragment sourceDirectory = - cppCompileAction.getSourceFile().getPath().getParentDirectory().asFragment(); - includes.add(sourceDirectory); - spawnArguments.add("-iquote"); - spawnArguments.add(sourceDirectory.toString()); - - TreeSet<PathFragment> processedIncludes = new TreeSet<>(); - for (int i = 0; i < includes.size(); i++) { - PathFragment absolutePath; - if (!includes.get(i).isAbsolute()) { - absolutePath = execPath.getRelative(includes.get(i)).asFragment(); - } else { - absolutePath = includes.get(i); + validateAndAddMount(sandboxPath, fixedMounts, source, target); } - // CppCompileAction may provide execPath as one of the include directories. This is a big - // overestimation of what is actually needed and doesn't make for very hermetic sandbox - // (since everything from the workspace will be somehow accessed in the sandbox). To have - // some more hermeticity in this situation we mount all the include dirs in: - // sandbox-directory/include-prefix/actual-include-dir - // (where include-prefix is obtained from this.getSandboxIncludeDir(cppCompileAction)) - // and make so gcc looks there for includes. This should prevent the user from accessing - // files that technically should not be in the sandbox. - // TODO(bazel-team): change CppCompileAction so that include dirs contain only subsets of the - // execPath - if (absolutePath.equals(execPath.asFragment())) { - // we can't mount execPath because it will lead to a circular mount; instead mount its - // subdirs inside (other than the ones containing sandbox) - String[] subdirs = FilesystemUtils.readdir(absolutePath.toString()); - for (String dirName : subdirs) { - if (dirName.equals("_bin") || dirName.equals("bazel-out")) { - continue; - } - PathFragment child = absolutePath.getChild(dirName); - processedIncludes.add(child); + } + return ImmutableMultimap.copyOf(fixedMounts); + } + + /** + * Adds the new mount ("source" -> "target") to "mounts" after doing some validations on it. + * + * @return true if the mount was added to the multimap, or false if the multimap already contained + * the mount. + */ + private static boolean validateAndAddMount( + Path sandboxPath, SetMultimap<Path, Path> mounts, Path source, Path target) { + // The source must exist. + Preconditions.checkArgument(source.exists(), source.toString() + " does not exist"); + + // We cannot mount two different things onto the same target. + if (!mounts.containsEntry(source, target) && mounts.containsValue(target)) { + // There is a conflicting entry, find it and error out. + for (Entry<Path, Path> mount : mounts.entries()) { + if (mount.getValue().equals(target)) { + throw new IllegalStateException( + String.format( + "Cannot mount both '%s' and '%s' onto '%s'", mount.getKey(), source, target)); } - } else { - processedIncludes.add(absolutePath); } } - // pseudo random name for include directory inside sandbox, so it won't be accessed by accident - String prefix = getSandboxIncludeDir(cppCompileAction).toString(); + // Mounts must always mount into the sandbox, otherwise they might corrupt the host system. + Preconditions.checkArgument( + target.startsWith(sandboxPath), + String.format("(%s -> %s) does not mount into sandbox", source, target)); + + return mounts.put(source, target); + } - // change names in the invocation - for (int i = 0; i < spawnArguments.size(); i++) { - if (spawnArguments.get(i).startsWith("-I")) { - String argument = spawnArguments.get(i).substring(2); - spawnArguments.set(i, setIncludeDirSandboxPath(execPath, argument, "-I" + prefix)); + /** + * Mount a certain set of unix directories to make the usual tools and libraries available to the + * spawn that runs. + */ + private ImmutableMultimap<Path, Path> mountUsualUnixDirs(Path sandboxPath) throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + FileSystem fs = blazeDirs.getFileSystem(); + mounts.put(fs.getPath("/bin"), sandboxPath.getRelative("bin")); + mounts.put(fs.getPath("/etc"), sandboxPath.getRelative("etc")); + for (String entry : FilesystemUtils.readdir("/")) { + if (entry.startsWith("lib")) { + mounts.put(fs.getRootDirectory().getRelative(entry), sandboxPath.getRelative(entry)); } - if (spawnArguments.get(i).equals("-iquote") || spawnArguments.get(i).equals("-isystem")) { - spawnArguments.set(i + 1, setIncludeDirSandboxPath(execPath, - spawnArguments.get(i + 1), prefix)); + } + for (String entry : FilesystemUtils.readdir("/usr")) { + if (!entry.equals("local")) { + mounts.put( + fs.getPath("/usr").getRelative(entry), + sandboxPath.getRelative("usr").getRelative(entry)); } } - return ImmutableList.copyOf(processedIncludes); + return mounts.build(); + } + + /** + * Mount the embedded tools. + */ + private ImmutableMultimap<Path, Path> setupBlazeUtils(Path sandboxPath) throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + Path source = blazeDirs.getEmbeddedBinariesRoot().getRelative("build-runfiles"); + Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/")); + mounts.put(source, target); + return mounts.build(); } - private String setIncludeDirSandboxPath(Path execPath, String argument, String prefix) { - StringBuilder builder = new StringBuilder(prefix); - if (argument.charAt(0) != '/') { - // relative path - builder.append(execPath); - builder.append('/'); + /** + * Mount all runfiles that the spawn needs as specified in its runfiles manifests. + */ + private ImmutableMultimap<Path, Path> mountRunfilesFromManifests(Spawn spawn, Path sandboxPath) + throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + FileSystem fs = blazeDirs.getFileSystem(); + for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) { + String manifestFilePath = manifest.getValue().getPath().getPathString(); + Preconditions.checkState(!manifest.getKey().isAbsolute()); + Path targetDirectory = execRoot.getRelative(manifest.getKey()); + for (String line : Files.readLines(new File(manifestFilePath), Charset.defaultCharset())) { + String[] fields = line.split(" "); + Preconditions.checkState( + fields.length == 2, "'" + line + "' does not split into exactly 2 parts"); + Path source = fs.getPath(fields[1]); + Path targetPath = targetDirectory.getRelative(fields[0]); + Path targetInSandbox = sandboxPath.getRelative(targetPath.asFragment().relativeTo("/")); + mounts.put(source, targetInSandbox); + } } - builder.append(argument); + return mounts.build(); + } - return builder.toString(); + /** + * Mount all runfiles that the spawn needs as specified via its runfiles suppliers. + */ + private ImmutableMultimap<Path, Path> mountRunfilesFromSuppliers(Spawn spawn, Path sandboxPath) + throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + FileSystem fs = blazeDirs.getFileSystem(); + Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings = + spawn.getRunfilesSupplier().getMappings(); + for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : + rootsAndMappings.entrySet()) { + PathFragment root = rootAndMappings.getKey(); + if (root.isAbsolute()) { + root = root.relativeTo("/"); + } + for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { + Artifact sourceArtifact = mapping.getValue(); + Path source = (sourceArtifact != null) ? sourceArtifact.getPath() : fs.getPath("/dev/null"); + + Preconditions.checkArgument(!mapping.getKey().isAbsolute()); + Path target = sandboxPath.getRelative(root.getRelative(mapping.getKey())); + mounts.put(source, target); + } + } + return mounts.build(); + } + + /** + * Tests are a special case and we have to mount the TEST_SRCDIR where the test expects it to be + * and also provide a TEST_TMPDIR to the test where it can store temporary files. + */ + private ImmutableMultimap<Path, Path> mountRunfilesForTests(Spawn spawn, Path sandboxPath) + throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + FileSystem fs = blazeDirs.getFileSystem(); + if (spawn.getEnvironment().containsKey("TEST_TMPDIR")) { + Path source = fs.getPath(spawn.getEnvironment().get("TEST_TMPDIR")); + Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/")); + FileSystemUtils.createDirectoryAndParents(target); + } + return mounts.build(); + } + + /** + * Mount all inputs of the spawn. + */ + private ImmutableMultimap<Path, Path> mountInputs( + Spawn spawn, Path sandboxPath, ActionExecutionContext actionExecutionContext) + throws IOException { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + + List<ActionInput> inputs = + ActionInputHelper.expandMiddlemen( + spawn.getInputFiles(), actionExecutionContext.getMiddlemanExpander()); + + if (spawn.getResourceOwner() instanceof CppCompileAction) { + CppCompileAction action = (CppCompileAction) spawn.getResourceOwner(); + if (action.shouldScanIncludes()) { + inputs.addAll(action.getAdditionalInputs()); + } + } + + for (ActionInput input : inputs) { + if (input.getExecPathString().contains("internal/_middlemen/")) { + continue; + } + Path source = execRoot.getRelative(input.getExecPathString()); + Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/")); + mounts.put(source, target); + } + return mounts.build(); + } + + /** + * 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 ImmutableMultimap<Path, Path> mountRunUnderCommand(Spawn spawn, Path sandboxPath) { + ImmutableMultimap.Builder<Path, Path> mounts = ImmutableMultimap.builder(); + + 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 source; + if (sourceFragment.isAbsolute()) { + source = blazeDirs.getFileSystem().getPath(sourceFragment); + } else if (blazeDirs.getExecRoot().getRelative(sourceFragment).exists()) { + source = blazeDirs.getExecRoot().getRelative(sourceFragment); + } else { + List<Path> searchPath = + SearchPath.parse(blazeDirs.getFileSystem(), blazeRuntime.getClientEnv().get("PATH")); + source = SearchPath.which(searchPath, runUnder.getCommand()); + } + if (source != null) { + Path target = sandboxPath.getRelative(source.asFragment().relativeTo("/")); + mounts.put(source, target); + } + } + } + return mounts.build(); } @Override diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java index 7720b787c6..b68590c1ca 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/NamespaceSandboxRunner.java @@ -14,17 +14,13 @@ package com.google.devtools.build.lib.sandbox; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.io.Files; import com.google.devtools.build.lib.actions.ActionInput; -import com.google.devtools.build.lib.actions.Artifact; -import com.google.devtools.build.lib.actions.Spawn; -import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.shell.Command; import com.google.devtools.build.lib.shell.CommandException; import com.google.devtools.build.lib.unix.FilesystemUtils; -import com.google.devtools.build.lib.util.Fingerprint; 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; @@ -32,179 +28,30 @@ import com.google.devtools.build.lib.vfs.PathFragment; import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; /** * Helper class for running the namespace sandbox. This runner prepares environment inside the - * sandbox (copies inputs, creates file structure), handles sandbox output, performs cleanup and - * changes invocation if necessary. + * sandbox, handles sandbox output, performs cleanup and changes invocation if necessary. */ public class NamespaceSandboxRunner { - private final boolean debug; - private final PathFragment sandboxDirectory; - private final Path sandboxPath; - private final List<String> mounts; - private final Path embeddedBinaries; - private final ImmutableList<PathFragment> includeDirectories; - private final PathFragment includePrefix; - private final Spawn spawn; private final Path execRoot; + private final Path sandboxPath; + private final Path sandboxExecRoot; + private final ImmutableMultimap<Path, Path> mounts; + private final boolean debug; - public NamespaceSandboxRunner(BlazeDirectories directories, Spawn spawn, - PathFragment includePrefix, List<PathFragment> includeDirectories, boolean debug) { - String md5sum = Fingerprint.md5Digest(spawn.getResourceOwner().getPrimaryOutput().toString()); - this.sandboxDirectory = new PathFragment("sandbox-root-" + md5sum); - this.sandboxPath = - directories.getExecRoot().getRelative("sandboxes").getRelative(sandboxDirectory); + public NamespaceSandboxRunner( + Path execRoot, Path sandboxPath, ImmutableMultimap<Path, Path> mounts, boolean debug) { + this.execRoot = execRoot; + this.sandboxPath = sandboxPath; + this.sandboxExecRoot = sandboxPath.getRelative(execRoot.asFragment().relativeTo("/")); + this.mounts = mounts; this.debug = debug; - this.mounts = new ArrayList<>(); - this.embeddedBinaries = directories.getEmbeddedBinariesRoot(); - this.includePrefix = includePrefix; - this.includeDirectories = ImmutableList.copyOf(includeDirectories); - this.spawn = spawn; - this.execRoot = directories.getExecRoot(); } - private void createFileSystem(Collection<? extends ActionInput> outputs) throws IOException { - // create the sandboxes' parent directory if needed - // TODO(bazel-team): create this with rest of the workspace dirs - if (!sandboxPath.getParentDirectory().isDirectory()) { - FilesystemUtils.mkdir(sandboxPath.getParentDirectory().getPathString(), 0755); - } - - FilesystemUtils.mkdir(sandboxPath.getPathString(), 0755); - String[] dirs = { "bin", "etc" }; - for (String dir : dirs) { - FilesystemUtils.mkdir(sandboxPath.getChild(dir).getPathString(), 0755); - mounts.add("/" + dir); - } - - // usr - String[] dirsUsr = { "bin", "include" }; - FilesystemUtils.mkdir(sandboxPath.getChild("usr").getPathString(), 0755); - Path usr = sandboxPath.getChild("usr"); - for (String dir : dirsUsr) { - FilesystemUtils.mkdir(usr.getChild(dir).getPathString(), 0755); - mounts.add("/usr/" + dir); - } - FileSystemUtils.createDirectoryAndParents(usr.getChild("local").getChild("include")); - mounts.add("/usr/local/include"); - - // shared libs - String[] rootDirs = FilesystemUtils.readdir("/"); - for (String entry : rootDirs) { - if (entry.startsWith("lib")) { - FilesystemUtils.mkdir(sandboxPath.getChild(entry).getPathString(), 0755); - mounts.add("/" + entry); - } - } - - String[] usrDirs = FilesystemUtils.readdir("/usr/"); - for (String entry : usrDirs) { - if (entry.startsWith("lib")) { - String lib = usr.getChild(entry).getPathString(); - FilesystemUtils.mkdir(lib, 0755); - mounts.add("/usr/" + entry); - } - } - - if (this.includePrefix != null) { - FilesystemUtils.mkdir(sandboxPath.getRelative(includePrefix).getPathString(), 0755); - - for (PathFragment fullPath : includeDirectories) { - // includeDirectories should be absolute paths like /usr/include/foo.h. we want to combine - // them into something like sandbox/include-prefix/usr/include/foo.h - for that we remove - // the leading '/' from the path string and concatenate with sandbox/include/prefix - FileSystemUtils.createDirectoryAndParents(sandboxPath.getRelative(includePrefix) - .getRelative(fullPath.getPathString().substring(1))); - } - } - - // output directories - for (ActionInput output : outputs) { - PathFragment parentDirectory = - new PathFragment(output.getExecPathString()).getParentDirectory(); - FileSystemUtils.createDirectoryAndParents(sandboxPath.getRelative(parentDirectory)); - } - } - - public void setupSandbox(List<? extends ActionInput> inputs, - Collection<? extends ActionInput> outputs) throws IOException { - createFileSystem(outputs); - setupBlazeUtils(); - includeManifests(); - includeRunfiles(); - copyInputs(inputs); - } - - private void copyInputs(List<? extends ActionInput> inputs) throws IOException { - for (ActionInput input : inputs) { - if (input.getExecPathString().contains("internal/_middlemen/")) { - continue; - } - Path target = sandboxPath.getRelative(input.getExecPathString()); - Path source = execRoot.getRelative(input.getExecPathString()); - FileSystemUtils.createDirectoryAndParents(target.getParentDirectory()); - File targetFile = new File(target.getPathString()); - // TODO(bazel-team): mount inputs inside sandbox instead of copying - Files.copy(new File(source.getPathString()), targetFile); - FilesystemUtils.chmod(targetFile, 0755); - } - } - - private void includeRunfiles() throws IOException { - Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings = - spawn.getRunfilesSupplier().getMappings(); - for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : - rootsAndMappings.entrySet()) { - PathFragment root = rootAndMappings.getKey(); - for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { - Artifact sourceArtifact = mapping.getValue(); - String sourcePath = (sourceArtifact != null) ? sourceArtifact.getPath().getPathString() - : "/dev/null"; - File source = new File(sourcePath); - - String targetPath = root.getRelative(mapping.getKey()).getPathString(); - File target = new File(targetPath); - - Files.createParentDirs(target); - Files.copy(source, target); - } - } - } - - private void includeManifests() throws IOException { - for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) { - String path = manifest.getValue().getPath().getPathString(); - for (String line : Files.readLines(new File(path), Charset.defaultCharset())) { - String[] fields = line.split(" "); - String targetPath = sandboxPath.getPathString() + PathFragment.SEPARATOR_CHAR + fields[0]; - String sourcePath = fields[1]; - File source = new File(sourcePath); - File target = new File(targetPath); - Files.createParentDirs(target); - Files.copy(source, target); - } - } - } - - private void setupBlazeUtils() throws IOException { - Path bin = this.sandboxPath.getChild("_bin"); - if (!bin.isDirectory()) { - FilesystemUtils.mkdir(bin.getPathString(), 0755); - } - Files.copy(new File(this.embeddedBinaries.getChild("build-runfiles").getPathString()), - new File(bin.getChild("build-runfiles").getPathString())); - FilesystemUtils.chmod(bin.getChild("build-runfiles").getPathString(), 0755); - } - - /** * Runs given * @@ -214,65 +61,86 @@ public class NamespaceSandboxRunner { * @param outErr - error output to capture sandbox's and command's stderr * @throws CommandException */ - public void run(List<String> spawnArguments, ImmutableMap<String, String> env, File cwd, - FileOutErr outErr) throws CommandException { - List<String> args = new ArrayList<>(); - args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString()); + public void run( + List<String> spawnArguments, + ImmutableMap<String, String> env, + File cwd, + FileOutErr outErr, + Collection<? extends ActionInput> outputs, + int timeout) + throws IOException, CommandException { + createFileSystem(outputs); - // Only for c++ compilation - if (includePrefix != null) { - for (PathFragment include : includeDirectories) { - args.add("-n"); - args.add(include.getPathString()); - } + List<String> args = new ArrayList<>(); - args.add("-N"); - args.add(includePrefix.getPathString()); - } + args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString()); if (debug) { args.add("-D"); } + // Sandbox directory. args.add("-S"); args.add(sandboxPath.getPathString()); - for (String mount : mounts) { + + // Working directory of the spawn. + args.add("-W"); + args.add(cwd.toString()); + + // Kill the process after a timeout. + if (timeout != -1) { + args.add("-T"); + args.add(Integer.toString(timeout)); + } + + // Mount all the inputs. + for (ImmutableMap.Entry<Path, Path> mount : mounts.entries()) { + args.add("-M"); + args.add(mount.getKey().getPathString()); args.add("-m"); - args.add(mount); + args.add(mount.getValue().getPathString()); } - args.add("-C"); + args.add("--"); args.addAll(spawnArguments); - Command cmd = new Command(args.toArray(new String[] {}), env, cwd); + + Command cmd = new Command(args.toArray(new String[0]), env, cwd); cmd.execute( - /* stdin */new byte[] {}, - Command.NO_OBSERVER, - outErr.getOutputStream(), - outErr.getErrorStream(), - /* killSubprocessOnInterrupt */true); + /* stdin */ new byte[] {}, + Command.NO_OBSERVER, + outErr.getOutputStream(), + outErr.getErrorStream(), + /* killSubprocessOnInterrupt */ true); + + copyOutputs(outputs); } + private void createFileSystem(Collection<? extends ActionInput> outputs) throws IOException { + FileSystemUtils.createDirectoryAndParents(sandboxPath); - public void cleanup() throws IOException { - FilesystemUtils.rmTree(sandboxPath.getPathString()); + // Prepare the output directories in the sandbox. + for (ActionInput output : outputs) { + PathFragment parentDirectory = + new PathFragment(output.getExecPathString()).getParentDirectory(); + FileSystemUtils.createDirectoryAndParents(sandboxExecRoot.getRelative(parentDirectory)); + } } - - public void copyOutputs(Collection<? extends ActionInput> outputs, FileOutErr outErr) - throws IOException { + private void copyOutputs(Collection<? extends ActionInput> outputs) throws IOException { for (ActionInput output : outputs) { - Path source = this.sandboxPath.getRelative(output.getExecPathString()); - Path target = this.execRoot.getRelative(output.getExecPathString()); + Path source = sandboxExecRoot.getRelative(output.getExecPathString()); + Path target = execRoot.getRelative(output.getExecPathString()); FileSystemUtils.createDirectoryAndParents(target.getParentDirectory()); - // TODO(bazel-team): eliminate cases when there are excessive outputs in spawns - // (java compilation expects "srclist" file in its outputs which is sometimes not produced) if (source.isFile()) { Files.move(new File(source.getPathString()), new File(target.getPathString())); - } else { - outErr.getErrorStream().write(("Output wasn't created by action: " + output + "\n") - .getBytes(StandardCharsets.UTF_8)); } } } + + public void cleanup() throws IOException { + if (sandboxPath.exists()) { + FilesystemUtils.rmTree(sandboxPath.getPathString()); + } + } } 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 69b13af624..20b64c87ca 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 @@ -22,6 +22,8 @@ import com.google.devtools.build.lib.exec.ExecutionOptions; import com.google.devtools.build.lib.runtime.BlazeRuntime; import com.google.devtools.build.lib.util.OS; +import java.util.concurrent.ExecutorService; + /** * Provides the sandboxed spawn strategy. */ @@ -30,12 +32,13 @@ public class SandboxActionContextProvider extends ActionContextProvider { @SuppressWarnings("unchecked") private final ImmutableList<ActionContext> strategies; - public SandboxActionContextProvider(BlazeRuntime runtime, BuildRequest buildRequest) { + public SandboxActionContextProvider( + BlazeRuntime runtime, BuildRequest buildRequest, ExecutorService backgroundWorkers) { boolean verboseFailures = buildRequest.getOptions(ExecutionOptions.class).verboseFailures; Builder<ActionContext> strategies = ImmutableList.builder(); if (OS.getCurrent() == OS.LINUX) { - strategies.add(new LinuxSandboxedStrategy(runtime.getDirectories(), verboseFailures)); + strategies.add(new LinuxSandboxedStrategy(runtime, verboseFailures, backgroundWorkers)); } this.strategies = strategies.build(); 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 65d0c15379..bc8785b659 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 @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.lib.sandbox; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.Subscribe; import com.google.devtools.build.lib.actions.ActionContextConsumer; @@ -23,17 +24,22 @@ import com.google.devtools.build.lib.runtime.BlazeModule; import com.google.devtools.build.lib.runtime.BlazeRuntime; import com.google.devtools.build.lib.runtime.Command; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + /** * This module provides the Sandbox spawn strategy. */ public class SandboxModule extends BlazeModule { + private final ExecutorService backgroundWorkers = Executors.newCachedThreadPool(); private BuildRequest buildRequest; private BlazeRuntime runtime; @Override public Iterable<ActionContextProvider> getActionContextProviders() { return ImmutableList.<ActionContextProvider>of( - new SandboxActionContextProvider(runtime, buildRequest)); + new SandboxActionContextProvider(runtime, buildRequest, backgroundWorkers)); } @Override @@ -43,7 +49,12 @@ public class SandboxModule extends BlazeModule { @Override public void beforeCommand(BlazeRuntime runtime, Command command) { - this.runtime = runtime; + if (this.runtime == null) { + this.runtime = runtime; + } else { + // The BlazeRuntime is guaranteed to never change. + Preconditions.checkArgument(runtime == this.runtime); + } runtime.getEventBus().register(this); } @@ -51,4 +62,32 @@ public class SandboxModule extends BlazeModule { public void buildStarting(BuildStartingEvent event) { buildRequest = event.getRequest(); } + + /** + * Shut down the background worker pool in the canonical way. + * + * <p>See https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html + */ + @Override + public void blazeShutdown() { + // Disable new tasks from being submitted + backgroundWorkers.shutdown(); + + try { + // Wait a while for existing tasks to terminate + if (!backgroundWorkers.awaitTermination(5, TimeUnit.SECONDS)) { + backgroundWorkers.shutdownNow(); // Cancel currently executing tasks + + // Wait a while for tasks to respond to being cancelled and force-kill them if necessary + // after the timeout. + backgroundWorkers.awaitTermination(5, TimeUnit.SECONDS); + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted + backgroundWorkers.shutdownNow(); + + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + } } diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java index 92f7a3cad0..cdd2d70583 100644 --- a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java @@ -82,7 +82,7 @@ public class StandaloneSpawnStrategy implements SpawnActionContext { // Disable it for now to make the setup easier and to avoid further PATH hacks. // Ideally we should have a native implementation of process-wrapper for Windows. args.add(processWrapper.getPathString()); - args.add("" + timeout); + args.add(Integer.toString(timeout)); args.add("5"); /* kill delay: give some time to print stacktraces and whatnot. */ // TODO(bazel-team): use process-wrapper redirection so we don't have to diff --git a/src/main/tools/BUILD b/src/main/tools/BUILD index 8fe91a5202..02f0b8d26a 100644 --- a/src/main/tools/BUILD +++ b/src/main/tools/BUILD @@ -1,9 +1,18 @@ package(default_visibility = ["//src:__subpackages__"]) +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"], copts = ["-std=c99"], + linkopts = ["-lm"], + deps = [":process-tools"], ) cc_binary( @@ -18,6 +27,8 @@ cc_binary( "//conditions:default": ["namespace-sandbox.c"], }), copts = ["-std=c99"], + linkopts = ["-lm"], + deps = [":process-tools"], ) filegroup( diff --git a/src/main/tools/namespace-sandbox.c b/src/main/tools/namespace-sandbox.c index 5cf6b433be..060356d13f 100644 --- a/src/main/tools/namespace-sandbox.c +++ b/src/main/tools/namespace-sandbox.c @@ -1,5 +1,3 @@ -#define _GNU_SOURCE - // Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,404 +12,502 @@ // See the License for the specific language governing permissions and // limitations under the License. +#define _GNU_SOURCE + #include <errno.h> #include <fcntl.h> -#include <getopt.h> -#include <limits.h> -#include <linux/capability.h> +#include <libgen.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/time.h> #include <sys/types.h> -#include <sys/wait.h> #include <unistd.h> -static int global_debug = 0; +#include "process-tools.h" -#define PRINT_DEBUG(...) do { if (global_debug) {fprintf(stderr, "sandbox.c: " __VA_ARGS__);}} while(0) +#define PRINT_DEBUG(...) \ + do { \ + if (global_debug) { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \ + } \ + } while (0) -#define CHECK_CALL(x) if ((x) == -1) { perror(#x); exit(1); } -#define CHECK_NOT_NULL(x) if (x == NULL) { perror(#x); exit(1); } -#define DIE() do { fprintf(stderr, "Error in %d\n", __LINE__); exit(-1); } while(0); +static bool global_debug = false; +static double global_kill_delay; +static int global_child_pid; +static volatile sig_atomic_t global_signal; -const int kChildrenCleanupDelay = 1; +// The uid and gid of the user and group 'nobody'. +static const int kNobodyUid = 65534; +static const int kNobodyGid = 65534; -static volatile sig_atomic_t global_signal_received = 0; - -// -// Options parsing result -// +// Options parsing result. struct Options { - char **args; // Command to run (-C / --) - char *include_prefix; // Include prefix (-N) - char *sandbox_root; // Sandbox root (-S) - char *tools; // tools directory (-t) - char **mounts; // List of directories to mount (-m) - char **includes; // List of include directories (-n) - int num_mounts; // size of mounts - int num_includes; // size of includes - int timeout; // Timeout (-T) + 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) + const char *stdout_path; // Where to redirect stdout (-l) + const char *stderr_path; // Where to redirect stderr (-L) + char *const *args; // Command to run (--) + const char *sandbox_root; // Sandbox root (-S) + const char *working_dir; // Working directory (-W) + char **mount_sources; // Map of directories to mount, from + char **mount_targets; // sources -> targets (-m) + int num_mounts; // How many mounts were specified }; -// 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. -void Usage(int argc, char **argv, char *fmt, ...); -// Parse the command line flags and return the result in an -// Options structure passed as argument. -void ParseCommandLine(int argc, char **argv, struct Options *opt); - -// Signal hanlding -void PropagateSignals(); -void EnableAlarm(); -// Sandbox setup -void SetupDirectories(struct Options* opt); -void SetupSlashDev(); -void SetupUserNamespace(int uid, int gid); -void ChangeRoot(); -// Write the file "filename" using a format string specified by "fmt". -// Returns -1 on failure. -int WriteFile(const char *filename, const char *fmt, ...); -// Run the command specified by the argv array and kill it after -// timeout seconds. -void SpawnCommand(char **argv, int timeout); - - - -int main(int argc, char *argv[]) { - struct Options opt = { - .args = NULL, - .include_prefix = NULL, - .sandbox_root = NULL, - .tools = NULL, - .mounts = calloc(argc, sizeof(char*)), - .includes = calloc(argc, sizeof(char*)), - .num_mounts = 0, - .num_includes = 0, - .timeout = 0 - }; - ParseCommandLine(argc, argv, &opt); - int uid = getuid(); - int gid = getgid(); - - // parsed all arguments, now prepare sandbox - PRINT_DEBUG("%s\n", opt.sandbox_root); - // create new namespaces in which this process and its children will live - CHECK_CALL(unshare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER)); - CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL)); - // Create the sandbox directory layout - SetupDirectories(&opt); - // Set the user namespace (user_namespaces(7)) - SetupUserNamespace(uid, gid); - // make sandbox actually hermetic: - ChangeRoot(); +// 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); - // Finally call the command - free(opt.mounts); - free(opt.includes); - SpawnCommand(opt.args, opt.timeout); - return 0; + fprintf(stderr, + "\nUsage: %s [-S sandbox-root] [-W working-dir] [-M source -m " + "target] -- 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 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 directory\n" + " -t time to give the child to shutdown cleanly before sending it a " + "SIGKILL\n" + " -T timeout after which sandbox will be terminated\n" + " -t in case timeout occurs, how long to wait before killing the " + "child with SIGKILL\n" + " -M/-m system directory to mount inside the sandbox\n" + " Multiple directories can be specified and each of them will\n" + " be mounted readonly. The -M option specifies which directory\n" + " to mount, the -m option specifies where to mount it in the\n" + " sandbox.\n" + " -D if set, debug info will be printed\n" + " -l redirect stdout to a file\n" + " -L redirect stderr to a file\n"); + exit(EXIT_FAILURE); } -void SpawnCommand(char **argv, int timeout) { - for (int i = 0; argv[i] != NULL; i++) { - PRINT_DEBUG("arg: %s\n", argv[i]); - } +// Parse the command line flags and return the result in an Options structure +// passed as argument. +static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) { + extern char *optarg; + extern int optind, optopt; + int c; - // spawn child and wait until it finishes - pid_t cpid = fork(); - if (cpid == 0) { - CHECK_CALL(setpgid(0, 0)); - // if the execvp below fails with "No such file or directory" it means that: - // a) the binary is not in the sandbox (which means it wasn't included in - // the inputs) - // b) the binary uses shared library which is not inside sandbox - you can - // check for that by running "ldd ./a.out" (by default directories - // starting with /lib* and /usr/lib* should be there) - // c) the binary uses elf interpreter which is not inside sandbox - you can - // check for that by running "readelf -a a.out | grep interpreter" (the - // sandbox code assumes that it is either in /lib*/ or /usr/lib*/) - CHECK_CALL(execvp(argv[0], argv)); - PRINT_DEBUG("Exec failed near %s:%d\n", __FILE__, __LINE__); - exit(1); - } else { - // PARENT - // make sure that all signals propagate to children (mostly useful to kill - // entire sandbox) - PropagateSignals(); - // after given timeout, kill children - EnableAlarm(timeout); - int status = 0; - while (1) { - PRINT_DEBUG("Waiting for the child...\n"); - pid_t pid = wait(&status); - if (global_signal_received) { - PRINT_DEBUG("Received signal: %s\n", strsignal(global_signal_received)); - CHECK_CALL(killpg(cpid, global_signal_received)); - // give children some time for cleanup before they terminate - sleep(kChildrenCleanupDelay); - CHECK_CALL(killpg(cpid, SIGKILL)); - exit(128 | global_signal_received); - } - if (errno == EINTR) { - continue; - } - if (pid < 0) { - perror("Wait failed:"); - exit(1); - } - if (WIFEXITED(status)) { - PRINT_DEBUG("Child exited with status: %d\n", WEXITSTATUS(status)); - exit(WEXITSTATUS(status)); - } - if (WIFSIGNALED(status)) { - PRINT_DEBUG("Child terminated by a signal: %d\n", WTERMSIG(status)); - exit(WEXITSTATUS(status)); - } - if (WIFSTOPPED(status)) { - PRINT_DEBUG("Child stopped by a signal: %d\n", WSTOPSIG(status)); - } + while ((c = getopt(argc, argv, ":DS:W:t:T:M:m:l:L:")) != -1) { + switch (c) { + case 'S': + if (opt->sandbox_root == NULL) { + opt->sandbox_root = optarg; + } else { + Usage(argc, argv, + "Multiple sandbox roots (-S) specified, expected one."); + } + break; + case 'W': + if (opt->working_dir == NULL) { + opt->working_dir = optarg; + } else { + Usage(argc, argv, + "Multiple working directories (-W) specified, expected at most " + "one."); + } + 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 '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 'M': + if (opt->mount_sources[opt->num_mounts] != NULL) { + Usage(argc, argv, "The -M option must be followed by an -m option."); + } + opt->mount_sources[opt->num_mounts] = optarg; + break; + case 'm': + if (opt->mount_sources[opt->num_mounts] == NULL) { + Usage(argc, argv, "The -m option must be preceded by an -M option."); + } + if (opt->sandbox_root == NULL) { + Usage(argc, argv, + "The sandbox root must be set via the -S option before " + "specifying an" + " -m option."); + } + if (strstr(optarg, opt->sandbox_root) != optarg) { + Usage(argc, argv, + "A path passed to the -m option must start with the sandbox " + "root."); + } + opt->mount_targets[opt->num_mounts++] = optarg; + break; + case 'D': + global_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; } } -} -int WriteFile(const char *filename, const char *fmt, ...) { - int r; - va_list ap; - FILE *stream = fopen(filename, "w"); - if (stream == NULL) { - return -1; + if (opt->sandbox_root == NULL) { + Usage(argc, argv, "Sandbox root (-S) must be specified"); } - va_start(ap, fmt); - r = vfprintf(stream, fmt, ap); - va_end(ap); - if (r >= 0) { - r = fclose(stream); + + if (opt->mount_sources[opt->num_mounts] != NULL && + opt->mount_sources[opt->num_mounts] == NULL) { + Usage(argc, argv, "An -m option is missing."); } - return r; -} -// -// Signal handling -// -void SignalHandler(int signum, siginfo_t *info, void *uctxt) { - global_signal_received = signum; + opt->args = argv + optind; + if (argc <= optind) { + Usage(argc, argv, "No command specified."); + } } -void PropagateSignals() { - // propagate some signals received by the parent to processes in sandbox, so - // that it's easier to terminate entire sandbox - struct sigaction action = {}; - action.sa_flags = SA_SIGINFO; - action.sa_sigaction = SignalHandler; - - // handle all signals that could terminate the process - int signals[] = {SIGHUP, SIGINT, SIGKILL, SIGPIPE, SIGALRM, SIGTERM, SIGPOLL, - SIGPROF, SIGVTALRM, - // signals below produce core dump by default, however at the moment we'll - // just terminate - SIGQUIT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS, SIGTRAP, SIGXCPU, - SIGXFSZ, -1}; - for (int *p = signals; *p != -1; p++) { - sigaction(*p, &action, NULL); +static void CreateNamespaces() { + // This weird workaround is necessary due to unshare sometimes 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 = 5000000; + while (tries++ < max_tries) { + if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC) == + 0) { + PRINT_DEBUG("unshare succeeded after %d tries\n", tries); + return; + } else { + if (errno != EINVAL) { + perror("unshare"); + exit(EXIT_FAILURE); + } + } + usleep(delay); + delay = (delay * 3) / 2; } + fprintf(stderr, + "unshare failed with EINVAL even after %d tries, giving up.\n", + tries); + exit(EXIT_FAILURE); } -void EnableAlarm(int timeout) { - if (timeout <= 0) return; - - struct itimerval timer = {}; - timer.it_value.tv_sec = (long) timeout; - CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL)); +static void CreateFile(const char *path) { + int handle; + CHECK_CALL(handle = open(path, O_CREAT | O_WRONLY | O_EXCL, 0666)); + CHECK_CALL(close(handle)); } -// -// Sandbox setup -// -void SetupSlashDev() { +static void SetupDevices() { CHECK_CALL(mkdir("dev", 0755)); - const char *devs[] = { - "/dev/null", - "/dev/random", - "/dev/urandom", - "/dev/zero", - NULL - }; + const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero", + NULL}; for (int i = 0; devs[i] != NULL; i++) { - // open+close to create the file, which will become mount point for actual - // device - int handle = open(devs[i] + 1, O_CREAT | O_RDONLY, 0644); - CHECK_CALL(handle); - CHECK_CALL(close(handle)); + CreateFile(devs[i] + 1); CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL)); } + + CHECK_CALL(symlink("/proc/self/fd", "dev/fd")); } -void SetupDirectories(struct Options *opt) { - // 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)); - SetupSlashDev(); - // Mount blaze specific directories - tools/ and build-runfiles/. - if (opt->tools != NULL) { - PRINT_DEBUG("tools: %s\n", opt->tools); - CHECK_CALL(mkdir("tools", 0755)); - CHECK_CALL(mount(opt->tools, "tools", NULL, MS_BIND | MS_RDONLY, NULL)); +// 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; } - // Mount directories passed in argv; those are mostly dirs for shared libs. - for (int i = 0; i < opt->num_mounts; i++) { - CHECK_CALL(mount(opt->mounts[i], opt->mounts[i] + 1, NULL, MS_BIND | MS_RDONLY, NULL)); + 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; + } } - // C++ compilation - // C++ headers go in a separate directory. - if (opt->include_prefix != NULL) { - CHECK_CALL(chdir(opt->include_prefix)); - for (int i = 0; i < opt->num_includes; i++) { - // TODO(bazel-team): sometimes list of -iquote given by bazel contains - // invalid (non-existing) entries, ideally we would like not to have them - PRINT_DEBUG("include: %s\n", opt->includes[i]); - if (mount(opt->includes[i], opt->includes[i] + 1 , NULL, MS_BIND, NULL) > -1) { - continue; - } - if (errno == ENOENT) { - continue; - } - CHECK_CALL(-1); - } - CHECK_CALL(chdir("..")); + // Create the parent directory. + CHECK_CALL(CreateTarget(dirname(strdupa(path)), true)); + + if (is_directory) { + CHECK_CALL(mkdir(path, 0755)); + } else { + CreateFile(path); } + return 0; +} + +static void SetupDirectories(struct Options *opt) { + // 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)); + + // Setup /dev. + SetupDevices(); + CHECK_CALL(mkdir("proc", 0755)); CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL)); + + CHECK_CALL(mkdir("tmp", 0755)); + CHECK_CALL(mount("tmpfs", "tmp", "tmpfs", MS_NOSUID | MS_NODEV, + "size=25%,mode=1777")); + + // Make sure the home directory exists and is writable. + const char *homedir; + if ((homedir = getenv("HOME")) == NULL) { + homedir = getpwuid(getuid())->pw_dir; + } + + if (homedir[0] != '/') { + DIE("Home directory of user nobody must be an absolute path, but is %s", homedir); + } + + char *homedir_absolute = malloc(strlen(opt->sandbox_root) + strlen(homedir) + 1); + strcpy(homedir_absolute, opt->sandbox_root); + strcat(homedir_absolute, homedir); + + CreateTarget(homedir_absolute, true); + CHECK_CALL(mount("tmpfs", homedir_absolute, "tmpfs", MS_NOSUID | MS_NODEV, + "size=25%,mode=1777")); + + // Mount directories passed in argv + for (int i = 0; i < opt->num_mounts; i++) { + struct stat sb; + stat(opt->mount_sources[i], &sb); + + CHECK_CALL(CreateTarget(opt->mount_targets[i], S_ISDIR(sb.st_mode))); + + PRINT_DEBUG("mount -o rbind,ro %s %s\n", opt->mount_sources[i], + opt->mount_targets[i]); + CHECK_CALL(mount(opt->mount_sources[i], opt->mount_targets[i], NULL, + MS_REC | MS_BIND | MS_RDONLY, NULL)); + } } -void SetupUserNamespace(int uid, int gid) { +// 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) { // 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(-1); + exit(EXIT_FAILURE); } - // set group and user mapping from outer namespace to inner: - // no changes in the parent, be root in the child - CHECK_CALL(WriteFile("/proc/self/uid_map", "0 %d 1\n", uid)); - CHECK_CALL(WriteFile("/proc/self/gid_map", "0 %d 1\n", gid)); - CHECK_CALL(setresuid(0, 0, 0)); - CHECK_CALL(setresgid(0, 0, 0)); + // 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", kNobodyUid, uid)); + CHECK_CALL(WriteFile("/proc/self/gid_map", "%d %d 1\n", kNobodyGid, gid)); + + CHECK_CALL(setresuid(kNobodyUid, kNobodyUid, kNobodyUid)); + CHECK_CALL(setresgid(kNobodyGid, kNobodyGid, kNobodyGid)); } -void ChangeRoot() { +static void ChangeRoot(struct Options *opt) { // move the real root to old_root, then detach it char old_root[16] = "old-root-XXXXXX"; - CHECK_NOT_NULL(mkdtemp(old_root)); + 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)); + } } -// -// Command line parsing -// -void Usage(int argc, char **argv, char *fmt, ...) { - int i; - va_list ap; - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); +// Called when timeout or signal occurs. +void OnSignal(int sig) { + global_signal = sig; - fprintf(stderr, - "\nUsage: %s [-S sandbox-root] [-m mount] [-C|--] command arg1\n", - argv[0]); - fprintf(stderr, " provided:"); - for (i = 0; i < argc; i++) { - fprintf(stderr, " %s", argv[i]); + // 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, global_kill_delay); + } 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, global_kill_delay); } - fprintf(stderr, - "\nMandatory arguments:\n" - " [-C|--] command to run inside sandbox, followed by arguments\n" - " -S directory which will become the root of the sandbox\n" - "\n" - "Optional arguments:\n" - " -t absolute path to bazel tools directory\n" - " -T timeout after which sandbox will be terminated\n" - " -m system directory to mount inside the sandbox\n" - " Multiple directories can be specified and each of them will\n" - " be mount as readonly\n" - " -D if set, debug info will be printed\n"); - exit(1); } -void ParseCommandLine(int argc, char **argv, struct Options *opt) { - extern char *optarg; - extern int optind, optopt; - int c; +// Run the command specified by the argv array and kill it after timeout +// seconds. +static void SpawnCommand(char *const *argv, double timeout_secs) { + for (int i = 0; argv[i] != NULL; i++) { + PRINT_DEBUG("arg: %s\n", argv[i]); + } - opt->include_prefix = NULL; - opt->sandbox_root = NULL; - opt->tools = NULL; - opt->mounts = malloc(argc * sizeof(char*)); - opt->includes = malloc(argc * sizeof(char*)); - opt->num_mounts = 0; - opt->num_includes = 0; - opt->timeout = 0; - - while ((c = getopt(argc, argv, "+:S:t:T:m:N:n:DC")) != -1) { - switch(c) { - case 'S': - if (opt->sandbox_root == NULL) { - opt->sandbox_root = optarg; - } else { - Usage(argc, argv, - "Multiple sandbox roots (-S) specified (expected one)."); - } - break; - case 'm': - opt->mounts[opt->num_mounts++] = optarg; - break; - case 'D': - global_debug = 1; - break; - case 'T': - sscanf(optarg, "%d", &opt->timeout); - if (opt->timeout < 0) { - Usage(argc, argv, "Invalid timeout (-T) value: %d", opt->timeout); - } - break; - case 'N': - opt->include_prefix = optarg; - break; - case 'n': - opt->includes[opt->num_includes++] = optarg; - break; - case 'C': - break; // deprecated, ignore. - case 't': - opt->tools = optarg; - break; - case '?': - Usage(argc, argv, "Unrecognized argument: -%c (%d)", optopt, optind); - break; - case ':': - Usage(argc, argv, "Flag -%c requires an argument", optopt); - break; + 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)) { + exit(WEXITSTATUS(status)); + } else { + int sig = WTERMSIG(status); + UnHandle(sig); + raise(sig); } } +} - opt->args = argv + optind; - if (argc <= optind) { - Usage(argc, argv, "No command specified"); - } +int main(int argc, char *const argv[]) { + struct Options opt; + memset(&opt, 0, sizeof(opt)); + opt.mount_sources = calloc(argc, sizeof(char *)); + opt.mount_targets = calloc(argc, sizeof(char *)); + + ParseCommandLine(argc, argv, &opt); + global_kill_delay = opt.kill_delay_secs; + + 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(); + + // 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)); + + SetupDirectories(&opt); + SetupUserNamespace(uid, gid); + ChangeRoot(&opt); + + SpawnCommand(opt.args, opt.timeout_secs); + + free(opt.mount_sources); + free(opt.mount_targets); + + return 0; } diff --git a/src/main/tools/process-tools.c b/src/main/tools/process-tools.c new file mode 100644 index 0000000000..48441e8485 --- /dev/null +++ b/src/main/tools/process-tools.c @@ -0,0 +1,139 @@ +// Copyright 2015 Google Inc. 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 <unistd.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <errno.h> +#include <signal.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <fcntl.h> + +#include "process-tools.h" + +int SwitchToEuid() { + int uid = getuid(); + int euid = geteuid(); + if (uid != euid) { + CHECK_CALL(setreuid(euid, euid)); + } + return euid; +} + +int SwitchToEgid() { + int gid = getgid(); + int egid = getegid(); + if (gid != egid) { + CHECK_CALL(setregid(egid, egid)); + } + return egid; +} + +void Redirect(const char *target_path, int fd, const char *name) { + if (target_path != NULL && strcmp(target_path, "-") != 0) { + int fd_out; + CHECK_CALL(fd_out = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0666), + "Could not open %s for redirection of %s", target_path, name); + CHECK_CALL(dup2(fd_out, fd)); + CHECK_CALL(close(fd_out)); + } +} + +void RedirectStdout(const char *stdout_path) { + Redirect(stdout_path, STDOUT_FILENO, "stdout"); +} + +void RedirectStderr(const char *stderr_path) { + Redirect(stderr_path, STDERR_FILENO, "stderr"); +} + +void KillEverything(int pgrp, bool gracefully, double graceful_kill_delay) { + if (gracefully) { + kill(-pgrp, SIGTERM); + + // Round up fractional seconds in this polling implementation. + int kill_delay = (int)(ceil(graceful_kill_delay)); + + // If the process is still alive, give it some time to die gracefully. + while (kill_delay-- > 0 && kill(-pgrp, 0) == 0) { + sleep(1); + } + } + + kill(-pgrp, SIGKILL); +} + +void HandleSignal(int sig, void (*handler)(int)) { + struct sigaction sa = {.sa_handler = handler}; + CHECK_CALL(sigemptyset(&sa.sa_mask)); + CHECK_CALL(sigaction(sig, &sa, NULL)); +} + +void UnHandle(int sig) { HandleSignal(sig, SIG_DFL); } + +void ClearSignalMask() { + // Use an empty signal mask for the process. + sigset_t empty_sset; + CHECK_CALL(sigemptyset(&empty_sset)); + CHECK_CALL(sigprocmask(SIG_SETMASK, &empty_sset, NULL)); + + // Set the default signal handler for all signals. + for (int i = 1; i < NSIG; ++i) { + if (i == SIGKILL || i == SIGSTOP) { + continue; + } + struct sigaction sa = {.sa_handler = SIG_DFL}; + CHECK_CALL(sigemptyset(&sa.sa_mask)); + // Ignore possible errors, because we might not be allowed to set the + // handler for certain signals, but we still want to try. + sigaction(i, &sa, NULL); + } +} + +void SetTimeout(double timeout_secs) { + if (timeout_secs <= 0) { + return; + } + + double int_val, fraction_val; + fraction_val = modf(timeout_secs, &int_val); + + struct itimerval timer = {.it_interval.tv_sec = 0, + .it_interval.tv_usec = 0, + .it_value.tv_sec = (long)int_val, + .it_value.tv_usec = (long)(fraction_val * 1e6)}; + + CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL)); +} + +int WaitChild(pid_t pid, const char *name) { + int err, status; + + do { + err = waitpid(pid, &status, 0); + } while (err == -1 && errno == EINTR); + + if (err == -1) { + DIE("wait on %s (pid %d) failed\n", name, pid); + } + + return status; +} diff --git a/src/main/tools/process-tools.h b/src/main/tools/process-tools.h new file mode 100644 index 0000000000..ef9fcdccdf --- /dev/null +++ b/src/main/tools/process-tools.h @@ -0,0 +1,85 @@ +// Copyright 2015 Google Inc. 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 PROCESS_TOOLS_H__ +#define PROCESS_TOOLS_H__ + +#include <sys/types.h> +#include <stdbool.h> + +// see +// http://stackoverflow.com/questions/5641427/how-to-make-preprocessor-generate-a-string-for-line-keyword +#define S(x) #x +#define S_(x) S(x) +#define S__LINE__ S_(__LINE__) + +#define DIE(args...) \ + { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": " args); \ + exit(EXIT_FAILURE); \ + } + +#define CHECK_CALL(x, ...) \ + if ((x) == -1) { \ + fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \ + perror(#x); \ + exit(EXIT_FAILURE); \ + } + +#define CHECK_NOT_NULL(x) \ + if (x == NULL) { \ + perror(#x); \ + exit(EXIT_FAILURE); \ + } + +// Switch completely to the effective uid. +// Some programs (notably, bash) ignore the euid and just use the uid. This +// limits the ability for us to use process-wrapper as a setuid binary for +// security/user-isolation. +int SwitchToEuid(); + +// Switch completely to the effective gid. +int SwitchToEgid(); + +// Redirect stdout to the file stdout_path (but not if stdout_path is "-"). +void RedirectStdout(const char *stdout_path); + +// Redirect stderr to the file stdout_path (but not if stderr_path is "-"). +void RedirectStderr(const char *stderr_path); + +// Make sure the process group "pgrp" and all its subprocesses are killed. +// If "gracefully" is true, sends SIGKILL first and after a timeout of +// "graceful_kill_delay" seconds, sends SIGTERM. +// If not, send SIGTERM immediately. +void KillEverything(int pgrp, bool gracefully, double graceful_kill_delay); + +// Set up a signal handler for a signal. +void HandleSignal(int sig, void (*handler)(int)); + +// Revert signal handler for a signal to the default. +void UnHandle(int sig); + +// Use an empty signal mask for the process and set all signal handlers to their +// default. +void ClearSignalMask(); + +// Receive SIGALRM after the given timeout. No-op if the timeout is +// non-positive. +void SetTimeout(double timeout_secs); + +// Wait for "pid" to exit and return its exit code. +// "name" is used for the error message only. +int WaitChild(pid_t pid, const char *name); + +#endif // PROCESS_TOOLS_H__ diff --git a/src/main/tools/process-wrapper.c b/src/main/tools/process-wrapper.c index 27601bf212..d156463726 100644 --- a/src/main/tools/process-wrapper.c +++ b/src/main/tools/process-wrapper.c @@ -22,220 +22,117 @@ // die with raise(SIGTERM) even if the child process handles SIGTERM with // exit(0). -#ifndef _GNU_SOURCE #define _GNU_SOURCE -#endif #include <errno.h> -#include <fcntl.h> -#include <math.h> #include <signal.h> +#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/time.h> #include <sys/types.h> #include <sys/stat.h> -#include <sys/wait.h> #include <unistd.h> +#include "process-tools.h" + // Not in headers on OSX. extern char **environ; -static int global_pid; // Returned from fork(). -static int global_signal = -1; -static double global_kill_delay = 0.0; - -#define DIE(args...) { \ - fprintf(stderr, args); \ - fprintf(stderr, " --- "); \ - perror(NULL); \ - fprintf(stderr, "\n"); \ - exit(EXIT_FAILURE); \ +static double global_kill_delay; +static int global_child_pid; +static volatile sig_atomic_t global_signal; + +// Options parsing result. +struct Options { + double timeout_secs; + double kill_delay_secs; + const char *stdout_path; + const char *stderr_path; + char *const *args; +}; + +// 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 *const *argv) { + fprintf(stderr, + "Usage: %s <timeout-secs> <kill-delay-secs> <stdout-redirect> " + "<stderr-redirect> <command> [args] ...\n", + argv[0]); + exit(EXIT_FAILURE); } -#define CHECK_CALL(x) if (x != 0) { perror(#x); exit(1); } - -// Make sure the process and all subprocesses are killed. -static void KillEverything(int pgrp) { - kill(-pgrp, SIGTERM); - - // Round up fractional seconds in this polling implementation. - int kill_delay = (int)(global_kill_delay+0.999) ; - // If the process is still alive, give it some time to die gracefully. - while (kill(-pgrp, 0) == 0 && kill_delay-- > 0) { - sleep(1); +// Parse the command line flags and return the result in an Options structure +// passed as argument. +static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) { + if (argc <= 5) { + Usage(argv); } - kill(-pgrp, SIGKILL); + argv++; + if (sscanf(*argv++, "%lf", &opt->timeout_secs) != 1) { + DIE("timeout_secs is not a real number.\n"); + } + if (sscanf(*argv++, "%lf", &opt->kill_delay_secs) != 1) { + DIE("kill_delay_secs is not a real number.\n"); + } + opt->stdout_path = *argv++; + opt->stderr_path = *argv++; + opt->args = argv; } -// Called when timeout or Signal occurs. -static void OnSignal(int sig) { +// 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_pid); + KillEverything(global_child_pid, true, global_kill_delay); } else { // Signals should kill the process quickly, as it's typically blocking // the return of the prompt after a user hits "Ctrl-C". - kill(-global_pid, SIGKILL); - } -} - -// Set up a signal handler which kills all subprocesses when the -// given signal is triggered. -static void InstallSignalHandler(int sig) { - struct sigaction sa = {}; - - sa.sa_handler = OnSignal; - sigemptyset(&sa.sa_mask); - CHECK_CALL(sigaction(sig, &sa, NULL)); -} - -// Revert signal handler to default. -static void UnHandle(int sig) { - struct sigaction sa = {}; - sa.sa_handler = SIG_DFL; - sigemptyset(&sa.sa_mask); - CHECK_CALL(sigaction(sig, &sa, NULL)); -} - -// Enable the given timeout, or no-op if the timeout is non-positive. -static void EnableAlarm(double timeout) { - if (timeout <= 0) return; - - struct itimerval timer = {}; - timer.it_interval.tv_sec = 0; - timer.it_interval.tv_usec = 0; - - double int_val, fraction_val; - fraction_val = modf(timeout, &int_val); - timer.it_value.tv_sec = (long) int_val; - timer.it_value.tv_usec = (long) (fraction_val * 1e6); - CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL)); -} - -static void ClearSignalMask() { - // Use an empty signal mask and default signal handlers in the - // subprocess. - sigset_t sset; - sigemptyset(&sset); - sigprocmask(SIG_SETMASK, &sset, NULL); - for (int i = 1; i < NSIG; ++i) { - if (i == SIGKILL || i == SIGSTOP) continue; - - struct sigaction sa = {}; - sa.sa_handler = SIG_DFL; - sigemptyset(&sa.sa_mask); - sigaction(i, &sa, NULL); + KillEverything(global_child_pid, false, global_kill_delay); } } -static int WaitChild(pid_t pid, const char *name) { - int err = 0; - int status = 0; - do { - err = waitpid(pid, &status, 0); - } while (err == -1 && errno == EINTR); - - if (err == -1) { - DIE("wait on %s (pid %d) failed", name, pid); - } - return status; -} - -// Usage: process-wrapper -// <timeout_sec> <kill_delay_sec> <stdout file> <stderr file> -// [cmdline] -int main(int argc, char *argv[]) { - if (argc <= 5) { - DIE("Not enough cmd line arguments to process-wrapper"); - } - - int uid = getuid(); - int euid = geteuid(); - if (uid != euid) { - // Switch completely to the target uid. - // Some programs (notably, bash) ignore the euid and just use the uid. This - // limits the ability for us to use process-wrapper as a setuid binary for - // security/user-isolation. - if (setreuid(euid, euid) != 0) { - DIE("changing uid failed: setreuid"); - } - } - - int gid = getgid(); - int egid = getegid(); - if (gid != egid) { - // Switch completely to the target gid. - if (setregid(egid, egid) != 0) { - DIE("changing gid failed: setregid"); - } - } - - // Parse the cmdline args to get the timeout and redirect files. - argv++; - double timeout; - if (sscanf(*argv++, "%lf", &timeout) != 1) { - DIE("timeout_sec is not a real number."); - } - if (sscanf(*argv++, "%lf", &global_kill_delay) != 1) { - DIE("kill_delay_sec is not a real number."); - } - char *stdout_path = *argv++; - char *stderr_path = *argv++; - - if (strcmp(stdout_path, "-")) { - // Redirect stdout and stderr. - int fd_out = open(stdout_path, O_WRONLY|O_CREAT|O_TRUNC, 0666); - if (fd_out == -1) { - DIE("Could not open %s for stdout", stdout_path); - } - if (dup2(fd_out, STDOUT_FILENO) == -1) { - DIE("dup2 failed for stdout"); - } - CHECK_CALL(close(fd_out)); - } - - if (strcmp(stderr_path, "-")) { - int fd_err = open(stderr_path, O_WRONLY|O_CREAT|O_TRUNC, 0666); - if (fd_err == -1) { - DIE("Could not open %s for stderr", stderr_path); - } - if (dup2(fd_err, STDERR_FILENO) == -1) { - DIE("dup2 failed for stderr"); - } - CHECK_CALL(close(fd_err)); - } - - global_pid = fork(); - if (global_pid < 0) { - DIE("Fork failed"); - } else if (global_pid == 0) { +// Run the command specified by the argv array and kill it after timeout +// seconds. +static void SpawnCommand(char *const *argv, double timeout_secs) { + CHECK_CALL(global_child_pid = fork()); + if (global_child_pid == 0) { // In child. - if (setsid() == -1) { - DIE("Could not setsid from child"); - } + CHECK_CALL(setsid()); ClearSignalMask(); + // Force umask to include read and execute for everyone, to make // output permissions predictable. umask(022); - execvp(argv[0], argv); // Does not return. - DIE("execvpe %s failed", argv[0]); + // Does not return unless something went wrong. + CHECK_CALL(execvp(argv[0], argv)); } else { // In parent. - InstallSignalHandler(SIGALRM); - InstallSignalHandler(SIGTERM); - InstallSignalHandler(SIGINT); - EnableAlarm(timeout); - int status = WaitChild(global_pid, argv[0]); + // 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); - // The child is done, but may have grandchildren. - kill(-global_pid, SIGKILL); if (global_signal > 0) { // Don't trust the exit code if we got a timeout or signal. UnHandle(global_signal); @@ -249,3 +146,21 @@ int main(int argc, char *argv[]) { } } } + +int main(int argc, char *argv[]) { + struct Options opt; + memset(&opt, 0, sizeof(opt)); + + ParseCommandLine(argc, argv, &opt); + global_kill_delay = opt.kill_delay_secs; + + SwitchToEuid(); + SwitchToEgid(); + + RedirectStdout(opt.stdout_path); + RedirectStderr(opt.stderr_path); + + SpawnCommand(opt.args, opt.timeout_secs); + + return 0; +} diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java index 0e4edbc882..7a91f21620 100644 --- a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java +++ b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java @@ -29,6 +29,7 @@ public class TestConstants { public static final ImmutableList<String> EMBEDDED_TOOLS = ImmutableList.of( "build-runfiles", "process-wrapper", + "namespace-sandbox", "build_interface_so"); /** diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD index e3a9c62780..8bc52c8bba 100644 --- a/src/test/shell/bazel/BUILD +++ b/src/test/shell/bazel/BUILD @@ -50,6 +50,8 @@ filegroup( "//src/java_tools/buildjar:JavaBuilder_deploy.jar", "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/genclass:GenClass_deploy.jar", "//src/java_tools/singlejar:SingleJar_deploy.jar", + "//src/main/tools:namespace-sandbox", + "//src/main/tools:process-wrapper", "//src/test/shell:bashunit", "//third_party:srcs", "//third_party/ijar", @@ -180,10 +182,21 @@ sh_test( ) sh_test( + name = "process_wrapper_test", + srcs = ["process-wrapper_test.sh"], + data = [":test-deps"], +) + +sh_test( + name = "namespace_runner_test", + srcs = ["namespace-runner_test.sh"], + data = [":test-deps"], +) + +sh_test( name = "bazel_sandboxing_test", srcs = ["bazel_sandboxing_test.sh"], data = [":test-deps"], - tags = ["manual"], # Test is still flaky. ) sh_test( diff --git a/src/test/shell/bazel/bazel_sandboxing_test.sh b/src/test/shell/bazel/bazel_sandboxing_test.sh index d21269e861..24f6baa8f1 100755 --- a/src/test/shell/bazel/bazel_sandboxing_test.sh +++ b/src/test/shell/bazel/bazel_sandboxing_test.sh @@ -61,19 +61,24 @@ EOF } function set_up { - mkdir -p examples/genrule - cat << 'EOF' > examples/genrule/a.txt + mkdir -p examples/genrule + cat << 'EOF' > examples/genrule/a.txt foo bar bz EOF - cat << 'EOF' > examples/genrule/b.txt + cat << 'EOF' > examples/genrule/b.txt apples oranges bananas EOF - cat << 'EOF' > examples/genrule/BUILD + + # Create cyclic symbolic links to check whether the strategy catches that. + ln -sf cyclic2 examples/genrule/cyclic1 + ln -sf cyclic1 examples/genrule/cyclic2 + + cat << 'EOF' > examples/genrule/BUILD genrule( name = "works", srcs = [ "a.txt" ], outs = [ "works.txt" ], - cmd = "wc a.txt > $@", + cmd = "wc $(location :a.txt) > $@", ) sh_binary( @@ -117,6 +122,13 @@ genrule( # cmd = "ls /home > $@", ) + +genrule( + name = "breaks3", + srcs = [ "cyclic1", "cyclic2" ], + outs = [ "breaks3.txt" ], + cmd = "wc $(location :cyclic1) > $@", +) EOF cat << 'EOF' >> examples/genrule/datafile this is a datafile @@ -125,14 +137,14 @@ EOF #!/bin/sh set -e -cp examples/genrule/datafile $1 +cp $(dirname $0)/tool.runfiles/examples/genrule/datafile $1 echo "Tools work!" EOF -chmod +x examples/genrule/tool.sh + chmod +x examples/genrule/tool.sh } function test_sandboxed_genrule() { - bazel build --genrule_strategy=sandboxed --verbose_failures \ + bazel build --genrule_strategy=sandboxed \ examples/genrule:works \ || fail "Hermetic genrule failed: examples/genrule:works" [ -f "${BAZEL_GENFILES_DIR}/examples/genrule/works.txt" ] \ @@ -140,7 +152,7 @@ function test_sandboxed_genrule() { } function test_sandboxed_tooldir() { - bazel build --genrule_strategy=sandboxed --verbose_failures \ + bazel build --genrule_strategy=sandboxed \ examples/genrule:tooldir \ || fail "Hermetic genrule failed: examples/genrule:tooldir" [ -f "${BAZEL_GENFILES_DIR}/examples/genrule/tooldir.txt" ] \ @@ -150,7 +162,7 @@ function test_sandboxed_tooldir() { } function test_sandboxed_genrule_with_tools() { - bazel build --genrule_strategy=sandboxed --verbose_failures \ + bazel build --genrule_strategy=sandboxed \ examples/genrule:tools_work \ || fail "Hermetic genrule failed: examples/genrule:tools_work" [ -f "${BAZEL_GENFILES_DIR}/examples/genrule/tools.txt" ] \ @@ -158,7 +170,7 @@ function test_sandboxed_genrule_with_tools() { } function test_sandbox_undeclared_deps() { - bazel build --genrule_strategy=sandboxed --verbose_failures \ + bazel build --genrule_strategy=sandboxed \ examples/genrule:breaks1 \ && fail "Non-hermetic genrule succeeded: examples/genrule:breaks1" || true [ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks1.txt" ] || { @@ -168,7 +180,7 @@ function test_sandbox_undeclared_deps() { } function test_sandbox_block_filesystem() { - bazel build --genrule_strategy=sandboxed --verbose_failures \ + bazel build --genrule_strategy=sandboxed \ examples/genrule:breaks2 \ && fail "Non-hermetic genrule succeeded: examples/genrule:breaks2" || true [ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks2.txt" ] || { @@ -177,6 +189,16 @@ function test_sandbox_block_filesystem() { } } +function test_sandbox_cyclic_symlink_in_inputs() { + bazel build --genrule_strategy=sandboxed \ + examples/genrule:breaks3 \ + && fail "Genrule with cyclic symlinks succeeded: examples/genrule:breaks3" || true + [ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks3.txt" ] || { + output=$(cat "${BAZEL_GENFILES_DIR}/examples/genrule/breaks3.txt") + fail "Genrule with cyclic symlinks breaks3 suceeded with following output: $(output)" + } +} + check_kernel_version check_sandbox_allowed || exit 0 run_suite "sandbox" diff --git a/src/test/shell/bazel/namespace-runner_test.sh b/src/test/shell/bazel/namespace-runner_test.sh new file mode 100755 index 0000000000..af21543bee --- /dev/null +++ b/src/test/shell/bazel/namespace-runner_test.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# +# Copyright 2015 Google Inc. 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. +# +# Test sandboxing spawn strategy +# + +# Load test environment +source $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/test-setup.sh \ + || { echo "test-setup.sh not found!" >&2; exit 1; } + +readonly WRAPPER="${bazel_data}/src/main/tools/namespace-sandbox" +readonly OUT_DIR="${TEST_TMPDIR}/out" +readonly OUT="${OUT_DIR}/outfile" +readonly ERR="${OUT_DIR}/errfile" +readonly SANDBOX_DIR="${OUT_DIR}/sandbox" + +WRAPPER_DEFAULT_OPTS="-S $SANDBOX_DIR" +for dir in /bin* /lib* /usr/bin* /usr/lib*; do + WRAPPER_DEFAULT_OPTS="$WRAPPER_DEFAULT_OPTS -M $dir -m ${SANDBOX_DIR}${dir}" +done + +# namespaces which are used by the sandbox were introduced in 3.8, so +# test won't run on earlier kernels +function check_kernel_version { + if [ "${PLATFORM-}" = "darwin" ]; then + echo "Test will skip: sandbox is not yet supported on Darwin." + exit 0 + fi + MAJOR=$(uname -r | sed 's/^\([0-9]*\)\.\([0-9]*\)\..*/\1/') + MINOR=$(uname -r | sed 's/^\([0-9]*\)\.\([0-9]*\)\..*/\2/') + if [ $MAJOR -lt 3 ]; then + echo "Test will skip: sandbox requires kernel >= 3.8; got $(uname -r)" + exit 0 + fi + if [ $MAJOR -eq 3 ] && [ $MINOR -lt 8 ]; then + echo "Test will skip: sandbox requires kernel >= 3.8; got $(uname -r)" + exit 0 + fi +} + +# Some CI systems might deactivate sandboxing +function check_sandbox_allowed { + mkdir -p test + # Create a program that check if unshare(2) is allowed. + cat <<'EOF' > test/test.c +#define _GNU_SOURCE +#include <sched.h> +int main() { + return unshare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER); +} +EOF + cat <<'EOF' >test/BUILD +cc_test(name = "sandbox_enabled", srcs = ["test.c"], copts = ["-std=c99"]) +EOF + bazel test //test:sandbox_enabled || { + echo "Sandboxing disabled, skipping..." + return false + } +} + +function set_up { + rm -rf $OUT_DIR + rm -rf $SANDBOX_DIR + + mkdir -p $OUT_DIR + mkdir $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() { + $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/echo hi there || fail + assert_output "hi there" "" +} + +function test_to_stderr() { + $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "/bin/echo hi there >&2" || fail + assert_output "" "hi there" +} + +function test_exit_code() { + $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c "exit 71" || code=$? + assert_equals 71 "$code" +} + +function test_signal_death() { + $WRAPPER $WRAPPER_DEFAULT_OPTS -l $OUT -L $ERR -- /bin/bash -c 'kill -ABRT $$' || code=$? + assert_equals 134 "$code" # SIGNAL_BASE + SIGABRT = 128 + 6 +} + +function test_signal_catcher() { + $WRAPPER $WRAPPER_DEFAULT_OPTS -T 2 -t 3 -l $OUT -L $ERR -- /bin/bash -c \ + 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 + assert_stdout "later" +} + +function test_basic_timeout() { + $WRAPPER $WRAPPER_DEFAULT_OPTS -T 3 -t 3 -l $OUT -L $ERR -- /bin/bash -c "echo before; sleep 1000; echo after" && fail + assert_output "before" "" +} + +function test_timeout_grace() { + $WRAPPER $WRAPPER_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=$? + assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 + assert_stdout "beforeafter" +} + +function test_timeout_kill() { + $WRAPPER $WRAPPER_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=$? + assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 + assert_stdout "before" +} + +check_kernel_version +check_sandbox_allowed || exit 0 +run_suite "namespace-runner" diff --git a/src/test/shell/bazel/process-wrapper_test.sh b/src/test/shell/bazel/process-wrapper_test.sh new file mode 100755 index 0000000000..df96daab5f --- /dev/null +++ b/src/test/shell/bazel/process-wrapper_test.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# +# Copyright 2015 Google Inc. 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. +# +# Test sandboxing spawn strategy +# + +# Load test environment +source $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/test-setup.sh \ + || { echo "test-setup.sh not found!" >&2; exit 1; } + +readonly WRAPPER="${bazel_data}/src/main/tools/process-wrapper" +readonly OUT_DIR="${TEST_TMPDIR}/out" +readonly OUT="${OUT_DIR}/outfile" +readonly ERR="${OUT_DIR}/errfile" + +function set_up() { + rm -rf $OUT_DIR + mkdir -p $OUT_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() { + $WRAPPER -1 0 $OUT $ERR /bin/echo hi there || fail + assert_output "hi there" "" +} + +function test_to_stderr() { + $WRAPPER -1 0 $OUT $ERR /bin/bash -c "/bin/echo hi there >&2" || fail + assert_output "" "hi there" +} + +function test_exit_code() { + $WRAPPER -1 0 $OUT $ERR /bin/bash -c "exit 71" || code=$? + assert_equals 71 "$code" +} + +function test_signal_death() { + $WRAPPER -1 0 $OUT $ERR /bin/bash -c 'kill -ABRT $$' || code=$? + assert_equals 134 "$code" # SIGNAL_BASE + SIGABRT = 128 + 6 +} + +function test_signal_catcher() { + $WRAPPER 2 3 $OUT $ERR /bin/bash -c \ + 'trap "echo later; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 + assert_stdout "later" +} + +function test_basic_timeout() { + $WRAPPER 3 3 $OUT $ERR /bin/bash -c "echo before; sleep 1000; echo after" && fail + assert_stdout "before" +} + +function test_timeout_grace() { + $WRAPPER 2 3 $OUT $ERR /bin/bash -c \ + 'trap "echo -n before; sleep 1; echo -n after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 + assert_stdout "beforeafter" +} + +function test_timeout_kill() { + $WRAPPER 2 3 $OUT $ERR /bin/bash -c \ + 'trap "echo before; sleep 1000; echo after; exit 0" SIGINT SIGTERM SIGALRM; sleep 1000' || code=$? + assert_equals 142 "$code" # SIGNAL_BASE + SIGALRM = 128 + 14 + assert_stdout "before" +} + +run_suite "process-wrapper" |