astrid-runtime/astrid · critical · io::Error
PermissionDenied
PermissionDenied
Error message
private file identity changed while reading
What it means
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.
Source
Thrown at crates/astrid-core/src/platform_fs/windows/private_file.rs:41
)
})?;
let guard = TrustedPathGuard::capture(parent)?;
guard.verify_contract(BoundaryContract::ExactPrivateDirectory)?;
let _transaction_lock = acquire_private_file_transaction_lock(parent, &guard)?;
recover_private_file_transaction_locked(parent, &guard)?;
guard.verify_contract(BoundaryContract::ExactPrivateDirectory)?;
let mut file = open_guarded_regular_file(&guard, path, FileContract::ExactPrivate)?;
let identity = file_identity(&file)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
validate_file_contract(
file.as_raw_handle().cast(),
path,
FileContract::ExactPrivate,
)?;
if file_identity(&file)? != identity {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"private file identity changed while reading",
));
}
guard.verify_contract(BoundaryContract::ExactPrivateDirectory)?;
Ok(contents)
}
pub(in crate::platform_fs) fn atomic_write_private_file(
path: &Path,
bytes: &[u8],
) -> io::Result<()> {
validate_local_absolute_path(path)?;
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"private Windows file has no parent directory",
)View on GitHub (pinned to affd8760f4)
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.
Example fix
// before
let secret = read_private_file_to_string(path)?; // races with writer
// after
let secret = loop {
match read_private_file_to_string(path) {
Ok(s) => break s,
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
std::thread::sleep(RETRY_DELAY);
attempts += 1;
if attempts > MAX_ATTEMPTS { return Err(e); }
},
Err(e) => return Err(e),
}
}; Defensive patterns
Strategy: retry
Try / catch
const MAX: usize = 5;
for attempt in 0..MAX {
match read_private_file_to_string(path) {
Ok(s) => return Ok(s),
Err(e) if e.kind() == io::ErrorKind::PermissionDenied
&& e.to_string().contains("identity changed") => {
std::thread::sleep(Duration::from_millis(50 * (attempt as u64 + 1)));
},
Err(e) => return Err(e),
}
}
Err(io::Error::new(io::ErrorKind::PermissionDenied, "file keeps changing")) Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- projected path is redirected or not a regular file: {}
- layout migration source changed type: {}
- legacy state source changed type: {}
- named-pipe peer process changed during authentication
- trusted Windows parent has a null DACL: {description}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/20d8e68bae0c8e76.
Report an issue: GitHub.