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:
| Offset | Field | Value / notes |
|---|---|---|
| 0 | ProtocolId | 0xFE 'S' 'M' 'B' — the SMB2 magic |
| 4 | StructureSize | 64 (always) |
| 8 | CreditCharge | 0 for client requests |
| 12 | Status | 0 in requests; server fills response status |
| 16 | Command | 0=NEGOTIATE 1=SESSION_SETUP 3=TREE_CONNECT 5=READ 13=QUERY_DIRECTORY |
| 24 | MessageId | Monotonically increasing per connection |
| 40 | SessionId | Assigned by server after SESSION_SETUP |
| 48 | Signature | 16 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:
- Transport not bound (
send_fn == 0) → returns -1. No data touched. - Root-path query → resolved without a live socket (same root-fh shortcut as NFS).
statfs→ stays -2. SMB 2 has no direct statfs analog; the QUERY_FS_INFO command is a follow-on.
Test coverage
The test string is NHGAFRV:
| Character | What it tests |
|---|---|
N | NEGOTIATE build: magic, dialect, structure size |
H | Header magic verification |
G | NEGOTIATE parse: dialect round-trip |
A | fhandle store: two distinct handles → different slot IDs |
F | fhandle free: slot reusable after free |
R | fail-closed read: send_fn==0 → -1, no crash |
V | root-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:
| Provider | ID | Protocol | Status |
|---|---|---|---|
| NFS | 11 | NFS v3 / ONC RPC / XDR over TCP | ✅ GALR + FTRVU (307) |
| SMB | 12 | SMB 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.