{"record":{"id":"62eef92b0b9552f4","repo":"Hmbown/CodeWhale","slug":"refusing-non-regular-or-reparse-point-config-lock","errorCode":null,"errorMessage":"refusing non-regular or reparse-point config lock at {}","messagePattern":"refusing non-regular or reparse-point config lock at (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/config_document.rs","lineNumber":241,"sourceCode":"    use std::ffi::OsString;\n    use std::os::windows::ffi::OsStringExt as _;\n    use std::os::windows::fs::MetadataExt as _;\n    use std::os::windows::io::AsRawHandle as _;\n    use windows_sys::Win32::Storage::FileSystem::{\n        FILE_ATTRIBUTE_REPARSE_POINT, FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW,\n        VOLUME_NAME_DOS,\n    };\n\n    let metadata = file.metadata().with_context(|| {\n        format!(\n            \"failed to inspect config lock at {}\",\n            crate::quote_os_path(expected_path)\n        )\n    })?;\n    if !metadata.file_type().is_file()\n        || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0\n    {\n        bail!(\n            \"refusing non-regular or reparse-point config lock at {}\",\n            crate::quote_os_path(expected_path)\n        );\n    }\n\n    let handle = file.as_raw_handle();\n    let flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS;\n    // SAFETY: `handle` remains owned by `file`; a null output buffer asks for\n    // the required UTF-16 length.\n    let needed = unsafe { GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, flags) };\n    if needed == 0 {\n        return Err(std::io::Error::last_os_error()).with_context(|| {\n            format!(\n                \"failed to resolve config lock at {}\",\n                crate::quote_os_path(expected_path)\n            )\n        });\n    }","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/config_document.rs#L223-L259","documentation":"Windows-only hardening of the config write lock: after opening the expected lock file, its metadata is inspected and the open is refused if the file is not a plain regular file or carries FILE_ATTRIBUTE_REPARSE_POINT (symlink/junction). This blocks symlink-swap and lock-redirection attacks where an attacker points the lock elsewhere so two writers can hold 'different' locks for the same config.","triggerScenarios":"The expected lock path on Windows resolves to a directory, a symlink/junction/hardlink-style reparse point, or another non-regular file kind when with_config_write_lock opens it.","commonSituations":"Config directory inside a symlinked/junctioned path (e.g. dotfiles managers, redirected profile, subst drives) that materialized the lock itself as a link; leftover malicious or tool-created symlink at the lock location; copying a config tree that preserved reparse points.","solutions":["Delete the offending lock file (it is transient and safe to remove when no Codewhale process is running) so a regular file is recreated","Remove the symlink/junction at or above the config directory so the lock path is a direct NTFS path","Move the config directory to a non-redirected location and point CODEWHALE/config at it","If you intentionally redirect config via junctions, exclude the lock file name from the redirection"],"exampleFix":"# before\nC:\\Users\\me\\.config\\codewhale\\.config.toml.lock -> symlink into dotfiles repo (reparse point)\n\n# after (PowerShell, no Codewhale running)\nRemove-Item -Force C:\\Users\\me\\.config\\codewhale\\*.lock\n# next config write recreates a regular lock file","handlingStrategy":"validation","validationCode":"// On Windows, before relying on the config lock, verify it is a regular file:\n#[cfg(windows)]\nfn lock_is_regular(p: &std::path::Path) -> bool {\n    use std::os::windows::fs::MetadataExt;\n    std::fs::symlink_metadata(p)\n        .map(|m| m.file_type().is_file() && (m.file_attributes() & 0x400) == 0) // FILE_ATTRIBUTE_REPARSE_POINT\n        .unwrap_or(true) // absent lock is fine; it will be created\n}","typeGuard":null,"tryCatchPattern":"match with_config_write_lock(&path, op) {\n    Ok(v) => v,\n    Err(e) if e.to_string().contains(\"reparse-point config lock\") => {\n        // instruct: delete the lock file, remove symlink indirection, retry once\n        user_action_then_retry(e)\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never symlink the config directory or lock file on Windows","Clean stale *.lock files while Codewhale is stopped","Keep the config path a direct NTFS path, not behind junctions/subst"],"tags":["windows","lock","symlink","security","config"],"backgroundTag":"symlink-security-check","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}