From 27707bbe0e7acb72ea3008126d2095b48b1fcc04 Mon Sep 17 00:00:00 2001 From: Warren Date: Sun, 21 Jun 2026 00:17:24 +0800 Subject: [PATCH] Implement SMB Oplocks Phase 1-2 Phase 1: Data structures - Add oplock_level and share_access fields to Open struct - Update Open::new() signature with new parameters - Update handlers/create.rs to pass oplock params Phase 2: OplockManager - Create oplock.rs with OplockManager struct - OplockEntry for tracking per-client oplock state - can_grant() - check ShareAccess compatibility - register() / unregister() - lifecycle management - break_oplock() - generate OPLOCK_BREAK_NOTIFICATION - Add OplockManager to ServerState - Add Hash trait to SmbPath for HashMap key All 229 tests pass. --- vendor/smb-server/src/conn/state.rs | 7 + vendor/smb-server/src/handlers/create.rs | 8 +- vendor/smb-server/src/lib.rs | 1 + vendor/smb-server/src/oplock.rs | 176 +++++++++++++++++++++++ vendor/smb-server/src/path.rs | 2 +- vendor/smb-server/src/server.rs | 3 + 6 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 vendor/smb-server/src/oplock.rs diff --git a/vendor/smb-server/src/conn/state.rs b/vendor/smb-server/src/conn/state.rs index 6919571..37558ae 100644 --- a/vendor/smb-server/src/conn/state.rs +++ b/vendor/smb-server/src/conn/state.rs @@ -294,6 +294,9 @@ pub struct Open { pub is_directory: bool, pub delete_on_close: bool, pub search_state: Option, + // Oplock fields (MS-SMB2 §2.2.13 / §2.2.14) + pub oplock_level: u8, + pub share_access: u32, } impl Open { @@ -304,6 +307,8 @@ impl Open { last_path: SmbPath, is_directory: bool, delete_on_close: bool, + oplock_level: u8, + share_access: u32, ) -> Self { Self { file_id, @@ -313,6 +318,8 @@ impl Open { is_directory, delete_on_close, search_state: None, + oplock_level, + share_access, } } } diff --git a/vendor/smb-server/src/handlers/create.rs b/vendor/smb-server/src/handlers/create.rs index 789eaab..ea65a95 100644 --- a/vendor/smb-server/src/handlers/create.rs +++ b/vendor/smb-server/src/handlers/create.rs @@ -153,6 +153,10 @@ pub async fn handle( // Allocate FileId, register Open. let tree = tree_arc.write().await; let file_id = tree.alloc_file_id(); + + // Phase 4: Oplock support - determine oplock level + let granted_oplock = 0; // Phase 1 placeholder (will be dynamic in Phase 4) + let open = Open::new( file_id, handle, @@ -160,6 +164,8 @@ pub async fn handle( path, info.is_directory, delete_on_close, + granted_oplock, // oplock_level + req.share_access, // share_access ); let open_arc = Arc::new(tokio::sync::RwLock::new(open)); tree.opens.write().await.insert(file_id, open_arc); @@ -172,7 +178,7 @@ pub async fn handle( }; let resp = CreateResponse { structure_size: 89, - oplock_level: 0, + oplock_level: granted_oplock, // Phase 4: will be dynamic flags: 0, create_action, creation_time: info.creation_time, diff --git a/vendor/smb-server/src/lib.rs b/vendor/smb-server/src/lib.rs index 01fb8ae..0aa39d9 100644 --- a/vendor/smb-server/src/lib.rs +++ b/vendor/smb-server/src/lib.rs @@ -26,6 +26,7 @@ mod fs; mod handlers; pub(crate) mod info_class; pub mod ntstatus; +mod oplock; mod path; mod proto; mod server; diff --git a/vendor/smb-server/src/oplock.rs b/vendor/smb-server/src/oplock.rs new file mode 100644 index 0000000..6e35694 --- /dev/null +++ b/vendor/smb-server/src/oplock.rs @@ -0,0 +1,176 @@ +//! Oplock Manager — global state tracking for opportunistic locking. +//! +//! MS-SMB2 §2.2.13 / §2.2.14: Oplocks allow clients to cache file data locally, +//! reducing network round-trips. The server tracks all opens per file and +//! triggers OPLOCK_BREAK_NOTIFICATION when conflicting opens occur. + +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::builder::Access; +use crate::path::SmbPath; +use crate::proto::messages::{FileId, OplockBreakNotification, OplockLevel}; + +/// An entry tracking one client's oplock on a file. +#[derive(Debug, Clone)] +pub struct OplockEntry { + pub file_id: FileId, + pub tree_id: u32, + pub session_id: u64, + pub oplock_level: u8, + pub share_access: u32, + pub granted_access: Access, + pub connection_id: u64, // For notification routing +} + +/// Global oplock state manager (MS-SMB2 §3.3.1.6). +pub struct OplockManager { + /// File path → all opens with oplocks on that file. + file_opens: RwLock>>, +} + +impl OplockManager { + pub fn new() -> Self { + Self { + file_opens: RwLock::new(HashMap::new()), + } + } + + /// Check if requested oplock can be granted (MS-SMB2 §3.3.5.9). + /// Returns the granted level (may be lower than requested). + pub async fn can_grant( + &self, + path: &SmbPath, + requested_level: u8, + share_access: u32, + granted_access: Access, + ) -> Option { + let file_opens = self.file_opens.read().await; + let existing = file_opens.get(path); + + // No existing opens → grant requested level + if existing.is_none() || existing.unwrap().is_empty() { + return Some(requested_level); + } + + let existing_opens = existing.unwrap(); + + // Check ShareAccess conflicts (MS-SMB2 §3.3.5.9) + for entry in existing_opens { + // If existing open doesn't allow sharing, deny oplock + if !share_access_compatible(entry.share_access, share_access) { + return None; + } + + // If existing has exclusive/batch oplock, can only grant Level II + if entry.oplock_level == OplockLevel::Exclusive as u8 + || entry.oplock_level == OplockLevel::Batch as u8 + { + // Can grant Level II if share access compatible + if requested_level == OplockLevel::Ii as u8 + && share_access_compatible(entry.share_access, share_access) + { + return Some(OplockLevel::Ii as u8); + } + // Otherwise deny + return None; + } + } + + // All existing opens are Level II → grant requested level + Some(requested_level) + } + + /// Register a new open with oplock (MS-SMB2 §3.3.5.9). + pub async fn register(&self, path: &SmbPath, entry: OplockEntry) { + let mut file_opens = self.file_opens.write().await; + file_opens + .entry(path.clone()) + .or_insert_with(Vec::new) + .push(entry); + } + + /// Remove an open when closed (MS-SMB2 §3.3.5.7). + pub async fn unregister(&self, path: &SmbPath, file_id: &FileId) { + let mut file_opens = self.file_opens.write().await; + if let Some(entries) = file_opens.get_mut(path) { + entries.retain(|e| e.file_id != *file_id); + if entries.is_empty() { + file_opens.remove(path); + } + } + } + + /// Trigger oplock break when conflicting open occurs (MS-SMB2 §3.3.5.9). + /// Returns notifications to send to affected clients. + pub async fn break_oplock( + &self, + path: &SmbPath, + new_share_access: u32, + new_granted_access: Access, + ) -> Vec { + let mut notifications = Vec::new(); + let mut file_opens = self.file_opens.write().await; + + if let Some(entries) = file_opens.get_mut(path) { + for entry in entries.iter_mut() { + // Check if new open conflicts with existing oplock + if !share_access_compatible(entry.share_access, new_share_access) { + // Need to break the oplock + let new_level = OplockLevel::Ii as u8; // Downgrade to Level II + + // Build notification (MS-SMB2 §2.2.23.1) + notifications.push(OplockBreakNotification { + structure_size: 24, + oplock_level: new_level, + reserved: 0, + reserved2: 0, + file_id: entry.file_id, + }); + + // Update entry's oplock level + entry.oplock_level = new_level; + } + } + } + + notifications + } + + /// Get all opens for a file (for diagnostics). + pub async fn get_opens(&self, path: &SmbPath) -> Vec { + let file_opens = self.file_opens.read().await; + file_opens.get(path).cloned().unwrap_or_default() + } +} + +impl Default for OplockManager { + fn default() -> Self { + Self::new() + } +} + +/// Check ShareAccess compatibility (MS-SMB2 §3.3.5.9). +pub fn share_access_compatible(existing: u32, new: u32) -> bool { + const FILE_SHARE_READ: u32 = 0x00000001; + const FILE_SHARE_WRITE: u32 = 0x00000002; + const FILE_SHARE_DELETE: u32 = 0x00000004; + + // If existing denies read sharing and new wants read → conflict + if (existing & FILE_SHARE_READ) == 0 && (new & FILE_SHARE_READ) != 0 { + return false; + } + + // If existing denies write sharing and new wants write → conflict + if (existing & FILE_SHARE_WRITE) == 0 && (new & FILE_SHARE_WRITE) != 0 { + return false; + } + + // If existing denies delete sharing and new wants delete → conflict + if (existing & FILE_SHARE_DELETE) == 0 && (new & FILE_SHARE_DELETE) != 0 { + return false; + } + + true +} \ No newline at end of file diff --git a/vendor/smb-server/src/path.rs b/vendor/smb-server/src/path.rs index c1955a3..738efcc 100644 --- a/vendor/smb-server/src/path.rs +++ b/vendor/smb-server/src/path.rs @@ -12,7 +12,7 @@ use crate::error::{SmbError, SmbResult}; /// A validated, component-list path. No `..`, no Windows-forbidden chars, no /// alternate streams. Always relative to the share root — the empty path is /// the root. -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)] pub struct SmbPath { components: Vec, } diff --git a/vendor/smb-server/src/server.rs b/vendor/smb-server/src/server.rs index 31f0fb5..6079b60 100644 --- a/vendor/smb-server/src/server.rs +++ b/vendor/smb-server/src/server.rs @@ -196,6 +196,8 @@ pub struct ServerState { /// iteration and connection loops abandon their next read. pub shutdown: Arc, pub shutting_down: Arc, + /// Global oplock state manager (Phase 2). + pub oplock_manager: Arc, } impl ServerState { @@ -208,6 +210,7 @@ impl ServerState { server_start_filetime: now_filetime(), shutdown: Arc::new(Notify::new()), shutting_down: Arc::new(AtomicBool::new(false)), + oplock_manager: Arc::new(crate::oplock::OplockManager::new()), } }