Skip to content

security: reject symlink roots before canonicalization - #227

Draft
seonghobae wants to merge 50 commits into
mainfrom
security/symlink-root-admission-v1
Draft

security: reject symlink roots before canonicalization#227
seonghobae wants to merge 50 commits into
mainfrom
security/symlink-root-admission-v1

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Purpose

Fix #226 by rejecting caller-supplied symlink/reparse roots before canonicalization and preserving an opened no-follow root identity for read-only audit/recovery/materialization boundaries.

Exact current state

  • Exact source head: 457c9ebb2a69a6b234ef9ce8d882fcc3355892ec.
  • Independently resolved live main: 7eb131c4b4ac628226b262d6406a663fc7dc632f.
  • PR is open, Draft, mergeable, and directly based on the current main tip.
  • Current-head Test 32727270416, Release 32727270516, Security Scan 32727270533, and SAST Semgrep 32727270436 are queued; none is passing evidence yet.
  • No qualifying independent approval or repository-wide exact 100% coverage is claimed.

Test-first security lineage

The branch retains the existing fail-closed symlink-root diagnostics at the public read-only boundaries and now restores the Unix descriptor-relative root primitive that had regressed out of the current head:

  • read_dir_names() enumerates through a duplicated directory descriptor and fdopendir/readdir;
  • entry_kind() walks components with openat(O_DIRECTORY|O_NOFOLLOW) and inspects the leaf with fstatat(..., AT_SYMLINK_NOFOLLOW);
  • open_file() opens the leaf relative to the bound descriptor with openat(..., O_NOFOLLOW|O_NONBLOCK);
  • macOS uses F_GETPATH only for legacy/display-compatible stable_path() exposure, not as the only available child-I/O primitive.

A new executable Unix regression, bound_read_root_descriptor_replacement.rs, opens a real root, renames it, installs a replacement directory at the caller pathname, and proves descriptor-relative enumeration/type inspection/file reads still return the original object while final canonical_path() publication authority fails closed.

The RED commit was 2081f7784e96e91dc7eec7b7e7ac39266dd384de; the narrow primitive repair is a3dc596e5229e469dab87e3588edc61598e67cec.

Compatibility-path note

Not every consumer in this PR has been rewritten to descriptor-relative child I/O. duplicate_audit and some incomplete-download compatibility paths still operate through the identity-checked stable_path() namespace and final caller-path revalidation. Those read-only paths do not gain mutation authority, but this PR no longer claims that every child access is descriptor-relative. The restored primitive and replacement regression prevent the stronger Unix authority boundary from silently disappearing again and allow remaining consumers to migrate without reintroducing an unsafe root primitive.

Scope

Production/security changes remain bounded to root admission and read-only root-object binding. No delete, rename, extraction, recovery execution, upload, discard, or broader mutation authority is added. Existing descendant no-follow checks, lineage/fingerprint validation, coverage, security, and release gates remain fail-closed.

Required before merge

Keep Draft until an unchanged exact head has every applicable required native/central workflow passing, current valid findings are addressed, live-base ancestry remains current, any required independent review is satisfied, effective branch/ruleset enforcement is restored, and repository-wide exact 100% owned-production statement/region-equivalent, branch, function, and line coverage is proven without exclusions or threshold weakening. Pending, queued, skipped-required, cancelled, absent, failed, stale, predecessor, synthetic, diagnostic-only, status-only, model-only, author-only, or no-source-scanner evidence is non-passing.

Summary by CodeRabbit

  • 보안 및 안정성 개선

    • 파일 감사, 복구 검증 및 아카이브 처리가 원본 디렉터리 범위 내에서 안전하게 수행됩니다.
    • 심볼릭 링크와 Windows 정션을 통한 외부 경로 접근을 차단합니다.
    • 처리 중 기준 디렉터리가 변경되면 안전하지 않은 작업으로 감지합니다.
    • 경로 교체 상황에서도 최초에 확인한 파일과 디렉터리를 계속 읽도록 개선했습니다.
  • 테스트

    • Linux, macOS, Windows 환경에서 경로 교체, 링크 차단 및 권한 오류 관련 회귀 테스트를 추가했습니다.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

5개 읽기 전용 감사·복구·materialization API가 BoundReadRoot에 루트를 바인딩합니다. 자식 접근은 상대 경로와 no-follow 핸들을 사용합니다. 심볼릭 링크, junction, 경로 교체, 완료 후 루트 변경을 거부합니다.

Changes

바운드 읽기 루트

Layer / File(s) Summary
읽기 루트 바인딩 구현
src-tauri/src/bound_read_root.rs
Unix 디스크립터와 Windows 핸들 체인으로 상대 경로를 읽습니다. read_dir_names, entry_kind, open_file이 심볼릭 링크와 reparse point를 따르지 않습니다.
감사 API 상대 경로 탐색
src-tauri/src/duplicate_audit.rs, src-tauri/src/incomplete_download.rs, src-tauri/src/multipart_archive.rs
3개 감사 API가 bound root 기준 상대 경로로 순회, 메타데이터 조회, 파일 해싱을 수행합니다. 완료 후 canonical 루트 변경을 검사합니다.
복구 및 materialization 파일 검증
src-tauri/src/incomplete_download_recovery.rs, src-tauri/src/incomplete_download_materialization.rs
후보 파일을 bound root에서 검증하고 열린 파일로 PNG·ZIP 검증과 범위 해싱을 수행합니다. active-use probe에는 canonical 경로를 사용합니다.
루트 경계 회귀 검증
src-tauri/tests/root_identity_binding_contract.rs, src-tauri/tests/root_symlink_admission.rs, src-tauri/tests/bound_read_root_descriptor_replacement.rs, src-tauri/tests/windows_junction_no_follow.rs, src-tauri/tests/bound_read_root_macos_traversal.rs, src-tauri/tests/multipart_archive_unreadable_unrelated.rs
계약 및 플랫폼별 테스트가 루트 바인딩, 심볼릭 링크·junction 거부, 원본 객체 읽기, 감사 증거 완전성을 검증합니다.
플랫폼별 CI 검증
.github/workflows/test.yml, src-tauri/tests/windows_junction_workflow_contract.rs
Rust 테스트를 직렬 실행하고 Windows junction 및 macOS bound-root 회귀 테스트를 CI에 추가합니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to 1f86c

The PR hardens symlink/reparse root admission, but macOS read-only audit and recovery paths can still follow a replaced caller path and produce empty or degraded evidence, weakening the intended security boundary. The required validation checks are also still pending, so this should not merge until the macOS contract and replacement regression coverage are added and all required checks pass.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ReadOnlyAPI
  participant BoundReadRoot
  participant Filesystem
  Caller->>ReadOnlyAPI: source_root 전달
  ReadOnlyAPI->>BoundReadRoot: BoundReadRoot::open(source_root)
  BoundReadRoot->>Filesystem: no-follow 루트 핸들 바인딩
  ReadOnlyAPI->>BoundReadRoot: 상대 경로 탐색 및 파일 열기
  BoundReadRoot-->>ReadOnlyAPI: 검증된 파일 핸들
  ReadOnlyAPI->>Filesystem: canonical 경로에서 active-use probe
  ReadOnlyAPI->>BoundReadRoot: 완료 후 canonical_path() 재검증
  BoundReadRoot-->>ReadOnlyAPI: 결과 또는 *-root-unsafe 오류
  ReadOnlyAPI-->>Caller: 감사·복구·materialization 결과
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning root admission 변경과 관련 회귀 테스트는 #226의 핵심 요구를 충족합니다. 그러나 macOS에서 root replacement 후에도 child I/O가 stable_path()를 사용할 수 있어, 경로가 변경된 대상에 대한 읽기를 먼저 수행한 뒤 canonical_path() 검사를 실행할 가능성이 남아 있습니다. macOS를 포함한 모든 플랫폼에서 child traversal과 파일 읽기에 read_dir_names, entry_kind, open_file 등 identity-bound BoundReadRoot API만 사용하십시오. stable_path()를 child I/O 또는 외부 probe 경로로 사용하지 마십시오. 최종 canonical_path() identity 검사는 유지하고, root replacement 및 관련 보…
Docstring Coverage ⚠️ Warning Docstring coverage is 73.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 67 functions across 12 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 다섯 개 읽기 전용 경계에서 canonicalization 전에 symlink root를 거부하는 주요 변경을 정확하고 간결하게 설명합니다.
Out of Scope Changes check ✅ Passed BoundReadRoot 구현, 다섯 개 read-only 경계의 root admission 및 traversal 변경, 회귀 테스트와 CI 조정은 #226의 보안 수정과 검증 범위에 해당합니다. 관련 없는 기능 추가나 mutation 권한 확장은 확인되지 않습니다.
Full details: Linked Issues check

Resolution

macOS를 포함한 모든 플랫폼에서 child traversal과 파일 읽기에 read_dir_names, entry_kind, open_file 등 identity-bound BoundReadRoot API만 사용하십시오. stable_path()를 child I/O 또는 외부 probe 경로로 사용하지 마십시오. 최종 canonical_path() identity 검사는 유지하고, root replacement 및 관련 보안·회귀 테스트를 다시 실행하십시오.

Full details: Docstring Coverage

Explanation

Docstring coverage is 73.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 67 functions across 12 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/symlink-root-admission-v1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae marked this pull request as ready for review August 15, 2026 20:31
coderabbitai[bot]

This comment was marked as resolved.

@seonghobae
seonghobae marked this pull request as draft August 15, 2026 20:34

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact current head f4cf0024b9975af1b1c98b21c6ac1e8ec0696b28. Re-evaluate the already-addressed root-binding/macOS traversal findings against this head and report only still-valid source-backed issues; predecessor findings/reviews do not transfer.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== checked-out HEAD =='
git rev-parse HEAD
printf '%s\n' '== requested commit =='
git rev-parse f4cf0024b9975af1b1c98b21c6ac1e8ec0696b28

printf '%s\n' '== changed files =='
git diff-tree --no-commit-id --name-status -r f4cf0024b9975af1b1c98b21c6ac1e8ec0696b28

printf '%s\n' '== exact diff =='
git diff --find-renames --find-copies f4cf0024b9975af1b1c98b21c6ac1e8ec0696b28^ f4cf0024b9975af1b1c98b21c6ac1e8ec0696b28 -- \
  src-tauri/src/bound_read_root.rs \
  src-tauri/src/duplicate_audit.rs \
  src-tauri/src/multipart_archive.rs \
  src-tauri/src/incomplete_download.rs \
  src-tauri/src/incomplete_download_recovery.rs \
  src-tauri/src/incomplete_download_materialization.rs \
  src-tauri/tests/root_symlink_admission.rs \
  src-tauri/tests/root_identity_binding_contract.rs

printf '%s\n' '== module outlines =='
ast-grep outline src-tauri/src/bound_read_root.rs --items all --view expanded
for f in duplicate_audit.rs multipart_archive.rs incomplete_download.rs incomplete_download_recovery.rs incomplete_download_materialization.rs; do
  found="$(fd -a -t f "^${f}$" src-tauri/src | head -n 1 || true)"
  if [ -n "$found" ]; then
    ast-grep outline "$found" --items all --view expanded
  fi
done

Length of output: 38449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== bound root implementation =='
nl -ba src-tauri/src/bound_read_root.rs | sed -n '1,460p'

printf '%s\n' '== crate module ownership =='
nl -ba src-tauri/src/lib.rs | sed -n '1,120p'

for spec in \
  'src-tauri/src/duplicate_audit.rs:400:805' \
  'src-tauri/src/multipart_archive.rs:420:568' \
  'src-tauri/src/incomplete_download.rs:280:952' \
  'src-tauri/src/incomplete_download_recovery.rs:300:1036' \
  'src-tauri/src/incomplete_download_materialization.rs:140:579' \
  'src-tauri/tests/root_identity_binding_contract.rs:1:240' \
  'src-tauri/tests/root_symlink_admission.rs:1:180'
do
  IFS=: read -r file start end <<EOF
$spec
EOF
  printf '\n== %s, lines %s-%s ==\n' "$file" "$start" "$end"
  nl -ba "$file" | sed -n "${start},${end}p"
done

printf '%s\n' '== all BoundReadRoot use sites =='
rg -n -C 3 --glob '*.rs' '\b(BoundReadRoot::open|\.read_dir_names\(|\.entry_kind\(|\.open_file\(|\.canonical_path\(|\.stable_path\()' src-tauri

Length of output: 1366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show_range() {
  local file="$1" start="$2" end="$3"
  printf '\n== %s, lines %s-%s ==\n' "$file" "$start" "$end"
  awk -v start="$start" -v end="$end" 'NR >= start && NR <= end { printf "%6d\t%s\n", NR, $0 }' "$file"
}

show_range src-tauri/src/bound_read_root.rs 1 460
show_range src-tauri/src/lib.rs 1 120
show_range src-tauri/src/duplicate_audit.rs 400 805
show_range src-tauri/src/multipart_archive.rs 420 568
show_range src-tauri/src/incomplete_download.rs 280 952
show_range src-tauri/src/incomplete_download_recovery.rs 300 1036
show_range src-tauri/src/incomplete_download_materialization.rs 140 579
show_range src-tauri/tests/root_identity_binding_contract.rs 1 240
show_range src-tauri/tests/root_symlink_admission.rs 1 180

printf '%s\n' '== all BoundReadRoot use sites =='
rg -n -C 3 --glob '*.rs' '\b(BoundReadRoot::open|\.read_dir_names\(|\.entry_kind\(|\.open_file\(|\.canonical_path\(|\.stable_path\()' src-tauri

Length of output: 170708


@seonghobae Found one remaining source-backed issue on exact head f4cf0024b9975af1b1c98b21c6ac1e8ec0696b28.

Root replacement can still redirect macOS child I/O before the final fail-closed check.
BoundReadRoot::stable_path() explicitly states that the macOS F_GETPATH result is not rename-stable in src-tauri/src/bound_read_root.rs, Lines 435-438. However, four consumers use this pathname as I/O authority:

  • src-tauri/src/duplicate_audit.rs, Lines 565-576
  • src-tauri/src/incomplete_download.rs, Lines 858-869
  • src-tauri/src/incomplete_download_recovery.rs, Lines 949-963
  • src-tauri/src/incomplete_download_materialization.rs, Lines 388-390 and Line 440

On macOS, an attacker can rename the admitted root and replace the original pathname after stable_path() returns. The subsequent read_dir, symlink_metadata, and File::open calls can then access the replacement directory. Each consumer checks root_guard.canonical_path() only after this access, so it rejects the final report but does not prevent redirected child traversal.

Migrate these consumers to BoundReadRoot::read_dir_names, BoundReadRoot::entry_kind, and BoundReadRoot::open_file. Keep the final identity check as a publication guard.

You are interacting with an AI system.

@seonghobae
seonghobae marked this pull request as ready for review August 16, 2026 21:16
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@seonghobae
seonghobae marked this pull request as draft August 17, 2026 09:01
@seonghobae
seonghobae marked this pull request as ready for review August 17, 2026 15:16
@seonghobae
seonghobae marked this pull request as draft August 18, 2026 06:16
@opencode-agent opencode-agent Bot added area: security Security boundary, hardening, or vulnerability prevention priority: high High-priority or P1 work status: draft Draft pull request type: security Security vulnerability or security-specific remediation labels Aug 22, 2026
@seonghobae
seonghobae marked this pull request as ready for review August 24, 2026 05:24
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae
seonghobae marked this pull request as ready for review August 25, 2026 21:12
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@seonghobae
seonghobae enabled auto-merge (squash) August 25, 2026 21:12
devin-ai-integration[bot]

This comment was marked as resolved.

@opencode-agent
opencode-agent Bot disabled auto-merge August 25, 2026 21:24
devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae
seonghobae enabled auto-merge (squash) August 25, 2026 22:31
@seonghobae
seonghobae marked this pull request as draft August 25, 2026 23:06
auto-merge was automatically disabled August 25, 2026 23:06

Pull request was converted to draft

@seonghobae
seonghobae marked this pull request as ready for review August 26, 2026 00:10
coderabbitai[bot]

This comment was marked as resolved.

@seonghobae
seonghobae marked this pull request as draft August 26, 2026 00:22

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread .github/workflows/test.yml
@seonghobae
seonghobae marked this pull request as ready for review August 26, 2026 02:30
@seonghobae
seonghobae marked this pull request as draft August 26, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: security Security boundary, hardening, or vulnerability prevention priority: high High-priority or P1 work status: draft Draft pull request type: security Security vulnerability or security-specific remediation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: reject symlink roots before canonicalization in read-only audits and recovery

1 participant