Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --incompatible_sandbox_hermetic_tmp #16336

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.exec.TreeDeleter;
import com.google.devtools.build.lib.exec.local.LocalEnvProvider;
import com.google.devtools.build.lib.exec.local.PosixLocalEnvProvider;
Expand All @@ -52,14 +54,17 @@
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;

/** Spawn runner that uses linux sandboxing APIs to execute a local subprocess. */
final class LinuxSandboxedSpawnRunner extends AbstractSandboxSpawnRunner {

// Since checking if sandbox is supported is expensive, we remember what we've checked.
private static final Map<Path, Boolean> isSupportedMap = new HashMap<>();
private static final AtomicBoolean warnedAboutNonHermeticTmp = new AtomicBoolean();

/**
* Returns whether the linux sandbox is supported on the local machine by running a small command
Expand Down Expand Up @@ -119,6 +124,7 @@ private static boolean computeIsSupported(CommandEnvironment cmdEnv, Path linuxS
@Nullable private final SandboxfsProcess sandboxfsProcess;
private final boolean sandboxfsMapSymlinkTargets;
private final TreeDeleter treeDeleter;
private final Reporter reporter;

/**
* Creates a sandboxed spawn runner that uses the {@code linux-sandbox} tool.
Expand Down Expand Up @@ -158,6 +164,7 @@ private static boolean computeIsSupported(CommandEnvironment cmdEnv, Path linuxS
this.sandboxfsMapSymlinkTargets = sandboxfsMapSymlinkTargets;
this.localEnvProvider = new PosixLocalEnvProvider(cmdEnv.getClientEnv());
this.treeDeleter = treeDeleter;
this.reporter = cmdEnv.getReporter();
}

@Override
Expand All @@ -178,6 +185,26 @@ protected SandboxedSpawn prepareSpawn(Spawn spawn, SpawnExecutionContext context
sandboxExecRoot.getParentDirectory().createDirectory();
sandboxExecRoot.createDirectory();

Path sandboxTmp = null;
if (getSandboxOptions().sandboxHermeticTmp) {
PathFragment tmpRoot = PathFragment.create("/tmp");
// With a tmpfs on /tmp, mounting a disk-based hermetic /tmp isn't necessary.
if (!getSandboxOptions().sandboxTmpfsPath.contains(tmpRoot)) {
// Mounting a tmpfs strictly below the hermetic /tmp isn't supported. We fall back to
// non-hermetic /tmp in that case, but print a warning mentioning the problematic mount.
Optional<PathFragment> tmpfsPathUnderTmp = getSandboxOptions().sandboxTmpfsPath.stream()
.filter(path -> path.startsWith(tmpRoot))
.findFirst();
if (tmpfsPathUnderTmp.isEmpty()) {
sandboxTmp = sandboxPath.getRelative("_tmp");
sandboxTmp.createDirectoryAndParents();
} else if (warnedAboutNonHermeticTmp.compareAndSet(false, true)) {
reporter.handle(Event.warn(String.format(
"Falling back to non-hermetic /tmp in sandbox due to '%s' being a tmpfs path", tmpfsPathUnderTmp.get())));
}
}
}

ImmutableMap<String, String> environment =
localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), binTools, "/tmp");

Expand All @@ -196,7 +223,7 @@ protected SandboxedSpawn prepareSpawn(Spawn spawn, SpawnExecutionContext context
.addExecutionInfo(spawn.getExecutionInfo())
.setWritableFilesAndDirectories(writableDirs)
.setTmpfsDirectories(ImmutableSet.copyOf(getSandboxOptions().sandboxTmpfsPath))
.setBindMounts(getReadOnlyBindMounts(blazeDirs, sandboxExecRoot))
.setBindMounts(getBindMounts(blazeDirs, sandboxExecRoot, sandboxTmp))
.setUseFakeHostname(getSandboxOptions().sandboxFakeHostname)
.setCreateNetworkNamespace(
!(allowNetwork
Expand Down Expand Up @@ -282,15 +309,31 @@ protected ImmutableSet<Path> getWritableDirs(Path sandboxExecRoot, Map<String, S
return writableDirs.build();
}

private SortedMap<Path, Path> getReadOnlyBindMounts(
BlazeDirectories blazeDirs, Path sandboxExecRoot) throws UserExecException {
private SortedMap<Path, Path> getBindMounts(
BlazeDirectories blazeDirs, Path sandboxExecRoot, @Nullable Path sandboxTmp) throws UserExecException {
Path tmpPath = fileSystem.getPath("/tmp");
final SortedMap<Path, Path> bindMounts = Maps.newTreeMap();
boolean buildUnderTmp = false;
if (blazeDirs.getWorkspace().startsWith(tmpPath)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these two following remounts work with hermetic tmp?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as far as I can tell. I used the commands from test_write_hermetic_tmp to run it within /tmp and elsewhere, and it works elsewhere but fails within /tmp:

$ /tmp/bazel-hermeticsandbox --bazelrc=/dev/null  test pkg:tmp_test --spawn_strategy=sandboxed --incompatible_sandbox_hermetic_tmp --sandbox_debug --verbose_failures
INFO: Analyzed target //pkg:tmp_test (1 packages loaded, 2 targets configured).
INFO: Found 1 test target...
ERROR: /tmp/hermetictmptest/pkg/BUILD:1:8: Testing //pkg:tmp_test failed: (Exit 1): linux-sandbox failed: error executing command 
  (cd /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/5aee95154b6da9eb556a8fb39d46f41c/sandbox/linux-sandbox/8/execroot/__main__ && \
  exec env - \
    EXPERIMENTAL_SPLIT_XML_GENERATION=1 \
    JAVA_RUNFILES=bazel-out/k8-fastbuild/bin/pkg/tmp_test.runfiles \
    PATH=/usr/local/google/home/larsrc/.local/bin:/usr/local/google/home/larsrc/bin:/usr/lib/google-golang/bin:/usr/local/buildtools/java/jdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/intellij-idea/bin:/usr/local/google/home/larsrc/bin \
    PYTHON_RUNFILES=bazel-out/k8-fastbuild/bin/pkg/tmp_test.runfiles \
    RUNFILES_DIR=bazel-out/k8-fastbuild/bin/pkg/tmp_test.runfiles \
    RUN_UNDER_RUNFILES=1 \
    TEST_BINARY=pkg/tmp_test \
    TEST_INFRASTRUCTURE_FAILURE_FILE=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.infrastructure_failure \
    TEST_LOGSPLITTER_OUTPUT_FILE=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.raw_splitlogs/test.splitlogs \
    TEST_NAME=//pkg:tmp_test \
    TEST_PREMATURE_EXIT_FILE=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.exited_prematurely \
    TEST_SHARD_INDEX=0 \
    TEST_SIZE=medium \
    TEST_SRCDIR=bazel-out/k8-fastbuild/bin/pkg/tmp_test.runfiles \
    TEST_TARGET=//pkg:tmp_test \
    TEST_TIMEOUT=300 \
    TEST_TMPDIR=_tmp/dce7d97fa01edd02eaaa2ea9bd7d11f5 \
    TEST_TOTAL_SHARDS=0 \
    TEST_UNDECLARED_OUTPUTS_ANNOTATIONS=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.outputs_manifest/ANNOTATIONS \
    TEST_UNDECLARED_OUTPUTS_ANNOTATIONS_DIR=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.outputs_manifest \
    TEST_UNDECLARED_OUTPUTS_DIR=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.outputs \
    TEST_UNDECLARED_OUTPUTS_MANIFEST=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.outputs_manifest/MANIFEST \
    TEST_UNDECLARED_OUTPUTS_ZIP=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.outputs/outputs.zip \
    TEST_UNUSED_RUNFILES_LOG_FILE=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.unused_runfiles_log \
    TEST_WARNINGS_OUTPUT_FILE=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.warnings \
    TEST_WORKSPACE=__main__ \
    TMPDIR=/tmp \
    TZ=UTC \
    XML_OUTPUT_FILE=bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.xml \
  /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/install/cd380c01deb6ae3b758a4940bc515c99/linux-sandbox -t 15 -w /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/5aee95154b6da9eb556a8fb39d46f41c/sandbox/linux-sandbox/8/execroot/__main__ -w /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/5aee95154b6da9eb556a8fb39d46f41c/sandbox/linux-sandbox/8/execroot/__main__/_tmp/dce7d97fa01edd02eaaa2ea9bd7d11f5 -w /tmp -w /dev/shm -e /tmp -M /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/5aee95154b6da9eb556a8fb39d46f41c/sandbox/linux-sandbox/8/_tmp -m /tmp -M /tmp/hermetictmptest -S /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/5aee95154b6da9eb556a8fb39d46f41c/sandbox/linux-sandbox/8/stats.out -D -- external/bazel_tools/tools/test/generate-xml.sh bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.log bazel-out/k8-fastbuild/testlogs/pkg/tmp_test/test.xml 0 1)
Target //pkg:tmp_test up-to-date:
  bazel-bin/pkg/tmp_test
INFO: Elapsed time: 0.259s, Critical Path: 0.02s
INFO: 3 processes: 3 internal.
FAILED: Build did NOT complete successfully
//pkg:tmp_test                                                        NO STATUS

FAILED: Build did NOT complete successfully

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that doesn't seem to work. I think that getting this to work would be possible but slightly involved: It requires mounting the workspace/output base somewhere else first, then mounting /tmp, then mounting that "somewhere else" into the expected paths. That "somewhere else" may require cleanup though.

Do you think that this would be worth the effort? Alternatively, I could disable hermetic /tmp in this case and emit a warning once.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for now, let's disallow hermetic /tmp when inside /tmp, whether by disabling it with a warning or maybe downright failing. I would expect it to be rare.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a warning and am now also taking --sandbox_tmpfs_path=/tmp into account.

bindMounts.put(blazeDirs.getWorkspace(), blazeDirs.getWorkspace());
buildUnderTmp = true;
}
if (blazeDirs.getOutputBase().startsWith(tmpPath)) {
bindMounts.put(blazeDirs.getOutputBase(), blazeDirs.getOutputBase());
buildUnderTmp = true;
}
if (sandboxTmp != null) {
if (buildUnderTmp) {
if (warnedAboutNonHermeticTmp.compareAndSet(false, true)) {
reporter.handle(Event.warn("Falling back to non-hermetic /tmp in sandbox since workspace or output base " +
"lie under /tmp"));
}
} else {
// Mount a fresh, empty temporary directory as /tmp for each sandbox rather than reusing the
// host filesystem's /tmp. User-specified bind mounts can override this and use the host's
// /tmp instead by mounting /tmp to /tmp, if desired.
bindMounts.put(tmpPath, sandboxTmp);
}
}
for (ImmutableMap.Entry<String, String> additionalMountPath :
getSandboxOptions().sandboxAdditionalMounts) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,17 @@ public ImmutableSet<Path> getInaccessiblePaths(FileSystem fs) {
+ "then the input files will be copied instead.")
public boolean useHermetic;

@Option(
name = "incompatible_sandbox_hermetic_tmp",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
effectTags = {OptionEffectTag.EXECUTION},
help =
"If set to true, each Linux sandbox will have its own dedicated empty directory mounted as /tmp rather than"
+ "sharing /tmp with the host filesystem. Use --sandbox_add_mount_pair=/tmp to keep seeing the host's /tmp "
+ "in all sandboxes.")
public boolean sandboxHermeticTmp;

/** Converter for the number of threads used for asynchronous tree deletion. */
public static final class AsyncTreeDeletesConverter extends ResourceConverter {
public AsyncTreeDeletesConverter() {
Expand Down
10 changes: 10 additions & 0 deletions src/main/tools/linux-sandbox-pid1.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include <unistd.h>

#include <string>
#include <unordered_set>

#ifndef MS_REC
// Some systems do not define MS_REC in sys/mount.h. We might be able to grab it
Expand Down Expand Up @@ -288,8 +289,11 @@ static void MountFilesystems() {
opt.working_dir.c_str());
}

std::unordered_set<std::string> bind_mount_sources;

for (size_t i = 0; i < opt.bind_mount_sources.size(); i++) {
const std::string &source = opt.bind_mount_sources.at(i);
bind_mount_sources.insert(source);
const std::string &target = opt.bind_mount_targets.at(i);
PRINT_DEBUG("bind mount: %s -> %s", source.c_str(), target.c_str());
if (mount(source.c_str(), target.c_str(), nullptr, MS_BIND, nullptr) < 0) {
Expand All @@ -300,6 +304,12 @@ static void MountFilesystems() {

for (const std::string &writable_file : opt.writable_files) {
PRINT_DEBUG("writable: %s", writable_file.c_str());
if (bind_mount_sources.find(writable_file) != bind_mount_sources.end()) {
fmeum marked this conversation as resolved.
Show resolved Hide resolved
// Bind mount sources contained in writable_files will be kept writable in
// MakeFileSystemMostlyReadOnly, but have already been mounted at this
// point.
continue;
}
if (mount(writable_file.c_str(), writable_file.c_str(), nullptr,
MS_BIND | MS_REC, nullptr) < 0) {
DIE("mount(%s, %s, nullptr, MS_BIND | MS_REC, nullptr)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,62 @@ public void execAsync_statisticsCollectionDisabled_returnsEmptyStatistics() thro
assertThat(spawnResult.getNumInvoluntaryContextSwitches()).isEmpty();
}

@Test
public void hermeticTmp_tmpCreatedAndMounted() throws Exception {
runtimeWrapper.addOptions("--incompatible_sandbox_hermetic_tmp");
CommandEnvironment commandEnvironment = createCommandEnvironment();
LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
Spawn spawn = new SpawnBuilder().build();
SandboxedSpawn sandboxedSpawn = runner.prepareSpawn(spawn, createSpawnExecutionContext(spawn));

Path sandboxPath = sandboxedSpawn.getSandboxExecRoot().getParentDirectory().getParentDirectory();
Path hermeticTmpPath = sandboxPath.getRelative("_tmp");
assertThat(hermeticTmpPath.isDirectory()).isTrue();

assertThat(sandboxedSpawn).isInstanceOf(SymlinkedSandboxedSpawn.class);
String args = String.join(" ", sandboxedSpawn.getArguments());
assertThat(args).contains("-w /tmp");
assertThat(args).contains("-M " + hermeticTmpPath + " -m /tmp");
}

@Test
public void hermeticTmp_sandboxTmpfsOnTmp_tmpNotCreatedOrMounted() throws Exception {
runtimeWrapper.addOptions("--incompatible_sandbox_hermetic_tmp", "--sandbox_tmpfs_path=/tmp");
CommandEnvironment commandEnvironment = createCommandEnvironment();
LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
Spawn spawn = new SpawnBuilder().build();
SandboxedSpawn sandboxedSpawn = runner.prepareSpawn(spawn, createSpawnExecutionContext(spawn));

Path sandboxPath = sandboxedSpawn.getSandboxExecRoot().getParentDirectory().getParentDirectory();
Path hermeticTmpPath = sandboxPath.getRelative("_tmp");
assertThat(hermeticTmpPath.isDirectory()).isFalse();

assertThat(sandboxedSpawn).isInstanceOf(SymlinkedSandboxedSpawn.class);
String args = String.join(" ", sandboxedSpawn.getArguments());
assertThat(args).contains("-w /tmp");
assertThat(args).contains("-e /tmp");
assertThat(args).doesNotContain("-m /tmp");
}

@Test
public void hermeticTmp_sandboxTmpfsUnderTmp_tmpNotCreatedOrMounted() throws Exception {
runtimeWrapper.addOptions("--incompatible_sandbox_hermetic_tmp", "--sandbox_tmpfs_path=/tmp/subdir");
CommandEnvironment commandEnvironment = createCommandEnvironment();
LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
Spawn spawn = new SpawnBuilder().build();
SandboxedSpawn sandboxedSpawn = runner.prepareSpawn(spawn, createSpawnExecutionContext(spawn));

Path sandboxPath = sandboxedSpawn.getSandboxExecRoot().getParentDirectory().getParentDirectory();
Path hermeticTmpPath = sandboxPath.getRelative("_tmp");
assertThat(hermeticTmpPath.isDirectory()).isFalse();

assertThat(sandboxedSpawn).isInstanceOf(SymlinkedSandboxedSpawn.class);
String args = String.join(" ", sandboxedSpawn.getArguments());
assertThat(args).contains("-w /tmp");
assertThat(args).contains("-e /tmp");
assertThat(args).doesNotContain("-m /tmp");
}

private static LinuxSandboxedSpawnRunner setupSandboxAndCreateRunner(
CommandEnvironment commandEnvironment) throws IOException {
Path execRoot = commandEnvironment.getExecRoot();
Expand Down
153 changes: 153 additions & 0 deletions src/test/shell/bazel/bazel_sandboxing_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,159 @@ EOF
bazel build //pkg:a &>$TEST_log || fail "expected build to succeed"
}

function test_read_non_hermetic_tmp {
temp_dir=$(mktemp -d /tmp/test.XXXXXX)
trap 'rm -rf ${temp_dir}' EXIT

mkdir -p pkg
cat > pkg/BUILD <<'EOF'
sh_test(
name = "tmp_test",
srcs = ["tmp_test.sh"],
)
EOF

cat > pkg/tmp_test.sh <<EOF
[[ -f "${temp_dir}/file" ]]
EOF
chmod +x pkg/tmp_test.sh

touch "${temp_dir}/file"
bazel test //pkg:tmp_test \
--test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_read_hermetic_tmp {
if [[ "$(uname -s)" != Linux ]]; then
echo "Skipping test: --incompatible_sandbox_hermetic_tmp is only supported in Linux" 1>&2
return 0
fi

temp_dir=$(mktemp -d /tmp/test.XXXXXX)
trap 'rm -rf ${temp_dir}' EXIT

mkdir -p pkg
cat > pkg/BUILD <<'EOF'
sh_test(
name = "tmp_test",
srcs = ["tmp_test.sh"],
)
EOF

cat > pkg/tmp_test.sh <<EOF
[[ ! -f "${temp_dir}/file" ]]
EOF
chmod +x pkg/tmp_test.sh

touch "${temp_dir}/file"
bazel test //pkg:tmp_test --incompatible_sandbox_hermetic_tmp \
--test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_read_hermetic_tmp_user_override {
if [[ "$(uname -s)" != Linux ]]; then
echo "Skipping test: --incompatible_sandbox_hermetic_tmp is only supported in Linux" 1>&2
return 0
fi

temp_dir=$(mktemp -d /tmp/test.XXXXXX)
trap 'rm -rf ${temp_dir}' EXIT

mkdir -p pkg
cat > pkg/BUILD <<'EOF'
sh_test(
name = "tmp_test",
srcs = ["tmp_test.sh"],
)
EOF

cat > pkg/tmp_test.sh <<EOF
[[ -f "${temp_dir}/file" ]]
EOF
chmod +x pkg/tmp_test.sh

touch "${temp_dir}/file"
bazel test //pkg:tmp_test --incompatible_sandbox_hermetic_tmp --sandbox_add_mount_pair=/tmp \
--test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_write_non_hermetic_tmp {
temp_dir=$(mktemp -d /tmp/test.XXXXXX)
trap 'rm -rf ${temp_dir}' EXIT

mkdir -p pkg
cat > pkg/BUILD <<'EOF'
sh_test(
name = "tmp_test",
srcs = ["tmp_test.sh"],
)
EOF

cat > pkg/tmp_test.sh <<EOF
touch "${temp_dir}/file"
EOF
chmod +x pkg/tmp_test.sh

bazel test //pkg:tmp_test \
--test_output=errors &>$TEST_log || fail "Expected test to pass"
[[ -f "${temp_dir}/file" ]] || fail "Expected ${temp_dir}/file to exist"
}

function test_write_hermetic_tmp {
if [[ "$(uname -s)" != Linux ]]; then
echo "Skipping test: --incompatible_sandbox_hermetic_tmp is only supported in Linux" 1>&2
return 0
fi

temp_dir=$(mktemp -d /tmp/test.XXXXXX)
trap 'rm -rf ${temp_dir}' EXIT

mkdir -p pkg
cat > pkg/BUILD <<'EOF'
sh_test(
name = "tmp_test",
srcs = ["tmp_test.sh"],
)
EOF

cat > pkg/tmp_test.sh <<EOF
mkdir -p "${temp_dir}"
touch "${temp_dir}/file"
EOF
chmod +x pkg/tmp_test.sh

bazel test //pkg:tmp_test --incompatible_sandbox_hermetic_tmp \
--test_output=errors &>$TEST_log || fail "Expected test to pass"
[[ ! -f "${temp_dir}" ]] || fail "Expected ${temp_dir} to not exit"
}

function test_write_hermetic_tmp_user_override {
if [[ "$(uname -s)" != Linux ]]; then
echo "Skipping test: --incompatible_sandbox_hermetic_tmp is only supported in Linux" 1>&2
return 0
fi

temp_dir=$(mktemp -d /tmp/test.XXXXXX)
trap 'rm -rf ${temp_dir}' EXIT

mkdir -p pkg
cat > pkg/BUILD <<'EOF'
sh_test(
name = "tmp_test",
srcs = ["tmp_test.sh"],
)
EOF

cat > pkg/tmp_test.sh <<EOF
touch "${temp_dir}/file"
EOF
chmod +x pkg/tmp_test.sh

bazel test //pkg:tmp_test --incompatible_sandbox_hermetic_tmp --sandbox_add_mount_pair=/tmp \
--test_output=errors &>$TEST_log || fail "Expected test to pass"
[[ -f "${temp_dir}/file" ]] || fail "Expected ${temp_dir}/file to exist"
}

# The test shouldn't fail if the environment doesn't support running it.
check_sandbox_allowed || exit 0

Expand Down