{"record":{"id":"54f74b29e765854e","repo":"Hmbown/CodeWhale","slug":"automation-lock-must-not-be-a-reparse-point","errorCode":null,"errorMessage":"Automation lock must not be a reparse point","messagePattern":"Automation lock must not be a reparse point","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":1036,"sourceCode":"        let file = options\n            .open(&path)\n            .with_context(|| format!(\"open {}\", path.display()))?;\n        let metadata = file.metadata()?;\n        if !metadata.is_file() {\n            bail!(\"Automation lock must be a regular file\");\n        }\n        #[cfg(unix)]\n        {\n            use std::os::unix::fs::MetadataExt as _;\n            if metadata.nlink() != 1 {\n                bail!(\"Automation lock must not have hard links\");\n            }\n        }\n        #[cfg(windows)]\n        {\n            use std::os::windows::fs::MetadataExt as _;\n            if metadata.file_attributes() & 0x400 != 0 {\n                bail!(\"Automation lock must not be a reparse point\");\n            }\n        }\n        Ok(fd_lock::RwLock::new(file))\n    }\n\n    fn with_transaction<T>(&self, operation: impl FnOnce() -> Result<T>) -> Result<T> {\n        let mut lock = self.open_lock(\"state.lock\")?;\n        let _guard = lock.write().context(\"lock automation state\")?;\n        operation()\n    }\n\n    /// Short read/modify/write transaction shared with scheduler admission.\n    /// Returning None leaves an absent record absent; it does not delete one.\n    pub(crate) fn edit_automation(\n        &self,\n        id: &str,\n        edit: impl FnOnce(Option<AutomationRecord>) -> Result<Option<AutomationRecord>>,\n    ) -> Result<Option<AutomationRecord>> {","sourceCodeStart":1018,"sourceCodeEnd":1054,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/automation_manager.rs#L1018-L1054","documentation":"The automation manager guards its lock file against symlink/reparse-point attacks. On Windows, before wrapping the lock file in an fd_lock::RwLock, it checks the FILE_ATTRIBUTE_REPARSE_POINT bit (0x400) in the file's attributes. If set — meaning the 'lock' is actually a symlink, mount point, or junction — it refuses to proceed so automation state cannot be redirected to an attacker-controlled location.","triggerScenarios":"Opening/initializing the automation manager's lock when the file at the lock path on Windows carries the reparse-point attribute (0x400), e.g. the lock file path is a symlink or junction.","commonSituations":"Users who symlink their config/state directory (e.g. dotfiles managed via symlinks, or state moved to another drive via a junction) hit this on Windows because the lock file itself is reached through a reparse point.","solutions":["Replace the symlink/junction at the automation lock path with a real regular file","Move the real state directory instead of symlinking it (e.g. change the state dir setting to the actual path)","On non-attack scenarios, copy the lock file contents to a new regular file and delete the reparse point"],"exampleFix":"// before: config/state/automation.lock -> D:\\state\\automation.lock (junction)\n// after: copy the real file to config/state/automation.lock and remove the junction\nCopy-Item D:\\state\\automation.lock config\\state\\automation.lock\nRemove-Item config\\state\\automation.lock.link  # or the junction itself","handlingStrategy":"try-catch","validationCode":"#[cfg(windows)]\nfn lock_path_is_regular(path: &Path) -> std::io::Result<bool> {\n    use std::os::windows::fs::MetadataExt;\n    Ok(path.metadata()?.file_attributes() & 0x400 == 0)\n}","typeGuard":"fn is_reparse_point(m: &std::fs::Metadata) -> bool { #[cfg(windows)] { use std::os::windows::fs::MetadataExt; m.file_attributes() & 0x400 != 0 } #[cfg(not(windows))] { let _ = m; false } }","tryCatchPattern":"match init_automation_manager() {\n    Err(e) if e.to_string().contains(\"reparse point\") => eprintln!(\"lock path is a symlink/junction; use a real directory\"),\n    Err(e) => return Err(e),\n    Ok(m) => m,\n}","preventionTips":["Don't symlink or junction the app's state/config directory on Windows","Use the app's own settings to relocate state instead of filesystem redirection","Check with `fsutil reparsepoint query` if you suspect redirection"],"tags":["windows","filesystem","security","symlink"],"backgroundTag":"unsupported-operation","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}