SMB 1.0 (CIFS) Message Code Structure
How to lay out and implement SMB 1.0 message code in the Manticore repo under network/smb/smb_v10. This is the SMB message layer — distinct from the DCE/RPC interface layer (see the dcerpc-interface-structure skill). DCE/RPC rides on SMB_COM_TRANSACTION/named-pipe reads, not on the NT_TRANSACT subcommands covered here, so the two skills do not overlap.
Everything here was derived from implementing the full CIFS command set, the MS-SMB server-response extensions, and the NT_TRANSACT subcommand payloads against the official MS-CIFS / MS-SMB / MS-FSCC specifications.
Core principle
Verify every wire layout against the official spec before writing code, and never guess a format the spec does not pin down.
- WebFetch the exact MS-CIFS / MS-SMB / MS-FSCC section for the command/structure and copy field names and byte sizes verbatim. The section is linked from the issue or found via the [MS-CIFS] SMB_COM command-code table.
- Cross-spec: a command in MS-CIFS often references a record defined in MS-FSCC (e.g.
FILE_NOTIFY_INFORMATION[MS-FSCC] 2.7.1,FILE_QUOTA_INFORMATION[MS-FSCC] 2.4.41) or a self-relativeSECURITY_DESCRIPTORin MS-DTYP 2.4.6. Fetch the referenced page too. - If the spec gives no marshallable format (obsolete/reserved commands; opaque blobs like a security descriptor), model it as a documented empty stub or opaque
[]byte— do not reconstruct a legacy[XOPEN-SMB]/[SMB-LM1X]layout from memory. - All multi-byte integers are little-endian.
Directory layout
network/smb/smb_v10/
message/
commands/ one file per SMB_COM_* command
<Command>Request.go e.g. NtTransactRequest.go, NtCreateAndxResponse.go
<Command>Response.go
0.command_casting.go the command-code → struct dispatcher (two switches)
command_interface/ the CommandInterface + embedded Command base
andx/ the AndX block (chained commands)
codes/codes.go SMB_COM_* CommandCode constants + name map
parameters/ the SMB parameter block (WordCount + Words)
data/ the SMB data block (ByteCount + Bytes)
header/, header/flags{,2}/ SMB header + flag bits
subcommands/ TRANSACTION / TRANSACTION2 / NT_TRANSACT subcommand
payload structs + enums (FSCTL codes, FILE_* records)
types/ SMB/MS-DTYP type aliases (UCHAR, USHORT, ULONG, …)
Key fact: types.UCHAR = uint8 is an alias, so []types.UCHAR is []byte — pass command Parameters/Data slices straight to functions taking []byte with no conversion.
A command Request/Response struct
Each command embeds command_interface.Command and implements Marshal/Unmarshal plus a New<Command>Request() constructor that sets the command code. The base Command provides GetParameters()/GetData() (the parameters.Parameters and data.Data blocks). Mirror an existing well-formed command (e.g. OpenResponse.go) rather than starting blank.
Marshal pattern:
- Lazily create Parameters/Data if nil.
- If AndX, fold the AndX words into the parameter block first (
for _, p := range c.GetAndX().GetParameters() { c.GetParameters().AddWord(p) }). - Build
rawParametersContent []bytefield-by-field (little-endian), thenc.GetParameters().AddWordsFromBytesStream(rawParametersContent). - Build
rawDataContent []byte, thenc.GetData().Add(rawDataContent). - Marshal parameters then data.
The parameters block enforces WordCount == len(Words) (parameters.Marshal errors otherwise, and AddWordsFromBytesStream sets WordCount = len(bytes)/2). You therefore cannot emit a WordCount that disagrees with the actual word bytes — see the extended-response quirk below.
Unmarshal pattern: unmarshal the Parameters and Data blocks, early-return on the all-empty case (it is an error response carrying only a header Status), then decode fields by offset. For AndX responses, skip the 4-octet AndX block (offset += 4) before the first real parameter.
Always add the command code to allCommandCodes in nil_unmarshal_sweep_test.go — a sweep test that calls Unmarshal on a fresh struct with 3 zero bytes and must not panic. (This is why Unmarshal must lazily init Parameters/Data.)
The command-casting dispatcher
0.command_casting.go has two switches, CreateRequestCommand and CreateResponseCommand, mapping a codes.CommandCode to New<Command>Request() / New<Command>Response(). Both end in a default that returns command code not supported: %d. A command is only reachable once it has a case in both switches. The two default blocks are textually identical — when scripting an insertion, split the file on the default block and insert before the first (request) and second (response) occurrence.
Reserved / obsolete command stubs
Many SMB_COM_* codes are declared in codes.go but were never implemented. Two MS-CIFS statuses:
- N — reserved, never defined (e.g.
SMB_COM_NEW_FILE_SIZE,SMB_COM_QUERY_SERVER). - X — obsolete, real in a legacy dialect but the format lives only in
[SMB-LM1X]/[XOPEN-SMB]/[SMB-CORE], which MS-CIFS does not reproduce (e.g.SMB_COM_COPY,SMB_COM_MOVE,SMB_COM_READ_MPX_SECONDARY).
For both N and X codes, the spec only says "servers SHOULD/MUST return STATUS_NOT_IMPLEMENTED" and gives no marshallable format, and the completeness issues ask only to "route the code to a defined handler instead of failing." So both get the same empty-framing stub, differing only in doc wording:
<Name>Request/<Name>Responseembeddingcommand_interface.Command, constructorNew<Name>Request()setting the code.Marshal/Unmarshalemit/consume only the emptyWordCount(1) +ByteCount(2) framing — i.e. marshalled output is exactly00 / 00 00.- Doc comment cites the MS-CIFS section and the SHOULD/MUST
STATUS_NOT_IMPLEMENTEDrule; for X codes note the legacy reference ([SMB-LM1X]/[XOPEN-SMB]/[SMB-CORE]) that MS-CIFS does not republish. - Wire into both dispatcher switches; add to the
allCommandCodessweep. - Test: dispatch resolves (not the
defaulterror) + the empty00 / 00 00framing round-trips.
SMB_COM_NO_ANDX_COMMAND (0xFF) terminates an AndX chain; SMB_COM_INVALID (0xFE) is not a real code.
AndX extended-response variants (MS-SMB)
Some AndX responses have a base CIFS form and a larger MS-SMB form selected by a request flag (NT_CREATE_ANDX extended response [MS-SMB] 2.2.4.9.2; TREE_CONNECT_ANDX extended response [MS-SMB] 2.2.4.7.2). Model the extension on the existing response struct:
- Add an
Extended boolselector field plus the extra fields (e.g.VolumeGUID [16]byte,FileId uint64,MaximalAccessRights types.ULONG). Marshalappends the extra fields after the base fields only whenExtended— the base layout stays byte-identical otherwise.Unmarshaldetects the extended form by the presence of the extra parameter octets after the base fields (e.g. "if ≥32 more parameter octets followDirectory, setExtendedand read them"), then decodes them.
WordCount quirk (NT_CREATE_ANDX extended): [MS-SMB] says the extended WordCount SHOULD be 0x2A (42), but the extended Words block is actually 50 words (the 4 extra fields add 32 octets to the 68-octet base). Because the parameters layer enforces WordCount == len(Words), the implementation emits the self-consistent 0x32 (50) and documents the spec's fixed 0x2A in a comment. Do not try to force 0x2A — it would desync framing. TREE_CONNECT_ANDX has no such quirk (3 → 7 words, consistent).
NT_TRANSACT subcommands
SMB_COM_NT_TRANSACT (0xA0) is a generic container ([MS-CIFS] 2.2.4.62.1). Its Words end with …SetupCount(1), Function(2), Setup[SetupCount] — Function (the subcommand code) is separate from the Setup words, and Setup is SetupCount two-byte words after Function. The SMB_Data is Pad1, NT_Trans_Parameters[ParameterCount], Pad2, NT_Trans_Data[DataCount].
Where each subcommand puts its arguments varies — always check the per-subcommand section:
| Subcommand | Function | Args carried in | Notes |
|---|---|---|---|
| NOTIFY_CHANGE | 0x0004 | Setup (CompletionFilter4+FID2+WatchTree1+Reserved1) | response: FILE_NOTIFY_INFORMATION list in NT_Trans_Parameters |
| QUERY/SET_SECURITY_DESC | 0x0006 / 0x0003 | NT_Trans_Parameters (FID2+Reserved2+SecurityInformation4) | descriptor = opaque NT_Trans_Data; query response param = LengthNeeded4 |
| QUERY_QUOTA | 0x0007 | NT_Trans_Parameters (16 octets) | SidList = NT_Trans_Data; response = DataLength4 + FILE_QUOTA_INFORMATION list |
| IOCTL (copychunk, snapshots, resume-key) | 0x0002 | Setup = FunctionCode4+FID2+IsFsctl1+IsFlags1 | FSCTL payload = NT_Trans_Data |
Subcommand payload structs live in subcommands/ (e.g. srv_copychunk.go, srv_snapshot_array.go, nt_transact_quota.go, nt_transact_notify_change.go, nt_transact_security_desc.go), each with Marshal/Unmarshal and SetupCount/ChunkCount/length fields derived from slice lengths, not stored redundantly. The FSCTL function codes (FSCTL_SRV_REQUEST_RESUME_KEY 0x00140078, FSCTL_SRV_COPYCHUNK 0x001440F2, FSCTL_SRV_ENUMERATE_SNAPSHOTS 0x00144064) live with their structs.
Wiring subcommand structs into NtTransactRequest/Response
NtTransactRequest carries Setup []types.USHORT (added so notify-change and IOCTL subcommands are representable; SetupCount is derived from len(Setup) on marshal). Builders + parsers live in message/commands/NtTransactSubcommandBuilders.go (the commands package may import subcommands — subcommands imports nothing from commands, so there is no cycle).
- Request builders return a populated
*NtTransactRequest: setFunction, theSetupwords (notify-change setup, orNewNtTransactIoctlSetup(fsctl, fid, true, 0)for the FSCTLs),NT_Trans_Parameters/NT_Trans_Datafrom the subcommand struct'sMarshal(), and the counts (TotalParameterCount/ParameterCount,TotalDataCount/DataCount,MaxParameterCount/MaxDataCount). - Response parsers are methods on
*NtTransactResponsethat readc.Parameters/c.Data(both[]types.UCHAR=[]byte) into the typed structs. - To pack a subcommand's little-endian setup byte block into
Setup []USHORT, read it back as LEuint16words.
Offset-sensitivity gotcha: a full NtTransactRequest Marshal→Unmarshal round-trip reconstructs Pad1/Pad2 from the header-relative ParameterOffset/DataOffset. Builders deliberately do not set those offsets — they are computed at message-assembly time once the header position is known. Consequence for tests: round-trip the setup-only case (notify-change) end-to-end, and validate the param/data builders by re-parsing the populated NT_Trans_Parameters/NT_Trans_Data fields rather than a full request round-trip. Large transfers spanning NT_TRANSACT_SECONDARY are also a higher-level concern, not the builder's.
Tests
- Golden bytes for fixed layouts: marshal a struct with known field values and assert the exact octet sequence (use real example bytes from the spec trace where available, e.g. the 24-byte copychunk resume key).
- Round-trip for variable/derived layouts: marshal → unmarshal → compare.
- Dispatch for new command codes:
CreateRequestCommand/CreateResponseCommandresolve without the generic error. - Add the code to the
nil_unmarshal_sweep_test.goallCommandCodeslist. - None of these obscure paths are live-validated against a server — say so in the PR; golden + round-trip tests are the acceptance gate.
Checklist when adding an SMB command or subcommand
- WebFetch the MS-CIFS/MS-SMB/MS-FSCC section; copy field names + byte sizes verbatim. Note the SHOULD/MUST status and whether a format is even defined.
- Decide the shape: full command struct, reserved/obsolete empty stub, AndX extended variant, or NT_TRANSACT subcommand payload + builder/parser.
- Implement
Marshal/Unmarshal(little-endian; derive counts from lengths; lazily init Parameters/Data). - For commands: wire both dispatcher switches + add to the
allCommandCodessweep. For subcommands: add builder(s) + response parser(s); ensureSetupis set for setup-carrying subcommands. - Tests: golden + round-trip (+ dispatch for commands).
gofmt,go build ./...,go vet,go test ./network/smb/smb_v10/.... - Per the Bug-Fixes-Commit-and-PR / Issue-Commit-and-PR skills: branch from
main, focused commit with aFixes #<n>closing keyword, PR via the template, no Claude attribution. Note in the PR that the path is not live-validated.
Source: TheManticoreProject/Manticore — distributed by TomeVault.