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;
|
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.
|
// Stat before closing if needed.
|
||||||
let info_before_close = if want_attrs {
|
let info_before_close = if want_attrs {
|
||||||
if let Some(h) = handle.as_ref() {
|
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 std::sync::Arc;
|
||||||
|
|
||||||
use crate::proto::header::Smb2Header;
|
use crate::proto::header::Smb2Header;
|
||||||
use crate::proto::messages::LockResponse;
|
use crate::proto::messages::{LockElement, LockRequest, LockResponse};
|
||||||
|
|
||||||
use crate::conn::state::Connection;
|
use crate::conn::state::Connection;
|
||||||
use crate::dispatch::HandlerResponse;
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::lookup_session_tree;
|
||||||
|
use crate::ntstatus;
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
pub async fn handle(
|
pub async fn handle(
|
||||||
_server: &Arc<ServerState>,
|
server: &Arc<ServerState>,
|
||||||
_conn: &Arc<Connection>,
|
conn: &Arc<Connection>,
|
||||||
_hdr: &Smb2Header,
|
hdr: &Smb2Header,
|
||||||
_body: &[u8],
|
body: &[u8],
|
||||||
) -> HandlerResponse {
|
) -> 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();
|
let mut buf = Vec::new();
|
||||||
LockResponse::default().write_to(&mut buf).expect("encode");
|
LockResponse::default().write_to(&mut buf).expect("encode");
|
||||||
HandlerResponse::ok(buf)
|
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_FILE_CLOSED: u32 = 0xC000_0128;
|
||||||
pub const STATUS_INVALID_INFO_CLASS: u32 = 0xC000_0003;
|
pub const STATUS_INVALID_INFO_CLASS: u32 = 0xC000_0003;
|
||||||
pub const STATUS_NO_EAS_ON_FILE: u32 = 0xC000_0052;
|
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,
|
//! 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
|
//! reducing network round-trips. The server tracks all opens per file and
|
||||||
//! triggers OPLOCK_BREAK_NOTIFICATION when conflicting opens occur.
|
//! 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::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -173,4 +175,130 @@ pub fn share_access_compatible(existing: u32, new: u32) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
true
|
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>,
|
pub shutting_down: Arc<AtomicBool>,
|
||||||
/// Global oplock state manager (Phase 2).
|
/// Global oplock state manager (Phase 2).
|
||||||
pub oplock_manager: Arc<crate::oplock::OplockManager>,
|
pub oplock_manager: Arc<crate::oplock::OplockManager>,
|
||||||
|
/// Global byte-range lock manager (Phase 7).
|
||||||
|
pub lock_manager: Arc<crate::oplock::LockManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
@@ -211,6 +213,7 @@ impl ServerState {
|
|||||||
shutdown: Arc::new(Notify::new()),
|
shutdown: Arc::new(Notify::new()),
|
||||||
shutting_down: Arc::new(AtomicBool::new(false)),
|
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||||
oplock_manager: Arc::new(crate::oplock::OplockManager::new()),
|
oplock_manager: Arc::new(crate::oplock::OplockManager::new()),
|
||||||
|
lock_manager: Arc::new(crate::oplock::LockManager::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user