Hmbown/CodeWhale · error · anyhow::Error

refusing to replace an existing xAI OAuth retirement path

Error message

refusing to replace an existing xAI OAuth retirement path

What it means

During a revocation on unix, each active credential is renamed to a tombstone `.xai-oauth-retired-<pid>-<nanos>-<counter>.tmp`; rename_raw fail-closed refuses to overwrite an existing destination. A collision requires the same pid, an identical nanosecond timestamp, and the same counter, so in practice this only happens after clock skew (VM snapshot restore, NTP step) combined with pid reuse.

Source

Thrown at crates/config/src/xai_credentials.rs:735

        if unsafe { libc::unlinkat(self.directory_handle.as_raw_fd(), name.as_ptr(), 0) } != 0 {
            let error = std::io::Error::last_os_error();
            if error.kind() == std::io::ErrorKind::NotFound {
                return Ok(false);
            }
            return Err(error).context("removing Codewhale-owned xAI OAuth file");
        }
        Ok(true)
    }

    fn rename_raw(&self, from: &str, to: &str) -> Result<()> {
        use std::os::fd::AsRawFd as _;
        validate_private_basename(from)?;
        validate_private_basename(to)?;
        let source = self
            .open_owned_file_for_read(from)?
            .context("xAI OAuth source disappeared before retirement")?;
        validate_owned_file_handle(&source, &self.directory.join(from))?;
        anyhow::ensure!(
            self.open_owned_file_for_read(to)?.is_none(),
            "refusing to replace an existing xAI OAuth retirement path"
        );
        drop(source);
        let from = CString::new(from).context("xAI OAuth basename contains an interior NUL")?;
        let to = CString::new(to).context("xAI OAuth basename contains an interior NUL")?;
        // SAFETY: both names are one component relative to the same pinned
        // directory descriptor.
        if unsafe {
            libc::renameat(
                self.directory_handle.as_raw_fd(),
                from.as_ptr(),
                self.directory_handle.as_raw_fd(),
                to.as_ptr(),
            )
        } != 0
        {
            return Err(std::io::Error::last_os_error()).context("retiring xAI OAuth file");

View on GitHub (pinned to 8880682c63)

Solutions

  1. Retry the operation: the fresh timestamp/counter nonce makes a second attempt collide with near-zero probability
  2. When no codewhale process is running, delete stale .xai-oauth-retired-*.tmp files in $CODEWHALE_HOME/credentials
  3. If the clock was rewound (VM restore), let NTP stabilize time before retrying
Defensive patterns

Strategy: retry

Try / catch

let mut attempt = 0;
loop {
    match codewhale_config::with_xai_oauth_revocation_transaction(&op) {
        Ok(v) => break Ok(v),
        Err(e) if e.to_string().contains("refusing to replace an existing xAI OAuth retirement path")
            && attempt < 2 =>
        {
            attempt += 1; // nonce collision: a retry gets a fresh timestamp
        }
        Err(e) => break Err(e),
    }
}

Prevention

When it happens

Trigger: with_xai_oauth_revocation_transaction / stage_revocation where a stale tombstone from an earlier run received an identical generated name because SystemTime::now went backwards (VM restore, clock step) and the pid was reused.

Common situations: Laptops or VMs restored from snapshots with rewound clocks; CI runners with unstable clocks; otherwise effectively unreachable.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/bea83247d8394810. Report an issue: GitHub.