# CVE-2026-25188 — Windows Telephony Service `tapisrv.dll` Heap Overflow in `LForward` via Unvalidated Cumulative Size of a `LINEFORWARDLIST`

---

## Summary

| | |
|---|---|
| **Product** | Microsoft Windows — `tapisrv.dll` (Telephony / TAPI Service) |
| **CVE ID** | CVE-2026-25188 |
| **Impact** | Elevation of Privilege |
| **MSRC severity** | Important |
| **CWE** | CWE-122: Heap-based Buffer Overflow |
| **Patch Date** | March 10, 2026 |
| **Pre-patch binary** | `tapisrv.dll` 10.0.26100.7623 (KB5074105, Jan 2026) — SHA256 `3eda9356b2b1dbc9af6b3ea719536e05ba4011f82bbc12aff0cbb0a37b0efeb2` |
| **Post-patch binary** | `tapisrv.dll` 10.0.26100.8036 (KB5079473, Mar 2026) — SHA256 `2f6946261b077051b3626b946a94e64b80b7360eef015cbd454b5f2d656bf1ec` |
| **Feature flag** | `Feature_3967746362` — **the fix is CFR-gated** |

---

## Product Description

The Telephony Service (`tapisrv.dll`, hosted in a `svchost.exe`) implements the
server side of the classic TAPI 2.x API. Client processes call functions such as
`lineForward` through `tapi32.dll`, which marshals the request over the TAPI
RPC/ALPC surface to `tapisrv`. Because the service runs with high privilege and
the request buffers are fully controlled by the (potentially low-privileged)
client, TAPI request handlers are a long-standing local elevation-of-privilege
surface — `tapisrv.dll` has produced a run of EoP bugs including CVE-2024-26230,
CVE-2024-43626 and CVE-2026-42912.

`LForward` is the server-side handler for `lineForward`. It parses a client
`LINEFORWARDLIST` — a header followed by an array of `LINEFORWARD` entries, each
carrying variable-length caller-address and callee-address blobs referenced by
offset/size pairs into the same buffer.

---

## Vulnerability Summary

`LForward` allocated a heap buffer sized at `2 × dwTotalSize` (the total size the
client declares in the list header) and copied the list into it, widening the
embedded ANSI address strings to Unicode. Each entry's caller/callee address
block was bounds-checked **individually** with `IsBadSizeOffset`, but the
**cumulative** size of all entries' variable-length address data was never
checked against the allocation.

```c
// LForward (tapisrv.dll 10.0.26100.7623) — PRE-PATCH, from our ghidriff diff
required = header + (nEntries-1)*stride;                 // local_94 = uVar12 + uVar6
if ((uVar6 <= required) && (required <= dwTotalSize)) {  // fixed part fits the header claim
    for (i = 0; i < nEntries; i++) {                     // per-entry checks only
        if (bad dwForwardMode)                    goto error;
        if (IsBadSizeOffset(dwTotalSize, required, entry->CallerOff, entry->CallerSize)) goto error;
        if (IsBadSizeOffset(dwTotalSize, required, entry->CalleeOff, entry->CalleeSize)) goto error;
        entry = (uint*)((char*)entry + stride);
    }
    if (param_2[0xb] != 0xffffffff && nEntries != 0) {
        if ((ulonglong)dwTotalSize * 2 > 0xffffffff) goto error;
        _Dst = HeapAlloc(ghTapisrvHeap, 8, dwTotalSize * 2);   // *** 2 x dwTotalSize ***
        ...
        memcpy(_Dst, list, (nEntries-1)*stride + header);
        *_Dst = *_Dst * 2;                                     // records the doubled size
        // NewToOld / OldToNew widening then writes each entry's address strings
        // (MultiByteToWideChar) into _Dst — total expanded bytes can exceed 2*dwTotalSize
    }
}
```

`IsBadSizeOffset` only proves that a *single* entry's `offset + size` lands
inside `dwTotalSize`. Nothing sums the caller/callee address bytes across all
entries. A `LINEFORWARDLIST` whose individual entries each pass the per-entry
check, but whose **aggregate** variable-length address data — especially when the
ANSI→Unicode widening doubles it — exceeds the `2 × dwTotalSize` heap
allocation, overflows the buffer with client-controlled bytes. Overlapping /
aliased caller and callee offsets (the same bytes counted for multiple entries)
are the natural way to make the declared `dwTotalSize` small while the expanded
output is large.

---

## Prerequisites and Constraints

- Local authenticated session — any process that can open the TAPI client
  interface and issue `lineForward`. No administrative rights.
- No user interaction, no special hardware.
- The corrupted allocation is in the `tapisrv` service heap (`ghTapisrvHeap`),
  in the high-privileged service process — a heap overflow there is the EoP
  primitive.
- The overflow contents are attacker-controlled (the widened address strings),
  so this is a *controlled* overflow suitable for grooming, not a blind smash.

---

## Vulnerability Details

### Call Chain

```
Low-privileged client:
  tapi32!lineForward  ->  TAPI RPC/ALPC  ->  tapisrv
                                               |
Telephony service (tapisrv.dll):
  <request dispatch>
    -> LForward                         [*** heap overflow ***]
         LineProlog / ValidateCallParams   (parse + per-entry IsBadSizeOffset)
         HeapAlloc(ghTapisrvHeap, 8, dwTotalSize*2)
         memcpy + NewToOldLineforwardlist / OldToNewLineforwardlist   (ANSI->Unicode widen)
```

### Root Cause

Two facts combine:

1. **The allocation is sized from the client's declared `dwTotalSize`**
   (`2 × dwTotalSize`), not from the actual sum of the entries' variable-length
   address data.
2. **Validation is per-entry, never cumulative.** `IsBadSizeOffset` bounds each
   caller/callee block on its own; the running total of all blocks — which is
   what actually gets written during the widening copy — is never compared
   against the allocation. Overlapping offsets let the declared size stay small
   while the written size grows.

### The patch

The March build inserts a **cumulative, integer-overflow-checked size
accumulator** into the per-entry loop and validates it against `dwTotalSize`
before the copy:

```c
// LForward (10.0.26100.8036) — PATCHED, feature-enabled branch
if (Feature_3967746362__private_IsEnabledDeviceUsageNoInline()) {
    // aliased caller==callee case: advance dwTotalSize by the shared block once
    if (((entry->CalleeOff == entry->CallerOff) && (entry->CalleeSize == entry->CallerSize) &&
         (t = entry->CalleeSize + dwTotalSize, wrapped = t < dwTotalSize, dwTotalSize = t, wrapped))
        || (t = entry->CallerSize + acc, t < acc)) goto error;         // add-overflow check
    acc = entry->CalleeSize + t;                                       // running required size
    if ((acc < t) || (dwTotalSize < acc)) goto error;                 // stays within the buffer
}
```

The accumulator `acc` (`local_c4`) is seeded with the fixed-part size and grows
by each entry's caller and callee address sizes, with every addition checked for
32-bit wraparound and the total checked against `dwTotalSize` on each iteration.
A list whose aggregate address data would overrun the allocation is now rejected
before `HeapAlloc`/`memcpy`.

### Patch Completeness Assessment

**The fix is gated behind a Controlled Feature Rollout flag.** `LForward` in
`10.0.26100.8036` calls `Feature_3967746362__private_IsEnabledDeviceUsageNoInline()`
at three points, and the cumulative bounds check runs **only when the flag
returns non-zero**. With the flag disabled, the patched binary executes the
original per-entry-only validation and remains overflowable. Patch state cannot
be inferred from file version alone — the corrected path is live only where the
CFR flag is enabled.

This is consistent with the pattern across this corpus: the CLFS 2025–2026 fixes,
the August 2026 CVEs, and even the CVE-2024-38077 (`lserver.dll`) pre-auth RCE
all ship their memory-safety fix behind a feature flag while the vulnerable code
path remains compiled in.

---

## Detection Guidance

**Crash signature.** Heap-corruption bugchecks or `svchost.exe` (TapiSrv host)
crashes with the faulting allocation in `ghTapisrvHeap`, stack through
`tapisrv!LForward` → `NewToOldLineforwardlist` / `OldToNewLineforwardlist` /
`memcpy`. Enabling pageheap on the TapiSrv `svchost` makes the overrun land on a
guard page at `LForward`.

**Behavioural.** Non-telephony processes opening the TAPI client interface and
issuing `lineForward` with a `LINEFORWARDLIST` carrying many entries and/or
entries whose caller/callee address offsets overlap or point at the same bytes —
a legitimate forwarding list has a small entry count and non-overlapping blobs.

**Config note.** Because the fix is CFR-gated, confirming `Feature_3967746362` is
enabled is the only way to verify the corrected path is live; file version alone
is insufficient.

---

## Related tapisrv.dll CVEs

`LForward` and its neighbours in `tapisrv.dll` are a recurring EoP surface:

- **CVE-2024-26230** — Telephony Service EoP (use-after-free)
- **CVE-2024-43626** — Telephony Service EoP
- **CVE-2026-42912** — Telephony Service EoP

CVE-2026-25188 adds a cumulative-size validation defect to that history, in the
same `lineForward` handling path.

---

## References

- MSRC advisory — CVE-2026-25188 (Windows Telephony Service, Elevation of Privilege)
- Full binary diff: `/data/patch_diffs/tapisrv_dll-cve-2026-25188-ghidriff.md`
