Skip to content

security: reject symlinked cache cleanup roots - #169

Merged
seonghobae merged 19 commits into
mainfrom
security/cache-root-symlink-v1
Aug 11, 2026
Merged

security: reject symlinked cache cleanup roots#169
seonghobae merged 19 commits into
mainfrom
security/cache-root-symlink-v1

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Purpose

Test-first hardening for cache cleanup scope. A catalog path can currently be a symlink to an unrelated directory: lexical catalog admission succeeds, read_dir follows the root symlink, and cleanup enumeration can expose children outside the intended cache root. Child-symlink filtering does not protect the root itself.

RED

Exact protected-main base: 7b81efd43c48439d7a4a5508f6d09ac15f141546.
RED head: 9a69606d68244a78ecf78deea6437d7aa8c03744.
The added Unix regressions require both catalog scope admission and child enumeration to reject a symlinked cache root. The current implementation is expected to fail those assertions because it performs lexical equality and calls read_dir directly on the symlink path.

Intended fix

Fail closed before enumeration: require the exact catalog root itself to be a real directory according to symlink_metadata, reject a root symlink, and retain the existing child-symlink exclusion. No deletion authority, cache catalog breadth, or protected-path policy is broadened.

Keep Draft until RED is observed, the narrow fix makes the same regressions green, unchanged exact-head Test/Release/Security/SAST evidence is acquired, and live-base/review state is revalidated.

Summary by CodeRabbit

  • 버그 수정

    • 카탈로그 루트가 실제 디렉터리인지 검증하고, 심볼릭 링크·Windows 재분석 지점은 거부합니다.
    • 경로 교체 상황에서도 원래 카탈로그를 안전하게 유지합니다.
    • 승인되지 않은 경로의 캐시 탐색 및 변경을 차단해 외부 데이터를 보호합니다.
    • 안전한 휴지통 작업이 불가능한 경우 캐시 정리를 수행하지 않습니다.
  • 변경 사항

    • 캐시 정리 UI를 읽기 전용으로 전환하고, 개발 아티팩트만 선택해 삭제할 수 있도록 개선했습니다.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bdba6555-b942-45f9-87f4-82efbfbe5d57

📥 Commits

Reviewing files that changed from the base of the PR and between 0156cb3 and 69a1e24.

📒 Files selected for processing (7)
  • src-tauri/src/cache_cleanup.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/rules.rs
  • src/lib/Cleanup.svelte
  • src/lib/cacheCleanupAtomicTrashContract.test.ts
  • src/lib/cacheCleanupFlowContract.test.ts
  • src/lib/cacheCleanupReadOnlyUiContract.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src-tauri/src/rules.rs

📝 Walkthrough

Walkthrough

카탈로그 루트를 실제 디렉터리로 검증한다. 심링크와 Windows reparse point를 거부한다. 열린 핸들로 캐시 후보와 정리 대상을 조회한다. 캐시 정리는 원자적 휴지통 작업이 없으면 실패한다. UI는 캐시를 읽기 전용으로 표시한다.

Changes

카탈로그 및 캐시 정리

Layer / File(s) Summary
실제 카탈로그 루트 검증
src-tauri/src/rules.rs
심링크와 Windows reparse point를 거부한다. 플랫폼별 디렉터리 핸들과 안정적인 namespace 경로를 사용한다.
고정된 카탈로그 루트 조회
src-tauri/src/rules.rs
CatalogRoot이 루트 동일성을 확인한다. 캐시 후보, 크기, 직계 자식을 열린 루트 기준으로 조회한다. 경로 교체와 심링크 동작을 테스트한다.
캐시 정리 명령과 등록
src-tauri/src/cache_cleanup.rs, src-tauri/src/lib.rs, src/lib/cacheCleanup*
clean_cache_contents가 안전하지 않은 경로와 원자적 휴지통 작업 부재를 오류로 처리한다. Tauri 명령과 계약 테스트를 추가한다. 지원되지 않는 운영체제의 컴파일을 차단한다.
캐시 읽기 전용 UI
src/lib/Cleanup.svelte, src/lib/cacheCleanupReadOnlyUiContract.test.ts
개발 아티팩트만 정리한다. 캐시 선택과 삭제 흐름을 제거하고 읽기 전용 안내를 표시한다.

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

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant CleanupUI
  participant Tauri
  participant CacheCleanup
  participant FileSystem
  Operator->>CleanupUI: select developer artifacts
  CleanupUI->>Tauri: cleanPaths(artifact paths)
  Tauri->>CacheCleanup: clean_cache_contents(cache path)
  CacheCleanup->>FileSystem: validate catalog root and filesystem identity
  FileSystem-->>CacheCleanup: validation result
  CacheCleanup-->>Tauri: success or fail-closed error
  Tauri-->>CleanupUI: cleanup result
Loading

Possibly related issues

Possibly related PRs

  • ContextualWisdomLab/disksage#171: 동일한 Cleanup.svelte 개발 아티팩트 정리 흐름을 수정하지만, 이 PR은 카탈로그 및 캐시 경로 안전성에 중점을 둔다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 심볼릭 링크된 캐시 정리 루트를 거부하는 핵심 보안 변경을 정확하고 간결하게 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/cache-root-symlink-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.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 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 coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/rules.rs`:
- Around line 84-97: Replace the separate is_real_directory validation and later
path-based use with a single no-follow root-opening operation, then pass the
acquired directory handle into scanner::scan_dir_with_interval and read_dir for
scanning and enumeration. Ensure the handle cannot be redirected by concurrent
symlink replacement; on platforms where no-follow opening is unavailable, reject
the root fail-closed instead of continuing with path-based access.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fdd950e-2582-490f-81e6-409adb3b9162

📥 Commits

Reviewing files that changed from the base of the PR and between 7b81efd and 38346fb.

📒 Files selected for processing (1)
  • src-tauri/src/rules.rs

Comment thread src-tauri/src/rules.rs Outdated
@seonghobae
seonghobae marked this pull request as ready for review August 11, 2026 07:07

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src-tauri/src/rules.rs (2)

194-224: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

재귀 깊이 제한과 중복 메타데이터 조회를 검토하십시오.

현재 구현은 하위 디렉터리마다 CatalogRoot::open을 호출합니다. openlstat 2회와 디렉터리 열기 2회를 수행하고, stable_path는 추가로 핸들 복제와 재열기를 수행합니다. 깊은 캐시 트리에서 syscall 비용이 크게 증가합니다. 또한 재귀에 깊이 제한이 없어 매우 깊은 트리에서 스택이 고갈될 수 있습니다.

각 엔트리에서 entry.file_type() 대신 symlink_metadata를 다시 호출하는 부분도 중복입니다. Unix에서 DirEntry::file_type()은 심링크를 따라가지 않습니다. 크기 계산에는 파일에서만 metadata가 필요합니다.

추가로 scanner 기반 계산이 제거되면서 취소·간격 제어가 사라졌습니다. cache_candidates는 IPC 스레드에서 호출되므로(src-tauri/src/commands.rs:401) 큰 캐시에서 응답이 지연될 수 있습니다. 깊이 상한 또는 취소 신호 도입을 검토하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/rules.rs` around lines 194 - 224, Update directory_size to
recurse directly through directory entries instead of calling CatalogRoot::open
and stable_path for every child, while enforcing a finite recursion-depth limit.
Use each DirEntry’s file_type to skip symlinks and identify directories,
fetching metadata only for regular files when adding sizes. Preserve saturating
accumulation and ensure cache_candidates remains bounded or cancellable when
invoked from the IPC thread.

146-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

지원되지 않는 플랫폼에서 결과가 조용히 비워집니다.

handle_namespace_pathNone을 반환하는 플랫폼(예: FreeBSD)에서는 stable_path가 항상 실패합니다. 그러면 directory_size는 0을 반환하고 child_paths는 빈 목록을 반환합니다. 반면 cache_candidatesexiststrue이고 is_catalog_pathtrue입니다. 사용자에게는 "존재하지만 0바이트, 정리 대상 없음"으로 보입니다.

동작 자체는 fail-closed 이므로 안전합니다. 다만 원인을 알 수 없는 상태입니다. 컴파일 타임에 미지원 플랫폼을 명시적으로 차단하거나, 최소한 한 번 경고 로그를 남기는 방식을 검토하십시오.

Also applies to: 194-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/rules.rs` around lines 146 - 149, Make unsupported targets
explicit in the cfg-gated handle_namespace_path path instead of silently
returning None: add a compile-time rejection for platforms outside Linux, macOS,
and Windows, while preserving the existing supported-platform implementations
and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/rules.rs`:
- Around line 118-121: Update the Unix open_directory_handle function to open
directories with kernel-level O_NOFOLLOW and O_DIRECTORY flags, rather than
relying on Handle::from_path, so symlink swaps fail closed and non-directories
are rejected during open. Use the existing direct libc dependency if available,
and return None on unsupported platforms or flag failures.
- Around line 226-249: Revalidate the catalog root immediately before deletion
in clean_paths, rather than trusting paths produced by expandCleanTargets.
Verify the root is still the expected directory and has not become a symlink or
been replaced, or perform child deletion relative to an already validated root
handle before calling trash_delete. Preserve existing behavior only after this
validation succeeds.

---

Nitpick comments:
In `@src-tauri/src/rules.rs`:
- Around line 194-224: Update directory_size to recurse directly through
directory entries instead of calling CatalogRoot::open and stable_path for every
child, while enforcing a finite recursion-depth limit. Use each DirEntry’s
file_type to skip symlinks and identify directories, fetching metadata only for
regular files when adding sizes. Preserve saturating accumulation and ensure
cache_candidates remains bounded or cancellable when invoked from the IPC
thread.
- Around line 146-149: Make unsupported targets explicit in the cfg-gated
handle_namespace_path path instead of silently returning None: add a
compile-time rejection for platforms outside Linux, macOS, and Windows, while
preserving the existing supported-platform implementations and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267b9f3-9ce7-4208-aa4a-83f85d90e1e3

📥 Commits

Reviewing files that changed from the base of the PR and between 38346fb and 0156cb3.

📒 Files selected for processing (1)
  • src-tauri/src/rules.rs

Comment thread src-tauri/src/rules.rs Outdated
Comment thread src-tauri/src/rules.rs
@seonghobae
seonghobae marked this pull request as draft August 11, 2026 07:16

@opencode-agent opencode-agent 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.

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 0fdfed2f31f3f3466153e905a4b6e16d159217c0.

  • Head SHA: 0fdfed2f31f3f3466153e905a4b6e16d159217c0

  • Workflow run: 31483369754

  • Workflow attempt: 1

Coverage evidence

Coverage Decision

  • Result: FAIL
  • Test evidence: not proven passing
  • Docstring evidence: not proven passing when configured
  • Failure count: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (7 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (7 files)"]
  R1 --> V1["required checks"]
Loading

@opencode-agent

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 0fdfed2f31f3f3466153e905a4b6e16d159217c0
  • Workflow run: 31483369754
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 0fdfed2f31f3f3466153e905a4b6e16d159217c0.

  • Head SHA: 0fdfed2f31f3f3466153e905a4b6e16d159217c0

  • Workflow run: 31483369754

  • Workflow attempt: 1

Coverage evidence

Coverage Decision

  • Result: FAIL
  • Test evidence: not proven passing
  • Docstring evidence: not proven passing when configured
  • Failure count: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (7 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (7 files)"]
  R1 --> V1["required checks"]
Loading

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review the unchanged exact current head ce6b212e2c923f394118c7b20c4dc13b2961cbe6 against live protected main 4f1728789bd65b6a1371018a77e85873d5949718. Prior review evidence predates this head; do not transfer predecessor-head findings or approval.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh pr view 169 \
  --repo ContextualWisdomLab/disksage \
  --json number,state,isDraft,headRefOid,baseRefName,baseRefOid \
  --jq '{number, state, isDraft, headRefOid, baseRefName, baseRefOid}'

gh api repos/ContextualWisdomLab/disksage/git/ref/heads/main \
  --jq '.object.sha'

Length of output: 668


@seonghobae Verified the live comparison.

  • Head: ce6b212e2c923f394118c7b20c4dc13b2961cbe6
  • Protected main: 4f1728789bd65b6a1371018a77e85873d5949718

I will review this exact range. I will not transfer findings or approval from an earlier head.

⚠️ Action not completed

No files to review.

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.

@seonghobae
seonghobae merged commit 50eba15 into main Aug 11, 2026
33 checks passed
@seonghobae
seonghobae deleted the security/cache-root-symlink-v1 branch August 11, 2026 17:10
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.

1 participant