mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 00:47:00 +01:00
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:
1 parent
9d55ffb9f6
commit
fe75998cea
8 files changed
+434
-28
No files matched your search
@@ -153,10 +153,12 @@ pub struct StagedWrite {
|
||||
/// The identity of a remote revision, as far as we can observe it: a file whose
|
||||
/// size and mtime both still match the ones a queued change was made against has
|
||||
/// not been rewritten by anyone else in the meantime.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Baseline {
|
||||
pub mtime: i64,
|
||||
pub size: u64,
|
||||
#[serde(default)]
|
||||
pub hash: Option<String>,
|
||||
}
|
||||
|
||||
/// Content cache rooted at a directory, with a sibling pin-registry file.
|
||||
@@ -431,7 +433,12 @@ impl ContentCache {
|
||||
// 64-char hex key with no `.`, so this appends rather than replaces.
|
||||
// `store_thumbnail` cannot use the same idiom — see the note there.
|
||||
let tmp = blob.with_extension("tmp");
|
||||
std::fs::write(&tmp, bytes)?;
|
||||
{
|
||||
let mut f = std::fs::File::create(&tmp)?;
|
||||
use std::io::Write;
|
||||
f.write_all(bytes)?;
|
||||
f.sync_all()?;
|
||||
}
|
||||
std::fs::rename(&tmp, &blob)?;
|
||||
let meta = serde_json::to_vec(&Meta { mtime, size })?;
|
||||
std::fs::write(self.meta_path(uid), meta)?;
|
||||
@@ -464,6 +471,9 @@ impl ContentCache {
|
||||
if std::fs::hard_link(src, &tmp).is_err() {
|
||||
std::fs::copy(src, &tmp)?;
|
||||
}
|
||||
if let Ok(f) = std::fs::File::open(&tmp) {
|
||||
let _ = f.sync_all();
|
||||
}
|
||||
let bytes = std::fs::metadata(&tmp)?.len();
|
||||
std::fs::rename(&tmp, &blob)?;
|
||||
let meta = serde_json::to_vec(&Meta { mtime, size })?;
|
||||
@@ -526,7 +536,12 @@ impl ContentCache {
|
||||
let tmp = self
|
||||
.block_dir
|
||||
.join(format!("{}.b{idx}.tmp", Self::key(uid)));
|
||||
std::fs::write(&tmp, bytes)?;
|
||||
{
|
||||
let mut f = std::fs::File::create(&tmp)?;
|
||||
use std::io::Write;
|
||||
f.write_all(bytes)?;
|
||||
f.sync_all()?;
|
||||
}
|
||||
std::fs::rename(&tmp, &blob)?;
|
||||
std::fs::write(
|
||||
self.block_meta(uid, idx),
|
||||
@@ -1759,6 +1774,7 @@ mod tests {
|
||||
based_on: Some(Baseline {
|
||||
mtime: 100,
|
||||
size: 40,
|
||||
hash: None,
|
||||
}),
|
||||
};
|
||||
let staged = c.stage_write(&meta, &scratch).unwrap();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user