{"record":{"id":"0a9320d0cd880489","repo":"jdx/mise","slug":"cached-rustc-output-has-an-unsafe-file-mode","errorCode":null,"errorMessage":"cached rustc output has an unsafe file mode: {}","messagePattern":"cached rustc output has an unsafe file mode: (.+?)","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/cache/rustc.rs","lineNumber":848,"sourceCode":"#[cfg(unix)]\nfn file_mode(metadata: &std::fs::Metadata) -> u32 {\n    use std::os::unix::fs::PermissionsExt as _;\n    metadata.permissions().mode() & 0o644\n}\n\n#[cfg(windows)]\nfn file_mode(_metadata: &std::fs::Metadata) -> u32 {\n    0\n}\n\n#[cfg(unix)]\nfn validate_file_mode(node: &CacheFileNode) -> Result<()> {\n    if node.executable\n        || node.mode & !0o777 != 0\n        || node.mode & 0o111 != 0\n        || node.mode & 0o022 != 0\n    {\n        bail!(\"cached rustc output has an unsafe file mode: {}\", node.name);\n    }\n    Ok(())\n}\n\n#[cfg(windows)]\nfn validate_file_mode(node: &CacheFileNode) -> Result<()> {\n    if node.executable || node.mode != 0 {\n        bail!(\"cached rustc output has an unsafe file mode: {}\", node.name);\n    }\n    Ok(())\n}\n\n#[cfg(unix)]\nfn apply_file_mode(temporary: &Path, mode: u32) -> Result<()> {\n    use std::os::unix::fs::PermissionsExt as _;\n    std::fs::set_permissions(temporary, std::fs::Permissions::from_mode(mode))?;\n    Ok(())\n}","sourceCodeStart":830,"sourceCodeEnd":866,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/cache/rustc.rs#L830-L866","documentation":"validate_file_mode() enforces a security invariant on every restored file (src/cache/rustc.rs:842-859): on Unix a cached node must not be executable (executable false, no 0o111 bits), must have no bits outside 0o777, and must not be group/world-writable (0o022); Windows requires mode 0 and executable false. The publisher only ever stores modes masked to 0o644 (src/cache/rustc.rs:831-834), so a violating entry means cache tampering, a poisoned shared cache, or an entry written by other/older software — restoring it could plant executable or world-writable files, so the restore is refused.","triggerScenarios":"A crafted CAS entry carrying exec/setuid bits or group-write permissions; a cache populated by a different tool sharing the CAS layout; an older mise build that stored unmasked permission bits.","commonSituations":"Public or broadly shared remote cache namespaces accepting entries from untrusted contributors; a compromised CI cache; mixed tooling writing into one CAS.","solutions":["Purge the cache and repopulate it from a single trusted build","Do not share remote cache namespaces across tools, teams, or untrusted contributor bases","Treat recurring occurrences as a security signal — the guard exists to block poisoned entries; audit who can write to the namespace","Keep remote tokens scoped and namespaces private per project"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"fn mode_safe(node: &CacheFileNode) -> bool {\n    if node.executable {\n        return false;\n    }\n    #[cfg(unix)]\n    { node.mode & !0o777 == 0 && node.mode & 0o111 == 0 && node.mode & 0o022 == 0 }\n    #[cfg(windows)]\n    { node.mode == 0 }\n}","typeGuard":null,"tryCatchPattern":"On any unsafe-mode entry, discard the entry and the surrounding cache and recompile from trusted sources; do not 'fix' permissions on restored files and keep them — the mode is part of the cached content's integrity contract.","preventionTips":["Keep remote cache namespaces private and access-controlled","Do not share one CAS across tools or untrusted contributor bases","Audit recurring unsafe-mode warnings as potential cache poisoning, not noise"],"tags":["rust","cache","security","file-permissions","cache-poisoning"],"backgroundTag":"unsafe-cached-file-permissions","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}