Skip to content

fix(io): attempt atomic publication of generated index files - #296

Draft
seonghobae wants to merge 2 commits into
masterfrom
sentinel-fix-toctou-16582868529218540884
Draft

fix(io): attempt atomic publication of generated index files#296
seonghobae wants to merge 2 commits into
masterfrom
sentinel-fix-toctou-16582868529218540884

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Reliability problem

write_index_file already writes the complete UTF-8 document to a same-directory temporary file before replacing index.html, but the final Files.move uses only REPLACE_EXISTING. On filesystems that support atomic moves, a single atomic replacement gives concurrent readers a clearer all-old-or-all-new publication boundary.

This is a reliability/availability hardening change. It is not evidence that every TOCTOU or symlink threat is eliminated, and the REPLACE_EXISTING fallback is explicitly non-atomic.

Proposed bounded change

  • attempt Files.move(..., ATOMIC_MOVE, REPLACE_EXISTING) through an injectable move boundary;
  • catch only AtomicMoveNotSupportedException and use the existing non-atomic replacement fallback;
  • retain same-directory temporary-file creation and unconditional cleanup; and
  • exercise the unsupported-atomic-move branch without weakening existing failure tests.

Required before ready

This head is intentionally not merge-ready. It must first update to the protected master after canonical CSP PR #363 integrates, preserve every current dependency/supply-chain/security control, and then add:

  1. a product-level all-old-or-all-new/concurrent-reader test where deterministic;
  2. failure/cleanup and existing-target tests for both paths;
  3. complete JaCoCo statement and branch coverage for the changed production surface;
  4. beginner-readable KDoc;
  5. CHANGELOG.md and APA 7 doctoring grounded in current Oracle/JDK Files.move and StandardCopyOption.ATOMIC_MOVE documentation;
  6. exact-current-head CI, coverage, Security Scan, Semgrep, automated review, and independent non-author approval.

Queued, pending, skipped-required, cancelled, absent, stale-head, or failed checks are not success. No review or check from duplicate atomic-move branches is reusable.

@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

write_index_file은 임시 파일을 먼저 ATOMIC_MOVE로 교체합니다. 파일 시스템이 이를 지원하지 않으면 REPLACE_EXISTING으로 대체합니다. 테스트는 대체 경로 호출과 최종 파일 내용을 검증합니다.

Changes

원자적 index.html 쓰기

Layer / File(s) Summary
원자적 이동 및 대체 경로
src/main/kotlin/html4tree/main.kt, src/test/kotlin/html4tree/MainTest.kt, .jules/sentinel.md
write_index_file이 이동 함수를 주입받도록 변경되었습니다. ATOMIC_MOVE가 지원되지 않으면 일반 교체 이동을 수행합니다. 테스트는 대체 경로와 최종 index.html 내용을 검증합니다. 보안 항목이 문서에 추가되었습니다.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 TOCTOU 완화를 위한 원자적 파일 쓰기 도입이라는 주요 변경 사항을 명확하게 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentinel-fix-toctou-16582868529218540884

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

@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

🧹 Nitpick comments (2)
src/test/kotlin/html4tree/MainTest.kt (1)

365-375: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fallback 테스트에서 기존 대상 교체를 검증하세요.

현재 Line 367에서 기존 index.html directory를 삭제한 뒤 fallback을 실행합니다. 따라서 대상이 없는 경우만 테스트합니다. REPLACE_EXISTING이 기존 파일을 실제로 교체하는지 검증하지 않습니다. (docs.oracle.com)

기존 일반 파일을 먼저 생성한 뒤 AtomicMoveNotSupportedException을 주입하세요. 새 내용으로 교체되는지 확인하세요. 가능하면 심볼릭 링크 대상이 변경되지 않는지도 fallback 경로에서 확인하세요.

As per coding guidelines, any new Kotlin code or branch must have covering tests because JaCoCo enforces 100% coverage through check.

🤖 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/test/kotlin/html4tree/MainTest.kt` around lines 365 - 375, The fallback
test around write_index_file must verify replacement of an existing index.html
file, not only creation when the target is absent. Replace the
indexDir.deleteRecursively setup with creation of an existing regular index.html
containing different content, inject AtomicMoveNotSupportedException through
mockMoveAtomic, and assert fallbackCalled plus the file’s updated content; if
the test already supports symlinks, also confirm the symlink target remains
unchanged.

Source: Coding guidelines

.jules/sentinel.md (1)

86-90: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Fallback의 비원자성 및 보장 범위를 문서에 명시하세요.

REPLACE_EXISTING fallback은 ATOMIC_MOVE 보장을 유지하지 않습니다. ATOMIC_MOVE 없이 수행하는 이동은 대상 확인과 실제 이동이 다른 파일시스템 작업과 원자적이지 않을 수 있습니다. (docs.oracle.com)

현재 제목과 Prevention 문구는 fallback에서도 TOCTOU와 경쟁 상태가 방지되는 것처럼 읽힐 수 있습니다. 원자 이동은 지원되는 파일시스템에서만 보장되며 fallback은 호환성을 위한 비원자 경로라는 점을 명시하세요. 보안 보장이 필수이면 fallback 대신 실패하도록 구현과 문서를 맞추세요.

🤖 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 @.jules/sentinel.md around lines 86 - 90, Update the html4tree atomic file
move documentation to state that ATOMIC_MOVE provides the atomicity guarantee
only on supporting filesystems, while the REPLACE_EXISTING fallback is
non-atomic and retained solely for compatibility. Clarify in the Prevention
guidance that security-critical workflows should fail when atomic movement is
unavailable, and align the documented implementation behavior with that
requirement.
🤖 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/main/kotlin/html4tree/main.kt`:
- Around line 233-248: Update write_index_file in
src/main/kotlin/html4tree/main.kt:233-248 to define and document the replacement
policy, explicitly handle replacement failures beyond
AtomicMoveNotSupportedException, and either reject or deliberately use the
non-atomic fallback while preserving cleanup. In
src/test/kotlin/html4tree/MainTest.kt:365-375, add coverage that keeps an
existing index.html and verifies fallback replacement. In
.jules/sentinel.md:86-90, state that the fallback is a compatibility path and
does not guarantee atomicity.

---

Nitpick comments:
In @.jules/sentinel.md:
- Around line 86-90: Update the html4tree atomic file move documentation to
state that ATOMIC_MOVE provides the atomicity guarantee only on supporting
filesystems, while the REPLACE_EXISTING fallback is non-atomic and retained
solely for compatibility. Clarify in the Prevention guidance that
security-critical workflows should fail when atomic movement is unavailable, and
align the documented implementation behavior with that requirement.

In `@src/test/kotlin/html4tree/MainTest.kt`:
- Around line 365-375: The fallback test around write_index_file must verify
replacement of an existing index.html file, not only creation when the target is
absent. Replace the indexDir.deleteRecursively setup with creation of an
existing regular index.html containing different content, inject
AtomicMoveNotSupportedException through mockMoveAtomic, and assert
fallbackCalled plus the file’s updated content; if the test already supports
symlinks, also confirm the symlink target remains unchanged.
🪄 Autofix (Beta)

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: f25aee6a-84ac-4cb9-bf9a-3399bf0f6d5d

📥 Commits

Reviewing files that changed from the base of the PR and between 669c4ef and 3beb6d4.

📒 Files selected for processing (3)
  • .jules/sentinel.md
  • src/main/kotlin/html4tree/main.kt
  • src/test/kotlin/html4tree/MainTest.kt

Comment on lines +233 to +248
fun write_index_file(
curr_dir: File,
content: String,
moveAtomic: (java.nio.file.Path, java.nio.file.Path) -> Unit = { source, target ->
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
}
) {
val indexPath = curr_dir.toPath().resolve("index.html")
val tempPath = Files.createTempFile(curr_dir.toPath(), ".index-", ".html")
try {
Files.write(tempPath, content.toByteArray(Charsets.UTF_8))
Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING)
try {
moveAtomic(tempPath, indexPath)
} catch (e: AtomicMoveNotSupportedException) {
Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target implementation ---'
sed -n '210,270p' src/main/kotlin/html4tree/main.kt
printf '%s\n' '--- related test ---'
sed -n '335,395p' src/test/kotlin/html4tree/MainTest.kt
printf '%s\n' '--- sentinel entry ---'
sed -n '75,100p' .jules/sentinel.md
printf '%s\n' '--- relevant usages and declarations ---'
rg -n -C 3 'write_index_file|AtomicMoveNotSupportedException|ATOMIC_MOVE|index\.html' src/main src/test .jules

Repository: ContextualWisdomLab/html4tree

Length of output: 27307


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
for p in [
    Path("src/main/kotlin/html4tree/main.kt"),
    Path("src/test/kotlin/html4tree/MainTest.kt"),
]:
    text = p.read_text()
    print(f"{p}: {len(text.splitlines())} lines")
    for i, line in enumerate(text.splitlines(), 1):
        if "write_index_file" in line or "AtomicMoveNotSupportedException" in line or "ATOMIC_MOVE" in line:
            print(f"{i}: {line}")
PY

Repository: ContextualWisdomLab/html4tree

Length of output: 1072


🌐 Web query:

Java Files.move ATOMIC_MOVE REPLACE_EXISTING existing target option ignored provider-specific replacement official documentation

💡 Result:

According to the official Java documentation for the java.nio.file.Files.move method, when the ATOMIC_MOVE option is specified, all other CopyOption arguments (including REPLACE_EXISTING) are ignored [1][2]. The behavior of ATOMIC_MOVE regarding an existing target is defined as implementation-specific: if the target file exists, the underlying file system implementation may either replace the existing file or fail by throwing an IOException [1][3]. Because ATOMIC_MOVE must be performed as an atomic file system operation, the method will throw an AtomicMoveNotSupportedException if the operation cannot be completed atomically—such as when the target location is on a different FileStore or is associated with a different file system provider [1][2]. In summary, you cannot rely on REPLACE_EXISTING to force a replacement when using ATOMIC_MOVE. If you require atomic behavior and the target file might exist, you must handle the potential for an IOException or implementation-dependent behavior [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
# Probe the host JDK's default provider with an existing regular target.
# This standalone probe does not load or execute repository code.
if command -v java >/dev/null 2>&1; then
  java -version 2>&1 | head -n 2
  tmpdir="$(mktemp -d)"
  trap 'rm -rf "$tmpdir"' EXIT
  cat >"$tmpdir/MoveProbe.java" <<'JAVA'
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class MoveProbe {
  public static void main(String[] args) throws Exception {
    Path dir = Paths.get(args[0]);
    Path source = Files.writeString(dir.resolve("source"), "new", StandardCharsets.UTF_8);
    Path target = Files.writeString(dir.resolve("target"), "old", StandardCharsets.UTF_8);
    try {
      Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
      System.out.println("atomic_move=" + Files.readString(target));
    } catch (Exception e) {
      System.out.println("atomic_move_exception=" + e.getClass().getName());
      System.out.println("target_after_atomic=" + Files.readString(target));
    }
    Path fallbackSource = Files.writeString(dir.resolve("fallback-source"), "fallback", StandardCharsets.UTF_8);
    try {
      Files.move(fallbackSource, target, StandardCopyOption.REPLACE_EXISTING);
      System.out.println("fallback_move=" + Files.readString(target));
    } catch (Exception e) {
      System.out.println("fallback_move_exception=" + e.getClass().getName());
    }
  }
}
JAVA
  javac "$tmpdir/MoveProbe.java"
  java -cp "$tmpdir" MoveProbe "$tmpdir"
else
  echo "java_unavailable"
fi

Repository: ContextualWisdomLab/html4tree

Length of output: 325


원자적 교체 정책을 코드, 테스트, 문서에서 일치시키세요.

ATOMIC_MOVE를 지정하면 REPLACE_EXISTING은 무시됩니다. 기존 index.html의 교체 여부는 파일 시스템 제공자에 따라 달라집니다. AtomicMoveNotSupportedException 이외의 교체 실패도 명시적으로 처리하고, 비원자적 fallback을 허용할지 결정하세요.

  • src/test/kotlin/html4tree/MainTest.kt:366-375: 기존 index.html을 남긴 상태에서 fallback 교체를 테스트하세요.
  • .jules/sentinel.md:87-90: fallback이 원자성을 보장하지 않는 호환성 경로임을 명시하세요.
🧰 Tools
🪛 detekt (1.23.8)

[warning] 246-246: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

📍 Affects 3 files
  • src/main/kotlin/html4tree/main.kt#L233-L248 (this comment)
  • src/test/kotlin/html4tree/MainTest.kt#L365-L375
  • .jules/sentinel.md#L86-L90
🤖 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/main/kotlin/html4tree/main.kt` around lines 233 - 248, Update
write_index_file in src/main/kotlin/html4tree/main.kt:233-248 to define and
document the replacement policy, explicitly handle replacement failures beyond
AtomicMoveNotSupportedException, and either reject or deliberately use the
non-atomic fallback while preserving cleanup. In
src/test/kotlin/html4tree/MainTest.kt:365-375, add coverage that keeps an
existing index.html and verifies fallback replacement. In
.jules/sentinel.md:86-90, state that the fallback is a compatibility path and
does not guarantee atomicity.

Source: Coding guidelines

@seonghobae seonghobae changed the title 🛡️ Sentinel: [MEDIUM] TOCTOU 취약점 완화를 위한 원자적 파일 쓰기 도입 fix(io): attempt atomic publication of generated index files Aug 5, 2026
@seonghobae
seonghobae marked this pull request as draft August 5, 2026 05:21
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