fix(fuse,core): remediate audit bugs B23, B26-B29, B33 and update BUGS tracking

- B23: Generate synthetic StagedWrite sidecars in rescue_scratch when sidecar is corrupted
- B26: Add open_count and unlinked state fields to defer node removal and cache eviction on unlinked open files until release
- B27: Discard orphaned write revisions for local UIDs unlinked prior to upload
- B28: Add emergency_evict to ContentCache for LRU blob eviction upon ENOSPC
- B29: Implement FUSE forget handler and forget_lookup in state to prune unreferenced inodes
- B33: Quarantine failing drain ops on HTTP 40x / max retries to unblock offline sync queue
- docs: Track audit items B18-B36 in docs/BUGS.md with fix details and verification results
This commit is contained in:
nvrl committed 2026-07-22 01:18:35 +02:00
1 parent fe75998cea
commit 1cd6a26be3
5 files changed
+427 -38

No files matched your search

+34
View File
@@ -18,6 +18,9 @@ pub(crate) struct Entry {
pub(crate) uid: NodeUid,
pub(crate) parent: u64,
pub(crate) node: Node,
pub(crate) lookup_count: u64,
pub(crate) open_count: u32,
pub(crate) unlinked: bool,
}
/// A set of non-overlapping `[start, end)` byte ranges, kept sorted and merged.
@@ -194,11 +197,26 @@ impl State {
uid: node.uid.clone(),
parent,
node,
lookup_count: 1,
open_count: 0,
unlinked: false,
},
);
ino
}
/// Decrement lookup count for an inode and prune if lookup_count == 0 && open_count == 0 && unlinked.
pub(crate) fn forget_lookup(&mut self, ino: u64, nlookup: u64) -> Option<(u64, String)> {
if let Some(entry) = self.entries.get_mut(&ino) {
entry.lookup_count = entry.lookup_count.saturating_sub(nlookup);
if entry.lookup_count == 0 && entry.open_count == 0 && entry.unlinked {
let uid = entry.uid.clone();
return self.forget(&uid);
}
}
None
}
/// Allocate (or reuse) a stable inode for a node that came *from* the
/// database, which is why nothing is written back.
pub(crate) fn intern_from_db(&mut self, parent: u64, node: Node) -> u64 {
@@ -253,6 +271,22 @@ impl State {
false
}
/// Forget a node or, if open handles exist (open_count > 0), mark it unlinked
/// and remove it from parent children so lookups fail while open reads succeed.
pub(crate) fn forget_or_unlink(&mut self, uid: &NodeUid) -> Option<(u64, String)> {
if let Some(&ino) = self.by_uid.get(uid)
&& let Some(entry) = self.entries.get_mut(&ino)
&& entry.open_count > 0
{
entry.unlinked = true;
if let Some(kids) = self.children.get_mut(&entry.parent) {
kids.retain(|&k| k != ino);
}
return Some((entry.parent, entry.node.name.clone()));
}
self.forget(uid)
}
/// Forget a node entirely: drop its inode, its uid mapping, its own cached
/// listing, its slot in its parent's listing, and its DB row. Returns
/// `(parent_ino, name)` when the node was known, so the caller can notify