cimfs.sys — signed integer-underflow hardening in CIM image metadata parser (CWE-190) fixed
KB5073724
1. Overview
| Item | Value |
|---|---|
| Unpatched binary | cimfs_unpatched.sys |
| Patched binary | cimfs_patched.sys |
| Overall similarity | 0.3743 (low — large recompile) |
| Matched functions | 176 |
| Changed functions | 141 |
| Identical functions | 35 |
| Unmatched (unpatched → patched) | 0 / 0 |
Verdict: The patch is a defense-in-depth hardening pass over the Composite Image File System (cimfs.sys) on-disk metadata parser. Across the Cim::ImageReader read/resolve helpers and the Cim::LinkTable::Parse family, unsigned-only comparisons on values derived from on-disk metadata are replaced with signed negative/underflow guards (js / jl / jg, and signed division via cqo; sar). The same recompile also removes the CimFs::StackEventTracer reference parameter from these functions and routes error reporting through inline ETW instead of ProcessTraceStatusOrigin.
These are real, security-relevant hardening changes in an attacker-facing parsing surface (a crafted CIM image is mounted, then file operations are performed). However, the audit against both binaries did not find a demonstrably reachable out-of-bounds primitive in the unpatched build: the offsets fed to the checks are masked to 48 bits and cross-validated before use, the copy operations are bounds-checked in both builds, and the link-table counts derive from a non-negative 32-bit field. There is no out-of-bounds write on any of these paths. The added guards remove a latent class of signed/unsigned confusion; the realistic worst-case direction, were any of the guarded values reachable with the sign bit set, is an out-of-bounds read (CWE-125). Severity is therefore assessed as low (defense-in-depth), not high. Exploitation of the parsing surface at all requires the privilege to mount a CIM volume.
2. Change Summary
The diff shows one consistent pattern applied in three related areas, all reachable from the volume-mount + file-operation surface.
Finding A — Signed underflow guards added to Cim::ImageReader::GetData (LOW / defense-in-depth)
- Class: CWE-190 integer underflow handling in a bounds check (read-side, CWE-125 impact direction).
- Affected functions:
Cim::ImageReader::GetData(unpatched 0x140018B08 → patched 0x1C000A1AC),GetDataTruncate(0x140018D68 → 0x1C0009F90),GetOffsetTruncate(0x140019AA4 → 0x1C0009CB8), and theGetArray<>templates.
What actually changed. In the unpatched build the in-region offset (rcx, resolved by GetOffsetTruncate) is validated against the region-table size (rax) with an unsigned compare before the subtraction: cmp rax, rcx; jb terminate. That branch guarantees size >= offset, so the following sub rax, rcx cannot underflow, and the second unsigned compare cmp rax, rdi; jb terminate rejects a request larger than the remaining bytes. The patched build removes the pre-subtraction unsigned guard and instead adds signed sanity checks: test rcx, rcx; js terminate on the offset, sub rax, rcx; js terminate on the subtraction result, test rdi, rdi; js terminate on the requested size, and a signed cmp rax, rdi; jl terminate. The net effect is that no intermediate value can be treated as negative (sign bit set).
Reachability note. The offset is not fully attacker-controlled at this point. GetOffsetTruncate masks the in-region offset to 48 bits (and r10, 0xFFFFFFFFFFFF) in both builds, so it is always non-negative, and it bound-checks the offset against the region entry before returning. The region index is the top 16 bits of the packed RegionOffset, also non-negative by construction. The added signed checks therefore guard values that, on the paths observed, are already masked and pre-validated. No input that passes the unpatched unsigned checks but produces an out-of-bounds span was demonstrated.
Finding B — Fixed-size File copy in Cim::ImageReader::GetFile (LOW)
- Class: refactor of a bounded copy; removal of a feature-gated zero-initialization.
- Affected function:
Cim::ImageReader::GetFile(unpatched 0x140018FB4 → patched 0x1C000A3DC).
What actually changed. GetFile reads a 16-bit entry_size field from a FileTableDirectoryEntry (loaded movzx at 0x14001938B) and compares it against 0x68 (cmp dx, r8w; jbe at 0x1400193A6). When entry_size exceeds 0x68 the entry is not rejected: the code emits an "Invalid entry size" trace and continues to the GetData call at 0x1400194E4 with entry_size as the read length and entry_size * slot as the offset. This is not an out-of-bounds access: GetData bounds-checks the requested read against the region (Finding A), and the subsequent gsl::copy is itself bounds-checked — the copy length is min(entry_size, data_len) (cmovb at 0x140019673) and the destination span is {0x68, File*}; if the length exceeds the destination, gsl::copy calls gsl::details::terminate() rather than overflowing. So an oversized entry_size yields at most a controlled termination, in both builds.
The patched build replaces the variable copy length with a fixed 0x58 clamped to the returned data length using signed comparisons (mov edx, 0x58; cmovg; test rcx,rcx; js; cmp rcx,rax; jg). It also drops the Feature_CimfsV1CompatGetFileFix__private-gated memset(File, 0, 0x68) that, in the unpatched build, zero-initialized the output File struct only when the feature was enabled. Removing that gate is a hardening of the uninitialized-tail behavior, but the File struct is an internal structure consumed by later QueryInformation handling, so any residual-data exposure is not shown to reach an unprivileged caller.
Finding C — Signed count arithmetic in Cim::LinkTable::Parse (LOW / defense-in-depth)
- Class: CWE-190 integer handling in metadata parsing.
- Affected functions:
Cim::LinkTable<Stream>::Parse(0x140029E8C → 0x1C000F324) andCim::LinkTable<FileId>::Parse(0x14002A038 → 0x1C000EE4C).
What actually changed. Both builds reject a span smaller than 8 bytes (cmp r9, 8; jb error) and require an 8-byte-aligned data pointer (test bl, 7; jnz error); there is no size-7 shortcut. The unpatched build computes the entry count from (size - 8) with unsigned shifts and compares (shr r11, 4; ja/jb). The patched build replaces these with signed division (cqo; and edx, mask; add; sar) and signed comparisons (test r9,r9; js; cmp r8,rax; jg; sub r9,rax; js). The disk_count field compared against the computed count is read as a zero-extended 32-bit value (mov r8d, [rax+4]), so it is always non-negative; the on-disk span size would have to reach roughly 2^63 for (size - 8) to appear negative. No reachable input that trips the new signed checks in the unpatched build was demonstrated. This is the standard compiler-emitted signed-division pattern applied as hardening.
Entry point: the link tables are parsed during MountVolumeFromFileSystem via Cim::ImageReader::GetStruct<Filesystem>, so this path executes at mount time, before any file operation.
3. Pseudocode Diff
Finding A — GetData (offset/size validation)
// === UNPATCHED Cim::ImageReader::GetData @ 0x140018B08 ===
rcx = resolved_offset; // from GetOffsetTruncate; 48-bit masked, pre-validated
rax = region_table[idx].size; // region-table entry
if (rax < rcx) goto terminate; // UNSIGNED guard: ensures size >= offset
rax = rax - rcx; // available; cannot underflow (guarded above)
if (rdi != -1 && rax < rdi) goto terminate; // UNSIGNED: reject request > available
return span(region.data_ptr + offset, rdi);
// === PATCHED Cim::ImageReader::GetData @ 0x1C000A1AC ===
rcx = resolved_offset;
if ((int64_t)rcx < 0) goto terminate; // ADDED signed check on offset
rax = region_table[idx].size;
rax = rax - rcx; // subtraction, no prior unsigned guard
if ((int64_t)rax < 0) goto terminate; // ADDED signed underflow check
if (rdi != -1) {
if ((int64_t)rdi < 0) goto terminate; // ADDED signed check on requested size
if ((int64_t)rax < (int64_t)rdi) goto terminate; // SIGNED comparison (jl)
}
return span(region.data_ptr + offset, rdi);
Finding B — GetFile (File copy)
// === UNPATCHED Cim::ImageReader::GetFile @ 0x140018FB4 ===
uint16_t entry_size = entry->entry_size; // movzx (0x14001938B)
if (entry_size > 0x68) trace("Invalid entry size"); // UNSIGNED cmp; traces, does NOT reject
GetData(&data, region_off, entry_size * slot, entry_size); // read is bounds-checked in GetData
if (Feature_CimfsV1CompatGetFileFix__private_IsEnabled())
memset(&File, 0, 0x68); // gated zero-init of output struct
copy_len = min(entry_size, data_len); // UNSIGNED min (cmovb, 0x140019673)
gsl::copy(span(src, copy_len), span(File, 0x68)); // bounds-checked; terminates if copy_len > 0x68
// === PATCHED Cim::ImageReader::GetFile @ 0x1C000A3DC ===
GetData(&data, region_off, entry_size * slot, entry_size);
copy_len = min((int64_t)data_len, 0x58); // fixed 0x58, SIGNED min (cmovg)
if ((int64_t)copy_len < 0 || copy_len > data_len) goto terminate; // js / jg
gsl::copy(span(src, copy_len), span(File, 0x58));
Finding C — LinkTable<Stream>::Parse (count arithmetic)
// === UNPATCHED Cim::LinkTable<Stream>::Parse @ 0x140029E8C ===
if (span.size() < 8) goto error; // cmp r9,8; jb
if (span.data() & 7) goto error; // test bl,7; jnz (alignment)
r8 = span.size() - 8;
count = r8 >> 4; // UNSIGNED (shr r11,4)
disk_count = *(uint32_t*)(entries + 4); // zero-extended, always >= 0
if (disk_count u> r8 / 0x14) goto error; // UNSIGNED (ja)
if (disk_count u> count) goto terminate; // UNSIGNED (ja)
// === PATCHED Cim::LinkTable<Stream>::Parse @ 0x1C000F324 ===
if (span.size() < 8) goto error; // same size floor
if (span.data() & 7) goto error; // same alignment check
r9 = span.size() - 8;
if ((int64_t)r9 < 0) goto terminate; // SIGNED (test r9,r9; js)
count = (r9 + ((r9>>63)&0xF)) >> 4; // SIGNED div-by-16 (cqo; and; add; sar)
if ((int64_t)count < 0) goto terminate; // SIGNED (js)
if ((int64_t)disk_count > (int64_t)count) goto terminate; // SIGNED (jg)
4. Assembly Analysis
Finding A — Cim::ImageReader::GetData bounds block
UNPATCHED (0x140018B08):
0000000140018B91 mov rcx, [rbp+var_78] ; rcx = resolved in-region offset (from GetOffsetTruncate)
0000000140018B95 shl rbx, 4
0000000140018B99 add rbx, [r14+8] ; rbx = ®ion_table[idx] {size, data_ptr}
0000000140018B9D mov rax, [rbx] ; rax = region size
0000000140018BA0 cmp rax, rcx ; UNSIGNED: size vs offset
0000000140018BA3 jb loc_140018D5C ; size < offset -> terminate (ensures size >= offset)
0000000140018BA9 sub rax, rcx ; available = size - offset (cannot underflow: guarded)
0000000140018BAC cmp rdi, 0FFFFFFFFFFFFFFFFh
0000000140018BB0 jnz short loc_140018BC1
0000000140018BC1 cmp rax, rdi ; UNSIGNED: available vs requested
0000000140018BC4 jb loc_140018D5C ; available < requested -> terminate
0000000140018BCE add rcx, [rbx+8] ; rcx = data_ptr + offset
0000000140018BDA movdqu xmmword ptr [rsi], xmm0 ; return span (len, ptr) to caller
0000000140018D5C call ?terminate@details@gsl@@YAXXZ
PATCHED (0x1C000A1AC):
00000001C000A227 mov rcx, [rbp+var_68] ; rcx = resolved in-region offset
00000001C000A22B shl rbx, 4
00000001C000A22F add rbx, [r14+8] ; rbx = ®ion_table[idx]
00000001C000A233 test rcx, rcx ; *** ADDED signed check on offset ***
00000001C000A236 js loc_1C000A3D0 ; offset < 0 -> terminate
00000001C000A23C mov rax, [rbx] ; region size
00000001C000A23F sub rax, rcx ; available = size - offset (no prior unsigned guard)
00000001C000A242 js loc_1C000A3D0 ; *** ADDED signed underflow -> terminate ***
00000001C000A248 cmp rdi, 0FFFFFFFFFFFFFFFFh
00000001C000A24C jnz short loc_1C000A254
00000001C000A254 test rdi, rdi ; *** ADDED signed check on requested size ***
00000001C000A257 js loc_1C000A3D0
00000001C000A25D cmp rax, rdi
00000001C000A260 jl loc_1C000A3D0 ; *** SIGNED less-than replaces unsigned jb ***
00000001C000A3D0 call ?terminate@details@gsl@@YAXXZ
Finding A (cont.) — Cim::ImageReader::GetOffsetTruncate
The offset resolution and its 48-bit mask are identical in both builds; the patch adds a signed index check.
UNPATCHED (0x140019AA4):
0000000140019AC2 mov rax, 0FFFFFFFFFFFFh ; 48-bit mask
0000000140019ACC shr rdx, 30h ; region index = RegionOffset >> 48
0000000140019AD9 and r10, rax ; in-region offset & 0xFFFFFFFFFFFF (always >= 0)
0000000140019ADC jz loc_140019C4D ; offset == 0 -> error
0000000140019AE2 cmp rdx, [rcx] ; index vs region count
0000000140019AE5 jnb loc_140019C4D ; UNSIGNED: index >= count -> error
0000000140019AF5 mov r9, [rax+rcx*8] ; region bound (after check)
0000000140019AFC jb short loc_140019B21
0000000140019B04 cmp rcx, r8 ; (bound - offset) vs requested len
0000000140019B07 jb short loc_140019B21
PATCHED (0x1C0009CB8):
00000001C0009CD2 mov rax, 0FFFFFFFFFFFFh ; 48-bit mask (same)
00000001C0009CDC shr rdx, 30h ; index
00000001C0009CE9 and r10, rax ; offset masked (same)
00000001C0009CEC jz loc_1C0009E4E
00000001C0009CF2 cmp rdx, [rcx] ; index vs region count
00000001C0009CF5 jge loc_1C0009E4E ; *** ADDED signed check on index ***
00000001C0009CFB jnb loc_1C0009F82 ; unsigned out-of-range -> terminate
00000001C0009D01 mov rax, [rbx+8] ; region table base (after both checks)
The region index is the top 16 bits of the packed RegionOffset, so it is non-negative by construction; the added jge is defensive.
Finding B — Cim::ImageReader::GetFile (entry size / copy)
; UNPATCHED @ 0x140018FB4
000000014001938B movzx edx, word ptr [rbp+var_B0+0Ah] ; entry_size (16-bit, zero-extended)
00000001400193A6 cmp dx, r8w ; compare vs 0x68
00000001400193AA jbe loc_1400194AE ; <=0x68 continue; else trace, then continue (no reject)
00000001400194AE movzx ecx, word ptr [rbp+var_B0+0Ah] ; entry_size as read length
00000001400194E4 call Cim::ImageReader::GetData ; read is bounds-checked inside GetData
0000000140019603 call Feature_CimfsV1CompatGetFileFix__private_IsEnabledDeviceUsageNoInline
000000014001960A jz short loc_140019654 ; feature off -> skip zero-init
0000000140019616 call memset ; feature on: zero the 0x68 output struct
0000000140019669 movzx edx, word ptr [rbp+var_B0+0Ah] ; entry_size for copy bound
0000000140019673 cmovb rcx, rdx ; copy_len = min(entry_size, data_len) UNSIGNED
000000014001967A ja loc_1400196BE ; copy_len > data_len -> terminate
00000001400196B2 call gsl::copy ; dst span = {0x68, File*}; terminates if len > 0x68
; PATCHED @ 0x1C000A3DC (success path)
00000001C000A717 mov [rbp+var_50+8], rbx ; rbx = output File*
00000001C000A71B mov edx, 58h ; *** fixed copy size 0x58 ***
00000001C000A729 mov rax, [rbp+var_30] ; data_len from GetData
00000001C000A72D cmp rax, rdx
00000001C000A733 cmovg rcx, rdx ; copy_len = min(data_len, 0x58) SIGNED
00000001C000A73A js short loc_1C000A77A ; copy_len < 0 -> terminate
00000001C000A73F jg short loc_1C000A77A ; copy_len > data_len -> terminate
00000001C000A771 call gsl::copy ; dst span = {0x58, File*}
The unpatched entry_size > 0x68 case traces and continues, but the read (GetData) and the copy (gsl::copy) are each bounds-checked and terminate rather than overflow, in both builds.
Finding C — Cim::LinkTable<Stream>::Parse (count arithmetic)
; UNPATCHED @ 0x140029E8C
0000000140029EC2 cmp r9, 8 ; span >= 8?
0000000140029EC6 jb loc_140029FE5 ; size < 8 -> error
0000000140029ED0 test bl, 7 ; data pointer 8-byte aligned?
0000000140029ED3 jnz loc_140029FE5 ; misaligned -> error
0000000140029EF0 lea r8, [r9-8] ; size - 8
0000000140029F0A shr r11, 4 ; UNSIGNED count = (size-8) / 16
0000000140029F0E mov r9d, [rax+4] ; disk_count (zero-extended uint32, >= 0)
0000000140029F1F shr rdx, 4 ; UNSIGNED (size-8)/0x14
0000000140029F23 cmp r9, rdx
0000000140029F26 ja loc_140029FC4 ; UNSIGNED disk_count > (size-8)/0x14 -> error
0000000140029F3A cmp r9, r11
0000000140029F3D ja loc_14002A02B ; UNSIGNED disk_count > count -> terminate
; PATCHED @ 0x1C000F324
00000001C000F35F cmp r8, 8 ; same size floor
00000001C000F363 jb loc_1C000F538
00000001C000F36D test r11b, 7 ; same alignment check
00000001C000F371 jnz loc_1C000F538
00000001C000F38D lea r9, [r8-8]
00000001C000F391 test r9, r9
00000001C000F394 js loc_1C000F61D ; *** SIGNED: (size-8) < 0 -> terminate ***
00000001C000F3C3 cqo ; *** SIGNED div-by-16 ***
00000001C000F3C8 and edx, 0Fh
00000001C000F3CB add rax, rdx
00000001C000F3CE sar rax, 4
00000001C000F3D2 js loc_1C000F61D ; SIGNED negative count -> terminate
00000001C000F3E9 cmp r8, rax
00000001C000F3EC jg loc_1C000F61D ; *** SIGNED (jg replaces ja) ***
FileId-variant Parse (0x14002A038 → 0x1C000EE4C) carries the same change.
5. Trigger Conditions
Privilege precondition: reaching any of these paths requires mounting a CIM volume. A standard user cannot directly mount a CIM image; assume a local-admin or container-host threat model.
Finding A — GetData / GetOffsetTruncate
- Create a CIM image and mount it via the cimfs mount path.
- Perform a file operation that triggers metadata parsing —
IRP_MJ_CREATE,IRP_MJ_READ(viaNonCachedRead),IRP_MJ_DIRECTORY_CONTROL, orIRP_MJ_QUERY_EA. - Observable difference between builds: if a region-table size / offset combination could be crafted so an intermediate value has the sign bit set, the patched build reaches
gsl::details::terminate()at the addedjs/jlchecks, whereas the unpatched build continues. On the observed paths the in-region offset is 48-bit masked and bound-checked byGetOffsetTruncatein both builds, so no such input was demonstrated to reach an out-of-bounds span.
Finding B — GetFile
- Craft a
FileTableDirectoryEntrywithentry_size > 0x68. - Mount the image and enumerate/open the file.
- Observable difference: the unpatched build emits an "Invalid entry size" trace and proceeds; the read and copy remain bounds-checked and terminate if they would exceed their targets. The patched build uses a fixed
0x58copy and no longer feature-gates the output-struct zero-init.
Finding C — LinkTable::Parse
- Forge the link-table descriptor in the filesystem metadata block.
- Mount the image — parsing occurs inside
MountVolumeFromFileSystem. - Observable difference: the patched build applies signed count arithmetic and rejects a computed size with the sign bit set at
js/jg; the unpatched build uses unsigned arithmetic. Becausedisk_countis a non-negative 32-bit field and the span size would need to approach2^63, no reachable trigger was demonstrated.
6. Impact Assessment
Nature of the change. The patch adds signed integer sanity guards to metadata-derived arithmetic and reduces one copy to a fixed size. It does not remove any existing sanitizer and does not introduce a new sink.
- There is no out-of-bounds write on any path examined. Every
gsl::copyis bounds-checked and terminates on violation in both builds. - The read-path helpers (
GetDatafamily) construct agsl::spandescribing a read window. The theoretical worst case of a missing signed guard here is an out-of-bounds read (CWE-125), not a write. - On the observed paths the guarded values are masked (48-bit offset), derived from non-negative fields (32-bit
disk_count, 16-bit region index), or already bound-checked before the guard, so a reachable out-of-bounds primitive in the unpatched build was not demonstrated.
Realistic outcome. This is defense-in-depth hardening of an image-parsing attack surface. It closes a latent signed/unsigned-confusion class rather than a proven exploitable primitive. No kernel information-disclosure or code-execution primitive is established by this diff.
7. Debugger Notes
Assume WinDbg/KD attached to a target running cimfs_unpatched.sys. Resolve the actual base first (lm m cimfs); the static offsets below assume a base of 0x140000000 and must be rebased (<base> + (0x140018B08 - 0x140000000), etc.).
Finding A — GetData / GetOffsetTruncate
Breakpoints:
bp cimfs!Cim::ImageReader::GetData
bp cimfs!Cim::ImageReader::GetOffsetTruncate
bp cimfs!Cim::ImageReader::GetFile
What to inspect:
r8atGetDataentry = the packedRegionOffset. The high 16 bits are the region index (shr r8, 0x30); the low 48 bits are the in-region offset.rcxat0x140018BA0= the in-region offset returned byGetOffsetTruncate(already 48-bit masked and bound-checked).raxat0x140018B9D= the region-table entry size;rbxat0x140018B99=®ion_table[idx], layout[+0x00 uint64 size][+0x08 char* data_ptr].raxafter0x140018BA9(sub rax, rcx) = available bytes; the patched build's addedjsat0x1C000A242would terminate here if this were negative.rdi= requested size;0xFFFFFFFFFFFFFFFFmeans "all available".
Key instruction offsets:
| Address | Instruction | Role |
|---|---|---|
0x140018BA0 |
cmp rax, rcx; jb |
Unsigned pre-subtraction guard (Finding A, unpatched) |
0x140018BA9 |
sub rax, rcx |
Available = size − offset (guarded, no underflow) |
0x140018BC1 |
cmp rax, rdi; jb |
Unsigned request check |
0x1C000A233 |
test rcx, rcx; js |
Patched: signed offset check |
0x1C000A242 |
sub rax, rcx; js |
Patched: signed underflow check |
0x1C000A257 |
test rdi, rdi; js |
Patched: signed size check |
0x1C000A260 |
jl |
Patched: signed available-vs-request check |
0x140019AD9 |
and r10, 0xFFFFFFFFFFFF |
48-bit offset mask (both builds) |
0x1400193A6 |
cmp dx, 0x68 |
Finding B entry-size check (traces, no reject) |
0x14001960A |
Feature_CimfsV1CompatGetFileFix gate |
Finding B feature-gated zero-init |
Finding C — LinkTable::Parse
bp cimfs!Cim::LinkTable<struct Cim::Format::Stream>::Parse
bp cimfs!Cim::LinkTable<enum Cim::Format::FileId>::Parse
[rdx]at entry = span size;[rdx+8]= data pointer (checked 8-byte aligned bytest bl, 7).- Watch the unsigned count arithmetic (
shr r11, 4at0x140029F0A,cmp r9, rdx; jaat0x140029F23); the patched code uses signedsar/js/jginstead.disk_countis the zero-extended uint32 read at[rax+4].
8. Changed Functions — Full Triage
| Function | Sim. | Change type | Note |
|---|---|---|---|
Cim::ImageReader::GetData |
0.9154 | hardening | Replaces the unsigned pre-subtraction guard with signed offset/underflow/size checks (js, jl). Read-side span construction. |
Cim::ImageReader::GetDataTruncate |
0.8595 | hardening | Same signed-guard pattern; adds test rdx,rdx; js and sub rcx,rdx; js. Also drops the StackEventTracer parameter. |
Cim::ImageReader::GetOffsetTruncate |
0.6174 | hardening | Offset 48-bit mask and region bound-check present in both; patch adds a signed cmp rdx, count; jge alongside the existing jnb. Error path moved from ProcessTraceStatusOrigin to inline ETW. |
Cim::ImageReader::GetFile |
0.6273 | refactor | Variable copy length (≤0x68, unsigned min) replaced with fixed 0x58 (signed min); removes the Feature_CimfsV1CompatGetFileFix__private-gated output zero-init. Copy is bounds-checked in both builds. |
Cim::ImageReader::GetArray<gsl::byte> |
0.9313 | hardening | Adds signed negative/upper-bound checks on the size returned by GetDataTruncate. |
Cim::ImageReader::GetArray<Cim::Format::PeImageMapping> |
0.9345 | hardening | Same signed-check pattern for the PE image-mapping array. |
Cim::LinkTable<FileId>::Parse |
0.7215 | hardening | Unsigned count arithmetic (shr/ja) replaced with signed division and checks (cqo; sar, js, jg). disk_count is a non-negative uint32. |
Cim::LinkTable<Stream>::Parse |
0.7324 | hardening | Same as above for stream link tables. |
Cim::FileSystem::GetStreamSegment |
0.4912 | behavioral | Heavily recompiled; StackEventTracer parameter removed and stream-handling control flow reorganized. No specific security-relevant mechanism verified against both builds here. |
Cim::FileSystem::GetReparseData |
0.5701 | behavioral | Heavily recompiled; signature and error reporting changed. No specific security-relevant mechanism verified against both builds here. |
Cim::FileSystem::GetDataSegment |
0.5089 | behavioral | Drops StackEventTracer parameter and ProcessTraceStatusPushLocation calls; struct field offsets shift. Not security-relevant. |
wil_InitializeFeatureStaging |
0.696 | cosmetic | Feature-staging configuration updates, including Feature_CimfsV1CompatGetFileFix__private registration. |
DriverEntry |
0.4708 | behavioral | Updated driver/device-name creation. Not security-relevant. |
The recurring StackEventTracer-removal / ProcessTraceStatusOrigin→ETW change and the cqo; and edx, mask; add; sar signed-division sequences are the dominant deltas; they are servicing/compiler-shape churn on top of the signed-guard hardening.
9. Unmatched Functions
None. unmatched_unpatched = 0, unmatched_patched = 0. The patch is an in-place rewrite — no sanitizers were removed and none of the existing bounds checks were deleted.
10. Confidence & Caveats
Confidence: high for the nature of the change; the claimed exploitability was not substantiated.
- The signed/unsigned hardening is unambiguous in the diff: unsigned
jb/ja/shrreplaced with signedjs/jl/jg/saracross theImageReaderread/resolve helpers andLinkTable::Parse. Direction is correct (the patched build is stricter). - The in-region offset is masked to 48 bits and bound-checked by
GetOffsetTruncatein both builds; the region index is a top-16-bit value; the link-tabledisk_countis a zero-extended uint32. On the paths observed, the values the new signed guards protect are already non-negative or pre-validated, so a reachable out-of-bounds primitive in the unpatched build was not demonstrated. - There is no out-of-bounds write on any examined path; every
gsl::copyis bounds-checked and terminates on violation in both builds. The realistic impact direction, if any, is an out-of-bounds read. - The exact CIM on-disk layout (
RegionOffsetpacking, region-table entry format,FileTableDirectoryEntryfields, link-table descriptor) is inferred from instruction operands. The region index occupies bits 48–63 and the in-region offset bits 0–47, as shown by theshr ..., 0x30/and ..., 0xFFFFFFFFFFFFpair. GetStreamSegmentandGetReparseDataare heavily recompiled; their specific behavioral changes were not resolved against both builds and are reported as behavioral churn rather than security fixes.- Mounting a CIM image requires elevated privilege; this limits the parsing surface to a privileged local attacker unless the cimfs mount path is exposed through a less-privileged broker.
DONE