{"record":{"id":"db7ffe93a972d196","repo":"Hmbown/CodeWhale","slug":"codewhale-owned-xai-oauth-path-was-redirected-whil","errorCode":null,"errorMessage":"Codewhale-owned xAI OAuth path was redirected while opening","messagePattern":"Codewhale-owned xAI OAuth path was redirected while opening","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/xai_credentials.rs","lineNumber":1375,"sourceCode":"    let flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS;\n    let handle = file.as_raw_handle();\n    // SAFETY: null output asks only for 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())\n            .context(\"resolving Codewhale-owned xAI OAuth handle path\");\n    }\n    let mut buffer = vec![0u16; needed as usize + 1];\n    // SAFETY: the buffer is writable and the handle remains valid.\n    let written = unsafe {\n        GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buffer.len() as u32, flags)\n    };\n    if written == 0 || written as usize >= buffer.len() {\n        return Err(std::io::Error::last_os_error())\n            .context(\"resolving Codewhale-owned xAI OAuth handle path\");\n    }\n    let actual = OsString::from_wide(&buffer[..written as usize]);\n    anyhow::ensure!(\n        normalize_windows_path_for_comparison(Path::new(&actual))?\n            == normalize_windows_path_for_comparison(expected)?,\n        \"Codewhale-owned xAI OAuth path was redirected while opening\"\n    );\n    Ok(metadata)\n}\n\n#[cfg(windows)]\nfn normalize_windows_path_for_comparison(path: &Path) -> Result<String> {\n    let text = path.to_str().ok_or_else(|| {\n        anyhow::anyhow!(\n            \"xAI OAuth path {} contains invalid Unicode and cannot be compared safely\",\n            crate::quote_os_path(path)\n        )\n    })?;\n    let without_device_prefix = text.strip_prefix(r\"\\\\?\\\").unwrap_or(text);\n    let normalized_prefix = without_device_prefix.strip_prefix(\"UNC\\\\\").map_or_else(\n        || without_device_prefix.to_string(),","sourceCodeStart":1357,"sourceCodeEnd":1393,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/xai_credentials.rs#L1357-L1393","documentation":"After opening a handle, the library resolves the handle's final path via GetFinalPathNameByHandle and compares it (normalized) to the expected path. A mismatch means the path was swapped or redirected between path resolution and handle opening — a TOCTOU/planting attack or an actively changing directory — so the operation aborts.","triggerScenarios":"Opening the credentials directory or file while another process renames/replaces the path (e.g. a concurrent logout deleting and recreating the directory, a sync tool swapping the folder), or the path resolving through a mapping that changes the final name.","commonSituations":"Running two Codewhale processes where one logs out while the other logs in; antivirus/quarantine replacing the file; a sync client doing atomic rename swaps on the credentials folder during open.","solutions":["Re-run the operation; if it was a transient race with logout/sync, retry when no other process is touching the directory","Ensure only one Codewhale instance manages the credentials at a time (stop other instances, pause sync tools)","Verify the expected path in config matches the real on-disk location (no drives/substitutions mapping it elsewhere)"],"exampleFix":"// before: two processes racing on the same store\n// process A: store.logout()?  process B: store.open()?  -> redirected path\n// after: serialize access\nlet _guard = lifecycle_lock.lock().unwrap();\nlet store = XaiCredentialStore::open(&dir)?;","handlingStrategy":"retry","validationCode":"fn same_final_path(handle_path: &str, expected: &str) -> bool {\n    let norm = |s: &str| s.trim_start_matches(r\"\\\\?\\\")\n        .trim_start_matches(\"UNC\").trim_start_matches(\"\\\\\").to_lowercase();\n    norm(handle_path) == norm(expected)\n}","typeGuard":null,"tryCatchPattern":"// transient races: retry a few times\nfor _ in 0..3 {\n    match XaiCredentialStore::open(&dir) {\n        Ok(s) => break s,\n        Err(e) if e.to_string().contains(\"redirected while opening\") => std::thread::sleep(std::time::Duration::from_millis(100)),\n        Err(e) => return Err(e.into()),\n    }\n}","preventionTips":["Ensure only one Codewhale process touches the credentials at a time (use the lifecycle lock)","Pause file-sync tools during login/logout","Don't rename or replace the credentials directory while operations are in flight"],"tags":["security","windows","race-condition","credentials"],"backgroundTag":"credentials-path-redirected","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"}