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

+56 -6
View File
@@ -748,14 +748,40 @@ impl ContentCache {
continue;
}
let side = Self::scratch_sidecar(&blob);
// Parsed, not merely present: an unreadable sidecar cannot drive a
// recovery, so the blob is rubbish like any other leftover.
if std::fs::read(&side)
if !side.exists() {
// Not marked durable, or durability marker was explicitly cleared on release
continue;
}
// Parsed if valid; if sidecar exists but is corrupted, build synthetic StagedWrite
let has_valid_sidecar = std::fs::read(&side)
.ok()
.and_then(|b| serde_json::from_slice::<StagedWrite>(&b).ok())
.is_none()
{
continue;
.is_some();
if !has_valid_sidecar {
if let Ok(meta) = std::fs::metadata(&blob)
&& meta.len() > 0
{
let blob_name = blob
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let synthetic = StagedWrite {
uid: format!("recovered~{blob_name}"),
len: meta.len(),
base_size: meta.len(),
base_mtime: 0,
authored: vec![(0, meta.len())],
complete: true,
based_on: None,
};
if let Ok(data) = serde_json::to_vec(&synthetic) {
let _ = std::fs::write(&side, data);
} else {
continue;
}
} else {
continue;
}
}
let Some(name) = blob.file_name() else {
continue;
@@ -770,6 +796,30 @@ impl ContentCache {
}
}
/// Emergency eviction of unpinned blobs when local disk runs out of space (ENOSPC).
pub fn emergency_evict(&self) {
let pinned: HashSet<String> = self
.db
.pinned_uids()
.unwrap_or_default()
.iter()
.map(|uid| Self::key_str(uid))
.collect();
let content_dir = self.content_dir.clone();
// Force pool enforcement ignoring current cap check to free disk space immediately
let Ok(list) = self.db.cache_eviction_candidates(KIND_BLOB, 10) else { return; };
for (key, bytes) in list {
if pinned.contains(&key) { continue; }
let p = content_dir.join(&key);
let m = content_dir.join(format!("{key}.meta"));
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(&m);
let _ = self.db.cache_remove(&key);
self.blob_bytes.fetch_sub(bytes, Ordering::Relaxed);
break; // Evict one item at a time until space is freed
}
}
/// Writes rescued from an unclean shutdown, as `(blob, meta)` pairs for the
/// daemon to hand to the upload queue. A blob whose sidecar did not survive
/// the rescue is deleted here — nothing can address it.