Finding an out-of-bounds read in ksmbd by asking “why does the sibling check this?”
This is a writeup of a small but real memory-safety bug I found and fixed in ksmbd, the in-kernel SMB server that ships with the Linux kernel. The bug is an out-of-bounds read in smb_check_perm_dacl() in fs/smb/server/smbacl.c, reachable over the network by an unprivileged SMB client. The fix has been applied by the maintainer to ksmbd-for-next-next and carries a Cc: stable tag, so it is queued to flow back into stable kernels.
I want to walk through it the way I actually found it, because the method — auditing a function against its own siblings — is more reusable than the specific bug.
1. The target: ksmbd and the DACL parsing path
ksmbd is the kernel-mode SMB3 server. Because it lives in the kernel and parses untrusted data straight off the network, its attack surface is unusually sensitive: a bug that would be a crash in a userspace daemon is a kernel-mode memory-safety issue here, and the data driving it comes from a remote, potentially unauthenticated client.
The specific code I was looking at handles Windows security descriptors — specifically the DACL (Discretionary Access Control List). When a client sends a security descriptor, ksmbd walks the ACL, and for each ACE (Access Control Entry) it parses an SID (Security Identifier). An SID carries a num_subauth field that says how many 32-bit sub-authority values follow it. That count is attacker-controlled, and the structure it describes is variable-length — which is exactly the shape of bug I look for: a length field from the wire that drives how far the parser reads.
2. How I found it: sibling-pattern mining
I wasn't fuzzing. I was reading smbacl.c function by function, and I noticed that the SID-parsing logic appears in more than one place. ksmbd has a few functions that each walk ACEs and dereference SIDs:
parse_sid()sid_to_id()/ the owner+group parsing pathssmb_check_perm_dacl()
The sibling functions guarded the SID before trusting it. Concretely, they validated that the SID's num_subauth wouldn't push the structure past the end of the buffer described by ace_size / the ACL bounds before reading the sub-authority array. The pattern looked like a defensive check tying num_subauth to the remaining buffer size.
In smb_check_perm_dacl(), that guard was missing. The loop advanced through ACEs and accessed the SID's sub-authorities without first confirming the ACE was large enough to actually contain num_subauth sub-authorities. So the structural invariant that every sibling enforced — "don't read sub-authorities you haven't bounds-checked" — simply wasn't enforced on this path.
This is the highest-yield audit heuristic I know: when the same dangerous parse appears in several functions, diff them against each other. The bug usually isn't "nobody thought of the check." It's "somebody thought of it everywhere except here."
3. Root cause: a missing num_subauth / ace_size bounds check
Stripped down, the dangerous shape is this. For each ACE, ksmbd reads the SID and then iterates over its sub-authorities:
/* simplified for illustration */
for (i = 0; i < dacl->num_aces; i++) {
struct smb_ace *ace = ...; /* points into the wire buffer */
struct smb_sid *sid = &ace->sid;
/* num_subauth comes straight from the client */
int n = sid->num_subauth;
/* sub-authorities are read here... */
for (j = 0; j < n; j++)
access |= le32_to_cpu(sid->sub_auth[j]); /* <-- OOB read if ace is too small */
}
The size of an SID is variable: a base header plus num_subauth * sizeof(__le32) of sub-authority data. If a malicious client sets num_subauth large but sends an ACE (and an ACL) that is too short to actually hold that many sub-authorities, the inner loop walks sub_auth[j] past the end of the received buffer. That's the out-of-bounds read.
The sibling functions avoided this by validating, before the inner loop, that the declared num_subauth was consistent with the ACE's ace_size and the bytes actually remaining in the ACL. The fix is to add that same guard here.
4. Confirming it under AddressSanitizer
A code-reading hypothesis isn't a bug until you make the machine agree with you. I built a kernel with KASAN (the kernel AddressSanitizer) enabled and drove ksmbd with a crafted security descriptor whose ACE declared a large num_subauth but whose buffer was too small to back it.
KASAN reported a slab-out-of-bounds read originating in smb_check_perm_dacl(), in the sub-authority access — exactly the line the static read predicted. Having the sanitizer name the function and the access type turns "this looks wrong" into "here is the controlled read, on this line, from this input." That report is what made the patch worth sending.
5. The fix
The fix is deliberately boring: bring smb_check_perm_dacl() in line with its siblings by bounds-checking the SID before reading its sub-authorities. In effect, before trusting num_subauth, confirm the ACE is large enough to contain that many sub-authority entries (and that we stay within the ACL); otherwise stop processing rather than read past the buffer.
Boring is the point. The bug existed because one path diverged from a safe pattern; the fix is to restore the invariant, not to invent a new one. Patches that look exactly like the surrounding code are easy for a maintainer to reason about and to backport.
6. Disclosure outcome
I posted the patch to the linux-cifs list on 2026-06-02:
- Subject: [PATCH] ksmbd: fix out-of-bounds read in smb_check_perm_dacl()
Fixes: d07b26f39246— pointing at the commit that introduced the pathCc: stable@vger.kernel.org— so it flows into stable kernels
The ksmbd maintainer, Namjae Jeon, replied that he applied it to ksmbd-for-next-next. The predecessor commit this fix targets already has a CVE assigned (CVE-2026-31712) for the same area, so this region is established as security-relevant. I'd describe my fix as a strong CVE candidate once it reaches mainline/stable — but I'm not claiming a CVE yet, because that's not mine to declare and it hasn't landed in mainline at the time of writing.
Patch thread on lore: lore.kernel.org/linux-cifs/...
7. Takeaway: audit against siblings, prove with a sanitizer
Two things carried this from a hunch to an applied kernel patch:
- Sibling-pattern mining. When a dangerous parse is duplicated across functions, the missing-check bug tends to hide in the one copy that drifted. Diffing a function against its own siblings — and against git history — surfaces these cheaply, without fuzzing infrastructure.
- Sanitizer-backed confirmation. KASAN/ASan converts a suspicious read into a named, reproducible out-of-bounds access. That evidence is what makes a patch credible to a maintainer and what tells you you've found a real bug rather than a misread.
None of this required novel tooling. It required reading the code carefully, noticing where one path failed to do what its neighbors all did, and then making the machine confirm it. For parsers that sit in the kernel and read untrusted bytes off the network, that's often enough.