{"record":{"id":"cd8a98f008c7478b","repo":"block/buzz","slug":"oauth-cache-is-not-a-regular-file","errorCode":null,"errorMessage":"oauth cache is not a regular file","messagePattern":"oauth cache is not a regular file","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"warning","filePath":"crates/buzz-agent/src/auth.rs","lineNumber":1803,"sourceCode":"///\n/// On Unix `O_NOFOLLOW` rejects a symlinked cache path at the kernel level\n/// (no stat/open TOCTOU), and `fchmod` on the already-open handle repairs a\n/// loose mode against the pinned inode rather than re-resolving the path.\n/// A cache that exists but cannot be secured is an error, so the caller fails\n/// closed instead of using an exposed file.\n#[cfg(unix)]\nfn read_private_cache(path: &Path) -> io::Result<Vec<u8>> {\n    use std::io::Read;\n    use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};\n\n    let mut file = fs::OpenOptions::new()\n        .read(true)\n        .custom_flags(nix::libc::O_NOFOLLOW)\n        .open(path)?;\n\n    let meta = file.metadata()?;\n    if !meta.file_type().is_file() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            \"oauth cache is not a regular file\",\n        ));\n    }\n    // Tighten in place on the open fd if any group/other bit is set. fchmod\n    // targets the inode we already hold, so no attacker can swap the path\n    // between the check and the repair.\n    if meta.permissions().mode() & 0o077 != 0 {\n        file.set_permissions(fs::Permissions::from_mode(0o600))?;\n    }\n\n    let mut body = Vec::new();\n    file.read_to_end(&mut body)?;\n    Ok(body)\n}\n\n/// Non-Unix: token persistence and reading are both disabled until a\n/// Windows-specific owner-only DACL is implemented. Any legacy token file","sourceCodeStart":1785,"sourceCodeEnd":1821,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-agent/src/auth.rs#L1785-L1821","documentation":"buzz-agent reads its OAuth token cache with hardening: it opens the cache path with O_NOFOLLOW (symlinks are rejected at the kernel level) and then fstats the open fd; if the entry is not a regular file (FIFO, device node, directory), it returns this InvalidData error. This is a deliberate fail-closed security check — a cache that exists but is not a plain file is treated as tampering. Callers (read_cache) convert the error to None and fall back to a fresh browser-based auth flow.","triggerScenarios":"read_private_cache at crates/buzz-agent/src/auth.rs:501-507 hits this when something replaced $HOME/.config/buzz-agent/oauth/<namespace>/<sha256>.json (or the cache_dir_override path) with a named pipe (`mkfifo`), a device file, or a directory. Note a symlink surfaces earlier as an ELOOP open error, not this message; this error is specifically 'opened fine but fstat says not a regular file'.","commonSituations":"A test or debugging session left a FIFO at the cache path; the cache directory is shared and another tool wrote a directory named like the cache file; tampering/compromised home dir. User-visible symptom is benign: the agent ignores the cache and forces a new OAuth login each run.","solutions":["Inspect the path: `file ~/.config/buzz-agent/oauth/*/<hash>.json` — expect 'JSON text data', not 'fifo' or 'directory'.","Remove the offending non-regular entry: `rm` (fifo/file) or `rmdir`, then re-run the agent to trigger a fresh interactive login that rewrites a proper 0600 cache file.","If you set a cache_dir_override, make sure it points at a directory only this agent manages.","If you did not create the entry yourself, treat it as a security incident on that home directory — the check exists precisely to catch planted files."],"exampleFix":"# before\nfile ~/.config/buzz-agent/oauth/default/<hash>.json\n# -> FIFO\n\n# after\nrm ~/.config/buzz-agent/oauth/default/<hash>.json\nbuzz-agent login   # recreates a regular, 0600 cache file","handlingStrategy":"fallback","validationCode":"// before reading, confirm the cache is a regular file (symlinks fail separately)\nuse std::os::unix::fs::FileTypeExt;\nfn cache_is_plain_file(p: &std::path::Path) -> bool {\n    std::fs::symlink_metadata(p)\n        .ok()\n        .map(|m| m.file_type().is_file())\n        .unwrap_or(false)\n}","typeGuard":"fn is_regular_file(p: &std::path::Path) -> bool {\n    std::fs::symlink_metadata(p)\n        .map(|m| m.file_type().is_file())\n        .unwrap_or(false)\n}","tryCatchPattern":"// read_cache already fails closed: map any read error to None and fall back\n// to interactive re-auth instead of using an untrusted file\nfn read_cache(path: &Path) -> Option<CachedToken> {\n    read_private_cache(path).ok()?\n        .pipe(|body| serde_json::from_slice(&body).ok())\n}","preventionTips":["Never replace the cache file with symlinks/FIFOs in scripts; only the agent should write it.","Give each agent user its own HOME/cache_dir_override; do not share cache directories across users.","If auth unexpectedly re-prompts every run, `file` the cache path — this error silently downgrades to re-login.","Treat any unexplained non-regular file at the path as tampering and investigate, not delete-and-forget."],"tags":["rust","security","oauth","filesystem","unix","cache"],"backgroundTag":"invalid-file-type","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}