1. Overview

Item Value
Unpatched binary ntfs_unpatched.sys (10.0.28000.2179)
Patched binary ntfs_patched.sys (10.0.28000.2340)
Overall similarity 0.9927
Matched functions 4417
Changed functions 9
Identical functions 4408
Unmatched (unpatched / patched) 0 / 0

Verdict (corrected): The single security-adjacent change is in TxfSatisfyFlushForTopsRange. The unpatched code allocates a pool buffer that embeds an RTL_BITMAP and sizes the bitmap storage as SizeOfBitMap_bits/8 bytes without rounding the total allocation up to a 32-bit ULONG boundary. Because the Rtl bitmap helpers touch whole 32-bit words, when the bit count is 16 mod 32 the trailing ULONG of the bitmap extends ~2 bytes past the logical end of the allocation. The patch rounds the allocation size up to a 4-byte multiple so the trailing word is fully backed.

This is a correctness / robustness (defense-in-depth) fix, not a demonstrable exploitable pool overflow. In practice the ~2 trailing bytes are absorbed by the pool allocator's 16-byte rounding slack (they never reach an adjacent allocation), the trailing-word access is a read plus a value-preserving read-modify-write (no attacker-controlled data is written out of bounds), and only RtlSetBits and one _bittest64 read ever touch the undersized buffer. The out-of-bounds access is observable as a fault only under Special Pool / Driver Verifier. Severity is therefore Low, not High.

Delivery status: STAGED, not unconditionally delivered. The fix is gated behind the WIL feature flag Feature_NTFS_BR_TxfSatisfyFlushForTopsRangeBitmapSizeAlignFix. When the flag is disabled the patched binary executes the old (unrounded) +0x58 code. The runtime default is resolved by WIL at run time (wil_details_IsEnabledFallback) and is not a compile-time constant readable from the binary, so it cannot be proven enabled from static analysis.

Of the 9 changed functions, only TxfSatisfyFlushForTopsRange carries the fix. The remaining 8 are recompilation/codegen drift (register renaming, ETW/logsup GUID renames, semantically identical branch inversions, inline/outline refactors) plus WIL feature-staging plumbing (wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState, wil_details_RegisterFeatureUsageProvider) that exists only to register and cache the new flag. An independent line-by-line pass over all 8 confirmed none of them adds or changes a bounds check, allocation size, memcpy/memset length, loop bound, or validation gate.


2. Vulnerability Summary

2.1 LOW — Undersized RTL_BITMAP pool allocation (bitmap not rounded to ULONG granularity) (CWE-131, latent CWE-125/CWE-787)

  • Affected function: TxfSatisfyFlushForTopsRange — unpatched @ 0x1401037F8, patched @ 0x140103808. Builds a TOPS (TxF Old Page Stream) log-array record that embeds an RTL_BITMAP inside a pooled 'Ntf0' buffer.
  • Root cause: The function derives a bitmap bit count v14 (v21 in the unpatched decompilation) that is always a multiple of 16, then sizes the pool allocation as (v14 >> 3) + 0x58 bytes — exactly v14/8 bytes of bitmap storage at offset 0x58 — without rounding the total up to a 32-bit ULONG boundary. SizeOfBitMap is stored (in bits) at +0x54.
  • +0x00: word = 1 (record type marker)
  • +0x04: dword = 0x15
  • +0x50: dword = v13 = v5 & ~0xF (16-aligned base bit)
  • +0x54: RTL_BITMAP.SizeOfBitMap = v14 (bit count, multiple of 16, may be 16 mod 32)
  • +0x56: word = 0x58
  • +0x58: RTL_BITMAP.Buffer (start of bitmap storage, v14/8 bytes)
  • The flawed operation (unpatched):
mov     eax, r14d
shr     eax, 0x3        ; eax = v14 / 8  (bitmap bytes)
add     ax,  0x58        ; +0x58 header; 16-bit add, NO ULONG rounding
movzx   eax, ax
  • Why it is a latent bug: The Rtl bitmap helpers operate on whole 32-bit ULONGs. When v14 % 32 == 16 (i.e. 16, 48, 80, 112, …), v14/8 ≡ 2 (mod 4), so the bitmap byte count is not a multiple of 4 and the final ULONG covering the bitmap extends 2 bytes beyond the v14/8-byte buffer — i.e. 2 bytes past the logical end of the allocation.
  • Why it is NOT a meaningful memory-corruption primitive in practice:
  • The only Rtl operation on the undersized buffer is RtlSetBits(&BitMapHeader, i - v13, 1) (single-bit sets). RtlFindSetBits, RtlClearBit, and RtlAreBitsClear in this function all operate on a different, persistent Scb bitmap at a2+0x140 ([rdi+0x28] in asm), not on the freshly allocated buffer.
  • Every bit index passed to RtlSetBits is in [0, v14), i.e. in-bounds. If RtlSetBits read-modify-writes the containing ULONG, it reads the 2 out-of-bounds trailing bytes and writes them back unchanged (the modified bit is always in-bounds). No attacker-controlled value is written out of bounds.
  • ExAllocatePoolWithTag rounds allocations up to 16-byte granularity. For every value of v14 the 2 trailing out-of-bounds bytes fall inside the block's own rounding slack (minimum slack in the tightest case is exactly 2 bytes), so on production pool the access never reaches an adjacent allocation.
  • The over-read feeds only the internal bitmap; nothing is returned to user mode, so there is no information disclosure to the attacker.
  • Net effect: a latent out-of-bounds access that is inert on default systems and surfaces only as a fault under Special Pool / Driver Verifier (which places the allocation against a guard page). That makes it a correctness / hardening fix.
  • What the patch does: Adds Feature_NTFS_BR_TxfSatisfyFlushForTopsRangeBitmapSizeAlignFix__private_IsEnabledDeviceUsageNoInline(). When enabled, the size becomes ((v14 >> 3) + 0x5B) & ~3 — round (v14/8 + 0x58) up to the next 4-byte boundary so the trailing bitmap ULONG is fully inside the allocation. When the flag is OFF the old (v14 >> 3) + 0x58 path runs, so the fix is not unconditional. SizeOfBitMap at +0x54 is unchanged; only the allocation grew.
  • Attacker reachability: TxfSatisfyFlushForTopsRange is on the Transactional-NTFS (TxF) old-page-stream flush path, reachable by a local user operating on a file opened inside a KTM transaction. The specific call chain and the exact user-controlled inputs that drive v14 to 16 mod 32 were not proven end-to-end here; they are consistent with the function's arithmetic but are inferred, not demonstrated.

3. Pseudocode Diff

3.1 TxfSatisfyFlushForTopsRange

// UNPATCHED — TxfSatisfyFlushForTopsRange @ 0x1401037F8  (Hex-Rays, verbatim variable names)
v13 = v5 & 0xFFFFFFF0;                                   // 16-aligned base bit
v21 = ((v5 + v4 - 1) & 0xFFFFFFF0) + 16 - (v5 & 0xFFFFFFF0);   // bit count, ALWAYS multiple of 16
Size = (unsigned __int16)((v21 >> 3) + 88);             // <-- size = v21/8 + 0x58, NOT rounded to ULONG
PoolWithTag = ExAllocatePoolWithTag((PoolType | 0x10), Size, 0x3066744E);  // 'Ntf0'
memset(PoolWithTag, 0, Size);
*(_WORD *)PoolWithTag = 1;
PoolWithTag[1] = 21;
*((_WORD *)PoolWithTag + 43) = 88;                      // +0x56
*((_WORD *)PoolWithTag + 42) = v21;                     // +0x54  RTL_BITMAP.SizeOfBitMap (bits)
PoolWithTag[20] = v5 & 0xFFFFFFF0;                      // +0x50
BitMapHeader.Buffer = PoolWithTag + 22;                 // +0x58  RTL_BITMAP.Buffer
BitMapHeader.SizeOfBitMap = *((unsigned __int16 *)PoolWithTag + 42);
for ( i = v5 & 0xFFFFFFF0; i < v13 + v21; ++i ) {
    if ( _bittest64(p_Buffer[6], i) && !_bittest64(p_Buffer[4], i) )
        RtlSetBits(&BitMapHeader, i - v13, 1u);         // single-bit sets, index always < v21
}
// later: RtlClearBit((PRTL_BITMAP)(p_Buffer + 5), m);  <-- operates on the Scb bitmap at a2+0x140, NOT this buffer
// PATCHED — TxfSatisfyFlushForTopsRange @ 0x140103808
v14 = ((v5 + v4 - 1) & 0xFFFFFFF0) + 16 - (v5 & 0xFFFFFFF0);   // same bit-count derivation
v15 = v14 >> 3;
if ( Feature_NTFS_BR_TxfSatisfyFlushForTopsRangeBitmapSizeAlignFix__private_IsEnabledDeviceUsageNoInline() )
    v16 = (v15 + 91) & 0xFFFC;                          // (v15 + 0x5B) & ~3 -> round v15+0x58 up to ULONG boundary
else
    v16 = v15 + 88;                                     // old (unrounded) path retained when flag OFF
PoolWithTag = ExAllocatePoolWithTag((PoolType | 0x10), v16, 0x3066744E);
// remainder identical: SizeOfBitMap still v14; only the allocation size may grow

Key changes: - Unpatched: Size = (v21 >> 3) + 0x58, computed with a 16-bit add ax, 0x58 and no rounding. - Patched (flag ON): v16 = ((v14 >> 3) + 0x5B) & ~3 — equivalent to (v15 + 0x58 + 3) & ~3, rounding the byte count up so the final 32-bit bitmap word is fully inside the allocation. - The fix is feature-gated: with the flag OFF the patched binary runs v16 = v15 + 0x58, identical to the unpatched code.


4. Assembly Analysis

4.1 TxfSatisfyFlushForTopsRange

Unpatched — size computation and bitmap setup (real addresses):

0000000140103916  mov     eax, r14d
0000000140103919  shr     eax, 3                 ; eax = v14 / 8 (bitmap bytes)
000000014010391C  add     ax, 58h                ; +0x58 header; 16-bit add, NO ULONG rounding
0000000140103920  movzx   eax, ax
0000000140103923  mov     [rsp+118h+Size], rax
...
000000014010393A  mov     edx, eax               ; NumberOfBytes (undersized)
000000014010393C  call    cs:__imp_ExAllocatePoolWithTag   ; tag 0x3066744E 'Ntf0'
...
000000014010397B  mov     [r15+54h], r14w        ; RTL_BITMAP.SizeOfBitMap = v14 (bits, multiple of 16)
0000000140103984  lea     rax, [r15+58h]         ; RTL_BITMAP.Buffer = P + 0x58
...
0000000140103A0B  call    cs:__imp_RtlSetBits    ; RtlSetBits(&BitMapHeader, i - v13, 1)  <-- only op on the undersized buffer

Notes: - 0x1401039190x14010391C (shr eax,3 ; add ax,0x58): the size calculation. The total is not rounded to a 4-byte boundary; v14/8 is a multiple of 2 but can be 2 mod 4 (e.g. 16 bits -> 2 bytes). - 0x14010385C / 0x1401038C5 (lea rcx,[rdi+28h] -> RtlAreBitsClear / RtlFindSetBits): rdi = rdx+0x118, so rdi+0x28 = a2+0x140. These operate on the persistent Scb bitmap, not the undersized allocation. - 0x140103A0B (RtlSetBits on &BitMapHeader, whose Buffer = P+0x58): the only Rtl write into the undersized buffer, single-bit, in-bounds index.

Patched — feature-gated aligned size (real addresses):

0000000140103930  movzx   r14d, ax
0000000140103934  call    Feature_NTFS_BR_TxfSatisfyFlushForTopsRangeBitmapSizeAlignFix__private_IsEnabledDeviceUsageNoInline
0000000140103939  test    eax, eax
000000014010393B  jz      short loc_140103947    ; flag OFF -> old size
000000014010393D  lea     rax, [r14+5Bh]         ; NEW: +0x5B
0000000140103941  and     rax, 0FFFFFFFFFFFFFFFCh ; & ~3 -> round total up to ULONG boundary
0000000140103945  jmp     short loc_14010394B
0000000140103947  loc_140103947:
0000000140103947  lea     rax, [r14+58h]         ; OLD size (unchanged) when flag OFF
000000014010394B  movzx   r14d, ax
...
0000000140103969  call    cs:__imp_ExAllocatePoolWithTag

Notes: - The lea rax,[r14+5Bh]; and rax,~3 pair exists only in the patched binary and only on the enabled branch. - With the flag disabled, execution falls through to lea rax,[r14+58h], identical to the unpatched size. Presence of the patch does not guarantee the fix is active.


5. Trigger Conditions

  1. As a local user, open a file inside a KTM transaction (CreateTransaction + CreateFileTransacted) on an NTFS volume so writes route through the TxF old-page-stream logging path.
  2. Issue writes that drive the TOPS flush (TxfSatisfyFlushForTopsRange) with a 16-aligned span v14 satisfying v14 & 0x1F == 0x10 (i.e. 16, 48, 80, … bits). The entry RtlAreBitsClear(a2+0x140, a3, a4) must return FALSE so the allocation/flush branch is taken.
  3. When that condition holds, RtlSetBits (and a _bittest64 read) touch the final ULONG whose upper ~2 bytes lie past the logical allocation end.
  4. Observable effect: with Special Pool / Driver Verifier enabled on tag Ntf0, the trailing-word access can fault against the guard page (DRIVER_VERIFIER_DETECTED_VIOLATION / PAGE_FAULT_IN_NONPAGED_AREA). Without Special Pool the access lands inside the allocation's 16-byte rounding slack and has no observable effect (no adjacent-object corruption, no info leak).

Important: the mapping from user-controlled write offset/length to v14 was not proven end-to-end. The condition v14 % 32 == 16 is reachable in principle (e.g. a minimal single-page span yields v14 = 16), but a concrete WriteFile sequence that both reaches this branch and lands in an unmapped page was not demonstrated.


6. Impact Assessment (corrected)

  • Proven primitive: a latent, ULONG-granular out-of-bounds access of up to ~2 bytes past a pooled RTL_BITMAP — a read plus a value-preserving read-modify-write. It is not a controllable out-of-bounds write of attacker data.
  • Why it does not yield a memory-corruption exploit:
  • The 2 trailing bytes are absorbed by 16-byte pool rounding on production systems, so no adjacent allocation is reached.
  • The RMW writes the out-of-bounds bytes back unchanged; the attacker controls neither the value nor a write past the pool block.
  • The over-read is consumed internally (bitmap logic) and never returned to user mode, so there is no information disclosure.
  • Realistic worst case: on a system with Special Pool / Driver Verifier active on tag Ntf0, the guard-page access is a local, non-privileged fault (bugcheck / DoS) — and even that requires the attacker to have Special Pool enabled, which is not the default. On a default system the effect is nil.
  • Not substantiated (removed from the prior version of this report): controllable pool out-of-bounds write, pool grooming to place a victim object, adjacent-object header/refcount corruption, chaining to arbitrary write, local privilege escalation to SYSTEM, and any KASLR / SMEP / CFG / CET / HVCI discussion. None of these are evidenced by the diff; the primitive does not support them.

7. Debugger Notes

Assume WinDbg/KD attached to the UNPATCHED binary.

Breakpoints:

bp ntfs!TxfSatisfyFlushForTopsRange              ; 0x1401037F8 entry: rcx=IrpContext, rdx=Scb/TOPS ctx, r8d=range start, r9d=range count
bp ntfs+0x10393C                                 ; ExAllocatePoolWithTag call: edx = requested (undersized) size, r8d = 'Ntf0'
ba w1 ntfs+0x103A0B                              ; RtlSetBits call site; observe the trailing-ULONG access

Rebase note: analysis base is 0x140000000; subtract it to get the RVA, then bp ntfs+<rva>.

Inspect: - At the ExAllocatePoolWithTag call (0x14010393C): edx = (v14>>3)+0x58. Compute v14 = (edx - 0x58) * 8; if v14 % 32 == 16 the bitmap trailing ULONG spills 2 bytes past the logical size. After the call rax = r15 = pool pointer; bitmap runs [r15+0x58 .. r15+0x58+(v14/8)), logical block end is r15 + edx.

? (edx - 0x58) * 8            ; = v14 (bits)
? ((edx - 0x58) * 8) % 0x20   ; == 0x10 marks the overflowing case
  • At RtlSetBits (0x140103A0B): RCX = &BitMapHeader (SizeOfBitMap = v14, Buffer = r15+0x58), RDX = start index (in-bounds), R8 = 1. The trailing word at r15 + 0x58 + ((v14/8) & ~3) is touched even though it lies past r15 + edx — but note this is inside the pool block's rounding slack unless Special Pool is active.

Expected observation: with Special Pool on tag Ntf0, an immediate fault at the bitmap trailing ULONG address. Without Special Pool, no observable effect: the access is inside the allocation's rounded size. On the patched build with the flag enabled, edx is (…)&~3 and the trailing word is fully backed.

Struct / offset notes:

'Ntf0' TOPS record layout (P = ExAllocatePoolWithTag result):
  +0x00  word   = 1
  +0x04  dword  = 0x15
  +0x50  dword  = v13 (= v5 & ~0xF, 16-aligned base bit)
  +0x54  ULONG  = SizeOfBitMap = v14 (bits, multiple of 16)   <-- RTL_BITMAP.SizeOfBitMap
  +0x56  word   = 0x58
  +0x58  ...    = RTL_BITMAP.Buffer (v14/8 bytes)

Size calc (unpatched): size = (v14 >> 3) + 0x58            ; NOT rounded
Overflowing case:      v14 & 0x1F == 0x10                  ; trailing ULONG spans ~2 bytes past logical size
Only buffer op:        RtlSetBits(&BitMapHeader, idx, 1)   ; single-bit, in-bounds index
Scb-bitmap ops (NOT this buffer): RtlAreBitsClear / RtlFindSetBits / RtlClearBit on a2+0x140

8. Changed Functions — Full Triage

  • TxfSatisfyFlushForTopsRange (0x1401037F8 -> 0x140103808) (similarity 0.9586, security-relevant): the only meaningful diff. Unpatched sizes the 'Ntf0' bitmap record as (v14>>3)+0x58 with a 16-bit add and no ULONG rounding. Patched adds the Feature_NTFS_BR_TxfSatisfyFlushForTopsRangeBitmapSizeAlignFix gate selecting ((v14>>3)+0x5B) & ~3. Correctness/hardening fix; feature-staged. Remaining register renames and ETW/helper address shifts are recompilation noise.
  • NtfsCommitCurrentTransaction (similarity 0.92, cosmetic): rsi<->rdi register swap, ETW GUID rename (1c495e8d…->98a93039…), a semantically identical branch-condition inversion, and an inline iso-snapshot cleanup loop outlined into a TxfCleanupIsoSnapshots() call. No behavioral change; no bounds/size change.
  • NtfsCheckpointForVolumeSnapshot (similarity 0.9388, cosmetic): NtfsIsVcbAvailableForThisRequest error/main paths swapped (control-flow inversion, identical behavior), ETW GUID rename, reordered NTSTATUS comparison chain, decompiler-reconstructed call arguments. Codegen noise.
  • NtfsCheckpointForVolumeSnapshot$fin$0 (similarity 0.9712, cosmetic): the differ tracked the exception-cleanup funclet of NtfsCheckpointForVolumeSnapshot; it does not surface as a distinct function in the decompiled C (folded into its parent). Consistent with the parent's churn; no behavioral change.
  • NtfsCheckpointVolume (similarity 0.9614, cosmetic): large function; a top-level branch inversion (if(a3==0) -> if(a3!=0)) with the guarded-mutex wait relocated to the end and rejoining the common path, register reallocation, and ETW/logsup renames. All allocation sizes, memmove/memset lengths, and restart-table counts are byte-identical; apparent offset differences are decompiler pointer-type artifacts. No added bounds/validation.
  • NtfsFlushLsnStreamsOnLogTooFull (similarity 0.9713, cosmetic): ETW logsup symbol renames and basic-block address shifts only. No behavioral change.
  • TxfPrepareFileForTxfLogging (similarity 0.9922, cosmetic): interprocedural refactor — the caller now passes transaction/target-scb/scb explicitly to TxfAddTxfFileId instead of the callee re-deriving them. All memset lengths unchanged. No bounds/size/overflow change.
  • wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState (similarity 0.975, behavioral but non-security): a new state-tracking bit 0x40000 is folded into the atomically-stored WIL feature-state word (mask 0xFFFFF63E -> 0xFFFBF63E, plus explicit | 0x40000). Feature-staging cache bookkeeping for the new flag; gates no memory operation, allocation, or bound.
  • wil_details_RegisterFeatureUsageProvider (similarity 0.9853, behavioral but non-security): initializes a previously-unset provider-descriptor version/flags field (v2 = 1; v3 = 0) before registration. WIL infrastructure tied to the new flag; not a memory-safety change.

Collapsed note: all eight non-target functions are recompilation/codegen drift or WIL feature-staging plumbing that exists only to register, cache, and gate the single fix in TxfSatisfyFlushForTopsRange. An independent per-function pass confirmed none adds or changes a bounds check, allocation size, copy length, loop bound, or input validation.


9. Unmatched Functions

  • Removed (unpatched only): None.
  • Added (patched only): None (the WIL helper Feature_NTFS_BR_...IsEnabled* was matched/attributed in-line, not reported as an unmatched add).
  • Implication: the change is additive within one existing function. It is feature-flag gated (Feature_NTFS_BR_TxfSatisfyFlushForTopsRangeBitmapSizeAlignFix): the unpatched binary lacks the ULONG rounding unconditionally, and the patched binary runs the old path when the flag is disabled.

10. Confidence & Caveats

  • Confidence: high on the code facts; the security classification (Low, staged) is high-confidence given the containment analysis.
  • Rationale:
  • The size arithmetic is unambiguous in both .c and .asm: (v14>>3)+0x58 (16-bit add, no rounding) vs the patched (v15+0x5B)&~3. The feature-flag name (…BitmapSizeAlignFix) corroborates a bitmap allocation-alignment fix (CWE-131).
  • The out-of-bounds access is real at the ULONG-granularity level but is (a) confined to the only buffer op that matters (RtlSetBits, single-bit), (b) a value-preserving RMW, (c) contained within 16-byte pool rounding slack, and (d) not exposed to user mode — so it does not constitute an exploitable memory-corruption primitive on default systems.
  • RtlFindSetBits / RtlClearBit / RtlAreBitsClear were confirmed to operate on the persistent Scb bitmap at a2+0x140, not on the undersized allocation (verified in asm: lea rcx,[rdi+0x28], rdi=a2+0x118).
  • Not proven end-to-end:
  • The exact WriteFile offset/length -> v14 % 32 == 16 mapping and the reachability of the allocation branch (RtlAreBitsClear returning FALSE) are inferred from the arithmetic, not demonstrated.
  • The runtime default of the WIL feature flag is resolved by wil_details_IsEnabledFallback at run time and is not statically determinable, so whether the fix is active in shipped configurations cannot be confirmed from the binary alone.