Implement SMB Byte-range Lock (Phase 7)
- Add LockManager to oplock.rs: - LockRange struct for tracking byte-range locks - acquire() - check conflicts before granting lock - release() - remove specific lock by offset/length - clear() - clear all locks when file closed - ranges_overlap() - helper for conflict detection - Add LockManager to ServerState - Update handlers/lock.rs: - Parse LockRequest and LockElement - Process each lock element (acquire/release) - Support FLAG_EXCLUSIVE_LOCK, FLAG_SHARED_LOCK, FLAG_UNLOCK - Return STATUS_LOCK_NOT_GRANTED on conflict - Update handlers/close.rs: - Clear all locks when file closed - Add STATUS_LOCK_NOT_GRANTED to ntstatus.rs All 229 tests pass.
This commit is contained in:
3
vendor/smb-server/src/handlers/close.rs
vendored
3
vendor/smb-server/src/handlers/close.rs
vendored
@@ -52,6 +52,9 @@ pub async fn handle(
|
||||
server.oplock_manager.unregister(&path, &req.file_id).await;
|
||||
}
|
||||
|
||||
// Phase 7: Clear all byte-range locks for this file
|
||||
server.lock_manager.clear(&req.file_id).await;
|
||||
|
||||
// Stat before closing if needed.
|
||||
let info_before_close = if want_attrs {
|
||||
if let Some(h) = handle.as_ref() {
|
||||
|
||||
56
vendor/smb-server/src/handlers/lock.rs
vendored
56
vendor/smb-server/src/handlers/lock.rs
vendored
@@ -3,18 +3,64 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::proto::header::Smb2Header;
|
||||
use crate::proto::messages::LockResponse;
|
||||
use crate::proto::messages::{LockElement, LockRequest, LockResponse};
|
||||
|
||||
use crate::conn::state::Connection;
|
||||
use crate::dispatch::HandlerResponse;
|
||||
use crate::handlers::shared::lookup_session_tree;
|
||||
use crate::ntstatus;
|
||||
use crate::server::ServerState;
|
||||
|
||||
pub async fn handle(
|
||||
_server: &Arc<ServerState>,
|
||||
_conn: &Arc<Connection>,
|
||||
_hdr: &Smb2Header,
|
||||
_body: &[u8],
|
||||
server: &Arc<ServerState>,
|
||||
conn: &Arc<Connection>,
|
||||
hdr: &Smb2Header,
|
||||
body: &[u8],
|
||||
) -> HandlerResponse {
|
||||
let req = match LockRequest::parse(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||
};
|
||||
|
||||
// Validate tree/session
|
||||
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||
Ok(t) => t,
|
||||
Err(s) => return HandlerResponse::err(s),
|
||||
};
|
||||
|
||||
// Phase 7: Process each lock element
|
||||
for lock in &req.locks {
|
||||
let exclusive = lock.flags & LockElement::FLAG_EXCLUSIVE_LOCK != 0;
|
||||
let unlock = lock.flags & LockElement::FLAG_UNLOCK != 0;
|
||||
|
||||
if unlock {
|
||||
// Release lock
|
||||
server.lock_manager.release(
|
||||
&req.file_id,
|
||||
lock.offset,
|
||||
lock.length,
|
||||
hdr.session_id,
|
||||
tree_arc.read().await.id,
|
||||
).await;
|
||||
} else {
|
||||
// Acquire lock
|
||||
let fail_immediately = lock.flags & LockElement::FLAG_FAIL_IMMEDIATELY != 0;
|
||||
|
||||
let result = server.lock_manager.acquire(
|
||||
&req.file_id,
|
||||
lock.offset,
|
||||
lock.length,
|
||||
exclusive,
|
||||
hdr.session_id,
|
||||
tree_arc.read().await.id,
|
||||
).await;
|
||||
|
||||
if result.is_err() && fail_immediately {
|
||||
return HandlerResponse::err(ntstatus::STATUS_LOCK_NOT_GRANTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
LockResponse::default().write_to(&mut buf).expect("encode");
|
||||
HandlerResponse::ok(buf)
|
||||
|
||||
1
vendor/smb-server/src/ntstatus.rs
vendored
1
vendor/smb-server/src/ntstatus.rs
vendored
@@ -39,3 +39,4 @@ pub const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
|
||||
pub const STATUS_FILE_CLOSED: u32 = 0xC000_0128;
|
||||
pub const STATUS_INVALID_INFO_CLASS: u32 = 0xC000_0003;
|
||||
pub const STATUS_NO_EAS_ON_FILE: u32 = 0xC000_0052;
|
||||
pub const STATUS_LOCK_NOT_GRANTED: u32 = 0xC000_0054; // Phase 7: byte-range lock conflict
|
||||
|
||||
128
vendor/smb-server/src/oplock.rs
vendored
128
vendor/smb-server/src/oplock.rs
vendored
@@ -3,6 +3,8 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! Also includes LockManager for byte-range locking (MS-SMB2 §2.2.26).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -173,4 +175,130 @@ pub fn share_access_compatible(existing: u32, new: u32) -> bool {
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Byte-range Lock Manager (MS-SMB2 §2.2.26)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A byte-range lock entry (MS-SMB2 §2.2.26.1).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LockRange {
|
||||
pub offset: u64,
|
||||
pub length: u64,
|
||||
pub exclusive: bool, // FLAG_EXCLUSIVE_LOCK vs FLAG_SHARED_LOCK
|
||||
pub session_id: u64,
|
||||
pub tree_id: u32,
|
||||
}
|
||||
|
||||
/// Global byte-range lock manager (MS-SMB2 §3.3.1.9).
|
||||
pub struct LockManager {
|
||||
/// FileId → active locks on that file.
|
||||
file_locks: RwLock<HashMap<FileId, Vec<LockRange>>>,
|
||||
}
|
||||
|
||||
impl LockManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
file_locks: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a lock (MS-SMB2 §3.3.5.14).
|
||||
/// Returns Ok(()) if lock acquired, Err if conflict.
|
||||
pub async fn acquire(
|
||||
&self,
|
||||
file_id: &FileId,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
exclusive: bool,
|
||||
session_id: u64,
|
||||
tree_id: u32,
|
||||
) -> Result<(), String> {
|
||||
let mut file_locks = self.file_locks.write().await;
|
||||
|
||||
// Check for conflicts with existing locks
|
||||
if let Some(locks) = file_locks.get(file_id) {
|
||||
for lock in locks {
|
||||
// Check if ranges overlap
|
||||
if Self::ranges_overlap(offset, length, lock.offset, lock.length) {
|
||||
// If either is exclusive, conflict
|
||||
if exclusive || lock.exclusive {
|
||||
// Same session can upgrade lock
|
||||
if lock.session_id == session_id && lock.tree_id == tree_id {
|
||||
continue; // Allow same session to overlap
|
||||
}
|
||||
return Err("Lock conflict".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No conflict → add lock
|
||||
file_locks
|
||||
.entry(*file_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(LockRange {
|
||||
offset,
|
||||
length,
|
||||
exclusive,
|
||||
session_id,
|
||||
tree_id,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Release a lock (MS-SMB2 §3.3.5.14).
|
||||
pub async fn release(
|
||||
&self,
|
||||
file_id: &FileId,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
session_id: u64,
|
||||
tree_id: u32,
|
||||
) {
|
||||
let mut file_locks = self.file_locks.write().await;
|
||||
|
||||
if let Some(locks) = file_locks.get_mut(file_id) {
|
||||
locks.retain(|lock| {
|
||||
// Keep locks that don't match this release
|
||||
!(lock.offset == offset
|
||||
&& lock.length == length
|
||||
&& lock.session_id == session_id
|
||||
&& lock.tree_id == tree_id)
|
||||
});
|
||||
|
||||
if locks.is_empty() {
|
||||
file_locks.remove(file_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if two byte ranges overlap.
|
||||
fn ranges_overlap(offset1: u64, length1: u64, offset2: u64, length2: u64) -> bool {
|
||||
let end1 = offset1 + length1;
|
||||
let end2 = offset2 + length2;
|
||||
|
||||
// Overlap if one range starts before the other ends
|
||||
offset1 < end2 && offset2 < end1
|
||||
}
|
||||
|
||||
/// Get all locks for a file (for diagnostics).
|
||||
pub async fn get_locks(&self, file_id: &FileId) -> Vec<LockRange> {
|
||||
let file_locks = self.file_locks.read().await;
|
||||
file_locks.get(file_id).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Clear all locks for a file (when file is closed).
|
||||
pub async fn clear(&self, file_id: &FileId) {
|
||||
let mut file_locks = self.file_locks.write().await;
|
||||
file_locks.remove(file_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LockManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
3
vendor/smb-server/src/server.rs
vendored
3
vendor/smb-server/src/server.rs
vendored
@@ -198,6 +198,8 @@ pub struct ServerState {
|
||||
pub shutting_down: Arc<AtomicBool>,
|
||||
/// Global oplock state manager (Phase 2).
|
||||
pub oplock_manager: Arc<crate::oplock::OplockManager>,
|
||||
/// Global byte-range lock manager (Phase 7).
|
||||
pub lock_manager: Arc<crate::oplock::LockManager>,
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
@@ -211,6 +213,7 @@ impl ServerState {
|
||||
shutdown: Arc::new(Notify::new()),
|
||||
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||
oplock_manager: Arc::new(crate::oplock::OplockManager::new()),
|
||||
lock_manager: Arc::new(crate::oplock::LockManager::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user