llvm / llvm/llvm-project

[compiler-rt] UBSan accepts environment-controlled log_path in privileged execution

Open
#220,925 2 comments 0 reactions 0 assignees View on GitHub
compiler-rt:sanitizer
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

Title: [compiler-rt] UBSan accepts environment-controlled log_path in privileged execution

Component: compiler-rt / sanitizer-common

### Summary

On Linux, the UBSan runtime accepts `UBSAN_OPTIONS=log_path=...` and uses the
value as a report-file path even when the process is running with elevated
credentials. The path reaches `open()` with `O_WRONLY | O_CREAT | O_TRUNC`.
Therefore a privileged UBSan process can create or truncate an
attacker-selected file; a symlink at the final path can redirect the write.

The same shared report-path sink is used by LSan and TSan. This issue uses
UBSan as the minimal reproducer.

### Reproducer

[reproduction.zip](https://github.com/user-attachments/files/31792385/reproduction.zip)

The attached `trigger.c`, `build.sh`, and `expected.txt` are self-contained.
Use a clang installation that includes compiler-rt UBSan:

```sh
CLANG=clang OUT_DIR="$PWD/out" ./build.sh
sudo chown root:root out/privileged-ubsan
sudo chmod u+s out/privileged-ubsan
sudo -u nobody env UBSAN_OPTIONS=log_path="$PWD/out/logs/ubsan-log" \
"$PWD/out/privileged-ubsan"
ls -l out/logs/ubsan-log.*
```

On an affected runtime, `out/logs/ubsan-log.` is created with the
privileged owner and contains the UBSan division-by-zero diagnostic.

### Version and source path

The path is present in LLVM main at
`303475a57c121e13b3363a4ef0e3fcd52523457c` (`clang 24.0.0git`). The relevant
current source is:

```text
GetEnv (/proc/self/environ)
-> FlagParser::ParseStringFromEnv
-> UBSan common_flags()->log_path
-> __sanitizer_set_report_path
-> ReportFile::SetReportPath / ReopenIfNecessary
-> OpenFile(..., WrOnly)
-> O_WRONLY | O_CREAT | O_TRUNC
```

The security-relevant handoff is visible in these current-main excerpts:

```cpp
// compiler-rt/lib/sanitizer_common/sanitizer_linux.cpp:733-746
if (!ReadFileToBuffer("/proc/self/environ", &environ, &environ_size, &len))
environ = nullptr;
...
else if (!internal_memcmp(p, name, namelen) && p[namelen] == '=')
return p + namelen + 1;
```

```cpp
// compiler-rt/lib/ubsan/ubsan_flags.cpp:64-68
parser.ParseStringFromEnv("UBSAN_OPTIONS");

// compiler-rt/lib/ubsan/ubsan_init.cpp:42-46
InitializeFlags();
__sanitizer_set_report_path(common_flags()->log_path);
```

```cpp
// compiler-rt/lib/sanitizer_common/sanitizer_file.cpp:205-211
if (!path || internal_strcmp(path, "stderr") == 0) {
fd = kStderrFd;
} else if (internal_strcmp(path, "stdout") == 0) {
fd = kStdoutFd;
} else {
ParseAndSetPath(path, path_prefix, kMaxPathLength);
RecursiveCreateParentDirs(path_prefix, fd);
}

// sanitizer_file.cpp:62-73; sanitizer_posix.cpp:170-175
internal_snprintf(full_path, kMaxPathLength, "%s.%zu", path_prefix, pid);
fd = OpenFile(full_path, WrOnly, &err);
// WrOnly -> O_WRONLY | O_CREAT | O_TRUNC
```

Relevant locations in the reviewed tree are:

- `compiler-rt/lib/sanitizer_common/sanitizer_linux.cpp:712-749`: `GetEnv`
reads `/proc/self/environ`.
- `compiler-rt/lib/sanitizer_common/sanitizer_flag_parser.cpp:128-131`: the
value is passed to `ParseString`.
- `compiler-rt/lib/ubsan/ubsan_flags.cpp:64-68` and
`compiler-rt/lib/ubsan/ubsan_init.cpp:42-47`: UBSan parses the environment
and forwards `common_flags()->log_path`.
- `compiler-rt/lib/sanitizer_common/sanitizer_file.cpp:37-73,183-212`: the
PID-suffixed path is opened; only `stderr` and `stdout` are special-cased.
- `compiler-rt/lib/sanitizer_common/sanitizer_posix.cpp:166-176`: `WrOnly` maps to
`O_WRONLY | O_CREAT | O_TRUNC` without `O_NOFOLLOW`.

No `AT_SECURE` or equivalent privilege check protects this sink.

### Expected behavior

During secure execution, a non-stdio `log_path` supplied by the environment
should be rejected or replaced before directory creation and file open. No
attacker-selected file should be created or truncated; the diagnostic should
go to a safe destination such as stderr. Normal non-secure `log_path.`
behavior should remain supported.

The open occurs inside compiler-rt after the privilege transition. The
loader, kernel, and linker do not revalidate the pathname selected by this
runtime code.

### Validation

An earlier setuid-root reproduction with the LLVM 20 UBSan runtime was run by
an unprivileged caller and created a root-owned `ubsan-log.` report
containing the division-by-zero diagnostic.

The source path above is still present at the stated LLVM main revision:
`GetEnv` reads `/proc/self/environ`, UBSan accepts `UBSAN_OPTIONS=log_path=...`,
and the shared sink still opens the resulting pathname with
`O_WRONLY | O_CREAT | O_TRUNC` without an `AT_SECURE` guard. A UBSan runtime
built from this revision also accepts `log_path` in a normal run and creates
the expected PID-suffixed report. No fix for this path is present in the
reviewed tree.

### Fix direction

Add a secure-execution check, using Linux `AT_SECURE` and suitable platform
fallbacks, in the shared report-path sink before parent directories are
created or the file is opened. Add a regression test covering UBSan and the
shared LSan/TSan path.

I-have-reviewed-SECURITY-md: yes
I-used-AI-in-this-report: yes; local source inspection, version capture, and reproducer validation were AI-assisted; a human reviewer must verify the source references, validation scope, impact, and attached files before filing.

Contributor guide

Open the contributing guide

Research direction

Start with compiler-rt/lib/sanitizer_common/sanitizer_file.cpp and sanitizer_posix.cpp, then trace environment handling through sanitizer_linux.cpp and the UBSan entry points in ubsan_flags.cpp and ubsan_init.cpp. Run the attached build.sh reproducer and add regression coverage for UBSan and the shared LSan/TSan path. Done means secure execution avoids attacker-selected non-stdio paths while normal log_path behavior remains supported.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, cpp, linux
Domain
compilers, operating-systems, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.