fix(filesystem): resolve critical data loss, POSIX non-compliance, and deadlock vulnerabilities

- Standardize POSIX unlink (EISDIR) and rmdir (ENOTDIR/ENOTEMPTY) checks
- Defer local directory eviction in mode switch until FUSE mount verification succeeds
- Enforce fsync before atomic renames in ContentCache store operations
- Add directory ancestor loop check in rename (EINVAL)
- Implement punch_hole interval tracking in fallocate
- Purge un-created local node write revisions on unlink
- Fix control socket permission race with octal umask 0o77
- Add unit tests for state, DB, and FUSE POSIX logic (208/208 tests passing)
This commit is contained in:
nvrl committed 2026-07-22 00:23:55 +02:00
1 parent 9d55ffb9f6
commit fe75998cea
8 files changed
+434 -28

No files matched your search

+12
View File
@@ -118,6 +118,18 @@ impl Db {
Ok(())
}
/// Check if a folder node has any non-trashed children in the database.
pub fn has_children(&self, parent_uid: &NodeUid) -> Result<bool> {
let uid_str = parent_uid.to_string();
let conn = self.conn.lock();
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM nodes WHERE parent_uid = ?1 AND trashed = 0",
params![uid_str],
|row| row.get(0),
)?;
Ok(count > 0)
}
/// Full-text search over node names, newest schema's trigram index giving
/// substring (not just prefix) matches. Returns up to `limit` non-trashed
/// hits, each with its mountpoint-relative path resolved. Queries shorter
+11
View File
@@ -230,6 +230,17 @@ impl Db {
Ok(n > 0)
}
/// Check if a queued create or mkdir op exists for `uid`.
pub fn has_create_op(&self, uid: &str) -> Result<bool> {
let conn = self.conn.lock();
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM pending_op WHERE uid = ?1 AND kind IN (?2, ?3)",
params![uid, OP_CREATE, OP_MKDIR],
|row| row.get(0),
)?;
Ok(count > 0)
}
/// Drop every op targeting a node **or anything queued beneath it**,
/// returning the staged blobs they held so the caller can delete them.
///
+88
View File
@@ -1522,3 +1522,91 @@ fn db_contention_under_fuse_worker_load() {
serial / total as u32
);
}
#[test]
fn test_db_has_children_and_trashed_filtering() {
use proton_drive_rs::proton_sdk::ids::{LinkId, VolumeId};
let db = Db::open_in_memory().unwrap();
let parent_uid = NodeUid::new(VolumeId::from("vol"), LinkId::from("parent"));
let child_uid = NodeUid::new(VolumeId::from("vol"), LinkId::from("child"));
assert!(
!db.has_children(&parent_uid).unwrap(),
"no children initially"
);
let parent_node = Node {
uid: parent_uid.clone(),
parent_uid: None,
name: "parent".into(),
kind: NodeKind::Folder,
creation_time: 100,
modification_time: 100,
trashed: false,
is_shared: false,
is_shared_publicly: false,
signature_email: None,
verification: Default::default(),
};
db.upsert_node(&parent_node).unwrap();
let mut child_node = Node {
uid: child_uid.clone(),
parent_uid: Some(parent_uid.clone()),
name: "child.txt".into(),
kind: NodeKind::File {
media_type: "text/plain".into(),
total_size_on_storage: 10,
active_revision_state: None,
claimed_size: Some(10),
claimed_modification_time: None,
},
creation_time: 100,
modification_time: 100,
trashed: false,
is_shared: false,
is_shared_publicly: false,
signature_email: None,
verification: Default::default(),
};
db.upsert_node(&child_node).unwrap();
assert!(db.has_children(&parent_uid).unwrap(), "child node present");
// Trashing child removes it from active children
child_node.trashed = true;
db.upsert_node(&child_node).unwrap();
assert!(
!db.has_children(&parent_uid).unwrap(),
"trashed child ignored"
);
}
#[test]
fn test_db_has_create_op() {
let db = Db::open_in_memory().unwrap();
let local_uid = "local~testnode12345678901234567890";
assert!(!db.has_create_op(local_uid).unwrap(), "no op initially");
let op = ops::PendingOp {
id: 0,
kind: ops::OP_CREATE.to_string(),
uid: local_uid.to_string(),
parent_uid: Some("vol~parent".to_string()),
name: Some("test.txt".to_string()),
blob_path: None,
meta_json: None,
created_at: 1000,
attempts: 0,
last_error: None,
next_attempt_at: 0,
};
db.enqueue_op(&op).unwrap();
assert!(db.has_create_op(local_uid).unwrap(), "create op present");
db.delete_ops_for_uid(local_uid).unwrap();
assert!(!db.has_create_op(local_uid).unwrap(), "op deleted");
}