{"record":{"id":"9a40d12943eaa728","repo":"astrid-runtime/astrid","slug":"principal-store-directory-changed-while-it-was-ope","errorCode":null,"errorMessage":"principal-store directory changed while it was opened","messagePattern":"principal-store directory changed while it was opened","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-storage/src/engine/durable/native_io.rs","lineNumber":60,"sourceCode":"        .open_with(name, &options)\n        .map(cap_std::fs::File::into_std)\n        .map_err(|source| io_error(\"create principal-store capability file\", source))?;\n    validate_regular(&file)?;\n    Ok(File::native(file))\n}\n\npub(super) fn open_directory(\n    parent: &Dir,\n    name: &Path,\n    create: bool,\n) -> Result<Option<Dir>, DurableError> {\n    let open = || -> io::Result<Dir> {\n        validate_directory_entry(parent, name)?;\n        let first = parent.open_dir(name)?;\n        validate_directory_entry(parent, name)?;\n        let second = parent.open_dir(name)?;\n        if directory_identity(&first)? != directory_identity(&second)? {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                \"principal-store directory changed while it was opened\",\n            ));\n        }\n        Ok(first)\n    };\n    match open() {\n        Ok(directory) => Ok(Some(directory)),\n        Err(source) if source.kind() == io::ErrorKind::NotFound && !create => Ok(None),\n        Err(source) if source.kind() == io::ErrorKind::NotFound => {\n            parent\n                .create_dir(name)\n                .or_else(|error| {\n                    (error.kind() == io::ErrorKind::AlreadyExists)\n                        .then_some(())\n                        .ok_or(error)\n                })\n                .map_err(|source| {","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-storage/src/engine/durable/native_io.rs#L42-L78","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nrm -rf store/principals && mkdir store/principals   # while store is open\n// after\nlock store; perform replacement; unlock; then reopen the store","handlingStrategy":"retry","validationCode":"// ensure no concurrent mutators before opening\nassert_no_active_gc_jobs(store_root)?;\nif !store_root.is_dir() { return Err(\"store root missing\".into()); }","typeGuard":null,"tryCatchPattern":"match open_store(path) {\n    Err(e) if e.to_string().contains(\"directory changed while it was opened\") => {\n        // serialize with a lock file and retry once\n        let _lock = LockFile::acquire(path)?;\n        open_store(path)\n    }\n    r => r,\n}","preventionTips":["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"],"tags":["io","filesystem","security","race-condition"],"backgroundTag":"file-changed-during-open","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"}