Skip to content

SMB ACL Fidelity

This is the interop reference for what the DittoFS SMB Security Descriptor (SD) path actually round-trips, derived from the code — not from the spec. It is the “documented interop matrix” required by #1228.

For the cross-protocol ACL model (why NFSv4 ACEs are the canonical form, the evaluation algorithm, mode sync), see Access Control. This document is narrower: it states, aspect by aspect, whether the SMB SD wire path is Works, Partial, or Unsupported, with the source of truth for each.

  • Works — round-trips faithfully through QUERY_INFO (read) → SET_INFO (write) → QUERY_INFO with no semantic loss.
  • Partial — round-trips with a documented caveat or under conditions.
  • Unsupported — dropped, stubbed, or not implemented; behavior noted.

Code paths referenced:

PathFunction(s)File
SD read (build)BuildSecurityDescriptor, buildDACL, buildSACL, buildEmptySACLinternal/adapter/smb/handlers/security.go
Read-side DACL synthesisSynthesizeWindowsDefault, SynthesizeFromModepkg/metadata/acl/synthesize.go
SD write (parse)ParseSecurityDescriptorWithOptions, parseDACL, parseACEs (shared DACL/SACL)internal/adapter/smb/handlers/security.go
SET_INFO SecuritysetSecurityInfo, checkSetInfoSecurityAccessinternal/adapter/smb/handlers/set_info.go
SID derivationUserSID, GroupSID, UIDFromSID, GIDFromSID, PrincipalToSID, SIDToPrincipalpkg/auth/sid/mapper.go
Generic-mask expansionExpandGenericMaskpkg/metadata/acl/generic.go
InheritanceComputeInheritedACL, PropagateACL, flag translationpkg/metadata/acl/inherit.go, pkg/metadata/acl/flags.go
StorageFileAttr.ACL / SetAttrs.ACLpkg/metadata/file_types.go

NFSv4 ACE mask bits and Windows ACCESS_MASK bits share identical positions by RFC 7530 design, so masks need no translation. Only the principal format differs (NFSv4 who@domain strings vs. binary SIDs) and the inheritance-flag bit for INHERITED_ACE (NFSv4 0x80 vs. Windows 0x10, translated in flags.go).

AspectStateNotes
Owner SID readWorksBuildSecurityDescriptor emits mapper.UserSID(file.UID). UID 0 → BUILTIN\Administrators (S-1-5-32-544); all other UIDs → domain SID S-1-5-21-{machine}-{uid*2+1000}.
Group SID readWorksmapper.GroupSID(file.GID)S-1-5-21-{machine}-{gid*2+1001}. The +1001 (vs +1000) offset guarantees UserSID(n) != GroupSID(n).
Owner SID writeWorks (round-trips local)Parsed only when OwnerSecurityInformation is requested AND UIDFromSID recognizes the SID (machine-domain, even RID offset ≥ 1000). Recognized → SetAttrs.UID. Unrecognized owner SID is silently ignored (UID unchanged).
Group SID writeWorks (round-trips local)GIDFromSID (odd RID offset); falls back to UIDFromSID if the group SID actually encodes a user. Unrecognized → ignored.
Owner/Group section gatingWorksWrite requires WRITE_OWNER on the open’s GrantedAccess (checkSetInfoSecurityAccess); both sections fold under one WRITE_OWNER gate per MS-DTYP §2.5.3.3.

Note: OWNER@/GROUP@ inside a DACL ACE resolve to the file’s current owner/group SID via principalToSID (UserSID/GroupSID) — NOT to CREATOR_OWNER/CREATOR_GROUP. The CREATOR placeholders (S-1-3-0/S-1-3-1) are distinct and only meaningful as inheritable placeholders (see Inheritance).

AspectStateNotes
ALLOW ACE (type 0x00)WorksaccessAllowedACETypeACE4_ACCESS_ALLOWED_ACE_TYPE, both directions.
DENY ACE (type 0x01)WorksaccessDeniedACETypeACE4_ACCESS_DENIED_ACE_TYPE. Stored and round-tripped; canonical ordering (deny before allow) is the ACL engine’s concern.
Access mask bitsWorksIdentical bit positions; written/parsed verbatim. Generic bits are expanded on write (see below).
ACE countPartialCapped at acl.MaxACECount (128). parseDACL rejects a DACL whose header AceCount exceeds the cap with an error → SET_INFO returns STATUS_INVALID_PARAMETER. DACL size cap is acl.MaxDACLSize (64 KiB).
Unknown ACE types on writePartialparseDACL skips ACE types it cannot map (anything other than ALLOW/DENY/AUDIT) — the ACE is dropped, the rest of the DACL still parses.
Empty DACL (0 ACEs)Worksfile.ACL != nil, len(ACEs)==0 emits a 0-ACE DACL (deny-all) on read; no default is synthesized.
nil ACL on readPartial (synthesized)file.ACL == nilSynthesizeWindowsDefault: ALLOW OWNER@ + ALLOW SYSTEM@, both FullControl, no inherit flags, Protected=true (SD control 0x9004). This is a display default; server-side access enforcement still uses POSIX mode bits for nil-ACL files. SynthesizeFromMode (POSIX-mode → Allow-only DACL) exists but is currently only exercised by tests.
FlagWireNFSv4StateNotes
OBJECT_INHERIT (OI)0x01ACE4_FILE_INHERIT_ACE 0x01WorksTranslated by flags.go.
CONTAINER_INHERIT (CI)0x02ACE4_DIRECTORY_INHERIT_ACE 0x02Works
NO_PROPAGATE (NP)0x04ACE4_NO_PROPAGATE_INHERIT_ACE 0x04WorksStops propagation at first child; honored in ComputeInheritedACL.
INHERIT_ONLY (IO)0x08ACE4_INHERIT_ONLY_ACE 0x08WorksIO ACEs skipped in evaluation, kept for children.
INHERITED_ACE0x10ACE4_INHERITED_ACE 0x80WorksBit position differs (0x10 ↔ 0x80) and is correctly remapped — a naive truncation would corrupt this.
Inheritance computation at createWorksWorksComputeInheritedACL mirrors Samba create_descriptor.c; CREATOR_OWNER/CREATOR_GROUP (S-1-3-0/S-1-3-1) placeholders are substituted with the creator’s frozen identity at create time.
Recursive propagation on parent changeWorksWorksPropagateACL recomputes inherited ACEs while preserving explicit ones and child per-SD flags.
FlagStateNotes
SE_SELF_RELATIVE (0x8000)WorksAlways set; all SDs are self-relative.
SE_DACL_PRESENT (0x0004)WorksSet when DACLSecurityInformation requested.
SE_DACL_PROTECTED (0x1000)WorksRead from ACL.Protected; written from inbound Control. Blocks inheritance from ancestors; never itself inherited onto children.
SE_DACL_AUTO_INHERITED (0x0400)Works (canonicalized)Round-trips via ACL.AutoInherited. Default canonicalization (MS-DTYP §2.5.3.4.2, mirrors Samba canonicalize_inheritance_bits): persisted only when SET_INFO carries BOTH AUTO_INHERITED and AUTO_INHERIT_REQ (0x0100). Per-share opt-out (acl flag inherited canonicalization = no) preserves it verbatim.
SE_DACL_AUTO_INHERIT_REQ (0x0100)Works (request-only)Processed as a request flag; never echoed back on read.
SE_SACL_PRESENT (0x0010)WorksSet when SACLSecurityInformation requested; the SACL body carries stored audit ACEs (or a valid 0-ACE SACL when none are stored) — see SACL below.
AspectStateNotes
ReadWorksACL.NullDACLSE_DACL_PRESENT set with daclOffset == 0 (no DACL body) = everyone-full-access semantics.
WriteWorksSD with SE_DACL_PRESENT and zero DACL offset → &acl.ACL{NullDACL: true}. Also produced when DACLSecurityInformation is requested but the SD carries no DACL (setSecurityInfo).
AspectStateNotes
SACL readWorksWhen SACLSecurityInformation is requested and the file carries stored SACL ACEs, buildSACL serializes them into the SD’s SACL section (same MS-DTYP §2.4.5 + §2.4.4.2 wire layout as a DACL). Windows Explorer’s “Auditing” tab shows the stored audit ACEs. buildEmptySACL is the fallback only when no SACL is stored (valid 0-ACE SACL).
SACL writeWorksParseSecurityDescriptorWithOptions parses the SACL body via the shared parseACEs and persists audit ACEs into ACL.SACL; setSecurityInfo installs it via mergeSecurityACL, preserving the unrequested DACL/SACL section. Access-gated (ACCESS_SYSTEM_SECURITY required).
AUDIT / ALARM ACE round-tripWorksACE4_SYSTEM_AUDIT_ACE_TYPEsystemAuditACEType and ACE4_SYSTEM_ALARM_ACE_TYPEsystemAlarmACEType map both directions (nfsToWindowsACEType / windowsToNFSACEType); the SUCCESSFUL/FAILED audit flags round-trip through NFSv4FlagsToWindowsFlags. Audit ACEs ride the SACL section, not the DACL.

The SACL is stored on the same acl.ACL carrier as the DACL (ACL.SACL slice) and never participates in access checks. The store-layer round-trip shipped under #1228; the SMB wire parse/serialize under #1381.

AspectStateNotes
GENERIC_READ / WRITE / EXECUTE / ALL on writeWorksparseDACL calls ExpandGenericMask on each ACE before storing, per MS-DTYP §2.5.3 / MS-FSA §2.1.5.1.2.1. Generic bits are expanded to file-object-specific rights and the generic bits stripped (e.g. GENERIC_ALL 0x10000000FILE_ALL_ACCESS 0x001F01FF).
Generic bits at inherit timeWorksComputeInheritedACL expands generic bits on effective (non-INHERIT_ONLY) ACEs against the child object type; INHERIT_ONLY placeholders keep generic bits for the eventual leaf.
Generic bits on readn/aStored masks are already specific (expanded on write), so read emits specific rights.
AspectStateNotes
Foreign domain SID → local UID/GID (write)Unsupported / out of scopeA SID outside the joined domain is not decodable to a local UID/GID. As an owner/group it is ignored; inside a DACL ACE it round-trips as the sid:<canonical> principal form (preserved verbatim by SIDToPrincipal/PrincipalToSID), so the ACE is kept but the SID maps to no local UID/GID. AD integration shipped under #1231 (Kerberos/NTLM logon, NETLOGON machine credential, idmap_rid for the joined domain); mapping a foreign (non-joined) domain SID to a local UID/GID remains out of scope. Foreign-SID → name display does resolve — see below.
Named NFS principal → SID (e.g. alice@EXAMPLE.COM)Partial (lossy)PrincipalToSID synthesizes a hash-based fallback SID (full 32-bit hash as a 6th sub-authority) so it is NOT decodable back to a numeric UID and round-trips through sid:. Deterministic but not a real AD SID.
SID → name resolution (display)WorksLSA \pipe\lsarpc is implemented (internal/adapter/smb/rpc/lsarpc.go, all 7 ops incl. LsarLookupSids2/3, LsarOpenPolicy3). Windows Explorer’s Security tab resolves machine-domain user/group SIDs and well-known SIDs to names; foreign AD-domain SIDs resolve via the foreign resolver (pkg/adapter/smb/lsarpc_foreign.go). Shipped under #236 / #1341.
ScenarioBehavior
Owner/Group SID not recognized by UIDFromSID/GIDFromSIDSection silently ignored; UID/GID unchanged. No error returned.
DACL ACE SID not a local domain SIDSIDToPrincipal returns sid:<canonical>; ACE stored with that Who. Preserved on subsequent reads via PrincipalToSID’s sid: round-trip.
Well-known SIDs (Everyone S-1-1-0, SYSTEM S-1-5-18, BUILTIN\Administrators S-1-5-32-544, OWNER_RIGHTS S-1-3-4, CREATOR_OWNER S-1-3-0, CREATOR_GROUP S-1-3-1)Mapped to/from their NFSv4 special principals (EVERYONE@, SYSTEM@, ADMINISTRATORS@, OwnerRights@, CreatorOwner@, CreatorGroup@). Works.
DACL AceCount > 128 or DACL > 64 KiBparseDACL errors → SET_INFO returns STATUS_INVALID_PARAMETER.

checkSetInfoSecurityAccess authorizes each requested section against the open’s GrantedAccess (captured at CREATE, per MS-SMB2 §3.3.5.21.3), not the file’s current DACL:

Requested sectionRequired right
DACL (0x04)WRITE_DAC
OWNER / GROUP (0x01 / 0x02)WRITE_OWNER
SACL (0x08)ACCESS_SYSTEM_SECURITY

Any requested section lacking its bit denies the whole request with STATUS_ACCESS_DENIED.

Behaviors confirmed against real clients (per repo history, CLAUDE.md, and project memory):

  • Windows 11 Explorer — Security tab. SD read/build path verified through the Phase 31/32 Windows integration work: explicit-Deny synthesis was removed in favor of Allow-only DACLs (Samba convention) so Explorer no longer shows spurious “Write: Deny”; CREATE-context (MxAc) parsing and SET_INFO attribute persistence (Hidden/ReadOnly) were fixed. SID-to-name resolution is implemented (LSA lsarpc pipe, #236), so Explorer resolves user/group and well-known SIDs to names; raw SIDs appear only for principals with no directory entry.
  • AD / Kerberos SMB. alice@REALM → uid 10001 verified live against an AD-DC over Kerberos-bound SMB (idmap_rid SID→UID), confirming the domain-SID derivation in mapper.go end-to-end for the local-domain case.
  • smbtorture smb2.acls. The SD build/parse, inheritance (INHERITANCE/INHERITFLAGS), GENERIC expansion, and AUTO_INHERITED canonicalization paths are exercised by the SMB conformance suite (see inline references in security.go, inherit.go, generic.go).
  • SACL round-trip. Audit-ACE build → parse → re-build is covered by security_sacl_test.go (TestBuildSD_SACL_RoundTrip): SE_SACL_PRESENT, the serialized ACE count, and the audit ACE’s type/flags/mask all survive a full SD round-trip.
  • macOS Finder. Not separately validated for the ACL Security path; treat as untested for ACL fidelity.
  1. SACL round-trips but does not drive audit-event generation. Audit/alarm ACEs are parsed, stored, and surfaced to the Windows “Auditing” tab (read/write/round-trip via ACL.SACL), and the ACCESS_SYSTEM_SECURITY gate is enforced. What is not implemented is acting on them: the server emits no audit log when an operation matches a SUCCESSFUL/FAILED audit ACE, and ALARM ACEs raise no alarm. The SACL is descriptive metadata only — it never participates in access checks or event emission.
  2. Foreign AD/LDAP SIDs are not mapped to local UID/GID. They round-trip as opaque sid:<canonical> principals (so the ACE survives) but resolve to no local UID/GID. AD interop shipped under #1231 (logon + idmap_rid for the joined domain); foreign-SID → local-UID idmap specifically stays out of scope. Foreign-SID → name display does resolve via lsarpc (see limitation 4).
  3. Hash-based fallback SIDs for named principals are lossy. Deterministic but not real AD SIDs; collisions are vanishingly rare but theoretically possible.
  4. LSA SID→name pipe is implemented (#236, internal/adapter/smb/rpc/lsarpc.go). Explorer resolves machine-domain and well-known SIDs — and foreign AD-domain SIDs via the foreign resolver (pkg/adapter/smb/lsarpc_foreign.go) — to names. Raw SIDs appear only for principals with no directory entry.
  5. nil-ACL access enforcement uses POSIX mode, not the synthesized DACL. The SynthesizeWindowsDefault DACL shown to SMB clients for nil-ACL files is display-only; server-side access checks fall back to Unix mode bits, which stay authoritative.
  6. Owner/Group live in FileAttr, not in the ACL. Changing owner/group does not emit ACL-change events (see Access Control).
  • #1228 — Windows-ACL / stable-handle fidelity (this matrix).
  • #1231 — AD/LDAP enterprise integration (shipped).
  • #236 — SID-to-name (LSA) resolution (shipped).
  • Access Control — cross-protocol ACL model and tradeoff analysis.
  • MS-DTYP §2.4.4–2.4.6 — SID / ACE / ACL / Security Descriptor formats.
  • RFC 7530 §6 — NFSv4 ACL model.