Post

A Use-After-Free in the Linux Kernel's ksmbd

A race in the Linux kernel's in-kernel SMB3 server frees a per-channel signing key while another connection is still reading it. This is the story of CVE-2026-23226, from the leaked key to the kernel panic, and how it is the exact same class of bug the ksmbd maintainers had already patched once.

A Use-After-Free in the Linux Kernel's ksmbd

CVE-2026-23226high 7.5

Here is the short version. The Linux kernel ships a built-in SMB file server called ksmbd. Inside it, two connections that belong to the same login can run at the same time. One of them can free a small piece of memory that holds a cryptographic signing key while the other is still using it. That is a use-after-free, and depending on timing it leaks the key, lets an attacker forge signed requests, or crashes the whole machine.

The part that makes this one worth writing down is that the maintainers had already fixed this exact pattern a few months earlier, in a different field of the same structure. They protected one list and left its neighbor wide open. This is CVE-2026-23226.

Before the kernel debugger output, let me build it up from zero.

What SMB and ksmbd actually are

SMB (Server Message Block) is the protocol behind Windows file sharing. When you open \\server\share and drag a file across, that is SMB. It also does printer sharing, authentication, and a lot of enterprise plumbing. The modern version is SMB3, which adds encryption and, importantly for this bug, two features called signing and multichannel.

For years the standard SMB server on Linux was Samba, which runs as a normal program in userspace. In 2021 the kernel gained a second option: ksmbd, an SMB3 server that runs inside the kernel itself. The pitch is performance. Serving files from kernel space skips a lot of copying back and forth between the kernel and a userspace program, so it is faster.

That speed comes with a bill, and the bill is the whole point of this post. A bug in Samba crashes or compromises one program. A bug in ksmbd is a bug in the kernel. There is no process boundary to contain it. A memory corruption here is not “the file server misbehaved,” it is “something is now scribbling on kernel memory,” which is the neighborhood where full machine takeover lives.

Two SMB3 features set the stage:

  • Signing stamps each request with a cryptographic signature (AES-128-CMAC) so the server can prove a message really came from the logged-in client and was not tampered with on the wire. The secret used for that stamp is a per-session, per-channel signing key.
  • Multichannel lets one login spread its traffic across several network connections at once, for throughput and failover. The client opens a second connection and “binds” it to the existing session instead of logging in again.

Hold onto that: one login, several connections, each carrying its own signing key, all running in parallel.

Use-after-free and races, in plain terms

Two concepts carry the whole bug.

A use-after-free is exactly what it sounds like. A program asks the system for a chunk of memory, uses it, then hands it back (“frees” it) when it is done. A use-after-free is when the program keeps a pointer to that chunk and reads or writes it after handing it back. By then the memory may have been handed to something else entirely, so you are reading another object’s data, or corrupting it. In the kernel, that other object might be anything, which is why these bugs are dangerous.

A race condition is when two things happen at the same time and the result depends on which one wins. If one thread is freeing a chunk of memory at the same moment another thread is reading it, you have a race, and sometimes the read lands after the free. That is how a use-after-free gets triggered on purpose: you line up a “free” on one thread and a “read” on another and run them until the timing lines up.

The tool that catches these is KASAN (Kernel Address Sanitizer), a debug build of the kernel that pokes a shadow byte next to every allocation and screams the moment freed memory is touched. KASAN is how this bug was confirmed, not how it was exploited.

The object in the middle

Every SMB session in ksmbd is a struct ksmbd_session. For multichannel, that session keeps a list of its active connections, called channels. Each channel is a small struct channel:

1
2
3
4
struct channel {
    __u8            smb3signingkey[SMB3_SIGN_KEY_SIZE]; // 16-byte AES-128-CMAC key
    struct ksmbd_conn *conn;                            // pointer to the connection
};

It is 24 bytes, so the kernel’s slab allocator hands it out of the kmalloc-32 cache. The first 16 bytes are the signing key. The pointer to the connection sits right after it, at offset 16. Remember both: whatever ends up reusing this freed slot decides whether the attacker reads a key or dereferences garbage.

The session stores these channels in an XArray, which is just the kernel’s resizable array keyed by an integer. The session looks them up with lookup_chann_list() and removes them with ksmbd_chann_del().

Here is the lookup, as it stood:

1
2
3
4
5
static struct channel *lookup_chann_list(struct ksmbd_session *sess,
                                         struct ksmbd_conn *conn)
{
    return xa_load(&sess->ksmbd_chann_list, (long)conn);
}

A bare xa_load(). No lock is taken. It reads from the array and hands back the pointer, trusting that nobody is tearing that entry down at the same instant.

And here is the removal path, which is the other half of the race:

1
2
3
4
5
6
7
8
9
10
void ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess)
{
    struct channel *chann;

    chann = xa_erase(&sess->ksmbd_chann_list, (long)conn);
    if (!chann)
        return;

    kfree(chann);   // freed immediately
}

It erases the entry from the array and immediately frees the struct channel. No lock around it, and no RCU grace period, which is the kernel’s usual way of saying “wait until every reader that might still hold this pointer has finished before freeing.” The free happens right now.

Why the two paths collide

In normal operation you would think a session is torn down cleanly. It is not, and multichannel is why.

When a connection goes away, ksmbd calls ksmbd_conn_wait_idle() to let in-flight work finish before cleanup. The catch is that ksmbd_conn_wait_idle() only drains the work queue of that one connection. In a multichannel session, the other connections keep running. They are still pulling SMB requests off the wire, and signed requests call lookup_chann_list() to fetch the channel so they can check the signature against its key.

So the sequence that kills it is:

  1. Connection A is torn down. ksmbd_chann_del() erases A’s channel from the array and kfree()s it.
  2. Connection B, in the same session, is mid-request on another CPU. It calls lookup_chann_list(), which had already loaded a pointer to a channel, and reaches into it to read the signing key.
  3. If B’s read lands after A’s free, B is reading freed memory.

In the words of the write-up I did at Orca: the teardown of one connection races freely against the ongoing work of another connection in the same session. Nothing serializes them, because the one list they both touch has no lock.

Three ways it lands

What actually happens next depends on what the allocator did with those freed 32 bytes in the sliver of time between the free and the read.

  • The signing key leaks. If the slot has not been reused yet, the 16 bytes B reads are still the real AES-128-CMAC signing key. An attacker who recovers that key can forge valid signatures for the session and inject signed requests of their choosing.
  • Signing silently breaks. If the slot was partially overwritten, B validates (or rejects) signatures against a garbage key. Messages that should fail pass, or the reverse, depending on the leftover bytes.
  • The kernel panics. The conn pointer at offset 16 gets dereferenced. If it now points at reused, unrelated data, the kernel follows a bad pointer and crashes. This denial of service was the most reliably observed outcome in the proof of concept.

To be clear about the ceiling: this is not remote code execution out of the box. Turning a race-based use-after-free into a reliable kernel RCE is a serious, multi-month effort on its own. What you get cheaply is a crash of the whole box, and under the right timing, a leaked signing key. For a kernel component, that is already plenty.

The part that stings: this was patched once already

A few months before this, ksmbd fixed CVE-2025-40039. Same structure, struct ksmbd_session. Same shape of bug: an XArray accessed without a lock while other threads mutated it. That fix added a lock called rpc_lock to protect the rpc_handle_list array.

But ksmbd_session holds three XArrays, and that fix locked just one of them. The fix protected the RPC handle list and left ksmbd_chann_list exactly as exposed as before, even though it stores a live signing key shared across threads. The right lesson from the first CVE was “every shared XArray in this struct needs synchronization.” The lesson actually applied was “that one specific list needs a lock.” So the same mistake shipped a second time, one field over.

The fix

The patch does the obvious thing the first fix should have generalized. It gives the channel list its own lock, a read/write semaphore added to the session:

1
2
3
4
5
struct ksmbd_session {
    ...
    struct rw_semaphore chann_lock;   // new
    ...
};

Then it wraps every access to the channel list. Readers take down_read() / up_read(), writers take down_write() / up_write(), around the xa_load(), xa_store(), and xa_erase() calls. The write lock around the erase is held only long enough to remove the entry and is released before the kfree(), so the free never happens while a reader could still be inside the array. A reader can no longer be walking the channel while a writer tears it down.

Nothing exotic. The entire bug is the absence of those few lock calls.

How it was confirmed

The confirmation was the interesting engineering, because triggering this requires something most SMB tooling cannot do: open a second connection and bind it to an existing session. That is the SESSION_FLAG_BINDING (0x01) flag in the session setup, and neither smbclient nor Impacket speak it out of the box.

The harness ran a KASAN-enabled kernel (Linux 6.12.0, CONFIG_KASAN_GENERIC, four vCPUs, kasan_multi_shot=1) inside QEMU/KVM, so that freed-memory accesses would be reported instead of silently working or silently crashing. Getting a client to send a bound session setup took a one-line patch to Impacket, flipping smb3._binding = True so the second connection carried the binding flag and joined the victim session instead of starting a new one. From there it was the classic race derby: hammer the bind-and-teardown loop and wait for the timing to line up.

It lined up often. KASAN reported the slab-use-after-free roughly 8 to 11 times per 500 iterations, with the first hit usually inside 60 to 120 seconds, and it fired across multiple CPU cores independently. A race that reproduces that readily on a four-core VM is not a theoretical corner case.

Who is actually exposed

The preconditions keep this from being an internet-wide worm, and they are worth stating honestly:

  • You need valid SMB credentials. This is an authenticated bug, not a pre-auth one.
  • You need network access to port 445.
  • The server must have SMB3 multichannel enabled, which is not the default in ksmbd.
  • The attacking client must send a bound session setup to join the session on a second connection.

So the realistic threat model is a multi-user environment where an attacker already has a low-privilege SMB account on a ksmbd server that has turned multichannel on. That is a normal shape for a shared file server. From that foothold, the cheap outcome is crashing the server for everyone, and the expensive-but-real outcome is recovering a session signing key.

Timeline

  • Feb 8, 2026: Reported to the Linux kernel security team.
  • Feb 9, 2026: Patch authored by the ksmbd maintainer. The embargo was waived because there was no working proof of concept at disclosure time.
  • Feb 16, 2026: Fix merged to mainline.
  • Feb 18, 2026: CVE-2026-23226 published.
  • March 2026: Proof of concept built and confirmed with KASAN.
  • April 8, 2026: Full write-up published.

Details and credits

  • Component: Linux kernel ksmbd (in-kernel SMB3 server)
  • Class: Use-after-free (CWE-416) via race condition (CWE-362)
  • CVSS: 7.5 High, AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H (NVD rates it 7.8 using AV:L, which I think understates the network reachability, but reasonable people score the attack vector differently here)
  • Introduced: commit 1d9c4172110e (December 2023)
  • Affected: 5.15.145+, select 6.1.x builds, and 6.7 through 6.19.0
  • Fixed: upstream 4f3a06cc5797, stable e4a8a96a93d0, merged February 16, 2026
  • Reported by: Igor Stepansky, Orca Security Research Pod
  • Patch by: Namjae Jeon (ksmbd maintainer), signed off by Steve French (Microsoft), committed by Greg Kroah-Hartman

References

This post is licensed under CC BY 4.0 by the author.