# CVE-2022-22715 — "Windows Dirty Pipe": npfs.sys `NpTranslateContainerLocalAlias` 16-bit Integer Overflow → Pool Out-of-Bounds Write (Sandbox Escape)

---

## Summary

| | |
|---|---|
| **Product** | Microsoft Windows — `npfs.sys` (Named Pipe File System driver) |
| **CVE ID** | CVE-2022-22715 |
| **Impact** | Elevation of Privilege — **sandbox escape** (AppContainer/restricted → kernel) |
| **MSRC severity** | Important |
| **CWE** | CWE-190: Integer Overflow or Wraparound → CWE-787: Out-of-bounds Write |
| **Patch Date** | February 8, 2022 |
| **Pre-patch binary** | `npfs.sys` 10.0.18362.267 (served through 2022-01) — SHA256 `d0179a8b31eb86b2c677c0aa3390f61510ae2f6ce48e42801f3b1352b4a291ed` |
| **Post-patch binary** | `npfs.sys` 10.0.18362.2094 (Feb 2022 fix) — SHA256 `28a29817b30798072d2e72df5fc403fdc63c480b3ef0bdae71a73bd16bcf309a` |
| **Fix gating** | **None** — unconditional widen-and-check (no CFR/KIR flag) |

---

## Product Description

`npfs.sys` implements the Named Pipe File System — the kernel driver behind
`\Device\NamedPipe`, the IPC primitive browsers and sandboxed applications
(old Edge, Adobe Reader) use between their broker and renderer processes. When
AppContainer arrived in Windows 8.1, npfs gained a name-translation path so a
sandboxed process's pipe names are rewritten into its per-container object
namespace. That path — present for ~10 years — is the bug, reachable **from
inside an AppContainer or restricted-token sandbox**, which is exactly what makes
this a renderer-to-kernel **sandbox escape** rather than an ordinary local EoP.

---

## Vulnerability Summary

Opening `\Device\NamedPipe\LOCAL\…` routes `NpFsdCreate` → `NpTranslateAlias`
(which matches the `LOCAL\` prefix) → **`NpTranslateContainerLocalAlias`**. That
function rewrites the name into
`Sessions\<n>\AppContainerNamedObjects\<sid>\…\<name>` and allocates a pool
buffer sized for the result. **Every size variable was 16-bit (`ushort`/`short`).**

```c
// NpTranslateContainerLocalAlias (npfs.sys 10.0.18362.267) — PRE-PATCH, from our diff
// (symbol names from the public PDB)
short sVar5;                                   // total size — 16-bit
...
if (ifslash) { MaximumLength -= 2; ... }       // remaining-name leading '\'
...
sVar5 = (short)prefix + 100 + (ushort)namelen; // 16-bit add
if (ifslash) sVar5 = sVar5 + 2;                // +2, still 16-bit
buf = ExAllocatePoolWithTag(PagedPool, sVar5, 'NpFn');   // *** sVar5 can wrap to 0 ***
if (buf) {
    if (ifslash) { buf++; MaximumLength -= 2; }          // 0 - 2 -> 0xFFFE (ushort underflow)
    RtlUnicodeStringPrintf(&dst, L"Sessions\\%ld\\AppContainerNamedObjects\\%wZ\\%wZ\\%wZ", ...);
    // copies up to dst.MaximumLength (0xFFFE) bytes into the tiny buffer
}
```

Two chained integer bugs:

1. **Overflow to 0.** With a long enough pipe name, the 16-bit `sVar5` wraps to
   exactly `0`, so `ExAllocatePoolWithTag` returns a minimal (0x20) `NpFn`
   allocation.
2. **Underflow to 0xFFFE.** With the leading-`\` (`ifslash`) case, the
   `UNICODE_STRING.MaximumLength` is decremented by 2 from `0` → **`0xFFFE`**.

`RtlUnicodeStringPrintf` then formats the translated name up to `MaximumLength`
(0xFFFE ≈ 64 KB) into the 0x20 buffer — a **paged-pool out-of-bounds write of
~64 KB of attacker-controlled name bytes**, corrupting adjacent 0x20 LFH
allocations and neighbouring pool. The crash lands in
`ExAcquirePushLockExclusiveEx` on a `0x41414141…` pointer from a smashed object.

---

## Prerequisites and Constraints

- **Local, but from inside a sandbox.** The function only proceeds when the
  caller's token is **AppContainer or restricted**
  (`SeQueryInformationToken(TokenIsAppContainer)` /
  `TokenIsRestricted`). A normal token returns early — so this is specifically a
  primitive for escaping a renderer/UWP sandbox, not a generic local trigger.
- The attacker controls the pipe name (length and bytes), hence both the
  allocation size (via the overflow) and the overflowing content.
- To reach the underflow, the total size must wrap to **exactly 0** (any other
  small value leaves `MaximumLength` small and no OOB) — note this
  precise-length requirement as the main triggering constraint.
- Primitive is a controlled paged-pool OOB write; k0shl chains it (WNF
  `_WNF_STATE_DATA` manager object + `_TOKEN` worker object, LFH/VS
  cross-subsegment grooming) into arbitrary kernel R/W and sets
  `ETHREAD.PreviousMode = 0` for EoP.

---

## Vulnerability Details

### Call Chain

```
Sandboxed (AppContainer/restricted) process:
  NtCreateFile("\Device\NamedPipe\LOCAL\<long name>")
        ↓  IRP_MJ_CREATE
  npfs!NpFsdCreate
    → npfs!NpTranslateAlias                     // matches "LOCAL\" prefix
      → npfs!NpTranslateContainerLocalAlias     [*** 16-bit overflow → OOB pool write ***]
           ExAllocatePoolWithTag(PagedPool, size16, 'NpFn')   // size wraps to 0 → 0x20 buf
           RtlUnicodeStringPrintf(dst[MaximumLength=0xFFFE], …) // ~64KB copy
```

### Root Cause

The translated-name length is computed and stored in 16-bit fields. A long pipe
name overflows the size to 0 (tiny allocation) while the `UNICODE_STRING`
`MaximumLength`, decremented past 0, underflows to `0xFFFE`, so the subsequent
`RtlUnicodeStringPrintf` writes far beyond the allocation. Root cause is the
16-bit width of the size arithmetic (CWE-190) driving an OOB write (CWE-787).

### The patch

The February 2022 build widens the size math to 32-bit and refuses any length
that would not fit a 16-bit `UNICODE_STRING`:

```c
// NpTranslateContainerLocalAlias (10.0.18362.2094) — PATCHED, from our diff
int  iVar5  = (uint)(ushort)prefix + (uint)(ushort)namelen + 0x78;   // 32-bit
int  iVar10 = (uint)(ushort)prefix + (uint)(ushort)namelen + 0x7a;   // 32-bit (+2 variant)
uint uVar6  = (uint)extra + iVar5;
if (ifslash) uVar6 = iVar10 + (uint)extra;
if (uVar6 < 0xffff) {                       // *** NEW BOUND CHECK (<= 0xFFFE) ***
    dst.MaximumLength = (ushort)uVar6;
    buf = ExAllocatePoolWithTag(PagedPool, uVar6 & 0xffff, 'NpFn');
    ...                                     // proceed only for in-range sizes
}
```

Because `uVar6` is a full 32-bit `uint`, the sum can no longer wrap in 16 bits,
and the `uVar6 < 0xffff` guard rejects any length that would overflow a
`UNICODE_STRING`. Both the tiny-allocation and the `0xFFFE` underflow are thereby
eliminated. (k0shl's Server-2022 build shows the same fix expressed with
`ExAllocatePool2` and `if (v23 <= 0xFFFE)`.)

### Patch Completeness Assessment

**Unconditional fix — no Controlled Feature Rollout flag or Known Issue Rollback
toggle.** As with the 2023 DHCPv6 fix (CVE-2023-28231), the February 2022 npfs
patch ships a straight widen-and-check with no gated fallback; patch state is
determined by file version. The "both paths ship, gated at runtime" servicing
pattern seen across the 2024–2026 CVEs in this corpus is not present here.

---

## Detection Guidance

**Crash signature.** Paged-pool corruption bugchecks with the faulting access on
a `0x4141…`-style pointer, stacks through `npfs!NpTranslateContainerLocalAlias`
/ `RtlUnicodeStringPrintf`, `NpFn` (Name block) pooltag near the overrun. Special
Pool on `npfs.sys` makes the OOB land on a guard page at the format call.

**Behavioural.** A **sandboxed** process (AppContainer / restricted token —
browser renderer, Adobe Reader) opening `\Device\NamedPipe\LOCAL\…` with an
unusually long pipe name. Legitimate LOCAL alias names are short; a name crafted
to wrap a 16-bit length (hundreds of chars, tuned so the total size hits exactly
0) is the direct indicator.

**Config.** File-version check is sufficient here — the fix is not feature-gated,
so a patched `npfs.sys` (≥ Feb 2022 build for the branch) is not vulnerable.

---

## References

- k0shl (Kunlun Lab) — *Break me out of sandbox in old pipe: CVE-2022-22715
  Windows Dirty Pipe*. `whereisk0shl.top`
- [CVE-2022-22715 PoC](https://github.com/k0keoyo/my_vulnerabilities/tree/master/CVE-2022-22715)
- MSRC advisory — CVE-2022-22715 (Named Pipe File System Elevation of Privilege)
- Full binary diff: `/data/patch_diffs/npfs_sys-cve-2022-22715-ghidriff.md`
