Skip to content

Python: harden Hyperlight output capture against symlinks - #6601

Merged
eavanvalkenburg merged 2 commits into
microsoft:mainfrom
eavanvalkenburg:eavanvalkenburg/hyperlight-output-symlink-hardening
Jun 19, 2026
Merged

Python: harden Hyperlight output capture against symlinks#6601
eavanvalkenburg merged 2 commits into
microsoft:mainfrom
eavanvalkenburg:eavanvalkenburg/hyperlight-output-symlink-hardening

Conversation

@eavanvalkenburg

Copy link
Copy Markdown
Member

Motivation & Context

The HyperlightExecuteCodeTool already hardens its input-staging path so that
symlinks in a user-supplied workspace_root / file_mounts tree are never
followed when staging files into the sandbox (_copy_path, _iter_real_entries,
_path_tree_signature). The mirror output-capture path did not have the same
protection: it enumerated and read files from the sandbox-controlled /output
directory using APIs that follow symlinks (Path.rglob, Path.is_file,
Path.read_bytes). This change brings the output path in line with the input
path so both sides treat symlinked entries consistently.

Description & Review Guide

  • What are the major changes?

    • _collect_output_relative_paths now walks the output directory via the
      existing symlink-safe _iter_real_entries helper instead of Path.rglob,
      so symlinked entries and symlinked directories are not surfaced.
    • New _is_safe_output_file helper validates each path component from the
      output root to the target with lstat, rejecting any component that is a
      symlink and requiring the final entry to be a regular file. It replaces the
      previous Path.is_file gate in _parse_output_files.
    • File reads go through a new _read_output_file_bytes helper that opens with
      os.O_NOFOLLOW instead of Path.read_bytes; the resulting OSError is
      handled alongside PermissionError in _parse_output_files.
    • Added regression tests covering the output path (final-component file
      symlink, symlinked directory, intermediate directory symlink in a
      backend-provided listing, and the real-file happy path).
  • What is the impact of these changes?

    • Behavior for genuine, non-symlinked output files is unchanged. Symlinked
      entries in /output are no longer returned as Content. No public API
      changes.
  • What do you want reviewers to focus on?

    • The _is_safe_output_file component-by-component validation and the
      O_NOFOLLOW read path.

Related Issue

N/A — no linked issue.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Mirror the input-staging symlink hardening on the output-capture path of
HyperlightExecuteCodeTool. Output discovery now walks via the symlink-safe
_iter_real_entries instead of rglob, per-file collection validates that no
path component is a symlink and the final entry is a regular file, and file
reads use os.O_NOFOLLOW. Adds regression tests for the output path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 18, 2026 12:38
@moonbox3 moonbox3 added the python Usage: [Issues, PRs], Target: Python label Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/hyperlight/agent_framework_hyperlight
   _execute_code_tool.py6189384%69, 171, 234, 266, 269, 304–305, 320, 322, 335, 353, 363, 388, 393, 400, 406, 414, 422–424, 426–431, 471, 476, 478, 480, 505–506, 512–513, 518–519, 538–539, 548–549, 584, 615, 621–624, 642–645, 653, 686–687, 694–695, 697, 707–708, 748–749, 756, 802, 858–864, 933, 960, 1030, 1066, 1072–1074, 1103–1107, 1111–1112, 1117, 1134–1138, 1142–1143, 1200–1201
TOTAL39891450488% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7993 34 💤 0 ❌ 0 🔥 2m 4s ⏱️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens Hyperlight’s Python HyperlightExecuteCodeTool output-capture path to avoid following symlinks under the sandbox-controlled /output directory, aligning it with the existing symlink-safe input-staging behavior.

Changes:

  • Replaces Path.rglob output enumeration with _iter_real_entries to avoid descending into symlinked directories.
  • Adds _is_safe_output_file (component-by-component lstat validation) and _read_output_file_bytes (final-component O_NOFOLLOW open) to reduce symlink/TOCTOU exfiltration risk.
  • Adds regression tests covering symlinked outputs and the “happy path” for real output files.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py Adds symlink-safe validation + reading helpers and switches output enumeration to symlink-safe walking.
python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py Adds regression tests for output capture behavior around symlinks and real output files.

Comment thread python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 5 | Confidence: 86%

✓ Correctness

This PR correctly hardens the output capture path against symlink attacks, mirroring protections already on the input path. _collect_output_relative_paths now uses the symlink-safe _iter_real_entries walker instead of Path.rglob. _is_safe_output_file walks each path component with lstat to reject symlinks at any level. _read_output_file_bytes uses O_NOFOLLOW for defense-in-depth against TOCTOU races. The except clause correctly broadens to OSError to catch ELOOP from O_NOFOLLOW. All three layers (enumeration, validation, read) are consistent and well-tested. No correctness issues found.

✓ Security Reliability

This PR correctly hardens the output-capture path against symlink-based sandbox escapes, mirroring protections already applied on the input side. The three-layer defense (symlink-safe enumeration via _iter_real_entries, component-by-component lstat validation in _is_safe_output_file, and O_NOFOLLOW on read) is well-designed. One concern: _read_output_file_bytes silently falls back to following symlinks on platforms that lack O_NOFOLLOW, which undermines the TOCTOU defense the docstring promises. Overall the changes are sound and well-tested.

✓ Test Coverage

The PR adds five well-structured tests covering the main symlink attack vectors on the output path: final-component file symlink, symlinked directory, intermediate directory symlink from backend listing, and a regression test for genuine files. The coverage of the two higher-level functions (_collect_output_relative_paths and _parse_output_files) is solid. The main gap is that _read_output_file_bytes—the TOCTOU defense layer explicitly highlighted in the PR description—has no direct test exercising its O_NOFOLLOW rejection behavior; it is only tested on the happy path through integration tests.

✓ Failure Modes

The PR correctly hardens the output-capture path against symlink-based exfiltration. _is_safe_output_file walks each path component with lstat, _iter_real_entries replaces Path.rglob, and _read_output_file_bytes uses O_NOFOLLOW as TOCTOU defense-in-depth. The except (PermissionError, OSError) broadening is justified by the new ELOOP from O_NOFOLLOW. The layered defenses are sound: _collect_output_relative_paths filters at enumeration, _is_safe_output_file validates before read, and O_NOFOLLOW catches TOCTOU races. Tests cover all key scenarios. No blocking failure modes found.

✓ Design Approach

I found one design-level gap in the new output-read hardening. The approach closes the TOCTOU window on platforms that provide os.O_NOFOLLOW, but it silently degrades back to a normal open on supported Windows runners, so the final-component symlink swap the PR is trying to prevent can still succeed there.


Automated review by eavanvalkenburg's agents

- _is_safe_output_file now rejects '.'/'..' components (lexical relative_to
  could otherwise escape root without a symlink)
- _read_output_file_bytes adds a cross-platform TOCTOU guard (lstat/fstat
  st_dev+st_ino identity check) since O_NOFOLLOW is absent on Windows
- fix intermediate-dir-symlink test to use a relative listing path so it
  exercises normalization + validation; add a parent-traversal unit test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eavanvalkenburg
eavanvalkenburg added this pull request to the merge queue Jun 19, 2026
Merged via the queue into microsoft:main with commit 7435dd4 Jun 19, 2026
37 checks passed
@eavanvalkenburg
eavanvalkenburg deleted the eavanvalkenburg/hyperlight-output-symlink-hardening branch June 30, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants