Skip to content

fix(cli,federation): exclusive, unpredictable staging for every atomic write - #154

Merged
tachyon-beep merged 3 commits into
release/1.6.0from
fix/atomic-write-exclusive-staging
Sep 1, 2026
Merged

fix(cli,federation): exclusive, unpredictable staging for every atomic write#154
tachyon-beep merged 3 commits into
release/1.6.0from
fix/atomic-write-exclusive-staging

Conversation

@tachyon-beep

Copy link
Copy Markdown
Collaborator

Rebase of Codex #145 (instructions.rs) and #150 (hooks_settings.rs) onto release/1.6.0, plus a third commit widening the fix to the whole class.

Problem

Every "write a sibling temp, then rename over the destination" site staged at a PID-derived name (<file>.tmp-<pid>) inside a directory the analyzed repository controls. A committed symlink at that name turned the staging write into a write-through to wherever it pointed: fs::write follows symlinks, and so does SQLite (db backup). #145/#150 fixed two of seven sites.

Fix

  • crates/loomweave-cli/src/atomic_fs.rs: staging_file_in / staging_dir_in / replace_fileO_CREAT|O_EXCL on a random name (tempfile), staged in the destination's own directory (rename stays same-filesystem atomic), 0o666 requested so the kernel applies the umask and the resulting mode is exactly what fs::write produced. The tempfile default (0600) would silently have made an installed CLAUDE.md / settings.json unreadable to other users of a shared checkout — that is the one behavioural correction over fix(cli): prevent symlink attacks during instruction writes #145/fix(cli): harden hook settings atomic write #150.
  • Converted: instructions, .claude/settings.json, .mcp.json, store .gitignore, db backup (staging path handed to SQLite), skill-pack swap (both the staging and the backup slot are exclusive dirs; rename(2) replaces an empty dir atomically), federation ephemeral.port (tempfile promoted to a runtime dep of loomweave-federation).
  • Cargo: tempfile was already a runtime dep of loomweave-cli on this branch, which is what made fix(cli): prevent symlink attacks during instruction writes #145/fix(cli): harden hook settings atomic write #150 conflict.

Tests

Each site plants the old predictable name as a symlink to a victim and asserts: victim untouched, destination a regular file with the expected content, planted link still a link. Helper tests pin no-leak on repeated writes and umask parity with fs::write. The db backup test spells the store leaf out so the worktree store-path audit does not read it as a runtime resolution site.

Gates run locally: fmt, clippy -D warnings (cli + federation), nextest for both crates (968 + 155, bins built first), cargo doc -D warnings, cargo deny.

Supersedes #145 and #150.

🤖 Generated with Claude Code

tachyon-beep and others added 3 commits September 2, 2026 05:10
…c write

Widen the two Codex fixes (instructions.rs, hooks_settings.rs) to the whole
class: every "stage a sibling temp, rename over the destination" site used a
PID-derived name inside a directory the analyzed repository controls, so a
committed symlink at that name turned the staging write into a write-through
(`fs::write` and SQLite both follow symlinks). New `atomic_fs` helpers create
the staging entry with O_CREAT|O_EXCL on a random name via `tempfile`, keep it
in the destination's directory so the rename stays atomic, and request 0o666
so the resulting mode follows the umask exactly as `fs::write` did (the
tempfile default of 0600 would have made CLAUDE.md / settings.json unreadable
to other users of a shared checkout).

Sites: instructions (CLAUDE.md/AGENTS.md), .claude/settings.json, .mcp.json,
the store .gitignore, `loomweave db backup`, the skill-pack directory swap
(staging and backup slots), and the federation `ephemeral.port` marker
(tempfile promoted to a runtime dependency of loomweave-federation).

Regression tests plant the old predictable name as a symlink to a victim file
at each site and assert the victim is untouched and the destination is a
regular file; the helper's own test pins umask parity with `fs::write`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0183ccEmYnCh5Nx2Y6weYfHy
Copilot AI lite review requested due to automatic review settings September 1, 2026 19:24
@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, add credits to your account and enable them for code reviews in your settings.

Copilot AI 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.

Pull request overview

This PR hardens all remaining “atomic write via sibling temp + rename” call sites by switching from predictable PID-derived staging names to exclusive, unpredictable staging entries (via tempfile) to prevent symlink write-through and pre-planted path attacks across CLI and federation.

Changes:

  • Introduces crates/loomweave-cli/src/atomic_fs.rs helpers for exclusive staging and atomic replacement, with umask-parity permissions on Unix.
  • Migrates multiple atomic-write sites (instructions, hooks settings, .mcp.json, store .gitignore, DB backup, skill-pack swap, federation ephemeral port) to use exclusive randomized staging.
  • Adds targeted Unix regression tests that plant the legacy predictable staging symlinks and assert the victim is not modified.

Reviewed changes

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

Show a summary per file
File Description
crates/loomweave-federation/src/loomweave_port.rs Uses exclusive tempfile staging for ephemeral.port writes + adds symlink regression test.
crates/loomweave-federation/Cargo.toml Promotes tempfile to a runtime dependency for federation.
crates/loomweave-cli/src/skill_pack.rs Replaces PID-named staging/backup dirs with exclusive temp dirs for crash-safe pack swaps.
crates/loomweave-cli/src/mcp_registration.rs Switches .mcp.json atomic write to atomic_fs::replace_file + adds symlink regression test.
crates/loomweave-cli/src/main.rs Wires in the new atomic_fs module.
crates/loomweave-cli/src/instructions.rs Switches instruction atomic writes to exclusive NamedTempFile staging + adds symlink regression test.
crates/loomweave-cli/src/install.rs Uses atomic_fs::replace_file for store .gitignore + adds symlink regression test.
crates/loomweave-cli/src/hooks_settings.rs Uses atomic_fs::replace_file for .claude/settings.json + adds symlink regression test.
crates/loomweave-cli/src/db.rs Uses exclusive staging for SQLite backup output, preventing symlink write-through + adds regression test.
crates/loomweave-cli/src/atomic_fs.rs New shared implementation for exclusive staging + atomic replace, with tests.
crates/loomweave-cli/Cargo.toml Promotes tempfile to a runtime dependency for the CLI.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +68 to +70
pub(crate) fn replace_file(dest: &Path, prefix: &str, bytes: &[u8]) -> Result<()> {
let dir = dest.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(dir).with_context(|| format!("mkdir {}", dir.display()))?;
@tachyon-beep
tachyon-beep merged commit 8ca6c80 into release/1.6.0 Sep 1, 2026
6 checks passed
@tachyon-beep
tachyon-beep deleted the fix/atomic-write-exclusive-staging branch September 1, 2026 20:04
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.

2 participants