# RCA — CVE-2025-29970 (bfs.sys use-after-free)

- **Binary:** `bfs.sys` (Brokering File System minifilter)
- **Patch:** KB5058411 (May 2025), `10.0.26100.3624` → `10.0.26100.4061`
- **Class:** CWE-416 use-after-free | **Impact:** EoP / BSOD | **ITW:** no
- **Primary source:** [PixiePoint Security](https://www.pixiepointsecurity.com/blog/nday-cve-2025-29970/)
- **Diff:** `ghidriff/CVE-2025-29970/output/bfs-10.0.26100.3624.sys-bfs-10.0.26100.4061.sys.ghidriff.md`

## Component

BFS is a filesystem minifilter shipped with Win32 App Isolation (AppSilo). It brokers
file/pipe/registry access for sandboxed apps via policies stored in a global
`PolicyTable` → `PolicyEntry` (per User+AppContainer SID) → `StorageObject` →
`DirectoryBlockList` (linked list of covered paths). Because it is reachable from
inside sandboxed processes, it is a high-value EoP surface.

## Root cause

`BfsCloseStorage()` frees the `DirectoryBlockList` **head** inside the cleanup loop:

```c
for ( directoryBlockList = entryStorageObject->DirectoryBlockList;
      ;
      ExFreePoolWithTag(directoryBlockList, 0) )            // head freed in iteration 1
{
    currentNode = directoryBlockList->LinkedListEntry.Flink; // head dereferenced in iteration 2 -> UAF
    ...
    ExFreePoolWithTag(currentNode->DirectoryBlockBuffer, 0);
    ExFreePoolWithTag(currentNode, 0);
}
```

When the list holds more than one entry, iteration 1 frees the list head and
iteration 2 dereferences the freed head to read `Flink` — a classic UAF read.

## Patch

The deallocation loop was split into a new function `BfsCloseRootDirectory()`; the
`ExFreePoolWithTag(DirectoryBlockList, 0)` for the head was moved **outside** the
loop, behind the `Feature_2777415992` feature gate. The ghidriff diff confirms:

- Added: `BfsCloseRootDirectory`, `Feature_2777415992__private_IsEnabledDeviceUsageNoInline`,
  `Feature_2777415992__private_IsEnabledFallback`
- Modified: `BfsCloseStorage` (79% match)

## Reaching the bug (userspace recipe)

| Requirement | Detail |
|---|---|
| Integrity level | Medium IL process (BFS device not accessible from Low IL AC) |
| Token | AppSilo token — `BfsIsApplicableToken()` checks `TokenIsAppSilo`; impersonate a LowBox token (e.g. duplicated from a packaged process such as `MicrosoftWindows.Client.WebExperience`) |
| Device | `\\?\GLOBALROOT\Device\Bfs`, `GENERIC_READ\|GENERIC_WRITE` |
| IOCTL add | `0x228004` `BfsProcessSetPolicyRequest` — struct `{ hToken, IsDirectory(0=file,2=dir), unkFlag=0x10000000, FilenameLength, FilenameBuffer, OperationType=0 }`; path must exist & be accessible to the package (use its `AC\Temp`) |
| IOCTL trigger | `0x228010` `BfsProcessDeletePolicyEntryRequest` — struct `{ hToken }` |

## Call flow

```
DeviceIoControl(0x228010)
  bfs!BfsDeviceIoControl
    bfs!BfsProcessDeletePolicyEntryRequest   // token checked (TokenIsAppSilo)
      bfs!BfsRemovePolicyEntry
        bfs!BfsDereferencePolicyEntryEx
          bfs!BfsCloseStorage                // UAF: head freed in-loop, dereferenced next iteration
```

## Detection notes (blue team)

- Crash signature: **bugcheck 0x50** in `bfs!BfsCloseStorage+0x4c` (stack above).
- Behavioral tell: a Medium-IL process opening `\\Device\Bfs` and issuing
  high-frequency IOCTL `0x228004`/`0x228010` pairs with a duplicated AppSilo token.
- The freed head is only 0x20 bytes and reused immediately, so exploitation needs
  many add/delete loops (~0x10000) — the IOCTL loop itself is the anomaly.
- PoC: `static/data/patch_diffs/poc/poc_cve_2025_29970.c`

## Related bfs.sys CVEs (same attack surface)

- CVE-2025-21372 — UAF race in `BfsReleaseNamedPipeMapping` (lock after refcount dec), KB5050009 Jan 2025
- CVE-2025-21315 — missing `PolicyTable` locks, same Jan 2025 update
- See `kb/CVE-2025-21372.md`, `kb/CVE-2025-21315.md`,
  [HT3Labs Jan-2025 analysis](https://ht3labs.com/Brokering-File-System-January-2025-Patch-Analysis.html)
