Fix symlink escape vulnerabilities in safe_copy and log file reading (CWE-59) - #1050
Conversation
…(CWE-59) Harden file operations to prevent container escape attacks where a profiled container can exploit symlinks to read/write arbitrary host files. Changes to gprofiler/utils/fs.py: - safe_copy(): Use O_EXCL for atomic temp file creation, preventing symlink attacks. Add defense-in-depth check for dst symlinks before rename. Fix fd leak if os.fdopen() fails. - Add safe_read_text(): Read files using O_NOFOLLOW to atomically reject symlinks at open time. Fix fd leak if os.fdopen() fails. - Add _O_NOFOLLOW constant with getattr fallback for portability. Changes to gprofiler/profilers/java.py: - Use safe_read_text() in _read_ap_log() to prevent exfiltration of host files via symlink at the async-profiler log path. These changes close two attack vectors: 1. Write escape: Attacker plants symlink at libasyncProfiler.so copy destination to overwrite arbitrary host files 2. Read escape: Attacker plants symlink at async-profiler log path to exfiltrate host file contents into gProfiler logs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Hardens filesystem operations used by the Java async-profiler integration to mitigate symlink-based container escape vectors (CWE-59), specifically around copying the profiler shared library and reading async-profiler log files.
Changes:
- Reworks
safe_copy()to useO_EXCLfor atomic temp-file creation and adds a symlink defense-in-depth check before renaming. - Adds
safe_read_text()which opens files withO_NOFOLLOW(Linux) to reject symlinks at open time. - Switches Java async-profiler log reading (
_read_ap_log) to usesafe_read_text().
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| gprofiler/utils/fs.py | Adds symlink-aware helpers and hardens atomic copy logic to reduce CWE-59 risk. |
| gprofiler/profilers/java.py | Uses the new safe log-reading helper to prevent symlink-based host file exfiltration via async-profiler logs. |
Comments suppressed due to low confidence (2)
gprofiler/utils/fs.py:112
safe_read_text()docstring says it "raises if path is a symlink", but on platforms withoutO_NOFOLLOWthe function may still follow symlinks (since_O_NOFOLLOWbecomes 0). This is a documentation/contract mismatch; the docstring should match the best-effort behavior described above.
is best-effort; the target platform for this code is Linux where O_NOFOLLOW
is always available.
Raises if path is a symlink.
"""
gprofiler/profilers/java.py:772
- This change is security-critical (CWE-59) but there’s no test asserting that a symlinked async-profiler log path is rejected (and handled) as expected. Since this module already has extensive Java integration tests, it would be valuable to add a regression test that creates a symlink at
_log_path_hostand verifies the hardened behavior.
ap_log = safe_read_text(self._log_path_host)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if _is_symlink_lstat(dst): | ||
| os.unlink(dst_tmp) | ||
| raise Exception(f"Refusing to copy: destination {dst} is a symlink (security restriction)") |
| ap_log = safe_read_text(self._log_path_host) | ||
| # clean immediately so we don't mix log messages from multiple invocations. | ||
| # this is also what AP's profiler.sh does. | ||
| log.unlink() | ||
| Path(self._log_path_host).unlink() | ||
| self._recreate_log() | ||
| return ap_log |
There was a problem hiding this comment.
Agree. Doing unlink/recreate in finally avoids skipping cleanup on exceptions and prevents persistent symlink-induced failures.
lennin-cp
left a comment
There was a problem hiding this comment.
Please consider Copilot reviews as well.
| """ | ||
| try: | ||
| # O_NOFOLLOW makes open() fail with ELOOP if the path is a symlink (Linux-specific behavior). | ||
| # On platforms without O_NOFOLLOW the flag is 0 and the call may follow symlinks; the target |
There was a problem hiding this comment.
Nice hardening here. One follow-up to consider: when O_NOFOLLOW is unavailable, failing closed with a clear exception may be safer than falling back to 0, since fallback can silently weaken symlink protection on non-Linux platforms.
| ap_log = safe_read_text(self._log_path_host) | ||
| # clean immediately so we don't mix log messages from multiple invocations. | ||
| # this is also what AP's profiler.sh does. | ||
| log.unlink() | ||
| Path(self._log_path_host).unlink() | ||
| self._recreate_log() | ||
| return ap_log |
There was a problem hiding this comment.
Agree. Doing unlink/recreate in finally avoids skipping cleanup on exceptions and prevents persistent symlink-induced failures.
Summary
Harden file operations to prevent container escape attacks (CWE-59) where a profiled container can exploit symlinks to read/write arbitrary host files.
O_EXCLfor atomic temp file creation, preventing symlink attacks. Add defense-in-depth check for dst symlinks before rename. Fix fd leak ifos.fdopen()fails.O_NOFOLLOWto atomically reject symlinks at open time. Fix fd leak ifos.fdopen()fails.safe_read_text()to prevent exfiltration of host files via symlink at the async-profiler log path.Attack vectors closed
Test plan
safe_copy()raises exception when temp file appears unexpectedly (race/attack)safe_copy()raises exception when final destination is a symlinksafe_read_text()raises exception when path is a symlink (ELOOP)🤖 Generated with Claude Code