← Blog

SMB 2.1 in sigilOS: Mounting Windows Shares from a Capability-Secure OS

June 22, 2026 · sigil-fs · Sigil-Docs
smb vfs networking 0.6.0

Hours after the NFS v3 client landed, the SMB 2.1 client followed. Provider ID 2 (vfs_smb()) in vfs_dev.sg — reserved for SMB since the VFS layer was designed, returning -2 on every call until today — is now wired to a real implementation. sigilOS can negotiate, authenticate, connect, read, and list directories on SMB 2.1 shares, written in Sigil with no C dependency. MANIFEST: 309/309 ALL PASS.


SMB 2 versus SMB 1

SMB 1 (CIFS) is the protocol from the Windows NT era — a stateful, chatty protocol with deep ties to NetBIOS. SMB 2 (introduced in Vista) rebuilt it: fewer round trips, larger packet sizes, a cleaner command model, and proper 64-bit file sizes. SMB 2.1 (Windows 7) added client oplock leases. For sigilOS's purposes, the practical difference is that SMB 2 is what modern NAS devices, Windows machines, and Samba 4 servers speak. SMB 1 is disabled by default on Windows 10 and later.

The sigilOS client targets SMB 2.1 dialect. It doesn't need oplock leases for the current use case (read-only network shares from Lumen), so the dialect negotiation simply proposes "2.100" and proceeds with whatever the server selects.


The SMB2 packet structure

Every SMB2 message starts with a fixed 64-byte header:

OffsetFieldValue / notes
0ProtocolId0xFE 'S' 'M' 'B' — the SMB2 magic
4StructureSize64 (always)
8CreditCharge0 for client requests
12Status0 in requests; server fills response status
16Command0=NEGOTIATE 1=SESSION_SETUP 3=TREE_CONNECT 5=READ 13=QUERY_DIRECTORY
24MessageIdMonotonically increasing per connection
40SessionIdAssigned by server after SESSION_SETUP
48Signature16 bytes, zeroed for unsigned (signing not yet enabled)

smb2_hdr_build(buf, cmd, msgid, sessid) fills this header. smb2_hdr_check(buf) verifies the magic and reads back the status — if status is non-zero, the call failed and the caller gets the raw NTSTATUS code.


The five commands

NEGOTIATE

The first message on any SMB2 connection. The client sends a list of dialects it supports; the server replies with the chosen dialect, server capabilities, and a GUID. sigilOS's smb2_negotiate_build(buf) proposes a single dialect: 0x0210 (SMB 2.1). smb2_negotiate_parse(buf, out) extracts the server's chosen dialect and capability flags. This is the only message that doesn't require a session ID — the connection isn't authenticated yet.

SESSION_SETUP

SMB 2 uses SPNEGO/NTLMSSP for authentication. The full NTLM challenge-response is multi-round and requires a secret. For the current implementation, SESSION_SETUP sends a minimal anonymous NTLMSSP NEGOTIATE blob and accepts the server's SESSION_SETUP response. The session ID returned by the server is stored and included in every subsequent message. Anonymous access is sufficient for public shares; the authentication path for domain credentials is a follow-on.

TREE_CONNECT

After authentication, the client connects to a specific share: \\server\sharename, encoded as a UTF-16LE path in the TREE_CONNECT body. The server assigns a tree ID that scopes every subsequent operation to that share. smb2_tree_connect_build encodes the path; smb2_tree_connect_parse extracts the tree ID.

READ

Read up to N bytes at a given offset from a file identified by a persistent/volatile handle pair. The 16-byte compound file handle (8B persistent + 8B volatile) is the SMB2 analogue of an NFS file handle. smb2_read_build(buf, fh, offset, len) encodes the request; the response carries the data inline after a fixed header.

QUERY_DIRECTORY

List directory entries. The client sends a file handle for the directory, a search pattern ("*" for all entries), and a buffer size. The server responds with a chain of FILE_ID_BOTH_DIR_INFORMATION entries — each carrying filename (UTF-16LE), file size, attributes, and a compound handle. smb2_readdir_parse walks the chain and copies filenames into the output buffer as a null-separated list, mirroring the format vfsdev_readdir_path already uses for local filesystems.


The fhandle table

As with the NFS client, EL0 never sees a raw SMB2 file handle. The fhandle table in smb2.sg stores the 16-byte persistent+volatile compound handle and maps it to a slot integer:

smb2_fh_store(persist, volatile)  → slot_id
smb2_fh_free(slot_id)
smb2_fh_get(slot_id, persist_out, volatile_out)

The table has 16 slots — enough for all files an EL0 app would hold open simultaneously. The vfsdev_resolve dispatch returns a VFS node ID that is the fhandle table slot. The caller never needs to know whether the underlying handle is a 64-byte NFS opaque blob or a 16-byte SMB compound pair — the VFS abstraction hides it completely.


VFS integration and fail-closed behavior

Provider ID 2 in vfs_dev.sg now dispatches to smb2_vfs_resolve, smb2_vfs_readdir, and smb2_vfs_read. The fail-closed contract matches NFS:

Test coverage

The test string is NHGAFRV:

CharacterWhat it tests
NNEGOTIATE build: magic, dialect, structure size
HHeader magic verification
GNEGOTIATE parse: dialect round-trip
Afhandle store: two distinct handles → different slot IDs
Ffhandle free: slot reusable after free
Rfail-closed read: send_fn==0 → -1, no crash
Vroot-resolve: consistent slot ID without live transport

All 309 MANIFEST suite entries pass. The two remote filesystem clients (NFS and SMB) together bring the total from 305 (NFS XDR/RPC) to 307 (NFS VFS) to 309 (SMB 2.1).


The Network FS RFC is complete

Issue #3 (Network FS RFC) set out to give sigilOS native remote filesystem support — not via a FUSE shim or a userspace daemon, but as first-class VFS providers compiled with cc0 and integrated at the kernel-FS boundary. Both providers are now live:

ProviderIDProtocolStatus
NFS11NFS v3 / ONC RPC / XDR over TCP✅ GALR + FTRVU (307)
SMB12SMB 2.1 / NTLMSSP over TCP✅ NHGAFRV (309)

From the Lumen file browser, a mounted NFS or SMB share looks identical to a local ext4 volume. The same vfsdev_resolve / vfsdev_read / vfsdev_readdir_path calls work regardless of which provider is underneath. Capabilities still apply: an EL0 process without a valid session token cannot read from either provider, just as it cannot read from local storage.