# RCA — CVE-2026-50458 (bfs.sys directory-cache refcount UAF)

- **Binary:** `bfs.sys` (Brokering File System minifilter)
- **Patch:** KB5101650 (July 2026), `10.0.26100.8521` → `10.0.26100.8875`
- **Class:** CWE-416 use-after-free (race + broken refcount contract) | **Impact:** EoP (MS: Exploitation Less Likely) | **ITW:** no
- **Diff:** `ghidriff/CVE-2026-50458/output/bfs-10.0.26100.8521.sys-bfs-10.0.26100.8875.sys.ghidriff.md`

## Root cause

BFS directory cache entries are handed out as `entry+8` pointers under a strict
invariant: **any function returning `entry+8` with success must hold a refcount
for the caller.** `BfsLocateDirectory` honors it (`lock inc [rax+98h]` before
returning). `BfsAddOrModifyEntry` relies on it — after each `BfsCreateDirectory`
it blindly calls `BfsDereferenceTableEntry(oldDir-8)`.

`BfsInsertDirectory` breaks the contract in the duplicate-insert path:

```c
entry = RtlInsertElementGenericTableAvl(table, &tmp, 0xa0, pNewElement);
*ppDirectory = entry + 8;                       // published either way
if (Agentic_branch_enabled)
    duplicate = (*pNewElement == false);        // checks the VALUE
else
    duplicate = (pNewElement == NULL);          // checks the ADDRESS (never true here)
if (duplicate) {
    /* frees temp resources; *** skips InterlockedIncrement(&entry->RefCount) *** */
} else {
    InterlockedIncrement(&entry->RefCount);     // only taken for real inserts
}
```

When N threads miss `BfsLocateDirectory("X")` and race into `BfsInsertDirectory`,
the first inserts and takes a ref; the rest get the **existing** entry back, skip
the ref++, yet still receive `entry+8`. Each later `BfsDereferenceTableEntry`
consumes a reference that was never taken. With ≥4 synchronized threads the
refcount reaches 0 while other threads still hold `entry+8` → the `Bfse`
allocation (0xd0, paged pool) is freed and then used again inside
`BfsInsertDirectoryEntry` (bitmap setup → `RtlFindClearBits`) or `BfsFindEntry`.

## Patch

The July 2026 build splits `BfsInsertDirectoryEntry` (`BfsInsertDirectoryEntry_OLD`
appears as a new symbol) and reworks the duplicate path; `BfsInsertDirectory`,
`BfsCreateDirectory` and callers are modified so the duplicate branch either
takes the reference or does not publish the pointer. (See diff; exact gating via
the existing `Feature_AgenticAppContainerBfsSupport` machinery.)

## Reaching the bug (userspace recipe)

| Requirement | Detail |
|---|---|
| IL | Medium (device + AppContainer profile creation blocked at Low) |
| Token | AppContainer token **with `AgenticAppContainer` capability** — derive via `RtlDeriveCapabilitySidsFromName(L"AgenticAppContainer")`, create profile (`CreateAppContainerProfile`), spawn suspended child, `OpenProcessToken` |
| Device | `\Device\Bfs` via `NtCreateFile`, one handle per worker |
| IOCTL | `0x228004` `BfsProcessSetPolicyRequest`, `BFS_SET_POLICY_REQUEST` (0x30): `{ TokenHandle, EntryType=2 (directory), PolicyType, PolicyFlags, PathLength, PathBuffer, Operation=0 (add) }` |
| Path | deep pre-created dir tree, e.g. `\??\C:\BfsRace\rN\a\b\c\d` — every component is a `BfsCreateDirectory` race opportunity; pre-create on disk to avoid the `IoCreateFile` fallback |
| Pressure | ≥4 threads hitting the SAME new component simultaneously (author: 4 = reliable on bare metal; VM needs more; reference PoC uses 128 workers × 2000 rounds, semaphore release) |

## Call flow

```
NtDeviceIoControlFile(0x228004)
  bfs!BfsDeviceIoControl
    bfs!BfsProcessSetPolicyRequest     // ObReferenceObjectByHandle + BfsIsApplicableToken (Agentic capability)
      bfs!BfsAddOrModifyEntry          // per-component loop, deref contract assumed
        bfs!BfsCreateDirectory         // lookup (ref'd) -> miss -> insert
          bfs!BfsInsertDirectory       // BUG: duplicate path skips ref++
        bfs!BfsInsertDirectoryEntry    // UAF use of freed Bfse -> RtlFindClearBits -> 0x3b
```

## Detection notes (blue team)

- Crash signature: **bugcheck 0x3b**, bucket `AV_bfs!BfsInsertDirectoryEntry`
  (or `bfs!BfsFindEntry` when the hole is reclaimed); stack matches the call flow above.
- Behavioral tell: Medium-IL process creating an AppContainer profile with the
  `AgenticAppContainer` capability, then bursting `IOCTL 0x228004` on
  `\Device\Bfs` from many threads against deep, newly-created directory trees.
- Author showed reclamation with `WNF_STATE_DATA` spray (0xd0 class) is possible
  but unreliable → MS rated Exploitation Less Likely; DoS is trivial.
- PoC: `static/data/patch_diffs/poc/poc_cve_2026_50458.c`

## Related bfs.sys CVEs (same minifilter, same bug family)

21 CVEs by the count, dominated by race→UAF (misplaced pushlocks,
rundown protection, deref errors): CVE-2025-29970, CVE-2025-21372/21315
(RCAs alongside this file), CVE-2026-25167/26181/32091/32219 (2026), and more —
see `kb/CVE-2026-50458.md` for the full table.
