{"record":{"id":"7bb3a2f47b07794c","repo":"Hmbown/CodeWhale","slug":"xai-oauth-file-must-not-have-multiple-filesystem-l","errorCode":null,"errorMessage":"xAI OAuth file must not have multiple filesystem links","messagePattern":"xAI OAuth file must not have multiple filesystem links","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/xai_credentials.rs","lineNumber":908,"sourceCode":"    }\n}\n\n#[cfg(unix)]\nfn validate_owned_file_handle(file: &File, path: &Path) -> Result<fs::Metadata> {\n    use std::os::unix::fs::MetadataExt as _;\n    let metadata = file.metadata().with_context(|| {\n        format!(\n            \"inspecting Codewhale-owned xAI OAuth file {}\",\n            crate::quote_os_path(path)\n        )\n    })?;\n    anyhow::ensure!(metadata.is_file(), \"xAI OAuth path must be a regular file\");\n    // SAFETY: geteuid(2) dereferences no pointers.\n    anyhow::ensure!(\n        metadata.uid() == unsafe { libc::geteuid() },\n        \"xAI OAuth file must be owned by the current user\"\n    );\n    anyhow::ensure!(\n        metadata.nlink() == 1,\n        \"xAI OAuth file must not have multiple filesystem links\"\n    );\n    Ok(metadata)\n}\n\n#[cfg(windows)]\nfn open_owned_credentials_directory(directory: &Path) -> Result<XaiOAuthCredentialStore> {\n    use std::os::windows::fs::OpenOptionsExt as _;\n    use windows_sys::Win32::Storage::FileSystem::{\n        FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ,\n        FILE_SHARE_READ, FILE_SHARE_WRITE, WRITE_DAC, WRITE_OWNER,\n    };\n\n    anyhow::ensure!(\n        directory.is_absolute(),\n        \"xAI OAuth credentials directory must be absolute\"\n    );","sourceCodeStart":890,"sourceCodeEnd":926,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/xai_credentials.rs#L890-L926","documentation":"This library hardens the xAI OAuth credential file against link-based attacks: it opens the file, then verifies the underlying metadata (regular file, owned by the current euid, hardlink count exactly 1). If `st_nlink` is greater than 1, someone could hardlink the credential file to another location and read or tamper with tokens outside the secured directory, so the open is rejected.","triggerScenarios":"Any read/write/remove/rename/open path through validate_owned_file_handle encounters a credentials file whose link count is not 1 — typically because the user (or a backup/restore/sync tool) created a hard link to the token file.","commonSituations":"Restoring credentials from a backup tool that hardlinks instead of copying; syncing the ~/.codewhale directory with a tool like rsync --link-dest; a developer manually running `ln` on the token file to share it between machines or profiles.","solutions":["Delete the file and log in again so the store recreates a fresh, single-link token file","Find and remove extra hard links: `find / -samefile <token-path>` then delete the stray link, keeping only the one in the credentials directory","Replace the hardlinked file with a real copy: `cp --remove-destination <file> <file>` (or `cp -L` then remove the link source)","Check sync/backup software configuration to copy rather than hardlink credential files"],"exampleFix":"// before (fails: file has 2 links)\nln ~/.codewhale/xai/auth.json ~/backup/auth.json\n// after\ncp ~/.codewhale/xai/auth.json ~/backup/auth.json","handlingStrategy":"validation","validationCode":"use std::os::unix::fs::MetadataExt;\nfn is_safe_credential_file(p: &std::path::Path) -> std::io::Result<bool> {\n    let m = std::fs::metadata(p)?;\n    Ok(m.is_file() && m.uid() == unsafe { libc::geteuid() } && m.nlink() == 1)\n}","typeGuard":"fn hardlink_count(p: &std::path::Path) -> std::io::Result<u64> {\n    Ok(std::fs::metadata(p)?.nlink())\n}","tryCatchPattern":"match store.load_tokens() {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"multiple filesystem links\") => {\n        eprintln!(\"credential file hardlinked; removing and re-authenticating\");\n        let _ = store.logout();\n        store.login()? // recreates a clean file\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never hardlink token files; always copy with `cp` (not `cp -l`)","Configure backup/sync tools (rsync, restic, dedup) to copy credential files","Restore credentials into place with copy, not link-based snapshots","Keep all Codewhale instances pointed at the same owned directory"],"tags":["security","filesystem","credentials","hardlink"],"backgroundTag":"credentials-file-hardlink-detected","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"}