Skip to main content

rustsmb/
state.rs

1//! Server-side state: configuration, sessions, tree connects, open handles
2//! and directory-search contexts.
3//!
4//! Per the architecture, shared metadata is refcounted (`Arc`) so an
5//! [`IoContext`](crate::dispatch::IoContext) can carry it for the lifetime of
6//! one request without borrowing the connection lock.
7
8#![forbid(unsafe_code)]
9#![deny(missing_docs)]
10
11use std::collections::HashMap;
12use std::collections::HashSet;
13use std::path::PathBuf;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicU16, Ordering};
16
17/// Durable-handle flag marking a persistent handle ([MS-SMB2] §2.2.14.2.12).
18const DHANDLE_FLAG_PERSISTENT: u32 = 0x0000_0002;
19/// Share type for a hidden IPC$ pipe share: STYPE_IPC | STYPE_SPECIAL
20/// ([MS-SRVS] §2.2.2.4).
21const STYPE_IPC_SPECIAL: u32 = 0x8000_0003;
22
23use smb_server_vfs::OpenFile;
24use smb_server_vfs::Vfs;
25
26use smb_server_handle_store::HandleStore;
27
28/// Milliseconds since the Unix epoch, used for durable-handle deadlines.
29pub fn now_ms() -> u64 {
30    std::time::SystemTime::now()
31        .duration_since(std::time::UNIX_EPOCH)
32        .map(|d| d.as_millis() as u64)
33        .unwrap_or(0)
34}
35
36/// A published share backed by a VFS instance.
37#[derive(Clone)]
38pub struct Share {
39    /// Share name as clients request it (lowercased).
40    pub name: String,
41    /// Physical root directory of the share.
42    pub root: PathBuf,
43    /// Backend serving this share.
44    pub vfs: Arc<dyn Vfs>,
45    /// True for the virtual `IPC$` pipe share.
46    pub is_ipc: bool,
47    /// True when the share requires SMB3 encryption (SMB2_SHAREFLAG_ENCRYPT_DATA):
48    /// its TREE_CONNECT response advertises the flag and every message on the
49    /// tree is sealed ([MS-SMB2] §3.3.5.7).
50    pub encrypt: bool,
51    /// True when the share advertises SMB2_SHAREFLAG_COMPRESS_DATA in its
52    /// TREE_CONNECT response ([MS-SMB2] §2.2.10).
53    pub compress: bool,
54    /// True for a continuously-available share ([MS-SMB2] §2.2.10): its
55    /// TREE_CONNECT response advertises SMB2_SHAREFLAG_CONTINUOUSLY_AVAILABLE
56    /// and SMB2_SHARE_CAP_CONTINUOUS_AVAILABILITY, and it may grant persistent
57    /// handles ([MS-SMB2] §3.3.5.9.11).
58    pub ca: bool,
59}
60
61/// Global (per-process) server configuration and user database.
62#[derive(Clone)]
63pub struct ServerShared {
64    /// Published shares keyed by lowercase name.
65    pub shares: HashMap<String, Share>,
66    /// Server GUID used by extended-security negotiate.
67    pub guid: [u8; 16],
68    /// NT domain / workgroup name.
69    pub domain: String,
70    /// NetBIOS server name.
71    pub server_name: String,
72    /// Configured accounts: lowercased username → password.
73    pub users: HashMap<String, String>,
74    /// When no users are configured every principal maps to guest.
75    pub allow_guest: bool,
76    /// Reject traffic from authenticated sessions that does not carry a
77    /// valid signature ([MS-SMB2] §3.3.5.2.3 signing policy).
78    pub require_signing: bool,
79    /// Seal sessions when the client supports encryption
80    /// (SMB2_SESSION_FLAG_ENCRYPT_DATA, [MS-SMB2] §2.2.5.2).
81    pub encrypt: bool,
82    /// Server-wide byte-range lock table ([MS-SMB2] §2.2.26) shared across
83    /// connections so conflicting locks from different clients are detected.
84    pub locks: Arc<LockManager>,
85    /// Server-wide open share-mode table for sharing-violation detection
86    /// ([MS-FSA] §2.1.5.1).
87    pub share_modes: Arc<ShareModeTable>,
88    /// Server-wide oplock table ([MS-SMB2] §2.2.23) — at most one exclusive
89    /// oplock per file, broken when a second open contends.
90    pub oplocks: Arc<OplockTable>,
91    /// Server-wide lease table ([MS-SMB2] §2.2.23.2) — one caching lease per
92    /// file path, broken down when an open with a different lease key contends.
93    pub leases: Arc<LeaseTable>,
94    /// Durable handles preserved across connection drops ([MS-SMB2] §3.3.1.10),
95    /// behind a pluggable store (in-memory, redb, or a replicated backend).
96    pub durables: Arc<dyn HandleStore>,
97    /// Established sessions, shared so channels can bind ([MS-SMB2] §3.3.5.5.3).
98    pub sessions: Arc<SessionTable>,
99    /// Application-instance opens for client failover ([MS-SMB2] §3.3.5.9.13).
100    pub app_instances: Arc<AppInstanceTable>,
101}
102
103/// Identifies the open that owns a byte-range lock: `(session_id, file_id)`.
104pub type LockOwner = (u64, [u8; 16]);
105
106#[derive(Clone, Copy)]
107struct HeldLock {
108    offset: u64,
109    length: u64,
110    exclusive: bool,
111    owner: LockOwner,
112}
113
114/// Whether `[off, off+len)` overlaps the held lock's range.
115fn overlaps(h: &HeldLock, off: u64, len: u64) -> bool {
116    off < h.offset.saturating_add(h.length) && h.offset < off.saturating_add(len)
117}
118
119/// Server-wide byte-range lock manager ([MS-SMB2] §2.2.26 / [MS-FSA]
120/// §2.1.4.10). Locks are keyed by the file's on-disk path so opens from
121/// different connections contend correctly.
122pub struct LockManager {
123    held: std::sync::Mutex<HashMap<String, Vec<HeldLock>>>,
124    released: tokio::sync::Notify,
125}
126
127impl Default for LockManager {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl LockManager {
134    /// Create an empty lock manager.
135    pub fn new() -> Self {
136        Self {
137            held: std::sync::Mutex::new(HashMap::new()),
138            released: tokio::sync::Notify::new(),
139        }
140    }
141
142    /// A new lock on `[off, off+len)` conflicts with `existing` when the ranges
143    /// overlap and either lock is exclusive. An exclusive lock conflicts even
144    /// with the same open's overlapping lock ([MS-FSA] §2.1.5.9); two shared
145    /// locks never conflict.
146    fn conflicts(existing: &HeldLock, off: u64, len: u64, excl: bool) -> bool {
147        let overlap = off < existing.offset.saturating_add(existing.length)
148            && existing.offset < off.saturating_add(len);
149        overlap && (excl || existing.exclusive)
150    }
151
152    /// Atomically acquire every range, or acquire none. `ranges` are
153    /// `(offset, length, exclusive)`. Returns true on success.
154    pub fn try_acquire(&self, path: &str, ranges: &[(u64, u64, bool)], owner: LockOwner) -> bool {
155        let mut held = self.held.lock().unwrap();
156        let list = held.entry(path.to_string()).or_default();
157        let clash = ranges
158            .iter()
159            .any(|&(off, len, excl)| list.iter().any(|h| Self::conflicts(h, off, len, excl)));
160        if clash {
161            return false;
162        }
163        for &(off, len, excl) in ranges {
164            list.push(HeldLock {
165                offset: off,
166                length: len,
167                exclusive: excl,
168                owner,
169            });
170        }
171        true
172    }
173
174    /// A write to `[off, off+len)` is blocked when it overlaps any shared lock
175    /// (a read lock forbids writes by every open, [MS-FSA] §2.1.5.9) or an
176    /// exclusive lock held by a different open.
177    pub fn write_conflict(&self, path: &str, off: u64, len: u64, owner: LockOwner) -> bool {
178        let held = self.held.lock().unwrap();
179        let Some(list) = held.get(path) else {
180            return false;
181        };
182        list.iter()
183            .any(|h| overlaps(h, off, len) && (!h.exclusive || h.owner != owner))
184    }
185
186    /// A read overlaps a conflicting lock only when another open holds an
187    /// exclusive lock on the range.
188    pub fn read_conflict(&self, path: &str, off: u64, len: u64, owner: LockOwner) -> bool {
189        let held = self.held.lock().unwrap();
190        let Some(list) = held.get(path) else {
191            return false;
192        };
193        list.iter()
194            .any(|h| overlaps(h, off, len) && h.exclusive && h.owner != owner)
195    }
196
197    /// Release specific `(offset, length)` ranges held by `owner`.
198    pub fn release(&self, path: &str, ranges: &[(u64, u64)], owner: LockOwner) {
199        let mut held = self.held.lock().unwrap();
200        if let Some(list) = held.get_mut(path) {
201            for &(off, len) in ranges {
202                if let Some(pos) = list
203                    .iter()
204                    .position(|h| h.owner == owner && h.offset == off && h.length == len)
205                {
206                    list.swap_remove(pos);
207                }
208            }
209            if list.is_empty() {
210                held.remove(path);
211            }
212        }
213        drop(held);
214        self.released.notify_waiters();
215    }
216
217    /// Drop every lock held by `owner` (on handle close or session teardown).
218    pub fn release_owner(&self, owner: LockOwner) {
219        let mut held = self.held.lock().unwrap();
220        held.retain(|_, list| {
221            list.retain(|h| h.owner != owner);
222            !list.is_empty()
223        });
224        drop(held);
225        self.released.notify_waiters();
226    }
227
228    /// Drop every lock held by any open of `session_id` (logoff/disconnect).
229    pub fn release_session(&self, session_id: u64) {
230        let mut held = self.held.lock().unwrap();
231        held.retain(|_, list| {
232            list.retain(|h| h.owner.0 != session_id);
233            !list.is_empty()
234        });
235        drop(held);
236        self.released.notify_waiters();
237    }
238
239    /// Notified when any lock is released, so blocked waiters can retry.
240    pub fn released(&self) -> &tokio::sync::Notify {
241        &self.released
242    }
243}
244
245/// One recorded open on a file: its granted access and the sharing it permits.
246#[derive(Clone, Copy)]
247struct OpenMode {
248    access: u32,
249    share: u32,
250    owner: LockOwner,
251}
252
253/// Server-wide table of open share modes, keyed by on-disk path, used to
254/// detect sharing violations ([MS-FSA] §2.1.5.1) across all connections.
255#[derive(Default)]
256pub struct ShareModeTable {
257    opens: std::sync::Mutex<HashMap<String, Vec<OpenMode>>>,
258}
259
260impl ShareModeTable {
261    /// Create an empty table.
262    pub fn new() -> Self {
263        Self {
264            opens: std::sync::Mutex::new(HashMap::new()),
265        }
266    }
267
268    /// Record a new open if it is compatible with every existing open on the
269    /// same file; returns false (registering nothing) on a sharing violation.
270    pub fn try_open(&self, path: &str, access: u32, share: u32, owner: LockOwner) -> bool {
271        let mut opens = self.opens.lock().unwrap();
272        let list = opens.entry(path.to_string()).or_default();
273        if list.iter().any(|e| !compatible(e, access, share)) {
274            if list.is_empty() {
275                opens.remove(path);
276            }
277            return false;
278        }
279        list.push(OpenMode {
280            access,
281            share,
282            owner,
283        });
284        true
285    }
286
287    /// Drop the open recorded for `owner` on `path`.
288    pub fn close(&self, path: &str, owner: LockOwner) {
289        let mut opens = self.opens.lock().unwrap();
290        if let Some(list) = opens.get_mut(path) {
291            if let Some(pos) = list.iter().position(|e| e.owner == owner) {
292                list.swap_remove(pos);
293            }
294            if list.is_empty() {
295                opens.remove(path);
296            }
297        }
298    }
299
300    /// Drop every open recorded for any handle of `session_id`.
301    pub fn close_session(&self, session_id: u64) {
302        let mut opens = self.opens.lock().unwrap();
303        opens.retain(|_, list| {
304            list.retain(|e| e.owner.0 != session_id);
305            !list.is_empty()
306        });
307    }
308
309    /// Number of opens currently recorded for `path`.
310    pub fn open_count(&self, path: &str) -> usize {
311        self.opens.lock().unwrap().get(path).map_or(0, |l| l.len())
312    }
313
314    /// True when any open exists on a strict descendant of `dir`, or on `dir`
315    /// itself by an open other than `exclude`. A directory rename fails while
316    /// such a subtree handle is held ([MS-FSA] §2.1.5.14.2).
317    pub fn has_open_under(&self, dir: &str, exclude: LockOwner) -> bool {
318        let prefix = format!("{dir}/");
319        let opens = self.opens.lock().unwrap();
320        opens.iter().any(|(path, list)| {
321            if path.starts_with(&prefix) {
322                !list.is_empty()
323            } else if path.as_str() == dir {
324                list.iter().any(|e| e.owner != exclude)
325            } else {
326                false
327            }
328        })
329    }
330}
331
332/// A recorded application-instance open ([MS-SMB2] §3.3.5.9.13).
333struct AppOpen {
334    app_instance_id: [u8; 16],
335    path: String,
336    client_guid: [u8; 16],
337    owner: LockOwner,
338    version: Option<(u64, u64)>,
339}
340
341/// Outcome of matching a CREATE's AppInstanceId against existing opens.
342pub enum AppInstanceMatch {
343    /// No matching prior open; proceed normally.
344    None,
345    /// A prior open was force-closed; drop its share-mode entry for `owner`.
346    ForceClose {
347        /// On-disk path of the force-closed open.
348        path: String,
349        /// Owner (session id, file id) of the force-closed open.
350        owner: LockOwner,
351    },
352    /// The prior open is newer or equal; the new CREATE must be rejected.
353    Reject,
354}
355
356/// Server-wide registry of application-instance opens: a later CREATE with the
357/// same AppInstanceId from a different client force-closes the prior open,
358/// enabling client failover ([MS-SMB2] §3.3.5.9.13).
359#[derive(Default)]
360pub struct AppInstanceTable {
361    opens: std::sync::Mutex<Vec<AppOpen>>,
362    forced: std::sync::Mutex<HashSet<LockOwner>>,
363}
364
365impl AppInstanceTable {
366    /// Create an empty table.
367    pub fn new() -> Self {
368        Self::default()
369    }
370
371    /// Resolve a CREATE carrying `id` against existing opens. A match from a
372    /// different client force-closes the prior open unless the 3.1.1 version
373    /// rules say the existing open is newer/equal (then Reject).
374    pub fn resolve(
375        &self,
376        id: [u8; 16],
377        path: &str,
378        client_guid: [u8; 16],
379        is_311: bool,
380        req_version: Option<(u64, u64)>,
381    ) -> AppInstanceMatch {
382        let mut opens = self.opens.lock().unwrap();
383        let Some(pos) = opens
384            .iter()
385            .position(|o| o.app_instance_id == id && o.path == path && o.client_guid != client_guid)
386        else {
387            return AppInstanceMatch::None;
388        };
389        if let Some((eh, el)) = opens[pos].version {
390            match req_version {
391                // Version-comparison rule: applies only when the reopening
392                // connection negotiated 3.1.1. The prior open wins on an equal
393                // or higher version.
394                Some((rh, rl)) if is_311 && (eh > rh || (eh == rh && el >= rl)) => {
395                    return AppInstanceMatch::Reject;
396                }
397                // No-version rule: the server implements 3.1.1, so a request
398                // that omits a version against a versioned open is rejected
399                // regardless of the reopening connection's dialect.
400                None => return AppInstanceMatch::Reject,
401                _ => {}
402            }
403        }
404        let victim = opens.remove(pos);
405        self.forced.lock().unwrap().insert(victim.owner);
406        AppInstanceMatch::ForceClose { path: victim.path, owner: victim.owner }
407    }
408
409    /// Record a new application-instance open.
410    pub fn register(
411        &self,
412        id: [u8; 16],
413        path: String,
414        client_guid: [u8; 16],
415        owner: LockOwner,
416        version: Option<(u64, u64)>,
417    ) {
418        self.opens.lock().unwrap().push(AppOpen {
419            app_instance_id: id,
420            path,
421            client_guid,
422            owner,
423            version,
424        });
425    }
426
427    /// Consume and report the force-closed flag for `owner`.
428    pub fn take_forced(&self, owner: LockOwner) -> bool {
429        self.forced.lock().unwrap().remove(&owner)
430    }
431
432    /// Drop the registry entry for a closed open.
433    pub fn close(&self, owner: LockOwner) {
434        self.opens.lock().unwrap().retain(|o| o.owner != owner);
435        self.forced.lock().unwrap().remove(&owner);
436    }
437
438    /// Drop every entry belonging to `session_id` (logoff/disconnect).
439    pub fn close_session(&self, session_id: u64) {
440        self.opens.lock().unwrap().retain(|o| o.owner.0 != session_id);
441        self.forced.lock().unwrap().retain(|o| o.0 != session_id);
442    }
443}
444
445/// Crypto material needed to sign/seal an unsolicited oplock break for a
446/// holder, snapshotted at grant time (mirrors the async completion path).
447#[derive(Clone)]
448pub struct BreakCrypto {
449    /// Negotiated dialect.
450    pub dialect: Option<u16>,
451    /// Signing key, if the session signs.
452    pub signing_key: Option<[u8; 16]>,
453    /// (c2s, s2c) cipher keys, if a cipher is negotiated.
454    pub enc_keys: Option<([u8; 32], [u8; 32])>,
455    /// Negotiated cipher id.
456    pub cipher: Option<u16>,
457    /// Session id for the transform header.
458    pub session_id: u64,
459    /// Seal the break notification.
460    pub encrypt: bool,
461    /// Sign the break notification.
462    pub signed: bool,
463}
464
465/// One lease-break notification: `(key, old_state, new_state, epoch,
466/// outbound, crypto)` for a holder that must be broken ([MS-SMB2] §3.3.4.7).
467pub type LeaseBreak = ([u8; 16], u32, u32, u16, tokio::sync::mpsc::Sender<Vec<u8>>, BreakCrypto);
468
469/// A granted exclusive oplock and the means to break it.
470pub struct OplockHolder {
471    /// Session that holds the oplock.
472    pub session_id: u64,
473    /// File id the oplock is bound to (named in the break notification).
474    pub file_id: [u8; 16],
475    /// Outbound queue of the holder's connection, to push the break.
476    pub outbound: tokio::sync::mpsc::Sender<Vec<u8>>,
477    /// Crypto material to protect the break notification.
478    pub crypto: BreakCrypto,
479}
480
481/// Server-wide oplock table: at most one exclusive oplock per file path.
482#[derive(Default)]
483pub struct OplockTable {
484    held: std::sync::Mutex<HashMap<String, OplockHolder>>,
485}
486
487impl OplockTable {
488    /// Create an empty table.
489    pub fn new() -> Self {
490        Self {
491            held: std::sync::Mutex::new(HashMap::new()),
492        }
493    }
494
495    /// Grant an exclusive oplock on `path` if none is held.
496    pub fn grant(&self, path: &str, holder: OplockHolder) -> bool {
497        let mut held = self.held.lock().unwrap();
498        if held.contains_key(path) {
499            return false;
500        }
501        held.insert(path.to_string(), holder);
502        true
503    }
504
505    /// Remove and return the current holder of `path`, if any.
506    pub fn take(&self, path: &str) -> Option<OplockHolder> {
507        self.held.lock().unwrap().remove(path)
508    }
509
510    /// Drop the oplock held by `owner` on `path` (on close), returning it.
511    pub fn release(&self, path: &str, owner: LockOwner) -> Option<OplockHolder> {
512        let mut held = self.held.lock().unwrap();
513        if held.get(path).map(|h| (h.session_id, h.file_id)) == Some(owner) {
514            held.remove(path)
515        } else {
516            None
517        }
518    }
519
520    /// Drop every oplock held by any open of `session_id`.
521    pub fn release_session(&self, session_id: u64) {
522        self.held
523            .lock()
524            .unwrap()
525            .retain(|_, h| h.session_id != session_id);
526    }
527}
528
529/// A granted caching lease and the means to break it ([MS-SMB2] §2.2.23.2).
530pub struct LeaseHolder {
531    /// Client-chosen lease key identifying the shared caching state.
532    pub key: [u8; 16],
533    /// Currently granted lease state (caching bits).
534    pub state: u32,
535    /// Lease epoch (v2), bumped on each downgrade.
536    pub epoch: u16,
537    /// True when the holder negotiated the v2 lease form.
538    pub v2: bool,
539    /// Session that holds the lease.
540    pub session_id: u64,
541    /// File id the lease was granted on (for per-open release).
542    pub file_id: [u8; 16],
543    /// Outbound queue of the holder's connection, to push the break.
544    pub outbound: tokio::sync::mpsc::Sender<Vec<u8>>,
545    /// Crypto material to protect the break notification.
546    pub crypto: BreakCrypto,
547    /// Client GUID that owns the lease, to validate a break acknowledgment.
548    pub client_guid: [u8; 16],
549    /// True while a break is outstanding and awaiting acknowledgment.
550    pub breaking: bool,
551    /// The lease state the holder is being broken to (ack must be a subset).
552    pub break_to: u32,
553}
554
555/// Why a lease-break acknowledgment was rejected ([MS-SMB2] §3.3.5.22.1).
556pub enum LeaseAckError {
557    /// No lease matches the key/client GUID.
558    NotFound,
559    /// The lease is not currently breaking.
560    NotBreaking,
561    /// The acknowledged state is not a subset of the break-to state.
562    StateNotAccepted,
563}
564
565/// Server-wide lease table: the set of caching-lease holders per file path.
566///
567/// Simplified relative to [MS-SMB2] §3.3.1.10 (leases are managed independently
568/// of oplocks), but READ and HANDLE caching are shareable, so multiple clients
569/// may hold a lease on the same path with distinct lease keys — notably several
570/// directory-lease holders that must each be broken on a conflicting access.
571#[derive(Default)]
572pub struct LeaseTable {
573    held: std::sync::Mutex<HashMap<String, Vec<LeaseHolder>>>,
574    /// One-shot signals delivered when a lease key's break is acknowledged, so
575    /// a conflicting open can wait for a HANDLE-caching break ([MS-SMB2] §3.3.1.4).
576    ack_waiters: std::sync::Mutex<HashMap<[u8; 16], Vec<tokio::sync::oneshot::Sender<()>>>>,
577}
578
579impl LeaseTable {
580    /// Create an empty table.
581    pub fn new() -> Self {
582        Self {
583            held: std::sync::Mutex::new(HashMap::new()),
584            ack_waiters: std::sync::Mutex::new(HashMap::new()),
585        }
586    }
587
588    /// Record a lease holder on `path`. Multiple clients may hold shareable
589    /// (READ / HANDLE) caching on the same path with distinct lease keys
590    /// ([MS-SMB2] §3.3.1.4); a duplicate of an existing key+open is ignored.
591    pub fn grant(&self, path: &str, holder: LeaseHolder) -> bool {
592        let mut held = self.held.lock().unwrap();
593        let holders = held.entry(path.to_string()).or_default();
594        if holders
595            .iter()
596            .any(|h| h.key == holder.key && h.file_id == holder.file_id)
597        {
598            return false;
599        }
600        holders.push(holder);
601        true
602    }
603
604    /// Snapshot a co-holder's state on `path` sharing lease key `key`, if any:
605    /// `(state, epoch, v2)`. A CREATE reusing an existing lease key shares that
606    /// caching state rather than establishing a new grant ([MS-SMB2]
607    /// §3.3.5.9.11).
608    pub fn peek_key(&self, path: &str, key: [u8; 16]) -> Option<(u32, u16, bool)> {
609        self.held
610            .lock()
611            .unwrap()
612            .get(path)?
613            .iter()
614            .find(|h| h.key == key)
615            .map(|h| (h.state, h.epoch, h.v2))
616    }
617
618    /// True when any lease is currently held on `path`.
619    pub fn has_holders(&self, path: &str) -> bool {
620        self.held
621            .lock()
622            .unwrap()
623            .get(path)
624            .is_some_and(|h| !h.is_empty())
625    }
626
627    /// Break every conflicting holder on `path` by clearing the `clear` caching
628    /// bits, skipping the caller's own open (`owner`) and any co-holder sharing
629    /// `req_key`. Returns one notification tuple `(key, old_state, new_state,
630    /// epoch, outbound, crypto)` per holder that must be broken ([MS-SMB2]
631    /// §3.3.4.7); READ / HANDLE caching is shareable, so multiple directory
632    /// holders may each require a break.
633    pub fn break_conflict(
634        &self,
635        path: &str,
636        owner: LockOwner,
637        req_key: Option<[u8; 16]>,
638        clear: u32,
639    ) -> Vec<LeaseBreak> {
640        let mut held = self.held.lock().unwrap();
641        let mut breaks = Vec::new();
642        let Some(holders) = held.get_mut(path) else {
643            return breaks;
644        };
645        for h in holders.iter_mut() {
646            if (h.session_id, h.file_id) == owner || req_key == Some(h.key) {
647                continue;
648            }
649            let new_state = h.state & !clear;
650            if new_state == h.state {
651                continue;
652            }
653            let old = h.state;
654            h.state = new_state;
655            h.epoch = h.epoch.wrapping_add(1);
656            h.breaking = true;
657            h.break_to = new_state;
658            breaks.push((
659                h.key,
660                old,
661                new_state,
662                h.epoch,
663                h.outbound.clone(),
664                h.crypto.clone(),
665            ));
666        }
667        breaks
668    }
669
670    /// Break every holder whose path is `dir` or a descendant of it, clearing
671    /// the `clear` caching bits and skipping the caller's own open (`owner`).
672    /// Renaming a directory invalidates the cached handles across its whole
673    /// subtree, so each such directory lease must be broken ([MS-SMB2]
674    /// §3.3.1.4). Returns one notification tuple per holder broken.
675    pub fn break_subtree(
676        &self,
677        dir: &str,
678        owner: LockOwner,
679        clear: u32,
680    ) -> Vec<LeaseBreak> {
681        let prefix = format!("{dir}/");
682        let mut held = self.held.lock().unwrap();
683        let mut breaks = Vec::new();
684        for (path, holders) in held.iter_mut() {
685            if path.as_str() != dir && !path.starts_with(&prefix) {
686                continue;
687            }
688            for h in holders.iter_mut() {
689                if (h.session_id, h.file_id) == owner {
690                    continue;
691                }
692                let new_state = h.state & !clear;
693                if new_state == h.state {
694                    continue;
695                }
696                let old = h.state;
697                h.state = new_state;
698                h.epoch = h.epoch.wrapping_add(1);
699                h.breaking = true;
700                h.break_to = new_state;
701                breaks.push((
702                    h.key,
703                    old,
704                    new_state,
705                    h.epoch,
706                    h.outbound.clone(),
707                    h.crypto.clone(),
708                ));
709            }
710        }
711        breaks
712    }
713
714    /// Record the state a holder settled on after acknowledging a break.
715    pub fn set_state(&self, key: [u8; 16], state: u32) {
716        let mut held = self.held.lock().unwrap();
717        if let Some(h) = held.values_mut().flatten().find(|h| h.key == key) {
718            h.state = state;
719        }
720    }
721
722    /// Validate and apply a client lease-break acknowledgment ([MS-SMB2]
723    /// §3.3.5.22.1): the lease must exist under this client GUID, be breaking,
724    /// and `acked` must be a subset of the break-to state.
725    pub fn acknowledge(
726        &self,
727        client_guid: [u8; 16],
728        key: [u8; 16],
729        acked: u32,
730    ) -> Result<u32, LeaseAckError> {
731        let mut held = self.held.lock().unwrap();
732        let h = held
733            .values_mut()
734            .flatten()
735            .find(|h| h.key == key && h.client_guid == client_guid)
736            .ok_or(LeaseAckError::NotFound)?;
737        if !h.breaking {
738            return Err(LeaseAckError::NotBreaking);
739        }
740        if acked & !h.break_to != 0 {
741            return Err(LeaseAckError::StateNotAccepted);
742        }
743        h.state = acked;
744        h.breaking = false;
745        Ok(acked)
746    }
747
748    /// Drop the lease held by `owner` on `path` (on close), returning it.
749    pub fn release(&self, path: &str, owner: LockOwner) -> Option<LeaseHolder> {
750        let mut held = self.held.lock().unwrap();
751        let holders = held.get_mut(path)?;
752        let idx = holders
753            .iter()
754            .position(|h| (h.session_id, h.file_id) == owner)?;
755        let removed = holders.remove(idx);
756        if holders.is_empty() {
757            held.remove(path);
758        }
759        Some(removed)
760    }
761
762    /// Register interest in a lease key's break acknowledgment; the returned
763    /// receiver fires when the holder acknowledges ([MS-SMB2] §3.3.1.4).
764    pub fn register_ack_wait(&self, key: [u8; 16]) -> tokio::sync::oneshot::Receiver<()> {
765        let (tx, rx) = tokio::sync::oneshot::channel();
766        self.ack_waiters.lock().unwrap().entry(key).or_default().push(tx);
767        rx
768    }
769
770    /// Wake any opens waiting on a lease key's break acknowledgment.
771    pub fn signal_ack(&self, key: [u8; 16]) {
772        if let Some(waiters) = self.ack_waiters.lock().unwrap().remove(&key) {
773            for w in waiters {
774                let _ = w.send(());
775            }
776        }
777    }
778
779    /// Drop every lease held by any open of `session_id`.
780    pub fn release_session(&self, session_id: u64) {
781        let mut held = self.held.lock().unwrap();
782        for holders in held.values_mut() {
783            holders.retain(|h| h.session_id != session_id);
784        }
785        held.retain(|_, holders| !holders.is_empty());
786    }
787}
788
789/// A durable file handle preserved across a connection drop so the client can
790/// reclaim it ([MS-SMB2] §3.3.1.10). Stored as `Send` metadata; on reconnect
791/// the file is re-opened (single-node model), so no live fd is held here.
792#[derive(Clone)]
793pub struct DurableEntry {
794    /// Persistent handle id the client reconnects with (the original FileId).
795    pub persistent_id: [u8; 16],
796    /// Client create-guid (v2 handles); zero for v1.
797    pub create_guid: [u8; 16],
798    /// Share-relative path to re-open on reconnect.
799    pub rel: String,
800    /// Whether the handle was a directory.
801    pub is_dir: bool,
802    /// Original desired-access mask, replayed on re-open.
803    pub access: u32,
804    /// Original create options, replayed on re-open.
805    pub options: u32,
806    /// Session that owned the handle.
807    pub session_id: u64,
808    /// User (SecurityContext) that owns the handle; a reconnect by a different
809    /// user is rejected with STATUS_ACCESS_DENIED ([MS-SMB2] §3.3.5.9.7).
810    pub owner_user: String,
811    /// Client GUID that opened the handle (validated on a lease reconnect).
812    pub client_guid: [u8; 16],
813    /// Lease key associated with the open, if it held a lease.
814    pub lease_key: Option<[u8; 16]>,
815    /// Granted lease state, recreated on a persistent resume ([MS-SMB2] §3.3.5.9.12).
816    pub lease_state: u32,
817    /// Persistent (survives server restart intent) vs. plain durable.
818    pub persistent: bool,
819    /// Requested handle timeout in milliseconds (0 = server default).
820    pub timeout: u32,
821    /// Absolute expiry after which the preserved handle may be evicted.
822    pub deadline: std::time::Instant,
823}
824
825impl DurableEntry {
826    /// Project into a store record, computing the absolute deadline from
827    /// `now_ms`. Persistent handles get deadline 0 (never swept).
828    pub fn into_record(self, now_ms: u64) -> smb_server_handle_store::HandleRecord {
829        smb_server_handle_store::HandleRecord {
830            create_guid: self.persistent_id,
831            path: self.rel,
832            share: String::new(),
833            session_id: self.session_id,
834            owner_user: self.owner_user,
835            access: self.access,
836            share_access: 0,
837            create_options: self.options,
838            is_dir: self.is_dir,
839            flags: if self.persistent {
840                DHANDLE_FLAG_PERSISTENT
841            } else {
842                0
843            },
844            match_guid: if self.create_guid == [0u8; 16] {
845                None
846            } else {
847                Some(self.create_guid)
848            },
849            owner_node: String::new(),
850            lease_key: self.lease_key,
851            lease_state: self.lease_state,
852            client_guid: self.client_guid,
853            timeout_ms: self.timeout as u64,
854            deadline_ms: if self.persistent {
855                0
856            } else {
857                now_ms + self.timeout as u64
858            },
859            delete_on_close: false,
860        }
861    }
862}
863
864/// Established-session crypto material, shared server-wide so a new connection
865/// can bind another channel to the session ([MS-SMB2] §3.3.5.5.3 multichannel).
866#[derive(Clone)]
867pub struct SessionEntry {
868    /// Exported NTLM session key.
869    pub session_key: Option<[u8; 16]>,
870    /// Dialect-specific signing key.
871    pub signing_key: Option<[u8; 16]>,
872    /// Negotiated dialect.
873    pub dialect: Option<u16>,
874    /// Authenticated user name.
875    pub user: String,
876    /// Whether the session mapped to guest.
877    pub guest: bool,
878    /// Negotiated cipher id.
879    pub cipher: Option<u16>,
880    /// (client-to-server, server-to-client) cipher keys.
881    pub enc_keys: Option<([u8; 32], [u8; 32])>,
882    /// Whether the session forces encryption.
883    pub encrypt_data: bool,
884}
885
886/// Server-wide table of established sessions, keyed by session id.
887#[derive(Default)]
888pub struct SessionTable {
889    sessions: std::sync::Mutex<HashMap<u64, SessionEntry>>,
890}
891
892impl SessionTable {
893    /// Create an empty table.
894    pub fn new() -> Self {
895        Self {
896            sessions: std::sync::Mutex::new(HashMap::new()),
897        }
898    }
899
900    /// Register or refresh an established session.
901    pub fn insert(&self, id: u64, entry: SessionEntry) {
902        self.sessions.lock().unwrap().insert(id, entry);
903    }
904
905    /// Look up a session's crypto material for channel binding.
906    pub fn get(&self, id: u64) -> Option<SessionEntry> {
907        self.sessions.lock().unwrap().get(&id).cloned()
908    }
909
910    /// Forget a session (on logoff).
911    pub fn remove(&self, id: u64) {
912        self.sessions.lock().unwrap().remove(&id);
913    }
914}
915
916/// Windows share-mode access bits ([MS-SMB2] §2.2.13).
917mod amask {
918    pub const FILE_READ_DATA: u32 = 0x0000_0001;
919    pub const FILE_WRITE_DATA: u32 = 0x0000_0002;
920    pub const FILE_APPEND_DATA: u32 = 0x0000_0004;
921    pub const FILE_EXECUTE: u32 = 0x0000_0020;
922    pub const DELETE: u32 = 0x0001_0000;
923    pub const GENERIC_ALL: u32 = 0x1000_0000;
924    pub const GENERIC_WRITE: u32 = 0x4000_0000;
925    pub const GENERIC_READ: u32 = 0x8000_0000;
926}
927
928/// Share flags ([MS-SMB2] §2.2.13).
929mod share {
930    pub const READ: u32 = 0x0000_0001;
931    pub const WRITE: u32 = 0x0000_0002;
932    pub const DELETE: u32 = 0x0000_0004;
933}
934
935fn wants_read(access: u32) -> bool {
936    access
937        & (amask::FILE_READ_DATA | amask::FILE_EXECUTE | amask::GENERIC_READ | amask::GENERIC_ALL)
938        != 0
939}
940fn wants_write(access: u32) -> bool {
941    access
942        & (amask::FILE_WRITE_DATA
943            | amask::FILE_APPEND_DATA
944            | amask::GENERIC_WRITE
945            | amask::GENERIC_ALL)
946        != 0
947}
948fn wants_delete(access: u32) -> bool {
949    access & (amask::DELETE | amask::GENERIC_ALL) != 0
950}
951
952/// A new `(access, share)` open is compatible with an `existing` open when each
953/// side permits the other's access through its share flags ([MS-FSA] §2.1.5.1).
954fn compatible(existing: &OpenMode, access: u32, share: u32) -> bool {
955    (!wants_read(access) || existing.share & share::READ != 0)
956        && (!wants_read(existing.access) || share & share::READ != 0)
957        && (!wants_write(access) || existing.share & share::WRITE != 0)
958        && (!wants_write(existing.access) || share & share::WRITE != 0)
959        && (!wants_delete(access) || existing.share & share::DELETE != 0)
960        && (!wants_delete(existing.access) || share & share::DELETE != 0)
961}
962
963impl ServerShared {
964    /// Share table as advertised through srvsvc NetShareEnum level 1
965    /// ([MS-SRVS] §2.2.4.26): disk shares carry STYPE_DISK, the virtual
966    /// IPC$ share carries STYPE_IPC|STYPE_SPECIAL.
967    pub fn share_infos(&self) -> Vec<crate::srvsvc::ShareInfo> {
968        let mut v: Vec<crate::srvsvc::ShareInfo> = self
969            .shares
970            .values()
971            .map(|s| crate::srvsvc::ShareInfo {
972                netname: s.name.clone(),
973                shi_type: if s.is_ipc { STYPE_IPC_SPECIAL } else { 0 },
974                remark: String::new(),
975            })
976            .collect();
977        v.sort_by(|a, b| a.netname.cmp(&b.netname));
978        v
979    }
980}
981
982/// An authenticated SMB session (one UID).
983#[derive(Debug, Clone)]
984pub struct Session {
985    /// Authenticated principal name (`nobody` for guests).
986    pub user: String,
987    /// True when mapped to guest rather than a configured account.
988    pub guest: bool,
989    /// Tree connects opened under this session.
990    pub trees: Vec<u16>,
991}
992
993/// Directory search continuation state keyed by search SID.
994pub struct SearchCtx {
995    /// Names still to be returned.
996    pub queue: std::vec::IntoIter<String>,
997    /// Information level requested at FIND_FIRST2 time.
998    pub level: u16,
999    /// Directory the search runs against.
1000    pub base_dir: PathBuf,
1001}
1002
1003/// Per-connection state machine.
1004pub struct ConnState {
1005    /// NTLM challenge generated per connection at negotiate.
1006    pub challenge: [u8; 8],
1007    /// NEGOTIATE completed.
1008    pub negotiated: bool,
1009    /// Current UID (0 until first session setup allocates one).
1010    pub uid: u16,
1011    /// Multi-leg authentication in progress.
1012    pub auth_pending: bool,
1013    /// Client upgraded from SMB1 multi-protocol negotiate to SMB2.
1014    pub upgraded_smb2: bool,
1015    /// Client initiated SPNEGO-wrapped NTLMSSP.
1016    pub spnego: bool,
1017    /// Session once authenticated (None during null-session setup).
1018    pub session: Option<Session>,
1019    /// TID → share-name map.
1020    pub trees: HashMap<u16, String>,
1021    /// FID → open handle table.
1022    pub handles: HashMap<u16, Box<OpenFile>>,
1023    /// Active directory searches keyed by SID.
1024    pub searches: HashMap<u16, SearchCtx>,
1025}
1026
1027impl ConnState {
1028    /// New connection state with the supplied per-connection challenge.
1029    pub fn new(challenge: [u8; 8]) -> Self {
1030        ConnState {
1031            challenge,
1032            negotiated: false,
1033            uid: 0,
1034            auth_pending: false,
1035            upgraded_smb2: false,
1036            spnego: false,
1037            session: None,
1038            trees: HashMap::new(),
1039            handles: HashMap::new(),
1040            searches: HashMap::new(),
1041        }
1042    }
1043}
1044
1045static UID: AtomicU16 = AtomicU16::new(0x0400);
1046static TID: AtomicU16 = AtomicU16::new(0x1000);
1047static FID: AtomicU16 = AtomicU16::new(0x2000);
1048static SID: AtomicU16 = AtomicU16::new(1);
1049
1050fn next_id(counter: &AtomicU16) -> u16 {
1051    loop {
1052        let v = counter.fetch_add(1, Ordering::Relaxed);
1053        if v != 0 && v != u16::MAX {
1054            return v;
1055        }
1056    }
1057}
1058
1059/// Allocate a fresh UID.
1060pub fn next_uid() -> u16 {
1061    next_id(&UID)
1062}
1063/// Allocate a fresh TID.
1064pub fn next_tid() -> u16 {
1065    next_id(&TID)
1066}
1067/// Allocate a fresh FID.
1068pub fn next_fid() -> u16 {
1069    let v = FID.fetch_add(1, Ordering::Relaxed);
1070    if v == u16::MAX {
1071        FID.fetch_add(1, Ordering::Relaxed)
1072    } else {
1073        v
1074    }
1075}
1076/// Allocate a fresh search SID.
1077pub fn next_sid() -> u16 {
1078    SID.fetch_add(1, Ordering::Relaxed)
1079}
1080
1081/// Convenience alias: refcounted shared server config.
1082pub type SharedServer = Arc<ServerShared>;
1083
1084#[cfg(test)]
1085mod lock_tests {
1086    use super::*;
1087
1088    fn owner(n: u8) -> LockOwner {
1089        (1, [n; 16])
1090    }
1091
1092    #[test]
1093    fn exclusive_lock_blocks_other_owner() {
1094        let m = LockManager::new();
1095        assert!(m.try_acquire("f", &[(0, 10, true)], owner(1)));
1096        // Different owner, overlapping exclusive → denied.
1097        assert!(!m.try_acquire("f", &[(5, 10, true)], owner(2)));
1098        // Non-overlapping → allowed.
1099        assert!(m.try_acquire("f", &[(100, 10, true)], owner(2)));
1100    }
1101
1102    #[test]
1103    fn shared_locks_coexist_but_block_exclusive() {
1104        let m = LockManager::new();
1105        assert!(m.try_acquire("f", &[(0, 10, false)], owner(1)));
1106        assert!(
1107            m.try_acquire("f", &[(0, 10, false)], owner(2)),
1108            "shared+shared ok"
1109        );
1110        assert!(
1111            !m.try_acquire("f", &[(0, 10, true)], owner(3)),
1112            "shared blocks exclusive"
1113        );
1114    }
1115
1116    #[test]
1117    fn exclusive_conflicts_even_for_same_owner() {
1118        let m = LockManager::new();
1119        assert!(m.try_acquire("f", &[(0, 10, true)], owner(1)));
1120        // An exclusive lock conflicts with an overlapping lock held by the same
1121        // open ([MS-FSA] §2.1.5.9); a non-overlapping range still succeeds.
1122        assert!(
1123            !m.try_acquire("f", &[(0, 10, true)], owner(1)),
1124            "overlapping exclusive conflicts"
1125        );
1126        assert!(
1127            m.try_acquire("f", &[(10, 10, true)], owner(1)),
1128            "adjacent range ok"
1129        );
1130    }
1131
1132    #[test]
1133    fn release_frees_the_range() {
1134        let m = LockManager::new();
1135        assert!(m.try_acquire("f", &[(0, 10, true)], owner(1)));
1136        assert!(!m.try_acquire("f", &[(0, 10, true)], owner(2)));
1137        m.release("f", &[(0, 10)], owner(1));
1138        assert!(
1139            m.try_acquire("f", &[(0, 10, true)], owner(2)),
1140            "range freed"
1141        );
1142    }
1143
1144    #[test]
1145    fn release_session_drops_all() {
1146        let m = LockManager::new();
1147        assert!(m.try_acquire("a", &[(0, 10, true)], (7, [1; 16])));
1148        assert!(m.try_acquire("b", &[(0, 10, true)], (7, [2; 16])));
1149        m.release_session(7);
1150        assert!(m.try_acquire("a", &[(0, 10, true)], (9, [3; 16])));
1151        assert!(m.try_acquire("b", &[(0, 10, true)], (9, [4; 16])));
1152    }
1153}
1154
1155#[cfg(test)]
1156mod share_mode_tests {
1157    use super::*;
1158
1159    const GENERIC_READ: u32 = 0x8000_0000;
1160    const GENERIC_WRITE: u32 = 0x4000_0000;
1161    const SHARE_READ: u32 = 0x1;
1162    const SHARE_WRITE: u32 = 0x2;
1163
1164    #[test]
1165    fn two_shared_readers_are_compatible() {
1166        let t = ShareModeTable::new();
1167        assert!(t.try_open("f", GENERIC_READ, SHARE_READ, (1, [1; 16])));
1168        assert!(t.try_open("f", GENERIC_READ, SHARE_READ, (1, [2; 16])));
1169    }
1170
1171    #[test]
1172    fn writer_without_share_write_blocks_second_writer() {
1173        let t = ShareModeTable::new();
1174        assert!(t.try_open("f", GENERIC_WRITE, SHARE_READ, (1, [1; 16])));
1175        // Second writer: first open did not permit FILE_SHARE_WRITE → violation.
1176        assert!(!t.try_open("f", GENERIC_WRITE, SHARE_READ | SHARE_WRITE, (1, [2; 16])));
1177    }
1178
1179    #[test]
1180    fn exclusive_open_blocks_everyone() {
1181        let t = ShareModeTable::new();
1182        assert!(
1183            t.try_open("f", GENERIC_READ, 0, (1, [1; 16])),
1184            "first open ok"
1185        );
1186        assert!(
1187            !t.try_open("f", GENERIC_READ, SHARE_READ, (1, [2; 16])),
1188            "no-share open blocks"
1189        );
1190    }
1191
1192    #[test]
1193    fn close_reopens_share() {
1194        let t = ShareModeTable::new();
1195        assert!(t.try_open("f", GENERIC_WRITE, 0, (1, [1; 16])));
1196        assert!(!t.try_open("f", GENERIC_READ, SHARE_READ, (1, [2; 16])));
1197        t.close("f", (1, [1; 16]));
1198        assert!(
1199            t.try_open("f", GENERIC_READ, SHARE_READ, (1, [2; 16])),
1200            "freed after close"
1201        );
1202    }
1203
1204    #[test]
1205    fn session_table_binds_and_forgets() {
1206        let t = SessionTable::new();
1207        assert!(t.get(7).is_none(), "unknown session");
1208        t.insert(
1209            7,
1210            SessionEntry {
1211                session_key: Some([9u8; 16]),
1212                signing_key: Some([8u8; 16]),
1213                dialect: Some(0x0311),
1214                user: "faraz".into(),
1215                guest: false,
1216                cipher: Some(2),
1217                enc_keys: None,
1218                encrypt_data: false,
1219            },
1220        );
1221        let e = t.get(7).expect("session present for binding");
1222        assert_eq!(
1223            e.signing_key,
1224            Some([8u8; 16]),
1225            "bound channel reuses signing key"
1226        );
1227        assert_eq!(e.dialect, Some(0x0311));
1228        t.remove(7);
1229        assert!(t.get(7).is_none(), "forgotten on logoff");
1230    }
1231}