ntfs.sys — $LogFile restart-table validation and integer-overflow checks made unconditional (CWE-20, CWE-190) — feature gate removed, fixed
KB5087538
1. Overview
- Unpatched Binary:
ntfs_unpatched.sys - Patched Binary:
ntfs_patched.sys - Overall Similarity Score:
0.9922 - Diff Statistics: 4047 matched functions, 10 changed functions, 4037 identical functions, and 0 unmatched functions in either direction.
- Verdict: The patch removes a WIL staging feature gate (
g_Feature_2859161914_59426036, evaluated byEvaluateCurrentState) from a set of NTFS Log File Service (LFS) restart-table routines. In the unpatched build, several restart-table consistency checks (zero entry-size/count detection, entry-count overflow detection, and a checked-addition on the entry count) only run when that feature evaluates true; when it evaluates false the checks are skipped. The patched build deletes the gate so the checks always run. The change is directionally a hardening: the patched build is strictly stricter. The reachable impact is modest — the affected structures are LFS restart tables (parsed from the$LogFilerestart area during volume mount/recovery), and the one integer-overflow site is fed by internally bounded counts, so no attacker-controlled kernel heap-overflow primitive is demonstrable from these binaries.
2. Vulnerability Summary
All affected routines operate on NTFS restart tables — the fixed-size, pool-allocated bookkeeping tables (open-attribute table, dirty-page table, transaction table) that the Log File Service uses during transaction logging and crash recovery. The on-disk source for the validated table is the volume's $LogFile restart area, which NtfsCheckRestartTable parses during mount/recovery.
The shared mechanism is a single WIL feature descriptor, g_Feature_2859161914_59426036_FeatureDescriptorDetails, queried through EvaluateCurrentState — the WIL feature-state evaluator (0x1C0030128 unpatched), not a resource-lock/ERESOURCE query. In the unpatched build the routines call EvaluateCurrentState and only execute their consistency checks when it returns non-zero (feature enabled); when it returns zero (feature disabled) the checks are branched over. The patched build removes every one of these calls from the restart-table routines (EvaluateCurrentState call count drops from 13 to 4), making the checks unconditional. The four remaining calls in the patched build are in an unrelated feature family (NtfsSetAllocationInfo, NtfsSetEndOfFileInfo, NtfsSetPositionInfo) that is still gated in both builds.
Finding 1: Gated integer-overflow check on restart-table growth
- Severity: Low
- Vulnerability Class: Integer Overflow (CWE-190) — defensive check made unconditional
- Affected Function:
NtfsExtendRestartTable(0x1C0024660unpatched,0x1C0024640patched) - Root Cause: When the feature is disabled, the function computes the new entry count with a direct 32-bit add (
add edx, edi) instead of the overflow-checkedRtlULongAdd. The result feedsInitializeNewTable(pool sizing) while the subsequentmemmovecopiesentry_size * old_countbytes. A wrap would produce an undersized allocation followed by an oversized copy. - Reachability: Not demonstrably attacker-controlled. The augend is the 16-bit
NumberEntriesfield ([rbx+2], ≤0xFFFF); the addend (edi/arg2) is a small growth increment supplied by callers (0x10fromNtfsAllocateRestartTableIndex; a division-derived count inNtfsAllocateRestartTableFromIndexandPageUpdateAnalysis). A 32-bit wrap requires an addend near0xFFFFFFFF, which none of the callers produce. TheRtlULongAddis therefore a corruption guard, not a reachable heap-overflow primitive.
Finding 2: Gated zero-size / count-overflow checks in restart-table deallocation
- Severity: Low
- Vulnerability Class: Missing Validation of restart-table fields (CWE-20)
- Affected Function:
NtfsAllocateRestartTableFromIndex(0x1C002DC38) - Root Cause: Two checks are gated: (1)
entry_size == 0 || NumberEntries == 0and (2) the in-use count ([rbx+4]) reaching0xFFFFor exceedingNumberEntriesafter increment. When the feature is disabled both are skipped. Unchecked, a corruptentry_size/count feeds thememsetsize and the free-list index arithmetic.
Finding 3: Gated header/count-overflow checks in restart-table allocation
- Severity: Low
- Vulnerability Class: Missing Validation / count overflow (CWE-20)
- Affected Function:
NtfsAllocateRestartTableIndex(0x1C000E124) - Root Cause: Same pattern as Finding 2: a gated
entry_size==0 || NumberEntries==0check and a gated post-increment count-overflow check (==0xFFFFor> NumberEntries). When skipped, the function can return a corrupt free-list index used by callers in pointer arithmetic.
Finding 4: Gated validation path in the on-disk restart-table validator
- Severity: Medium
- Vulnerability Class: Insufficient Validation of on-disk metadata (CWE-20)
- Affected Function:
NtfsCheckRestartTable(0x1C01BC2D4unpatched,0x1C01BC2C4patched) - Root Cause: The validator has two paths selected by the feature gate. With the feature enabled it runs the strong checks (
entry_size != 0,entry_size <= buffer_size,NumberEntries != 0,NumberInUse != 0, and a division-based check that the declared entry count fits the buffer). With the feature disabled it takes a weaker path, and a second gated check inside the free-list traversal (0x1C01BC49E) relaxes the entry-offset bounds test. This is the routine that parses the attacker-influenceable$LogFilerestart area during mount/recovery, so it carries the most meaningful reachable impact. The patched build removes both gates and always runs the strong path.
3. Pseudocode Diff
NtfsExtendRestartTable
// === UNPATCHED (0x1C0024660) ===
// Feature gate: overflow-checked add only when the feature is ENABLED.
if (EvaluateCurrentState(&g_Feature_2859161914_59426036) != 0) {
if (RtlULongAdd(NumberEntries, arg2, &new_count) < 0)
ExRaiseStatus(STATUS_DISK_CORRUPT_ERROR); // 0xC0000032
} else {
new_count = (uint32_t)NumberEntries + arg2; // unchecked 32-bit add
}
InitializeNewTable(entry_size, new_count, flags, Scb);
memmove(new_table + 0x18, old_table + 0x18, entry_size * NumberEntries);
// === PATCHED (0x1C0024640) ===
// Gate removed: overflow-checked add is unconditional.
if (RtlULongAdd(NumberEntries, arg2, &new_count) < 0)
ExRaiseStatus(STATUS_DISK_CORRUPT_ERROR);
InitializeNewTable(entry_size, new_count, flags, Scb);
memmove(new_table + 0x18, old_table + 0x18, entry_size * NumberEntries);
NtfsAllocateRestartTableFromIndex
// === UNPATCHED ===
// Zero-size / count checks gated behind the feature evaluation.
if (EvaluateCurrentState(&g_Feature_2859161914_59426036) != 0
&& (entry_size == 0 || NumberEntries == 0))
ExRaiseStatus(STATUS_DISK_CORRUPT_ERROR); // SKIPPED when feature disabled
memset(entry, 0, entry_size);
*entry = 0xffffffff;
in_use_count += 1;
if (EvaluateCurrentState(&g_Feature_2859161914_59426036) != 0) {
if (in_use_count == 0xffff || in_use_count > NumberEntries)
ExRaiseStatus(STATUS_DISK_CORRUPT_ERROR); // SKIPPED when feature disabled
}
// === PATCHED ===
if (entry_size == 0 || NumberEntries == 0)
ExRaiseStatus(STATUS_DISK_CORRUPT_ERROR);
memset(entry, 0, entry_size);
*entry = 0xffffffff;
in_use_count += 1;
if (in_use_count == 0xffff || in_use_count > NumberEntries)
ExRaiseStatus(STATUS_DISK_CORRUPT_ERROR);
(The same unconditional-check restoration applies to NtfsAllocateRestartTableIndex and to the single-path unification in NtfsCheckRestartTable, detailed in Section 4.)
4. Assembly Analysis
NtfsExtendRestartTable (0x1C0024660, unpatched)
00000001C002469E lea rcx, g_Feature_2859161914_59426036_FeatureDescriptorDetails
00000001C00246A5 call EvaluateCurrentState ; WIL feature evaluation
00000001C00246AA test eax, eax
00000001C00246AC jz loc_1C00246F5 ; feature OFF -> skip checked add
; --- feature ON: overflow-checked path ---
00000001C00246AE movzx ecx, word ptr [rbx+2] ; NumberEntries (augend)
00000001C00246B2 lea r8, [rsp+38h+pulResult]
00000001C00246B7 mov edx, edi ; arg2 (addend)
00000001C00246B9 call RtlULongAdd ; checked addition
00000001C00246BE test eax, eax
00000001C00246C0 jns loc_1C00246EF
00000001C00246C8 mov ebx, 0C0000032h ; STATUS_DISK_CORRUPT_ERROR
00000001C00246E2 call cs:__imp_ExRaiseStatus
; --- feature OFF: unchecked path ---
00000001C00246F5 movzx edx, word ptr [rbx+2]
00000001C00246F9 add edx, edi ; unchecked 32-bit add
00000001C00246FB movzx r8d, word ptr [rbx+6]
00000001C0024703 movzx ecx, word ptr [rbx]
00000001C0024706 call InitializeNewTable ; pool sizing uses new count
00000001C002470B movzx ecx, word ptr [rbx]
00000001C0024712 movzx eax, word ptr [rbx+2] ; original NumberEntries
00000001C002471D imul ecx, eax ; entry_size * original count
00000001C0024720 movsxd r8, ecx
00000001C0024727 call memmove ; copy old table body
NtfsExtendRestartTable (0x1C0024640, patched)
00000001C0024684 movzx ecx, r9w ; NumberEntries (augend)
00000001C0024688 lea r8, [rsp+38h+pulResult]
00000001C002468D call RtlULongAdd ; unconditional checked add
00000001C0024692 test eax, eax
00000001C0024694 jns loc_1C00246C3
00000001C002469C mov ebx, 0C0000032h ; STATUS_DISK_CORRUPT_ERROR
00000001C00246B6 call cs:__imp_ExRaiseStatus
00000001C00246D1 call InitializeNewTable
00000001C00246F2 call memmove
NtfsAllocateRestartTableFromIndex (0x1C002DC38, unpatched)
00000001C002DDCF lea rcx, g_Feature_2859161914_59426036_FeatureDescriptorDetails
00000001C002DDD6 call EvaluateCurrentState ; gate #1
00000001C002DDDB test eax, eax
00000001C002DDDD jz loc_1C002DDFC ; feature OFF -> skip zero-size check
00000001C002DDDF cmp word ptr [rbx], di ; entry_size == 0 ?
00000001C002DDE2 jz loc_1C002DDEA
00000001C002DDE4 cmp word ptr [rbx+2], di ; NumberEntries == 0 ?
00000001C002DDE8 jnz loc_1C002DDFC
00000001C002DDFC movzx r8d, word ptr [rbx] ; size for memset
00000001C002DE05 call memset
00000001C002DE0D lea rcx, g_Feature_2859161914_59426036_FeatureDescriptorDetails
00000001C002DE14 add word ptr [rbx+4], r12w ; in-use count += 1
00000001C002DE19 call EvaluateCurrentState ; gate #2
00000001C002DE1E test eax, eax
00000001C002DE20 jz loc_1C002DE76 ; feature OFF -> skip overflow check
00000001C002DE22 movzx eax, word ptr [rbx+4]
00000001C002DE26 mov ecx, 0FFFFh
00000001C002DE2B cmp ax, cx ; count == 0xffff ?
00000001C002DE2E jz loc_1C002DE36
00000001C002DE30 cmp ax, word ptr [rbx+2] ; count > NumberEntries ?
00000001C002DE34 jbe loc_1C002DE76
NtfsCheckRestartTable (0x1C01BC2D4, unpatched)
00000001C01BC2F6 lea rcx, g_Feature_2859161914_59426036_FeatureDescriptorDetails
00000001C01BC300 call EvaluateCurrentState ; path-selection gate
00000001C01BC305 movzx r9d, word ptr [rdi] ; entry_size
00000001C01BC30C test eax, eax
00000001C01BC30E jz loc_1C01BC381 ; feature OFF -> weaker path
; --- feature ON: strong path ---
00000001C01BC310 test r9w, r9w ; entry_size != 0
00000001C01BC31A cmp r10d, ebx ; entry_size <= buffer_size
00000001C01BC31F movzx r8d, word ptr [rdi+2] ; NumberEntries != 0
00000001C01BC32A movzx r11d, word ptr [rdi+4] ; NumberInUse != 0
; ... second gate inside free-list traversal ...
00000001C01BC49E lea rcx, g_Feature_2859161914_59426036_FeatureDescriptorDetails
00000001C01BC4A5 call EvaluateCurrentState
00000001C01BC4AA test eax, eax
00000001C01BC4AC jz loc_1C01BC4CE ; relaxed offset-bounds path
In the patched build (0x1C01BC2C4) the routine has no EvaluateCurrentState calls; it enters directly at the strong checks (entry_size != 0, entry_size <= buffer_size, NumberEntries != 0, NumberInUse != 0, then div to confirm the entry count fits the buffer).
5. Trigger Conditions
- Volume/log preparation: The validated table originates from the
$LogFilerestart area of a mounted NTFS volume. ReachingNtfsCheckRestartTablewith attacker-influenced fields requires presenting a crafted NTFS volume (VHD or physical media) whose log restart area is malformed. - Recovery/mount: Mount or otherwise trigger LFS recovery so the restart area is parsed and the restart tables are built, validated (
NtfsCheckRestartTable), and grown (NtfsExtendRestartTable,NtfsAllocateRestartTableIndex,NtfsAllocateRestartTableFromIndex). - Feature state: The bypass only exists when
g_Feature_2859161914_59426036evaluates false in the unpatched build. When it evaluates true (or in the patched build, always), the consistency checks run and malformed tables raiseSTATUS_DISK_CORRUPT_ERROR(0xC0000032). - Observable effect (if checks are absent): A malformed restart table with an inconsistent
entry_size/NumberEntries/NumberInUsecan drive incorrect free-list index arithmetic andmemset/pool sizing. This can surface asSTATUS_DISK_CORRUPT_ERRORwhen the checks are present, or as out-of-bounds access when they are skipped. A controlled, attacker-sized kernel heap overflow is not demonstrable from these binaries because the growth addend inNtfsExtendRestartTableis not attacker-scaled (see Finding 1).
6. Impact Assessment
- Nature: A staged consistency/overflow-check feature for LFS restart tables is made unconditional. The patched build is strictly stricter; the unpatched build skips the checks only when the WIL feature evaluates false.
- Reachable impact: Bounded.
NtfsCheckRestartTableparses the$LogFilerestart area, so its fields are influenceable by a crafted volume; skipping its strong path lets malformed restart tables through to routines that useentry_size/count fields formemsetsizing, pool sizing, and free-list index math. That is a plausible out-of-bounds risk on a mount-a-malicious-volume (local, physical/VHD) attack surface. - Not demonstrable: The integer overflow in
NtfsExtendRestartTable(Finding 1) is not reachable with attacker magnitude — the augend is a 16-bit field and every caller supplies a small growth addend, soNumberEntries + arg2cannot wrap 32 bits. No arbitrary-size pool overflow, information leak, or code-execution primitive is provable from these two binaries.
7. Debugger Notes
Useful breakpoints for observing the gate behavior in the unpatched build:
bp ntfs!NtfsExtendRestartTable ; 0x1C0024660
bp 0x1C00246A5 ; call EvaluateCurrentState (feature eval)
bp 0x1C00246F5 ; unchecked add path (feature OFF)
bp 0x1C0024727 ; memmove of the old table body
bp ntfs!NtfsCheckRestartTable ; 0x1C01BC2D4
bp 0x1C01BC300 ; path-selection feature eval
0x1C00246A5: aftercall EvaluateCurrentState,EAX == 0means the feature is disabled and the checked add is skipped (je 0x1C00246F5).0x1C00246F9(add edx, edi):EDX= zero-extendedNumberEntries([rbx+2]),EDI= growth addend. Both are small in practice.0x1C0024727(memmove):R8=entry_size * NumberEntries(original),RCX= new table body.0x1C01BC300:EAX != 0selects the strong validation path;EAX == 0selects the weaker path.
Restart-table header layout (structure at rbx/rdi)
+0x00(WORD)EntrySize+0x02(WORD)NumberEntries(allocated)+0x04(WORD)NumberInUse(in-use / free counter)+0x06(WORD)Flags(bit 0 selects fixed-size vs computed-size table layout)+0x0C(DWORD)FirstFreeEntryoffset+0x10(DWORD) free-list head offset+0x14(DWORD) last-free / hint offset+0x18(BYTE[]) entry data
8. Changed Functions — Full Triage
| Function Name (address) | Similarity | Change Type | Note |
|---|---|---|---|
NtfsExtendRestartTable (0x1C0024660) |
0.9096 | Security Relevant | Removes the feature gate; the RtlULongAdd overflow check on NumberEntries + growth is now unconditional. Overflow itself not attacker-reachable (Finding 1). |
NtfsAllocateRestartTableFromIndex (0x1C002DC38) |
0.9651 | Security Relevant | Removes the gate; zero entry-size/count and in-use-count overflow checks now unconditional. |
NtfsAllocateRestartTableIndex (0x1C000E124) |
0.8786 | Security Relevant | Removes the gate; zero entry-size/count and entry-count overflow checks now unconditional. |
NtfsCheckRestartTable (0x1C01BC2D4) |
0.8353 | Security Relevant | Unifies the two feature-selected validation paths into a single strict path (the on-disk $LogFile restart-area validator). |
InitializeNewTable (0x1C0019D10) |
0.9321 | Behavioral | Restart-table pool allocator; feature gate removed from a NumberInUse==0 early check before NtfsLinkRestartTable. |
PageUpdateAnalysis (0x1C0205D04) |
0.9572 | Behavioral | Removes the gate around a NumberEntries >= required bounds check; caller of the restart-table routines. |
EvaluateCurrentState (0x1C0030128) |
0.6686 | Behavioral | The WIL feature-state evaluator itself; refactored (relocated to 0x1C00303D0, signature simplified). Not the vulnerability. |
NtfsSetPositionInfo (0x1C01D2D80) |
0.9854 | Cosmetic | Feature call retargeted to the refactored evaluator; separate feature family, still gated in both builds. |
NtfsSetAllocationInfo (0x1C00DFA10) |
0.9928 | Cosmetic | Same evaluator retargeting; separate feature, gated in both builds. |
NtfsSetEndOfFileInfo (0x1C011B630) |
0.9929 | Cosmetic | Same evaluator retargeting; separate feature, gated in both builds. |
9. Unmatched Functions
There are no unmatched functions in either build. All changes occurred inline within existing functions. An independent function-level diff of the two decompilations found no additional security-relevant change outside the ten functions above.
10. Confidence & Caveats
- Confidence: High on the mechanism and direction. The gate is
EvaluateCurrentState(g_Feature_2859161914_59426036)(a WIL staging feature), not a resource-lock query; the unpatched build skips the restart-table checks when the feature is disabled, and the patched build removes the gate so they always run.EvaluateCurrentStatecall sites drop from 13 to 4 across the two builds, all restart-table sites removed. - Scope of impact: The affected structures are LFS restart tables, parsed from the
$LogFilerestart area during mount/recovery — a local, crafted-volume attack surface. The strongest reachable concern is thatNtfsCheckRestartTable's weak path lets malformed restart tables through to size/index computations. A specific, attacker-controlled kernel heap-overflow, information leak, or code-execution primitive is not provable from these binaries. - On the integer overflow: The
RtlULongAddinNtfsExtendRestartTableguardsNumberEntries (WORD) + growth. Every caller supplies a small growth increment, so a 32-bit wrap is not reachable; the check is defensive hardening.