Skip to content

Fix symlink escape vulnerabilities in safe_copy and log file reading (CWE-59) - #1050

Merged
mlim19 merged 1 commit into
masterfrom
fix-symlink-escape-v2
Aug 3, 2026
Merged

Fix symlink escape vulnerabilities in safe_copy and log file reading (CWE-59)#1050
mlim19 merged 1 commit into
masterfrom
fix-symlink-escape-v2

Conversation

@mlim19

@mlim19 mlim19 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Harden file operations to prevent container escape attacks (CWE-59) where a profiled container can exploit symlinks to read/write arbitrary host files.

  • 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.
  • safe_read_text(): New function to read files using O_NOFOLLOW to atomically reject symlinks at open time. Fix fd leak if os.fdopen() fails.
  • _read_ap_log(): Use safe_read_text() to prevent exfiltration of host files via symlink at the async-profiler log path.

Attack vectors closed

  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

Test plan

  • Verify safe_copy() raises exception when temp file appears unexpectedly (race/attack)
  • Verify safe_copy() raises exception when final destination is a symlink
  • Verify safe_read_text() raises exception when path is a symlink (ELOOP)
  • Verify normal Java profiling still works without symlinks present

🤖 Generated with Claude Code

…(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>
Copilot AI review requested due to automatic review settings July 24, 2026 18:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 use O_EXCL for atomic temp-file creation and adds a symlink defense-in-depth check before renaming.
  • Adds safe_read_text() which opens files with O_NOFOLLOW (Linux) to reject symlinks at open time.
  • Switches Java async-profiler log reading (_read_ap_log) to use safe_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 without O_NOFOLLOW the function may still follow symlinks (since _O_NOFOLLOW becomes 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_host and 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.

Comment thread gprofiler/utils/fs.py
Comment on lines +95 to +97
if _is_symlink_lstat(dst):
os.unlink(dst_tmp)
raise Exception(f"Refusing to copy: destination {dst} is a symlink (security restriction)")
Comment on lines +772 to 777
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Doing unlink/recreate in finally avoids skipping cleanup on exceptions and prevents persistent symlink-induced failures.

@dkorlovs
dkorlovs requested a review from lennin-cp July 29, 2026 16:27

@lennin-cp lennin-cp left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider Copilot reviews as well.

Comment thread gprofiler/utils/fs.py
"""
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +772 to 777
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Doing unlink/recreate in finally avoids skipping cleanup on exceptions and prevents persistent symlink-induced failures.

@mlim19
mlim19 merged commit b45210a into master Aug 3, 2026
75 of 81 checks passed
@mlim19
mlim19 deleted the fix-symlink-escape-v2 branch August 3, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants