{"record":{"id":"20d8e68bae0c8e76","repo":"astrid-runtime/astrid","slug":"permissiondenied-20d8e6","errorCode":"PermissionDenied","errorMessage":"private file identity changed while reading","messagePattern":"private file identity changed while reading","errorType":"error_code","errorClass":"io::Error","httpStatus":null,"severity":"critical","filePath":"crates/astrid-core/src/platform_fs/windows/private_file.rs","lineNumber":41,"sourceCode":"        )\n    })?;\n    let guard = TrustedPathGuard::capture(parent)?;\n    guard.verify_contract(BoundaryContract::ExactPrivateDirectory)?;\n    let _transaction_lock = acquire_private_file_transaction_lock(parent, &guard)?;\n    recover_private_file_transaction_locked(parent, &guard)?;\n    guard.verify_contract(BoundaryContract::ExactPrivateDirectory)?;\n\n    let mut file = open_guarded_regular_file(&guard, path, FileContract::ExactPrivate)?;\n    let identity = file_identity(&file)?;\n    let mut contents = String::new();\n    file.read_to_string(&mut contents)?;\n    validate_file_contract(\n        file.as_raw_handle().cast(),\n        path,\n        FileContract::ExactPrivate,\n    )?;\n    if file_identity(&file)? != identity {\n        return Err(io::Error::new(\n            io::ErrorKind::PermissionDenied,\n            \"private file identity changed while reading\",\n        ));\n    }\n    guard.verify_contract(BoundaryContract::ExactPrivateDirectory)?;\n    Ok(contents)\n}\n\npub(in crate::platform_fs) fn atomic_write_private_file(\n    path: &Path,\n    bytes: &[u8],\n) -> io::Result<()> {\n    validate_local_absolute_path(path)?;\n    let parent = path.parent().ok_or_else(|| {\n        io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"private Windows file has no parent directory\",\n        )","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/platform_fs/windows/private_file.rs#L23-L59","documentation":"read_private_file_to_string records the file's identity (from file_identity) before reading, then re-validates the handle contract and compares identity afterward. If the identity changed mid-read, the file was replaced/renamed/swapped concurrently (TOCTOU), so the library discards the result and returns PermissionDenied rather than return contents of a different file.","triggerScenarios":"Calling read_private_file_to_string while another process or thread replaces the target file during the read — e.g. a concurrent restrict_private_file/replace operation, an installer swapping the file, antivirus quarantine, or a sync client (OneDrive/Dropbox) rewriting the file.","commonSituations":"Two application instances racing on the same private file; deploy scripts replacing secret files while the app reads them; file-sync or backup tools touching the path; periodic key-rotation jobs colliding with readers.","solutions":["Retry the read — after a replacement the new file is usually stable; a simple backoff retry typically succeeds.","Serialize access: perform reads and replacements under the same application-level lock or use the library's locked-file APIs on both sides.","Identify the concurrent writer (installer, sync client, AV) and exclude the file's directory from its operations."],"exampleFix":"// before\nlet secret = read_private_file_to_string(path)?; // races with writer\n// after\nlet secret = loop {\n    match read_private_file_to_string(path) {\n        Ok(s) => break s,\n        Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {\n            std::thread::sleep(RETRY_DELAY);\n            attempts += 1;\n            if attempts > MAX_ATTEMPTS { return Err(e); }\n        },\n        Err(e) => return Err(e),\n    }\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"const MAX: usize = 5;\nfor attempt in 0..MAX {\n    match read_private_file_to_string(path) {\n        Ok(s) => return Ok(s),\n        Err(e) if e.kind() == io::ErrorKind::PermissionDenied\n            && e.to_string().contains(\"identity changed\") => {\n            std::thread::sleep(Duration::from_millis(50 * (attempt as u64 + 1)));\n        },\n        Err(e) => return Err(e),\n    }\n}\nErr(io::Error::new(io::ErrorKind::PermissionDenied, \"file keeps changing\"))","preventionTips":["Serialize all writes/replacements of private files through one owner (lock or single task).","Use the library's locked/atomic replace APIs on the writer side so readers never see swaps.","Exclude the data directory from file-sync clients and aggressive AV scanning.","Treat PermissionDenied during reads as transient and retry with backoff."],"tags":["windows","filesystem","security","race-condition","toctou"],"backgroundTag":"permission-denied","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}