{"record":{"id":"b671f7b1f9d81766","repo":"Hmbown/CodeWhale","slug":"refusing-a-symlinked-settings-lock-at","errorCode":null,"errorMessage":"Refusing a symlinked settings lock at {}","messagePattern":"Refusing a symlinked settings lock at (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/settings.rs","lineNumber":2375,"sourceCode":"    {\n        anyhow::bail!(\n            \"Refusing a non-regular settings lock at {}\",\n            lock_path.display()\n        );\n    }\n\n    let mut lock = fd_lock::RwLock::new(lock_file);\n    let _guard = lock\n        .write()\n        .with_context(|| format!(\"Failed to acquire settings lock at {}\", lock_path.display()))?;\n    operation()\n}\n\n/// Refuse to lock through a symlink: a planted `settings.toml.lock -> …` would\n/// otherwise let an attacker pick which file we create with our permissions.\nfn reject_settings_lock_symlink(lock_path: &Path) -> Result<()> {\n    match std::fs::symlink_metadata(lock_path) {\n        Ok(metadata) if metadata.file_type().is_symlink() => anyhow::bail!(\n            \"Refusing a symlinked settings lock at {}\",\n            lock_path.display()\n        ),\n        Ok(_) | Err(_) => Ok(()),\n    }\n}\n\n/// Replace `path` with `body` by writing an adjacent temporary file and\n/// renaming it into place.\n///\n/// A direct `fs::write` truncates first, so any concurrent reader — another\n/// Codewhale process, an editor, a `cat` — can observe a half-written file and\n/// parse it as truncated TOML, silently losing every key past the tear. A\n/// same-directory temp file plus the platform's replace primitive makes the\n/// swap atomic for readers: they see either the whole previous file or the\n/// whole new one.\n///\n/// The temp file inherits the existing file's permission bits when there is one","sourceCodeStart":2357,"sourceCodeEnd":2393,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/settings.rs#L2357-L2393","documentation":"Before locking, the code checks the lock path with symlink_metadata and refuses to proceed if it is a symlink. A planted `settings.toml.lock -> /some/target` would let an attacker choose which file the app creates with its own permissions, so locking through symlinks is rejected outright. This is a deliberate hardening measure, not a transient failure.","triggerScenarios":"Any settings lock acquisition when `settings.toml.lock` is a symlink (e.g. pointing elsewhere on disk), detected via std::fs::symlink_metadata in reject_settings_lock_symlink.","commonSituations":"Dotfile managers that symlink config directories wholesale (stow, homesick, dotbot); an attacker planting a symlink to escalate file creation; sync tools that replaced the lock with a link.","solutions":["Replace the symlink with a real regular file: `rm <settings.toml.lock> && touch <settings.toml.lock>`","Reconfigure your dotfile manager to copy rather than symlink this specific file","Retry the settings operation to recreate a fresh regular lock file"],"exampleFix":"// before (shell)\nls -la settings.toml.lock  # settings.toml.lock -> /etc/passwd\n// after (shell)\nrm settings.toml.lock && touch settings.toml.lock","handlingStrategy":"validation","validationCode":"fn lock_is_symlink(p: &std::path::Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| m.file_type().is_symlink()).unwrap_or(false)\n}\n// if true, replace with a real file before calling the library","typeGuard":null,"tryCatchPattern":"if lock_is_symlink(&lock_path) {\n    std::fs::remove_file(&lock_path).ok();\n    std::fs::File::create(&lock_path).ok();\n}\nlet lock = acquire_settings_lock()?;","preventionTips":["Configure dotfile managers (stow, dotbot) to copy, not symlink, this file","Don't plant symlinks in config directories","Run with least privilege so a planted symlink can't redirect file creation"],"tags":["filesystem","symlink","security","file-lock"],"backgroundTag":"permission-denied","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}