Implement SMB Byte-range Lock (Phase 7)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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:
Warren
2026-06-21 00:25:55 +08:00
parent 54ce0d6916
commit 276308af12
5 changed files with 186 additions and 5 deletions

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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

View File

@@ -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()
}
} }

View File

@@ -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()),
} }
} }