# tapisrv.dll Patch Diff — CVE-2026-42912

| | |
|---|---|
| Binary | tapisrv.dll (Windows Telephony Service) |
| Pre-patch version | 10.0.26100.8521 |
| Post-patch version | 10.0.26100.8655 |
| KB | KB5094126 |
| CVE | CVE-2026-42912 — Race condition, Elevation of Privilege |
| Diff tool | ghidriff (Ghidra VersionTrackingDiff engine) |
| Functions changed | 3 (all with code changes) |
| Functions added | 3 |

## Summary

CVE-2026-42912 is a race condition (CWE: concurrent execution using shared
resource with improper synchronization) in the Windows Telephony Service RPC
server (`tapisrv.dll`). The patch addresses two distinct race windows, both
gated behind new Windows Invariant (WIL) Controlled Feature Rollout flags
(`Feature_500158777` and `Feature_784066872`).

**Race 1 — Sub-mask index out-of-bounds** (`SetEventMasksOrSubMasks` /
`GetEventMasksOrSubMasks`): `GetSubMaskIndex()` derives an array index from a
64-bit handle/selector value and the result was used directly to index into the
per-object event mask array with no bounds validation. A concurrent thread
modifying the handle between the index derivation and the array access could
produce an out-of-bounds read or write inside the telephony service heap. The
patch introduces a new helper `GetEventMasksOrSubMasks` that validates the
computed index against the array bound (`0x1e` = 30, matching the 31-slot
`0x1f`-iteration loop) before accessing the array, returning `0x80000032` on
overflow. The same check is added to `SetEventMasksOrSubMasks`, both gated
behind `Feature_500158777`.

**Race 2 — Conference call participant list UAF** (`DestroytCall`): when
destroying a call that is a *participant* (not the host) in a conference call,
the old code zeroed the participant's conf-handle fields (`param_1[0x22/0x23]`)
directly, without first acquiring exclusive access to the conf call object. A
concurrent thread traversing the conf's participant slot array (e.g. via
`TGetEventMasksOrSubMasks`) could dereference the participant pointer while it
was being torn down, constituting a use-after-free. The patch adds a new branch
(gated behind `Feature_784066872`) that pins the conf call via
`ReferenceObject`, acquires exclusive access via `WaitForExclusivetCallAccess`,
locates and removes the participant entry from the conf's slot array under that
lock, and only then releases before zeroing the fields.

Because both fixes are feature-flagged, the vulnerable code path remains
present in the binary and the fix is inert until Microsoft enables the
respective flags for each device cohort via the Windows Feature Store.

## Functions changed

### SetEventMasksOrSubMasks

| | |
|---|---|
| Address | 18003787c -> 180037ad8 |
| Change type | code, length, address, called |
| Similarity | 0.51 |
| Instructions | 81 → 132 |
| Patch flag added | **Yes** — `Feature_500158777__private_IsEnabledDeviceUsageNoInline` |

Adds a bounds check on the sub-mask index before writing to the per-object
event mask array. The new call to `Feature_500158777` and the guard are
inserted on the `param_1 != 0` (sub-mask) path only; the full-mask
(`param_1 == 0`) loop is unchanged.

```c
// VULNERABLE
uVar1 = GetSubMaskIndex(param_2);
param_4[uVar1] = param_3;   // no bounds check

// PATCHED
uVar1 = GetSubMaskIndex(param_2);
uVar2 = Feature_500158777__private_IsEnabledDeviceUsageNoInline();
if ((uVar2 != 0) && (0x1e < uVar1)) {
    return 0x80000032;
}
param_4[uVar1] = param_3;
```

### TGetEventMasksOrSubMasks

| | |
|---|---|
| Address | 180037f10 -> 1800381a0 |
| Change type | code, length, address, called |
| Similarity | 0.31 |
| Instructions | 1431 → 938 |
| Patch flag added | No (delegates to new helper which contains the flag) |

Significantly refactored. The vulnerable version duplicated the event mask
read logic across all five object-type branches (type 0 = client, 1 = line
app, 2 = line client, 3 = call client, 4 = phone app, 5 = phone client),
each with its own inline `GetSubMaskIndex` call and unchecked array read.
The patch consolidates all of this into the new `GetEventMasksOrSubMasks`
helper, which is called once per branch after acquiring the per-object
critical section — shrinking the function by ~500 instructions and ensuring
the bounds check runs on every exit path.

```c
// VULNERABLE — per-branch inline (shown for type 1):
uVar1 = GetSubMaskIndex(uVar6);
piVar7 = (int *)(ulonglong)uVar1;
*piVar3 = piVar8[(longlong)piVar7];   // unchecked

// PATCHED — single call after branch dispatch:
uVar3 = GetEventMasksOrSubMasks(param_2[4], (ulonglong)piVar5,
                                 &local_res10, param_2 + 5, piVar2);
*param_2 = (int)uVar3;
```

### DestroytCall

| | |
|---|---|
| Address | 180004c88 (unchanged) |
| Change type | code, length, sig, called |
| Similarity | 0.37 |
| Instructions | 1046 → 1349 |
| Patch flag added | **Yes** — `Feature_784066872__private_IsEnabledDeviceUsageNoInline` |

Adds ~300 instructions to the conference-participant teardown path. In the
vulnerable code, when the call being destroyed was a conference *participant*
(not the host), `param_1[0x22]` and `param_1[0x23]` (the conf back-pointer
fields) were cleared directly after `RemoveCallFromLineList` with no
synchronisation against concurrent readers of the conf's participant slot
array.

The patch inserts a new branch (enabled by `Feature_784066872`) that:
1. Reads the conf call pointer from the participant struct
2. Pins the conf object with `ReferenceObject`
3. Acquires exclusive access via `WaitForExclusivetCallAccess`
4. Walks the conf's participant slot array and compacts out the entry for `param_1`
5. Releases the lock with `LeaveCriticalSection`
6. Drops the reference with `DereferenceObject`
7. Only then zeros `param_1[0x22/0x23]`

```c
// VULNERABLE
RemoveCallFromLineList((longlong)param_1);
// ... directly zeros param_1[0x22], param_1[0x23] ...

// PATCHED
RemoveCallFromLineList((longlong)param_1);
uVar7 = Feature_784066872__private_IsEnabledDeviceUsageNoInline();
if (uVar7 != 0) {
    p_Var11 = ReferenceObject(piVar9, uVar7, 0x4c4c4143 /*CALL*/);
    if (p_Var11 != NULL) {
        uVar8 = WaitForExclusivetCallAccess((int *)p_Var11, 0x4c4c4143);
        // ... scan and compact participant slot array ...
        LeaveCriticalSection(p_Var12);
        DereferenceObject(p_Var12, uVar7, 1);
    }
}
// param_1[0x22/0x23] cleared only after lock released
```

## Functions added

| Function | Address | Role |
|---|---|---|
| `GetEventMasksOrSubMasks` | 180037a28 | New helper encapsulating both the full-mask loop and the sub-mask index path (with bounds check) for all object-type read requests. Called by `TGetEventMasksOrSubMasks` and transitively by `SetEventMasksOrSubMasks`. |
| `Feature_500158777__private_IsEnabledDeviceUsageNoInline` | 1800379e4 | WIL DeviceUsage feature-flag entry point for `Feature_500158777`; guards the sub-mask bounds check in `SetEventMasksOrSubMasks` and `GetEventMasksOrSubMasks`. Falls back to `wil_details_IsEnabledFallback` on cache miss. |
| `Feature_784066872__private_IsEnabledDeviceUsageNoInline` | 180006c04 | WIL DeviceUsage feature-flag entry point for `Feature_784066872`; guards the conference-participant locking fix in `DestroytCall`. Falls back to `wil_details_IsEnabledFallback` on cache miss. |

---

<sub>Source: ghidriff diff of tapisrv-2026-05.dll (10.0.26100.8521, pre-patch, SHA256 be01d3bd28d2e12d81690790d3a68fbab6814a97b650a1534a9e73ad5ed5a01e) vs tapisrv-2026-06.dll (10.0.26100.8655, post-patch, SHA256 c00c8f51b6ba0bbd76927ac6eefb399a62e5e20fbbe41bdb45ef5901a8a1f169) — [download pre](/data/patch_diffs/binaries/tapisrv-2026-05.dll) / [download post](/data/patch_diffs/binaries/tapisrv-2026-06.dll).</sub>
