Skip to content

fix(specialist): make overwrites atomic#757

Open
PierrunoYT wants to merge 2 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-753-atomic-specialist-overwrite
Open

fix(specialist): make overwrites atomic#757
PierrunoYT wants to merge 2 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-753-atomic-specialist-overwrite

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • write specialist overwrites to a sibling temporary file with mode 0600
  • sync and close temporary content before atomically replacing the destination
  • reject symlink destinations and clean up temporary files on failure
  • add regression coverage for replacement, permissions, symlink safety, and cleanup

Verification

  • go vet ./...n- go test ./...n- go run ./cmd/zero-release buildn- go run ./cmd/zero-release smoken- git diff HEAD --check`n

Environment limitations

  • go test -race ./internal/specialist requires CGO, which is disabled in this environment
  • pinned golangci-lint and govulncheck currently build with Go 1.25 and cannot analyze this Go 1.26.5 repository
  • make fmt-check could not run because make is unavailable; touched files were formatted with gofmt`n
    Fixes fix(specialist): overwrite is race-prone and non-atomic #753

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability for overwrite operations by performing atomic specialist file writes and safer replacement.
    • Prevented incomplete or partially written files during overwrites via temporary-file + sync before replace.
    • Kept protection that rejects replacing when the existing target is a symbolic link.
    • Ensured written files have restrictive permissions, that destination directories are synced after replacement (non-Windows), and that temporary files are cleaned up.
  • Tests

    • Expanded coverage for atomic overwrite replacement, Windows rename retry behavior, directory sync error propagation, and temporary-file cleanup.

Copilot AI review requested due to automatic review settings July 19, 2026 13:39

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 specialist manifest overwrites by switching from in-place truncation writes to a write-to-temp + atomic replace flow, addressing the race/non-atomic overwrite risks described in issue #753.

Changes:

  • Implement atomic overwrite via sibling temp file creation (0600), sync/close, then rename into place.
  • Add symlink rejection to prevent overwriting a symlinked specialist destination.
  • Add regression tests covering atomic replacement behavior, permissions, symlink safety, and temp cleanup.

Reviewed changes

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

File Description
internal/specialist/storage.go Reworks overwrite path to write to a synced temp file and atomically replace the destination, rejecting symlink destinations.
internal/specialist/storage_test.go Adds regression coverage for overwrite replacement semantics, permissions, and temp-file cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/specialist/storage.go Outdated
Comment on lines +124 to +126
if err := os.Rename(tempPath, path); err != nil {
return fmt.Errorf("replace specialist file: %w", err)
}
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82f910a8-f228-4413-a649-966fdbe4b0ef

📥 Commits

Reviewing files that changed from the base of the PR and between c0ba649 and c346ba3.

📒 Files selected for processing (2)
  • internal/specialist/storage.go
  • internal/specialist/storage_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/specialist/storage.go

Walkthrough

Specialist storage creation now uses exclusive file creation for new files and atomic temporary-file replacement for overwrites. Tests cover replacement content, permissions, retry behavior, sync errors, and temporary-file cleanup.

Changes

Specialist overwrite safety

Layer / File(s) Summary
Atomic specialist write path
internal/specialist/storage.go
Storage.Create creates the target directory, preserves non-overwrite exclusivity, and uses a synced 0600 temporary file with symlink validation, retryable atomic rename, and directory syncing for overwrites.
Overwrite behavior validation
internal/specialist/storage_test.go
Tests verify complete replacement, platform-specific permissions, Windows rename retries, directory-sync error propagation, symlink rejection cleanup, and removal of temporary files.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Gitlawb/zero#117: Modifies specialist storage overwrite behavior and related file creation tests.
🚥 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 The title accurately and concisely describes the main change: making specialist overwrites atomic.
Linked Issues check ✅ Passed The implementation matches #753 by using a sibling temp file, syncing it, rejecting symlink destinations, and atomically renaming it over the target.
Out of Scope Changes check ✅ Passed The added helper logic and tests are all directly supporting atomic overwrite safety, cleanup, and error handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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 (1)
internal/specialist/storage.go (1)

124-128: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No directory fsync after rename — rename durability isn't guaranteed on crash.

os.Rename at Line 124 is not followed by an fsync on the parent directory. On some filesystems (notably ext4 without data=journal), a rename's directory-entry update is not guaranteed durable until the containing directory is fsynced — a crash immediately after a successful rename can leave the destination reverted to its pre-rename state. This is directly relevant to the PR's stated goal of avoiding files left "empty or partially written after disk, process, or I/O failures."

♻️ Proposed fix: fsync the parent directory after rename
 	if err := os.Rename(tempPath, path); err != nil {
 		return fmt.Errorf("replace specialist file: %w", err)
 	}
+	if dir, derr := os.Open(filepath.Dir(path)); derr == nil {
+		_ = dir.Sync()
+		_ = dir.Close()
+	}
 	return nil
 }
🤖 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 `@internal/specialist/storage.go` around lines 124 - 128, Update the atomic
replacement flow containing os.Rename to open the destination’s parent
directory, fsync the directory after a successful rename, and close it while
propagating any open, sync, or close errors. Preserve the existing wrapped
rename error and nil success behavior, and ensure cleanup handles failures
without masking the primary error.
🤖 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 `@internal/specialist/storage.go`:
- Around line 117-126: Update writeSpecialistAtomic to provide a
Windows-specific replacement path that guarantees atomic, crash-safe file
replacement instead of relying solely on os.Rename; isolate platform-specific
behavior behind appropriate build-specific helpers while preserving the existing
symlink checks and error handling.

---

Nitpick comments:
In `@internal/specialist/storage.go`:
- Around line 124-128: Update the atomic replacement flow containing os.Rename
to open the destination’s parent directory, fsync the directory after a
successful rename, and close it while propagating any open, sync, or close
errors. Preserve the existing wrapped rename error and nil success behavior, and
ensure cleanup handles failures without masking the primary error.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 48974b34-0cac-4aa8-8a9a-e20b6875d0f7

📥 Commits

Reviewing files that changed from the base of the PR and between ce4a996 and c0ba649.

📒 Files selected for processing (2)
  • internal/specialist/storage.go
  • internal/specialist/storage_test.go

Comment on lines +117 to +126
info, err := os.Lstat(path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("inspect specialist file: %w", err)
}
if err == nil && info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("refusing to overwrite symlink specialist file: %s", path)
}
if err := os.Rename(tempPath, path); err != nil {
return fmt.Errorf("replace specialist file: %w", err)
}

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 -euo pipefail

printf '\n== go.mod ==\n'
sed -n '1,80p' go.mod

printf '\n== relevant file excerpt ==\n'
nl -ba internal/specialist/storage.go | sed -n '100,150p'

printf '\n== search for Rename/atomicity notes ==\n'
rg -n "atomic|Rename|MoveFileEx|MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH" -S .

Repository: Gitlawb/zero

Length of output: 1706


🌐 Web query:

Go os.Rename Windows atomic non-Unix platforms atomicity Go docs

💡 Result:

According to the official Go documentation, the os.Rename function is not an atomic operation on non-Unix platforms, including Windows [1][2][3]. While os.Rename serves to rename or move a file—and will replace an existing destination file if it is not a directory—it does not provide the same atomic guarantees on Windows that it does on Unix-like systems [1][3]. Explicit documentation added to the Go standard library clarifies this limitation: "Even within the same directory, on non-Unix platforms Rename is not an atomic operation" [1][4]. Although some Windows API calls (such as certain uses of MoveFileEx) can provide atomic-like behaviors under specific conditions (e.g., when the source and destination are on the same volume/filesystem), the Go os.Rename implementation does not guarantee this atomicity for the programmer [5][6][7]. Consequently, when building applications that require strictly atomic file replacement across different operating systems, developers are cautioned against relying on os.Rename for that purpose on non-Unix platforms [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== internal/specialist/storage.go ==\n'
awk 'NR>=1 && NR<=220 { printf "%4d  %s\n", NR, $0 }' internal/specialist/storage.go

printf '\n== windows-specific checks in tests/code ==\n'
rg -n "GOOS != \"windows\"|windows|MoveFileEx|atomic" internal -S

printf '\n== os.Rename docs excerpt from local stdlib if available ==\n'
go doc os.Rename | sed -n '1,120p'

Repository: Gitlawb/zero

Length of output: 50369


🌐 Web query:

Go os.Rename doc Windows non-Unix not atomic operation same directory

💡 Result:

The Go documentation explicitly states that os.Rename is not an atomic operation on non-Unix platforms, including Windows, even when renaming files within the same directory [1][2]. Unlike POSIX-compliant operating systems where rename is guaranteed to be atomic [3], Windows does not provide a standard, reliable mechanism for atomic file replacement via the APIs typically utilized by os.Rename [2][3]. Consequently, if an error occurs during the operation, it can leave the system in an inconsistent state [4][5]. Users requiring atomic file replacement on Windows often must implement their own solutions using specialized system calls (such as certain NtSetInformationFile flags available in modern Windows versions) or higher-level patterns, though these are not exposed directly through the standard os.Rename function [5][3][6].

Citations:


writeSpecialistAtomic still depends on os.Rename, which is not atomic on Windows. If this helper is meant to guarantee crash-safe replacement cross-platform, add a Windows-specific replace path; otherwise, scope the atomicity promise/docs to Unix-like systems.

🤖 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 `@internal/specialist/storage.go` around lines 117 - 126, Update
writeSpecialistAtomic to provide a Windows-specific replacement path that
guarantees atomic, crash-safe file replacement instead of relying solely on
os.Rename; isolate platform-specific behavior behind appropriate build-specific
helpers while preserving the existing symlink checks and error handling.

Source: Coding guidelines

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Use an atomic replacement primitive on Windows
    internal/specialist/storage.go:131
    This still reaches os.Rename through RenameWithRetry; retries only handle sharing violations and do not change the replacement operation. Go explicitly documents that os.Rename is not atomic on non-Unix platforms, even for sibling paths, so a Windows --force overwrite can expose a missing/intermediate manifest if it is interrupted or observed concurrently—the exact failure this change is meant to eliminate. Add a Windows-specific replacement path that provides the required atomic/crash-safe semantics (and test that operation); the current Windows test only proves a retry followed by the same non-atomic rename.

  • [P1] Preserve the existing Windows DACL when publishing the replacement
    internal/specialist/storage.go:101
    Overwriting now publishes a newly created temporary file, so its inherited directory ACL replaces the destination file's ACL. temp.Chmod(0o600) does not make the file private on Windows—Go only uses the owner-write bit there—whereas the old in-place truncate retained an existing restrictive DACL. Consequently, overwriting a specialist that has been explicitly restricted inside a directory readable by a group can expose its system prompt to that group. Preserve the destination security descriptor (or apply an equivalent private DACL) as part of the Windows replacement, and cover that behavior with a Windows test.

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.

fix(specialist): overwrite is race-prone and non-atomic

3 participants