[7.0.1] Add support for bind mounts under `/tmp` with hermetic tmp (#20749)
This is achieved by rewriting the user-specified mounts to mounts onto
subdirectories of the hermetic sandbox tmp directory.
Fixes #20527
Closes #20583.
Commit
https://github.com/bazelbuild/bazel/commit/5e68afdaee34d4b645f74dc42c2cb93bf8f14785
PiperOrigin-RevId: 595815029
Change-Id: Ibfe5f67fb8fb59131b6c82a826ed5200f2b10a94
Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index 5e5e0a8..8c333be 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -14,13 +14,13 @@
package com.google.devtools.build.lib.sandbox;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.devtools.build.lib.sandbox.LinuxSandboxCommandLineBuilder.NetworkNamespace.NETNS_WITH_LOOPBACK;
import static com.google.devtools.build.lib.sandbox.LinuxSandboxCommandLineBuilder.NetworkNamespace.NO_NETNS;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ExecException;
@@ -61,6 +61,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
+import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
@@ -313,7 +314,8 @@
.addExecutionInfo(spawn.getExecutionInfo())
.setWritableFilesAndDirectories(writableDirs)
.setTmpfsDirectories(ImmutableSet.copyOf(getSandboxOptions().sandboxTmpfsPath))
- .setBindMounts(getBindMounts(blazeDirs, inputs, sandboxExecRootBase, sandboxTmp))
+ .setBindMounts(
+ prepareAndGetBindMounts(blazeDirs, inputs, sandboxExecRootBase, sandboxTmp))
.setUseFakeHostname(getSandboxOptions().sandboxFakeHostname)
.setEnablePseudoterminal(getSandboxOptions().sandboxExplicitPseudoterminal)
.setCreateNetworkNamespace(createNetworkNamespace ? NETNS_WITH_LOOPBACK : NO_NETNS)
@@ -403,50 +405,73 @@
return writableDirs.build();
}
- private ImmutableList<BindMount> getBindMounts(
+ private ImmutableList<BindMount> prepareAndGetBindMounts(
BlazeDirectories blazeDirs,
SandboxInputs inputs,
Path sandboxExecRootBase,
@Nullable Path sandboxTmp)
- throws UserExecException {
- Path tmpPath = fileSystem.getPath("/tmp");
- final SortedMap<Path, Path> bindMounts = Maps.newTreeMap();
+ throws UserExecException, IOException {
+ Path tmpPath = fileSystem.getPath(SLASH_TMP);
+ final SortedMap<Path, Path> userBindMounts = new TreeMap<>();
SandboxHelpers.mountAdditionalPaths(
- getSandboxOptions().sandboxAdditionalMounts, sandboxExecRootBase, bindMounts);
+ getSandboxOptions().sandboxAdditionalMounts, sandboxExecRootBase, userBindMounts);
for (Path inaccessiblePath : getInaccessiblePaths()) {
if (inaccessiblePath.isDirectory(Symlinks.NOFOLLOW)) {
- bindMounts.put(inaccessiblePath, inaccessibleHelperDir);
+ userBindMounts.put(inaccessiblePath, inaccessibleHelperDir);
} else {
- bindMounts.put(inaccessiblePath, inaccessibleHelperFile);
+ userBindMounts.put(inaccessiblePath, inaccessibleHelperFile);
}
}
- LinuxSandboxUtil.validateBindMounts(bindMounts);
+ LinuxSandboxUtil.validateBindMounts(userBindMounts);
+
+ if (sandboxTmp == null) {
+ return userBindMounts.entrySet().stream()
+ .map(e -> BindMount.of(e.getKey(), e.getValue()))
+ .collect(toImmutableList());
+ }
+
+ SortedMap<Path, Path> bindMounts = new TreeMap<>();
+ for (var entry : userBindMounts.entrySet()) {
+ Path mountPoint = entry.getKey();
+ Path content = entry.getValue();
+ if (mountPoint.startsWith(tmpPath)) {
+ // sandboxTmp should be null if /tmp is an explicit mount point since useHermeticTmp()
+ // returns false in that case.
+ if (mountPoint.equals(tmpPath)) {
+ throw new IOException(
+ "Cannot mount /tmp explicitly with hermetic /tmp. Please file a bug at"
+ + " https://github.com/bazelbuild/bazel/issues/new/choose.");
+ }
+ // We need to rewrite the mount point to be under the sandbox tmp directory, which will be
+ // mounted onto /tmp as the final mount.
+ mountPoint = sandboxTmp.getRelative(mountPoint.relativeTo(tmpPath));
+ mountPoint.createDirectoryAndParents();
+ }
+ bindMounts.put(mountPoint, content);
+ }
+
ImmutableList.Builder<BindMount> result = ImmutableList.builder();
bindMounts.forEach((k, v) -> result.add(BindMount.of(k, v)));
- if (sandboxTmp != null) {
- // First mount the real exec root and the empty directory created as the working dir of the
- // action under $SANDBOX/_tmp
- result.add(BindMount.of(sandboxTmp.getRelative(BAZEL_EXECROOT), blazeDirs.getExecRootBase()));
- result.add(
- BindMount.of(sandboxTmp.getRelative(BAZEL_WORKING_DIRECTORY), sandboxExecRootBase));
+ // First mount the real exec root and the empty directory created as the working dir of the
+ // action under $SANDBOX/_tmp
+ result.add(BindMount.of(sandboxTmp.getRelative(BAZEL_EXECROOT), blazeDirs.getExecRootBase()));
+ result.add(BindMount.of(sandboxTmp.getRelative(BAZEL_WORKING_DIRECTORY), sandboxExecRootBase));
- // Then mount the individual package roots under $SANDBOX/_tmp/bazel-source-roots
- inputs
- .getSourceRootBindMounts()
- .forEach(
- (withinSandbox, real) -> {
- PathFragment sandboxTmpSourceRoot = withinSandbox.asPath().relativeTo(tmpPath);
- result.add(BindMount.of(sandboxTmp.getRelative(sandboxTmpSourceRoot), real));
- });
+ // Then mount the individual package roots under $SANDBOX/_tmp/bazel-source-roots
+ inputs
+ .getSourceRootBindMounts()
+ .forEach(
+ (withinSandbox, real) -> {
+ PathFragment sandboxTmpSourceRoot = withinSandbox.asPath().relativeTo(tmpPath);
+ result.add(BindMount.of(sandboxTmp.getRelative(sandboxTmpSourceRoot), real));
+ });
- // Then mount $SANDBOX/_tmp at /tmp. At this point, even if the output base (and execroot)
- // and individual source roots are under /tmp, they are accessible at /tmp/bazel-*
- result.add(BindMount.of(tmpPath, sandboxTmp));
- }
-
+ // Then mount $SANDBOX/_tmp at /tmp. At this point, even if the output base (and execroot) and
+ // individual source roots are under /tmp, they are accessible at /tmp/bazel-*
+ result.add(BindMount.of(tmpPath, sandboxTmp));
return result.build();
}
diff --git a/src/test/shell/bazel/bazel_sandboxing_test.sh b/src/test/shell/bazel/bazel_sandboxing_test.sh
index 86fa7ad..ec7655a 100755
--- a/src/test/shell/bazel/bazel_sandboxing_test.sh
+++ b/src/test/shell/bazel/bazel_sandboxing_test.sh
@@ -316,21 +316,84 @@
sed -i.bak '/sandbox_tmpfs_path/d' $TEST_TMPDIR/bazelrc
+ local mounted=$(mktemp -d "/tmp/bazel_mounted.XXXXXXXX")
+ trap "rm -fr $mounted" EXIT
+ echo GOOD > "$mounted/data.txt"
+
mkdir -p pkg
- cat > pkg/BUILD <<'EOF'
+ cat > pkg/BUILD <<EOF
genrule(
name = "gen",
outs = ["gen.txt"],
- cmd = "cp /etc/data.txt $@",
+ # Verify that /tmp is still hermetic.
+ cmd = """[ ! -e "${mounted}/data.txt" ] && cp /etc/data.txt \$@""",
)
EOF
+ # This assumes the existence of /etc on the host system
+ bazel build --sandbox_add_mount_pair="$mounted:/etc" //pkg:gen || fail "build failed"
+ assert_contains GOOD bazel-bin/pkg/gen.txt
+}
+
+function test_add_mount_pair_tmp_target() {
+ if [[ "$PLATFORM" == "darwin" ]]; then
+ # Tests Linux-specific functionality
+ return 0
+ fi
+
+ create_workspace_with_default_repos WORKSPACE
+
+ sed -i.bak '/sandbox_tmpfs_path/d' $TEST_TMPDIR/bazelrc
+
+ local source_dir=$(mktemp -d "/tmp/bazel_mounted.XXXXXXXX")
+ trap "rm -fr $source_dir" EXIT
+ echo BAD > "$source_dir/data.txt"
+
+ mkdir -p pkg
+ cat > pkg/BUILD <<EOF
+genrule(
+ name = "gen",
+ outs = ["gen.txt"],
+ # Verify that /tmp is still hermetic.
+ cmd = """[ ! -e "${source_dir}/data.txt" ] && ls "$source_dir" > \$@""",
+)
+EOF
+
+
+ # This assumes the existence of /etc on the host system
+ bazel build --sandbox_add_mount_pair="/etc:$source_dir" //pkg:gen || fail "build failed"
+ assert_contains passwd bazel-bin/pkg/gen.txt
+}
+
+function test_add_mount_pair_tmp_target_and_source() {
+ if [[ "$PLATFORM" == "darwin" ]]; then
+ # Tests Linux-specific functionality
+ return 0
+ fi
+
+ create_workspace_with_default_repos WORKSPACE
+
+ sed -i.bak '/sandbox_tmpfs_path/d' $TEST_TMPDIR/bazelrc
+
local mounted=$(mktemp -d "/tmp/bazel_mounted.XXXXXXXX")
trap "rm -fr $mounted" EXIT
echo GOOD > "$mounted/data.txt"
- # This assumes the existence of /etc on the host system
- bazel build --sandbox_add_mount_pair="$mounted:/etc" //pkg:gen || fail "build failed"
+ local tmp_file=$(mktemp "/tmp/bazel_tmp.XXXXXXXX")
+ trap "rm $tmp_file" EXIT
+ echo BAD > "$tmp_file"
+
+ mkdir -p pkg
+ cat > pkg/BUILD <<EOF
+genrule(
+ name = "gen",
+ outs = ["gen.txt"],
+ # Verify that /tmp is still hermetic.
+ cmd = """[ ! -e "${tmp_file}" ] && cp "$mounted/data.txt" \$@""",
+)
+EOF
+
+ bazel build --sandbox_add_mount_pair="$mounted" //pkg:gen || fail "build failed"
assert_contains GOOD bazel-bin/pkg/gen.txt
}