[compiler-rt] UBSan accepts environment-controlled log_path in privileged execution
- 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
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