1. Overview

Field Value
Unpatched binary ntfs_unpatched.sys
Patched binary ntfs_patched.sys
Overall similarity 0.9924
Matched functions 4047
Changed functions (behavioral) 3
Changed functions (tracing metadata only) 14
Unmatched (unpatched) 0
Unmatched (patched) 0

Verdict: In the NTFS transaction-log ($LogFile) restart/recovery path, three validation checks — a copy-loop index bounds check, a restart-table size-consistency check, and an integer-overflow-guarded size check before a restart-table memmove — were gated behind a runtime feature-staging flag (g_Feature_2076398905_59354624, evaluated through EvaluateCurrentState/EvaluateFeature). The patch removes that feature gate in all three functions, so the checks now execute unconditionally. This is a real, correct-direction hardening of NTFS log-recovery parsing of on-disk restart-table structures. It is delivered by completing a staged rollout of these checks, not by adding a new mechanism; the runtime default state of the feature in the unpatched build cannot be determined from the binaries, so a reachable overflow in the shipped default configuration is not demonstrated here.

The gate is a WIL feature-staging flag, not a checked-build (#if DBG) assertion: EvaluateCurrentState is present and executed on retail builds. Fourteen additional functions differ only in WPP software-tracing metadata (trace line-number immediates and the per-build trace-GUID control-block symbol) and carry no logic change.


2. Vulnerability Summary

Finding 1 — Medium: Feature-gated out-of-bounds write bounds check in PageUpdateAnalysis

Field Detail
Severity Medium
CWE CWE-787 (Out-of-bounds Write)
Function PageUpdateAnalysis (unpatched 0x1C0205DC4, patched 0x1C0205D04)
Entry point Crafted NTFS volume (VHD / USB) whose $LogFile restart area is parsed during volume mount / log recovery

Root cause: The function copies extent entries from a source restart-table record (r15, loaded from [rsi+0x48]) into a destination restart-table entry (rdi) allocated by NtfsAllocateRestartTableIndex. The destination write index is computed as ecx = [r15+0x18] − [rdi+0x10] + loop_counter, where [r15+0x18] is a VCN-style field taken from the on-disk log record. A bounds check cmp ecx, [rdi+0xc] / jnb (index vs. the destination capacity at +0x0c) exists in both builds, but in the unpatched build it is reached only when EvaluateCurrentState(&g_Feature_2076398905_59354624) returns non-zero: the test eax,eax / jz at 0x1C020606F0x1C0206071 skips the cmp/jnb when the feature evaluates to zero, and the 8-byte write at 0x1C0206083 then proceeds with an unchecked index. The patch removes the EvaluateCurrentState call, the test, and the jz, so the bounds check always guards the write.

Call chain:

  1. Mount a crafted NTFS volume (VHD / USB); NTFS performs $LogFile restart/recovery.
  2. NtfsRestartVolume (0x1C016D7C0).
  3. AnalysisPass (0x1C0200EFC) — restart analysis pass over the restart tables.
  4. PageUpdateAnalysis (0x1C0205DC4) — restart-table entry copy at 0x1C02012A3.
  5. 8-byte write at 0x1C0206083: mov [rdi+rcx*8+0x20], rax.

Finding 2 — Low: Feature-gated restart-table size-consistency check in ReadRestartTable

Field Detail
Severity Low
CWE CWE-20 (Improper Input Validation)
Function ReadRestartTable (unpatched 0x1C020634C, patched 0x1C0206294)
Entry point Crafted NTFS volume whose $LogFile restart record has inconsistent size / entry-count fields

Root cause: The function computes an expected restart-table size edi = (entry_count << 3) + 0x20 (or 0x28 when entry_count <= 1) from [r15+0x0e] and compares it to the declared size [r15+0x04]. In the unpatched build the comparison cmp edi, [r15+4] / jnz is gated behind EvaluateCurrentState(&g_Feature_2076398905_59354624) (the test eax,eax / jz at 0x1C02064010x1C0206403). When the feature evaluates to zero the mismatch is not raised as corruption, and the function proceeds to call the validator NtfsCheckRestartTable (0x1C01BC394) with a length of [rsi+0x40] − declared_size. The patch makes the size-consistency comparison unconditional. NtfsCheckRestartTable is a restart-table validation routine, not a memory-copy primitive; the effect of a bypassed check here is a mis-validated restart table rather than a demonstrated out-of-bounds copy.

Call chain:

  1. Mount a crafted NTFS volume; $LogFile restart/recovery.
  2. NtfsRestartVolume (0x1C016D7C0) → InitializeRestartState (0x1C016DA08).
  3. ReadRestartTable (0x1C020634C) at 0x1C016DEF8.
  4. NtfsCheckRestartTable (0x1C01BC394) called with a length derived from the declared size.

Finding 3 — Medium: Feature-gated integer-overflow-guarded size check before restart-table memmove in InitializeRestartState

Field Detail
Severity Medium
CWE CWE-190 (Integer Overflow) leading to CWE-787 (Out-of-bounds Write)
Function InitializeRestartState (0x1C016DA08, same address both builds)
Entry point Same crafted-volume $LogFile restart/recovery surface

Root cause: This is the top-level restart-state setup function that calls ReadRestartTable and copies restart-table entries with memmove. It contains three EvaluateCurrentState(&g_Feature_2076398905_59354624) gates (at 0x1C016DF20, 0x1C016E01B, 0x1C016E11D). At the copy site around 0x1C016E01B, when the feature evaluates to non-zero the size for the copy is computed with RtlULongMult((count−1), 8) (an overflow-checked multiply) before the bound compare and the memmove at 0x1C016E099; when the feature evaluates to zero the size is computed with a plain shl rcx, 3 that performs no overflow detection before the same compare. The patch removes all three gates, so the overflow-checked computation and the associated size checks run unconditionally.


3. Pseudocode Diff

PageUpdateAnalysis — copy-loop bounds check

// ============ UNPATCHED (0x1C0206051-0x1C0206091) ============
for (ebx = 0; ebx < src->entry_count /* [r15+0x0e] */; ebx++) {

    // feature gate: check reached only when EvaluateCurrentState != 0
    int enabled = EvaluateCurrentState(&g_Feature_2076398905_59354624);

    unsigned idx = src->vcn        /* [r15+0x18] */
                 - dst->start_vcn  /* [rdi+0x10] */
                 + ebx;

    if (enabled && idx >= dst->capacity /* [rdi+0x0c] */)
        NtfsRaiseStatus(STATUS_DISK_CORRUPT);   // skipped when enabled == 0

    dst->extents[idx] = src->extents[ebx];      // 8-byte write, 0x1C0206083
}

// ============ PATCHED (0x1C0205F9C-0x1C0205FDB) ============
for (r8d = 0; r8d < src->entry_count; r8d++) {

    unsigned idx = src->vcn - dst->start_vcn + r8d;

    if (idx >= dst->capacity)                   // unconditional
        NtfsRaiseStatus(STATUS_DISK_CORRUPT);

    dst->extents[idx] = src->extents[r8d];
}

ReadRestartTable — size-consistency check

// ============ UNPATCHED (0x1C02063D3-0x1C0206428) ============
int computed = (entry_count <= 1) ? 0x28
                                   : (entry_count << 3) + 0x20;  // [r15+0x0e]

if (EvaluateCurrentState(&g_Feature_2076398905_59354624)
    && computed != declared_size /* [r15+0x04] */)
    NtfsRaiseStatus(STATUS_DISK_CORRUPT);        // skipped on feature == 0

NtfsCheckRestartTable(entry + computed,
                      length - declared_size /* [rsi+0x40] - [r15+0x04] */, ...);

// ============ PATCHED (0x1C0206315-0x1C0206355) ============
if (computed != declared_size)                   // unconditional
    NtfsRaiseStatus(STATUS_DISK_CORRUPT);
NtfsCheckRestartTable(...);

4. Assembly Analysis

PageUpdateAnalysis — Unpatched copy loop (0x1C0205DC4)

00000001C0206051  mov     ebx, esi
00000001C0206053  cmp     si, [r15+0Eh]                 ; counter vs entry count
00000001C0206058  jnb     short loc_1C0206093           ; loop exit
00000001C020605A  lea     rcx, g_Feature_2076398905_59354624_FeatureDescriptorDetails
00000001C0206061  call    EvaluateCurrentState          ; runtime feature evaluation
00000001C0206066  mov     ecx, [r15+18h]                ; src VCN field (from on-disk log record)
00000001C020606A  sub     ecx, [rdi+10h]                ; minus dest start VCN
00000001C020606D  add     ecx, ebx                       ; plus loop counter -> write index
00000001C020606F  test    eax, eax                       ; feature result
00000001C0206071  jz      short loc_1C020607C           ; feature == 0 -> skip bounds check
00000001C0206073  cmp     ecx, [rdi+0Ch]                ; index vs capacity
00000001C0206076  jnb     loc_1C020612E                 ; -> STATUS_DISK_CORRUPT
00000001C020607C  mov     eax, ebx
00000001C020607E  mov     rax, [r15+rax*8+20h]          ; read source extent
00000001C0206083  mov     [rdi+rcx*8+20h], rax          ; 8-byte write at computed index
00000001C0206088  inc     ebx
00000001C020608A  movzx   eax, word ptr [r15+0Eh]
00000001C020608F  cmp     ebx, eax
00000001C0206091  jmp     short loc_1C0206058

PageUpdateAnalysis — Patched copy loop (0x1C0205D04)

00000001C0205F9C  mov     r8d, r14d
00000001C0205F9F  cmp     r14w, [r13+0Eh]               ; counter vs entry count
00000001C0205FA4  jnb     short loc_1C0205FDD
00000001C0205FA6  mov     r9, [rsp+188h+var_148]        ; dest restart-table entry
00000001C0205FAB  mov     ecx, [r13+18h]                ; src VCN field
00000001C0205FAF  sub     ecx, [r9+10h]                 ; minus dest start VCN
00000001C0205FB3  add     ecx, r8d                       ; plus loop counter -> write index
00000001C0205FB6  cmp     ecx, [r9+0Ch]                 ; index vs capacity (unconditional)
00000001C0205FBA  jnb     short loc_1C0205FFE           ; -> STATUS_DISK_CORRUPT
00000001C0205FBC  mov     eax, r8d
00000001C0205FBF  mov     edx, ecx
00000001C0205FC1  mov     rcx, [r13+rax*8+20h]          ; read source extent
00000001C0205FC6  mov     rax, [rsp+188h+var_148]
00000001C0205FCB  mov     [rax+rdx*8+20h], rcx          ; 8-byte write at checked index
00000001C0205FD0  inc     r8d
00000001C0205FD3  movzx   eax, word ptr [r13+0Eh]
00000001C0205FD8  cmp     r8d, eax
00000001C0205FDB  jb      short loc_1C0205FAB

The unpatched build has the sequence lea rcx, g_Feature_2076398905_59354624 / call EvaluateCurrentState / test eax,eax / jz gating the cmp ecx,[rdi+0xc] / jnb. The patch removes those four instructions; the cmp/jnb always executes.

ReadRestartTable — Unpatched key sequence (0x1C020634C)

00000001C02063D3  movzx   eax, word ptr [r15+0Eh]       ; entry count
00000001C02063DE  cmp     ax, r14w
00000001C02063E2  jbe     short loc_1C02063F0
00000001C02063E7  lea     edi, ds:20h[rax*8]            ; computed = (count<<3)+0x20
00000001C02063EE  jmp     short loc_1C02063F5
00000001C02063F0  mov     edi, 28h
00000001C02063F5  lea     rcx, g_Feature_2076398905_59354624_FeatureDescriptorDetails
00000001C02063FC  call    EvaluateCurrentState
00000001C0206401  test    eax, eax
00000001C0206403  jz      short loc_1C0206412           ; feature == 0 -> skip size check
00000001C0206405  movzx   eax, word ptr [r15+4]         ; declared size
00000001C020640A  cmp     edi, eax
00000001C020640C  jnz     loc_1C02064CD                 ; -> STATUS_DISK_CORRUPT
00000001C0206412  mov     r12d, edi
00000001C0206415  add     r12, r15
00000001C0206420  sub     edx, eax                       ; length = [rsi+40h] - declared size
00000001C0206428  call    NtfsCheckRestartTable

In the patched build (0x1C0206294) the same region reads ... lea edi, ds:20h[rax*8] ... movzx ecx, word ptr [r8+4] / cmp edi, ecx / jnz <corrupt> with no EvaluateCurrentState gate.

InitializeRestartState — Unpatched gated copy path (0x1C016DA08)

00000001C016E01B  lea     rcx, g_Feature_2076398905_59354624_FeatureDescriptorDetails
00000001C016E022  call    EvaluateCurrentState
00000001C016E02A  mov     r10d, [rbx+0Ch]               ; entry count
00000001C016E02E  lea     ecx, [r10-1]
00000001C016E032  test    eax, eax
00000001C016E034  jz      short loc_1C016E06F           ; feature == 0 -> plain shl path
00000001C016E03E  lea     edx, [r11+8]
00000001C016E042  call    RtlULongMult                  ; overflow-checked (count-1)*8
00000001C016E047  test    eax, eax
00000001C016E049  js      loc_1C016E9E6                 ; overflow -> error
00000001C016E064  cmp     rcx, rax
00000001C016E067  jb      loc_1C016E9E6
00000001C016E06D  jmp     short loc_1C016E08A
00000001C016E06F  shl     rcx, 3                         ; no overflow check
00000001C016E081  cmp     rdx, rcx
00000001C016E084  jb      loc_1C016EA64
00000001C016E08A  mov     r8d, r10d
00000001C016E08D  shl     r8, 3
00000001C016E091  lea     rdx, [r13+24h]                ; Src
00000001C016E095  lea     rcx, [rbx+20h]                ; Dst
00000001C016E099  call    memmove

The patch removes the three EvaluateCurrentState gates in this function, so the overflow-checked RtlULongMult path and the associated size compares run unconditionally.


5. Trigger Conditions

  1. Prepare a crafted NTFS volume image (VHD or raw disk) whose transaction log ($LogFile) restart area and restart tables contain the manipulated fields below.

  2. Manipulate the restart-record fields consumed by the recovery path:

  3. ReadRestartTable reads the entry count at [record+0x0e] and the declared size at [record+0x04]; a mismatch with (count<<3)+0x20 is the input to Finding 2.
  4. PageUpdateAnalysis reads the source VCN field at [record+0x18] and the entry count at [record+0x0e]; the write index is source_vcn − dest_start_vcn + counter, compared against the destination capacity at [dest+0x0c] (Finding 1).
  5. InitializeRestartState reads the entry count at [entry+0x0c] used to size the restart-table memmove (Finding 3).

  6. Mount the volume (for example CreateVirtualDisk + AttachVirtualDisk, or attaching a physical device). NTFS runs log recovery on mount, reaching NtfsRestartVolume → InitializeRestartState → ReadRestartTable and NtfsRestartVolume → AnalysisPass → PageUpdateAnalysis.

  7. Observed behavior. On the unpatched build, whether the gated checks execute depends on the runtime state of g_Feature_2076398905_59354624. When a gated check does not execute, an inconsistent restart record is processed without that check; the patched build always raises STATUS_DISK_CORRUPT (0xC0000032) on the same inconsistency. A demonstrated pool overflow was not reproduced against the shipped default configuration.


6. Impact Assessment

  • Operation guarded (Finding 1): an 8-byte write into the destination restart-table entry's extent array at index source_vcn − dest_start_vcn + counter. If the feature-gated bounds check does not run and a crafted log record supplies an index beyond [dest+0x0c], the write lands past the allocation, which is why the corrected CWE is CWE-787.
  • Values involved: both the index and the written 8 bytes derive from the on-disk restart record, so a successful bypass would be a controllable pool write. This is stated as the guarded operation, not as a reproduced primitive.
  • Not demonstrated: the runtime default of the feature flag in the unpatched build, a concrete out-of-range index surviving the other log-record validators (NtfsCheckLogRecord, NtfsCheckRestartTable), and any exploitation beyond the write itself. Those claims are therefore not made here.

7. Verification Notes

Addresses use the preferred image base 0x1C000000; actual runtime addresses are the loaded module base plus the offset.

Finding 1 — PageUpdateAnalysis

Offset (unpatched) Instruction Role
0x1C0206061 call EvaluateCurrentState Feature evaluation — removed in patch
0x1C020606F test eax, eax Feature result — removed in patch
0x1C0206071 jz 0x1C020607C Skips bounds check when feature == 0 — removed in patch
0x1C0206073 cmp ecx, [rdi+0xc] Bounds check — unconditional in patch
0x1C0206076 jnb 0x1C020612E Branch to STATUS_DISK_CORRUPT
0x1C0206083 mov [rdi+rcx*8+0x20], rax The guarded 8-byte write

Restart-table entry fields referenced by the code: capacity at +0x0c, start VCN at +0x10, source VCN field at +0x18, entry count at +0x0e, extent array at +0x20.

Finding 2 — ReadRestartTable

Offset (unpatched) Instruction Role
0x1C02063D3 movzx eax, word [r15+0xe] Reads entry count
0x1C02063F5 lea rcx, g_Feature_2076398905_59354624 Feature descriptor — removed in patch
0x1C02063FC call EvaluateCurrentState Feature evaluation — removed in patch
0x1C02064010x1C0206403 test eax,eax / jz Skips size check when feature == 0 — removed in patch
0x1C020640A0x1C020640C cmp edi,eax / jnz Size-consistency check — unconditional in patch

Finding 3 — InitializeRestartState

Feature gates at 0x1C016DF20, 0x1C016E01B, 0x1C016E11D; the overflow-checked RtlULongMult size computation at 0x1C016E042 guards the restart-table memmove at 0x1C016E099.


8. Changed Functions — Full Triage

Behavioral changes (feature gate removed)

PageUpdateAnalysis (unpatched 0x1C0205DC4, patched 0x1C0205D04) — One of two EvaluateCurrentState(&g_Feature_2076398905_59354624) gates removed (the copy-loop gate). The cmp ecx,[rdi+0xc] / jnb bounds check, present in both builds, now runs unconditionally. Loop counter register changed (ebxr8d) and WPP trace-line immediates shifted (0x1E1758/0x1E17CD0x1E17AB/0x1E1739); the second feature gate on g_Feature_2859161914_59426036 is present in both builds and unchanged.

ReadRestartTable (unpatched 0x1C020634C, patched 0x1C0206294) — The single EvaluateCurrentState gate on the (count<<3)+0x20 size-consistency check removed; the cmp/jnz now runs unconditionally. Stack frame and register allocation shifted; remaining differences are WPP trace-constant shifts.

InitializeRestartState (0x1C016DA08, same address both builds) — All three EvaluateCurrentState gates removed. The overflow-checked RtlULongMult size path and the associated compares now run unconditionally ahead of the restart-table memmove copies. Stack frame reduced 0x4900x480 and registers reallocated; memmove call count is unchanged (both builds copy at the same sites).

Tracing-metadata-only changes (not security relevant)

Fourteen further functions differ only in WPP software-tracing metadata — trace line-number immediates and/or the per-build trace-GUID control-block symbol — with no control-flow, bounds-check, or copy change: AnalysisPass, DoAction, NtfsAbortTransaction, OpenAttributeForRestart, OpenAttributesForRestart, NtfsCloseAttributesFromRestart, NtfsIsTransactionCommitted, RedoPass, UndoPass, WPP_SF_qDLi, WPP_SF_qDLiD, WPP_SF_qDLiDDL, WPP_SF_qDLii, WPP_SF_qDLiiiiiiiii.


9. Unmatched Functions

No functions were added or removed. The patch consists entirely of in-place modifications: the removal of the g_Feature_2076398905_59354624 feature gate from three restart-recovery functions, plus tracing-metadata churn in fourteen others.

Implication: The validation logic was already present in the codebase behind a runtime WIL feature-staging flag. The patch promotes those checks to unconditional execution. No new validation helper or sanitizer was introduced.


10. Confidence & Caveats

Confidence: High for the mechanism; the exploitability is not established

The mechanism is verified directly against both builds: g_Feature_2076398905_59354624 is referenced five times across the three functions in the unpatched build and zero times in the patched build, and EvaluateCurrentState calls EvaluateFeature (WIL feature staging), so the gate is a runtime feature flag evaluated on retail builds — not a checked-build (#if DBG) assertion. The direction is confirmed: the patched build is strictly stricter (the checks always run).

What is and is not claimed

  1. Claimed: three real validation checks in NTFS $LogFile restart-table recovery were feature-gated in the unpatched build and made unconditional in the patched build; the guarded write in PageUpdateAnalysis uses an index and value derived from on-disk log-record fields.
  2. Not claimed: a reproduced pool overflow, the runtime default state of the feature flag in the unpatched build, or any exploitation chain. These could not be determined from the binaries and are not asserted.

What to verify to raise or lower severity

  • The runtime default of g_Feature_2076398905_59354624 in the unpatched build (enabled-by-default would mean the checks already ran on default retail; disabled-by-default would mean they did not).
  • Whether the upstream log-record validators (NtfsCheckLogRecord, NtfsCheckRestartTable) already constrain the entry count / VCN fields enough to prevent an out-of-range index reaching PageUpdateAnalysis.