astrid-runtime/astrid · error · io::Error
principal-store directory changed while it was opened
Error message
principal-store directory changed while it was opened
What it means
open_directory opens a principal-store subdirectory twice and compares the stable OS file identity (device+inode or file index) of both handles. If the identities differ, the directory entry was swapped or recreated between the two opens. The library throws this to defend capability-based storage against directory-swap attacks or race-condition corruption.
Source
Thrown at crates/astrid-storage/src/engine/durable/native_io.rs:60
.open_with(name, &options)
.map(cap_std::fs::File::into_std)
.map_err(|source| io_error("create principal-store capability file", source))?;
validate_regular(&file)?;
Ok(File::native(file))
}
pub(super) fn open_directory(
parent: &Dir,
name: &Path,
create: bool,
) -> Result<Option<Dir>, DurableError> {
let open = || -> io::Result<Dir> {
validate_directory_entry(parent, name)?;
let first = parent.open_dir(name)?;
validate_directory_entry(parent, name)?;
let second = parent.open_dir(name)?;
if directory_identity(&first)? != directory_identity(&second)? {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"principal-store directory changed while it was opened",
));
}
Ok(first)
};
match open() {
Ok(directory) => Ok(Some(directory)),
Err(source) if source.kind() == io::ErrorKind::NotFound && !create => Ok(None),
Err(source) if source.kind() == io::ErrorKind::NotFound => {
parent
.create_dir(name)
.or_else(|error| {
(error.kind() == io::ErrorKind::AlreadyExists)
.then_some(())
.ok_or(error)
})
.map_err(|source| {View on GitHub (pinned to affd8760f4)
Solutions
- Ensure no other process mutates (deletes/renames) directories under the principal-store root while the store is open
- Re-run the open operation; if transient, the second attempt will see a stable directory
- Audit for concurrent GC/recovery tooling operating on the same store path and serialize access with a lock
- Verify the store directory is on a filesystem you trust and not exposed to other local users
- Recover the store from backup if the directory was genuinely replaced and data is inconsistent
Example fix
// before rm -rf store/principals && mkdir store/principals # while store is open // after lock store; perform replacement; unlock; then reopen the store
Defensive patterns
Strategy: retry
Validate before calling
// ensure no concurrent mutators before opening
assert_no_active_gc_jobs(store_root)?;
if !store_root.is_dir() { return Err("store root missing".into()); } Try / catch
match open_store(path) {
Err(e) if e.to_string().contains("directory changed while it was opened") => {
// serialize with a lock file and retry once
let _lock = LockFile::acquire(path)?;
open_store(path)
}
r => r,
} Prevention
- Never delete/recreate directories under an open store
- Use an exclusive lock file around store maintenance
- Keep store directories off shared/network filesystems prone to rename races
- Restrict store-root permissions to the owning process user
When it happens
Trigger: Another process or thread removes and recreates the directory (e.g. rm -rf plus recreate, or an atomic rename swap) while open_directory executes its double-open identity check on the same path.
Common situations: Concurrent maintenance scripts garbage-collecting and recreating store directories; symlink/hardlink attacks by an untrusted local user; a stale background job racing a live store open on the same directory path.
Related errors
- legacy state source changed type: {}
- legacy source changed while opening: {}
- opened representation entry is redirected or not a regular f
- read materialized capsule metadata: {error:#}
- read materialized capsule authority: {error:#}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/9a40d12943eaa728.
Report an issue: GitHub.