bazelbuild / bazelbuild/bazel

linux-sandbox: TEST_TMPDIR from action env is used as -w bind mount without validation; TMPDIR is sanitized, TEST_TMPDIR is not

Open
#29,457 3 comments 0 reactions 1 assignee Claimed by @meisterT View on GitHub
P3 team-Local-Exec type: bug
Dominant language
Java
Stars
25.8k
Forks
4.6k
Avg merge
2d 18h
Merged PRs (30d)
75

Description

### Description of the bug:

---

**Description:**

### What happened

In `AbstractSandboxSpawnRunner.getWritableDirs()`, the `TEST_TMPDIR` value from the action's `env` dict is passed directly to `addWritablePath()` and ends up as a `-w ` flag to `linux-sandbox`. There is no validation that the path is inside `sandboxExecRoot`. If the path exists on the host, it gets remounted read-write inside the sandbox mount namespace.

By contrast, `TMPDIR` is stripped from the action env in `PosixLocalEnvProvider.java:49` before it reaches this code. `TEST_TMPDIR` has no equivalent filter.

### Why this looks like a bug (not WAI)

This is the same class of issue that motivated stripping `TMPDIR` in #3296. The fix appears to have been partial — `TMPDIR` was sanitized, `TEST_TMPDIR` was missed.

It also contradicts two documented guarantees:

[Test Encyclopedia](https://bazel.build/reference/test-encyclopedia)
[sandboxing docs](https://bazel.build/docs/sandboxing)
state that `linux-sandbox` makes the filesystem read-only outside the sandbox directory.

A rule-supplied `TEST_TMPDIR` becoming a writable bind-mount target inverts both.

### Reproducer

`escape.bzl`:
```python
def _impl(ctx):
out = ctx.actions.declare_file("done.txt")
ctx.actions.run_shell(
outputs = [out],
env = {"TEST_TMPDIR": ctx.attr.target},
command = 'echo hi > "{t}/proof.txt" && echo ok > "$1"'.format(t = ctx.attr.target),
arguments = [out.path],
)
return [DefaultInfo(files = depset([out]))]

pwn = rule(implementation = _impl, attrs = {"target": attr.string(mandatory = True)})
```

`BUILD.bazel`:
```python
load(":escape.bzl", "pwn")
pwn(name = "x", target = "/tmp/bazel_test_tmpdir_repro")
```

Run:
```
mkdir -p /tmp/bazel_test_tmpdir_repro
USE_BAZEL_VERSION=9.1.0 bazel build :x --spawn_strategy=sandboxed --sandbox_debug
cat /tmp/bazel_test_tmpdir_repro/proof.txt
```

`--sandbox_debug` shows the path being added as a `-w` flag:
```
writable: /tmp/bazel_test_tmpdir_repro
remount rw: /tmp/bazel_test_tmpdir_repro
linux-sandbox: ... -w /tmp/bazel_test_tmpdir_repro ...
```

### Suggested fix

Mirror the `TMPDIR` handling in `PosixLocalEnvProvider`: ignore the action-supplied `TEST_TMPDIR` value, allocate a sandbox-local directory (e.g. `sandboxExecRoot/_test_tmpdir/`), and inject that as the `TEST_TMPDIR` override. The action-supplied value should never reach `addWritablePath()`.

### Version

Bazel 9.1.0, Linux, `--spawn_strategy=sandboxed`.

---

### Which category does this issue belong to?

Local Execution

### What's the simplest, easiest way to reproduce this bug? Please provide a minimal example if possible.

---

## **Bazel Sandbox Escape: Unsanitized `TEST_TMPDIR`**

### **Reproduction**

**Setup:**
```bash
mkdir -p /tmp/bazel_repro && cd /tmp/bazel_repro
mkdir -p /tmp/test_tmpdir_target
```

**`MODULE.bazel`:**
```python
module(name = "repro", version = "0.0.1")
```

**`escape.bzl`:**
```python
def _impl(ctx):
out = ctx.actions.declare_file("done.txt")
ctx.actions.run_shell(
outputs = [out],
env = {"TEST_TMPDIR": ctx.attr.target},
command = 'echo "written from sandboxed action" > "{t}/proof.txt" && echo ok > "$1"'.format(t = ctx.attr.target),
arguments = [out.path],
)
return [DefaultInfo(files = depset([out]))]

demo = rule(
implementation = _impl,
attrs = {"target": attr.string(mandatory = True)},
)
```

**`BUILD.bazel`:**
```python
load(":escape.bzl", "demo")

demo(
name = "x",
target = "/tmp/test_tmpdir_target",
)
```

**Run:**
```bash
bazel build :x --spawn_strategy=sandboxed --sandbox_debug 2>&1 | grep -E "writable:|remount|linux-sandbox"
cat /tmp/test_tmpdir_target/proof.txt
```

**Expected:** the action cannot write outside `sandboxExecRoot`; `/tmp/test_tmpdir_target/proof.txt` should not exist.

**Actual:** `--sandbox_debug` shows `/tmp/test_tmpdir_target` added as a `-w` bind mount; `proof.txt` is written to the host path.
---

### Which operating system are you running Bazel on?

Linux (Arch Linux, x86_64)

### What is the output of `bazel info release`?

_No response_

### If `bazel info release` returns `development version` or `(@non-git)`, tell us how you built Bazel.

_No response_

### What's the output of `git remote get-url origin; git rev-parse HEAD` ?

```text
N/A — not building Bazel from source, using released version via bazelisk.
```

### If this is a regression, please try to identify the Bazel commit where the bug was introduced with bazelisk --bisect.

Not a regression as far as I can tell — the `TEST_TMPDIR` codepath in `AbstractSandboxSpawnRunner.getWritableDirs()` appears to predate the `TMPDIR` sanitization fix in #3296. The issue is that the fix for `TMPDIR` was not extended to the sibling `TEST_TMPDIR` case.

### Have you found anything relevant by searching the web?

- #3296 — prior issue/fix that sanitized `TMPDIR` in `PosixLocalEnvProvider` for the same hermeticity / sandbox-boundary class of concern. The same reasoning applies to `TEST_TMPDIR` but the fix was not extended.
- Test Encyclopedia: https://bazel.build/reference/test-encyclopedia (defines `TEST_TMPDIR` as sandbox-local)
- Sandboxing docs: https://bazel.build/docs/sandboxing (states linux-sandbox prevents host writes)

### Any other information, logs, or outputs that you want to share?

Happy to provide full `--sandbox_debug` output, `/proc/self/mountinfo` from inside the sandbox, or test against Bazel@HEAD if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.