1. Overview

Field Value
Unpatched binary appid_unpatched.sys
Patched binary appid_patched.sys
Overall similarity 0.9909
Matched functions 372
Changed functions 6
Identical functions 366
Unmatched (unpatched) 0
Unmatched (patched) 0

Verdict: The patch contains one security-relevant change and one non-security refactor in the Windows AppID (AppLocker) kernel driver.

  1. Finding 1 (security-relevant, Low): SmartlockerConstructOriginClaim allocates a paged pool buffer with ExAllocatePoolWithTag and leaves a few trailing bytes uninitialized. The uninitialized bytes are carried at the full allocation length into a process-token security attribute and a file extended attribute. The patched build adds a memset that zeroes the whole buffer, but this zeroing is gated behind the WIL feature flag Feature_965997883; when the feature is disabled the patched code takes the original, unzeroed path. The fix is therefore a staged rollout rather than an unconditionally delivered fix.

  2. Finding 2 (no security-relevant change): The wil_details_* feature-state cache functions were refactored from a single global feature descriptor to a per-descriptor context passed as a parameter. The unpatched code already performs a correct lock-free atomic compare-and-swap; there is no genuine data race, and none of these functions participate in an AppLocker allow/deny decision. This is Windows Implementation Library (WIL) feature-staging library churn, not a security fix.


2. Vulnerability Summary

Finding 1: Kernel Information Disclosure via Uninitialized Pool Memory

Attribute Value
Severity Low
Vulnerability class Use of Uninitialized Resource (CWE-908); disclosure consequence (CWE-200)
Affected function SmartlockerConstructOriginClaim @ 0x1C001F138 (both builds)
Patch mechanism Adds a memset(buf, 0, size) (zeroing) immediately after ExAllocatePoolWithTag, but only on the path taken when Feature_965997883__private_IsEnabledDeviceUsage @ 0x1C0002194 returns non-zero. When the feature is disabled, the patched code allocates without zeroing (identical to the unpatched behavior). The size base for the arg5 != NULL case is *arg5 + 0x40 when the feature is enabled and *arg5 + 0x42 (the original value) when it is disabled.

Root cause: The unpatched SmartlockerConstructOriginClaim @ 0x1C001F138 allocates a kernel paged pool buffer using ExAllocatePoolWithTag(PagedPool, size, 'smtA') but never zeroes the buffer. It then selectively initializes specific fields:

  • [0x00] = 1 (dword)
  • [0x04] = arg1 (dword)
  • [0x08] = 0 (dword)
  • [0x0C] = 1 (dword)
  • [0x10–0x1F] = 16 bytes from BCryptGenRandom
  • [0x20–0x2F] = either a second BCryptGenRandom call or a copy of [0x10–0x1F]
  • [0x30–0x33] = arg4 (dword)
  • [0x34–0x37] = global version/sequence (dword)
  • [0x38–0x3B] = conditional (only written when arg5 is non-NULL)
  • [0x3C+] = conditional data copied via memmove (sub_1c0006b80) (only when arg5 is non-NULL)

Gaps remain uninitialized: - When arg5 is NULL, the allocation is 0x40 bytes and only fields up through offset 0x38 (the dword at [rbx+0x38]) are written, leaving bytes 0x3C–0x3F (4 bytes) uninitialized. - When arg5 is non-NULL, the allocation is *arg5 + 0x42 bytes; fields through 0x38 plus *arg5 bytes copied from offset 0x3C are written, leaving roughly 6 trailing bytes uninitialized.

These gaps contain stale kernel paged pool residue from previously freed allocations.

Data flow:

  1. A process performs a file execution/creation operation on a system with AppLocker/WDAC enforcement active, routed through the AppID driver, reaching the policy evaluation dispatcher SmartlockerVerifyProcess @ 0x1C0020344.
  2. SmartlockerVerifyProcess calls SmartlockerManagedInstallerCheck @ 0x1C001F25C or SmartlockerSmartscreenProcessTokenCheck @ 0x1C001F374.
  3. Those helpers call SmartlockerConstructOriginClaim @ 0x1C001F138, which returns the buffer via *arg7 and its full allocation size via *arg6.
  4. Each caller passes the buffer at that full length to SmartlockerTagProcessToken @ 0x1C001F0C0 (SmartlockerConstructOriginClaim is called at 0x1C001F2DA then SmartlockerTagProcessToken at 0x1C001F2F6 in SmartlockerManagedInstallerCheck; correspondingly at 0x1C001F4C6 and 0x1C001F4E0 in SmartlockerSmartscreenProcessTokenCheck). That function wraps {pValue = buffer, ValueLength = size} as a CLAIM_SECURITY_ATTRIBUTE OCTET_STRING and calls SeSetSecurityAttributesToken (0x1C001F11F), placing the uninitialized tail into a process-token security attribute.
  5. SmartlockerManagedInstallerCheck also returns the buffer up through SmartlockerVerifyProcess, which passes it at the same length to AipSetEaFromOrigin, writing it into a file extended attribute.

Token security attributes and file extended attributes are, by Windows design, queryable from user mode (NtQueryInformationToken(TokenSecurityAttributes), NtQueryEaFile). That final user-mode read is performed by the operating system, not by this driver: the disassembly of appid.sys shows the uninitialized bytes being placed into these two standard Windows objects at full length, but does not itself contain an IRP/IOCTL output-buffer copy back to user mode. The disclosed quantity is small (4–6 bytes of paged pool residue per allocation), which bounds the practical impact.


Finding 2: WIL feature-state cache refactor (no security-relevant change)

Attribute Value
Severity None (not a security issue)
Nature Windows Implementation Library (WIL) feature-staging refactor: global feature descriptor moved to a per-descriptor context parameter
Affected functions wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C00028DC, wil_details_IsEnabledFallback @ 0x1C0002850, Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E00 (unpatched)
Change mechanism Introduces a per-descriptor context pointer (arg3) threaded through the call chain; the callee stops hard-coding the single global feature descriptor wil_details_featureDescriptors_a and instead receives the descriptor pointer as a parameter, reading *arg3

Why this is not a security issue: These functions are the WIL feature-enablement state cache embedded in the driver (feature-flag staging plus usage telemetry). They do not participate in any AppLocker allow/deny decision. In the unpatched binary the cache reaches its state through the global pointer wil_details_featureDescriptors_a. wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C00028DC sets device-usage bits (bit 0x10 for event type 3, bit 0x20 for event type 4) with a lock-free atomic compare-and-swap: it loads the current value at 0x1C00028F9, computes the new value, does lock cmpxchg [r9], edx at 0x1C00028DC+0x32 (0x1C000290E), and on failure the jnz at 0x1C0002913 branches back to 0x1C00028FC to re-read and retry. That is a correct, thread-safe CAS-with-retry loop in both builds — there is no torn read and no lost update, so there is no genuine data race to fix.

The patched version performs the same atomic CAS on *arg3 instead of on the global, plus a lock or [r9], r10d fast path (0x1C0001C89) when the per-descriptor bytes at arg3+0x1d/arg3+0x1e are set. This is a structural library change (global descriptor to per-descriptor context, a 2-argument to 3-argument ABI change across the wil_details_* cluster), not a synchronization fix.

Call chain (for reference):

  1. A WIL feature-enablement check for Feature_4060024122 reaches the thunk Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E00, which (unpatched) calls wil_details_IsEnabledFallback @ 0x1C0002850 with two arguments, inheriting the global descriptor.
  2. wil_details_IsEnabledFallback reads the global descriptor, reports usage via wil_details_FeatureReporting_ReportUsageToService @ 0x1C00023EC, then conditionally updates the state bits via wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C00028DC.
  3. The patched thunk Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E74 passes its own descriptor (lea r8, Feature_4060024122__private_descriptor at 0x1C0002E78) as the third argument, so each feature uses its own context.

3. Pseudocode Diff

Finding 1: SmartlockerConstructOriginClaim (sub_1C001F138) — Uninitialized Pool Allocation

// ============================================================
// UNPATCHED SmartlockerConstructOriginClaim (sub_1C001F138)
// ============================================================
NTSTATUS SmartlockerConstructOriginClaim (sub_1C001F138)(
    ULONG    arg1,       // event/type
    void*    arg2,
    void*    arg3,
    ULONG    arg4,       // stored at [buf+0x30]
    DATA*    arg5,       // optional: { WORD size; ...; void* data; }
    ULONG*   arg6,       // out: allocated size
    void**   arg7        // out: allocated buffer ptr
) {
    ULONG size = 0x40;
    if (arg5) {
        size = arg5->size + 0x42;         // BUG: larger alloc, more uninit tail
    }

    if (arg1 > 1)
        return STATUS_INVALID_PARAMETER;

    // *** VULNERABILITY: no zeroing after allocation ***
    void* buf = ExAllocatePoolWithTag(PagedPool, size, 'smtA');
    if (!buf)
        return STATUS_NO_MEMORY;

    // Partial initialization — gaps retain stale pool residue
    *(DWORD*)(buf + 0x00) = 1;
    *(DWORD*)(buf + 0x04) = arg1;
    *(DWORD*)(buf + 0x08) = 0;
    *(DWORD*)(buf + 0x0C) = 1;

    BCryptGenRandom(0, buf + 0x10, 0x10, BCRYPT_USE_SYSTEM_PREFERRED_RNG);

    if (arg1 == 0)
        memcpy(buf + 0x20, buf + 0x10, 0x10);   // copy bytes 0x10-0x1F
    else
        BCryptGenRandom(0, buf + 0x20, 0x10, BCRYPT_USE_SYSTEM_PREFERRED_RNG);

    *(DWORD*)(buf + 0x30) = arg4;
    *(DWORD*)(buf + 0x34) = global_version;

    if (arg5) {
        *(DWORD*)(buf + 0x38) = arg5->size;
        memmove (sub_1c0006b80)(buf + 0x3C, arg5->data, arg5->size);  // copy trailing data
    } else {
        *(DWORD*)(buf + 0x38) = 0;
        // *** bytes 0x3C-0x3F remain UNINITIALIZED ***
    }

    *arg6 = size;
    *arg7 = buf;
    return STATUS_SUCCESS;
}

// ============================================================
// PATCHED SmartlockerConstructOriginClaim (sub_1C001F138)
// ============================================================
NTSTATUS SmartlockerConstructOriginClaim_patched(/* same args */) {
    ULONG size = 0x40;
    if (arg5) {
        BOOL bFeature = Feature_965997883__private_IsEnabledDeviceUsage();  // WIL feature check
        size = arg5->size + 0x40;
        if (!bFeature)
            size += 2;               // feature OFF -> +0x42 (original value); feature ON -> +0x40
    }

    if (arg1 > 1)
        return STATUS_INVALID_PARAMETER;

    void* buf;
    if (Feature_965997883__private_IsEnabledDeviceUsage()) {   // feature ON path
        buf = ExAllocatePoolWithTag(PagedPool, size, 'smtA');
        if (!buf)
            return STATUS_NO_MEMORY;
        memset(buf, 0, size);        // *** THE FIX: zero the whole buffer ***
    } else {                          // feature OFF path — identical to unpatched
        buf = ExAllocatePoolWithTag(PagedPool, size, 'smtA');
        if (!buf)
            return STATUS_NO_MEMORY;
        // no zeroing — original behavior retained
    }

    // ... same partial initialization as before ...
}

The zeroing is delivered only when the WIL feature flag Feature_965997883 is enabled; the feature-off branch retains the original allocate-without-zero behavior. This is a staged rollout.

Finding 2: WIL feature-state cache — global descriptor to per-descriptor context

Both builds perform a correct lock-free atomic compare-and-swap; the only change is where the descriptor comes from (a hard-coded global vs. a parameter).

// ============================================================
// UNPATCHED: single global feature descriptor
// ============================================================

// Thunk
void Feature_4060024122__private_IsEnabledFallback (0x1C0002E00)(arg1, arg2) {
    wil_details_IsEnabledFallback(arg1, arg2);          // 2 args, inherits the global descriptor
}

// Orchestrator
void wil_details_IsEnabledFallback (0x1C0002850)(ULONG event_type, ULONG arg2) {
    wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState(
        wil_details_featureDescriptors_a,   // GLOBAL descriptor (hard-coded)
        event_type,
        &wil_details_featureDescriptors_a
    );
    wil_details_FeatureReporting_ReportUsageToService(/* hardcoded feature id */);
    wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath(event_type, arg2);   // CAS on global
}

// State modifier — correct lock-free CAS, not a race
void wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath (0x1C00028DC)(ULONG event_type, ULONG flags) {
    volatile DWORD* pState = wil_details_featureDescriptors_a;  // GLOBAL
    DWORD old;
    do {
        old = *pState;                       // re-read on each retry
    } while (lock_cmpxchg(pState, old | bit, old) != old);
    // atomic CAS-with-retry: thread-safe, no torn read, no lost update
}

// ============================================================
// PATCHED: per-descriptor context passed as a parameter
// ============================================================

// Thunk
void Feature_4060024122__private_IsEnabledFallback (0x1C0002E74)(arg1, arg2) {
    void* ctx = &Feature_4060024122__private_descriptor;   // this feature's own descriptor
    wil_details_IsEnabledFallback(arg1, arg2, ctx);        // 3 args
}

// Orchestrator
void wil_details_IsEnabledFallback (0x1C0001BB0)(ULONG event_type, ULONG arg2, void* ctx) {
    wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState(*ctx, event_type, ctx);
    wil_details_FeatureReporting_ReportUsageToService(ctx);
    wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath(event_type, arg2, ctx);   // CAS on *ctx
}

// State modifier — same CAS, now on *ctx, plus a lock-or fast path
void wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath (0x1C0001C44)(ULONG event_type, ULONG flags, void* ctx) {
    volatile DWORD* pState = (DWORD*)(*ctx);   // per-descriptor state
    if (ctx[0x1e] || ctx[0x1d]) {
        lock_or(pState, bit);                  // fast path
    } else {
        // same lock_cmpxchg-with-retry loop, on *ctx
    }
}

4. Assembly Analysis

Finding 1: SmartlockerConstructOriginClaim (sub_1C001F138) — Full Unpatched Assembly

; ---- SmartlockerConstructOriginClaim @ 0x1C001F138 ---- (UNPATCHED)
00000001C001F138  mov     [rsp+arg_0], rbx
00000001C001F13D  mov     [rsp+arg_8], rbp
00000001C001F142  mov     [rsp+arg_10], rsi
00000001C001F147  push    rdi
00000001C001F148  push    r14
00000001C001F14A  push    r15
00000001C001F14C  sub     rsp, 20h
00000001C001F150  mov     rdi, [rsp+38h+arg_20]   ; rdi = arg5
00000001C001F155  mov     r15d, r9d               ; arg4
00000001C001F158  mov     ebp, ecx                ; arg1
00000001C001F15A  mov     esi, 40h                ; default size = 0x40
00000001C001F15F  test    rdi, rdi
00000001C001F162  jz      short loc_1C001F16A
00000001C001F164  movzx   esi, word ptr [rdi]     ; size = *arg5
00000001C001F167  add     esi, 42h                ; size += 0x42
00000001C001F16A  mov     r14d, 1
00000001C001F170  cmp     ebp, r14d
00000001C001F173  jbe     short loc_1C001F17F
00000001C001F175  mov     eax, 0C000000Dh         ; STATUS_INVALID_PARAMETER
00000001C001F17A  jmp     loc_1C001F23A
00000001C001F17F  mov     edx, esi                ; NumberOfBytes
00000001C001F181  mov     r8d, 41746D73h          ; Tag 'smtA'
00000001C001F187  mov     ecx, r14d               ; PoolType = PagedPool (1)
00000001C001F18A  call    cs:__imp_ExAllocatePoolWithTag   ; no zeroing follows
00000001C001F191  nop     dword ptr [rax+rax+00h]
00000001C001F196  mov     rbx, rax
00000001C001F199  test    rax, rax
00000001C001F19C  jnz     short loc_1C001F1A8
00000001C001F19E  mov     eax, 0C0000017h         ; STATUS_NO_MEMORY
00000001C001F1A3  jmp     loc_1C001F23A
00000001C001F1A8  and     dword ptr [rax+8], 0    ; buf[0x08] = 0
00000001C001F1AC  lea     rdx, [rax+10h]
00000001C001F1B0  mov     r9d, 2                  ; BCRYPT_USE_SYSTEM_PREFERRED_RNG
00000001C001F1B6  mov     [rax], r14d             ; buf[0x00] = 1
00000001C001F1B9  xor     ecx, ecx
00000001C001F1BB  mov     [rax+4], ebp            ; buf[0x04] = arg1
00000001C001F1BE  mov     [rax+0Ch], r14d         ; buf[0x0C] = 1
00000001C001F1C2  lea     r8d, [r9+0Eh]           ; 0x10 bytes
00000001C001F1C6  call    cs:__imp_BCryptGenRandom ; -> buf[0x10..0x1F]
00000001C001F1CD  nop     dword ptr [rax+rax+00h]
00000001C001F1D2  lea     rdx, [rbx+20h]
00000001C001F1D6  test    ebp, ebp
00000001C001F1D8  jnz     short loc_1C001F1E4
00000001C001F1DA  movups  xmm0, xmmword ptr [rbx+10h]
00000001C001F1DE  movdqu  xmmword ptr [rdx], xmm0 ; buf[0x20..0x2F] = buf[0x10..0x1F]
00000001C001F1E2  jmp     short loc_1C001F1FA
00000001C001F1E4  xor     ecx, ecx
00000001C001F1E6  lea     r9d, [rcx+2]
00000001C001F1EA  lea     r8d, [rcx+10h]
00000001C001F1EE  call    cs:__imp_BCryptGenRandom ; -> buf[0x20..0x2F]
00000001C001F1F5  nop     dword ptr [rax+rax+00h]
00000001C001F1FA  mov     [rbx+30h], r15d         ; buf[0x30] = arg4
00000001C001F1FE  mov     eax, cs:dword_1C0016660 ; version/sequence
00000001C001F204  mov     [rbx+34h], eax          ; buf[0x34] = version
00000001C001F207  test    rdi, rdi
00000001C001F20A  jz      short loc_1C001F225
00000001C001F20C  movzx   eax, word ptr [rdi]
00000001C001F20F  lea     rcx, [rbx+3Ch]
00000001C001F213  mov     [rbx+38h], eax          ; buf[0x38] = *arg5
00000001C001F216  movzx   r8d, word ptr [rdi]     ; Size
00000001C001F21A  mov     rdx, [rdi+8]            ; Src
00000001C001F21E  call    memmove                 ; copy *arg5 bytes to buf[0x3C]
00000001C001F223  jmp     short loc_1C001F229
00000001C001F225  and     dword ptr [rbx+38h], 0  ; arg5==NULL: buf[0x38]=0, buf[0x3C..0x3F] left uninitialized
00000001C001F229  mov     rax, [rsp+38h+arg_28]   ; arg6
00000001C001F22E  mov     [rax], esi              ; *arg6 = size
00000001C001F230  mov     rax, [rsp+38h+arg_30]   ; arg7
00000001C001F235  mov     [rax], rbx              ; *arg7 = buffer ptr
00000001C001F238  xor     eax, eax                ; STATUS_SUCCESS
00000001C001F23A  mov     rbx, [rsp+38h+arg_0]
00000001C001F23F  mov     rbp, [rsp+38h+arg_8]
00000001C001F244  mov     rsi, [rsp+38h+arg_10]
00000001C001F249  add     rsp, 20h
00000001C001F24D  pop     r15
00000001C001F24F  pop     r14
00000001C001F251  pop     rdi
00000001C001F252  retn

The patched build at the same address 0x1C001F138 gates both the size adjustment and the zeroing on the WIL feature Feature_965997883:

; ---- SmartlockerConstructOriginClaim @ 0x1C001F138 ---- (PATCHED, allocation region)
00000001C001F160  test    rdi, rdi                ; arg5?
00000001C001F163  jz      short loc_1C001F17E
00000001C001F165  call    Feature_965997883__private_IsEnabledDeviceUsage
00000001C001F16A  movzx   esi, word ptr [rdi]     ; size = *arg5
00000001C001F16D  xor     edx, edx
00000001C001F16F  test    eax, eax
00000001C001F171  setnz   dl                      ; dl = feature enabled?
00000001C001F174  add     esi, 40h                ; size += 0x40
00000001C001F177  test    edx, edx
00000001C001F179  jnz     short loc_1C001F17E     ; feature ON -> keep +0x40
00000001C001F17B  add     esi, 2                  ; feature OFF -> +0x42 (original)
00000001C001F17E  cmp     r14d, 1
00000001C001F182  jbe     short loc_1C001F18E
00000001C001F184  mov     eax, 0C000000Dh
00000001C001F189  jmp     loc_1C001F27C
00000001C001F18E  mov     ebp, esi
00000001C001F190  call    Feature_965997883__private_IsEnabledDeviceUsage
00000001C001F195  mov     r8d, 41746D73h          ; Tag
00000001C001F19B  mov     edx, ebp                ; NumberOfBytes
00000001C001F19D  mov     ecx, 1                  ; PoolType
00000001C001F1A2  test    eax, eax
00000001C001F1A4  jz      short loc_1C001F20B     ; feature OFF -> allocate without zeroing
00000001C001F1A6  call    cs:__imp_ExAllocatePoolWithTag
00000001C001F1AD  nop     dword ptr [rax+rax+00h]
00000001C001F1B2  mov     rbx, rax
00000001C001F1B5  test    rax, rax
00000001C001F1B8  jz      short loc_1C001F21F
00000001C001F1BA  mov     r8d, ebp                ; Size
00000001C001F1BD  xor     edx, edx                ; Val = 0
00000001C001F1BF  mov     rcx, rax
00000001C001F1C2  call    memset                  ; *** THE FIX: zero the whole buffer ***
00000001C001F1C7  and     dword ptr [rbx+8], 0    ; (shared init from here)
...
00000001C001F20B  call    cs:__imp_ExAllocatePoolWithTag   ; feature OFF path
00000001C001F212  nop     dword ptr [rax+rax+00h]
00000001C001F217  mov     rbx, rax
00000001C001F21A  test    rax, rax
00000001C001F21D  jnz     short loc_1C001F1C7     ; jumps PAST the memset -> no zeroing
00000001C001F21F  mov     eax, 0C0000017h
00000001C001F224  jmp     short loc_1C001F27C

Key observations: - In the unpatched build the call ExAllocatePoolWithTag at 0x1C001F18A is followed directly by the NULL check at 0x1C001F199 — no zeroing call exists. - When arg5 is NULL, the branch at 0x1C001F225 only zeroes the dword at [rbx+0x38], leaving bytes [0x3C–0x3F] (the last 4 bytes of the 0x40-byte allocation) uninitialized. - When arg5 is non-NULL, memmove fills *arg5 bytes from [rbx+0x3C], but the allocation is *arg5 + 0x42, so roughly 6 trailing bytes are left uninitialized. - In the patched build the memset at 0x1C001F1C2 runs only on the feature-enabled path (0x1C001F1A6). The feature-disabled path (0x1C001F20B) allocates and then jumps to 0x1C001F1C7, skipping the memset — identical to the unpatched behavior. The fix is therefore staged behind the feature flag.

Finding 2: wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath — global descriptor vs. per-descriptor context

The unpatched CAS loop re-reads and retries on failure; it is a correct lock-free atomic operation, not a race.

; ---- wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C00028DC ---- (UNPATCHED)
00000001C00028DC  mov     r9, cs:wil_details_featureDescriptors_a  ; global descriptor
00000001C00028E3  sub     edx, 3
00000001C00028E6  jz      short loc_1C00028F3
00000001C00028E8  cmp     edx, 1
00000001C00028EB  jnz     short locret_1C0002915
00000001C00028ED  lea     r8d, [rdx+1Fh]          ; = 0x20 for event type 4
00000001C00028F1  jmp     short loc_1C00028F9
00000001C00028F3  mov     r8d, 10h                ; = 0x10 for event type 3
00000001C00028F9  mov     eax, [r9]               ; read state (re-read on each retry)
00000001C00028FC  test    al, 2
00000001C00028FE  jz      short locret_1C0002915
00000001C0002900  mov     edx, eax
00000001C0002902  xor     edx, ecx
00000001C0002904  test    dl, 1
00000001C0002907  jnz     short locret_1C0002915
00000001C0002909  mov     edx, eax
00000001C000290B  or      edx, r8d
00000001C000290E  lock cmpxchg [r9], edx          ; atomic CAS
00000001C0002913  jnz     short loc_1C00028FC      ; retry on failure -> correct CAS loop
00000001C0002915  retn

; ---- wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C0001C44 ---- (PATCHED)
00000001C0001C44  mov     r9, [r8]                 ; per-descriptor state = *arg3
00000001C0001C47  sub     edx, 3
00000001C0001C4A  jz      short loc_1C0001C57
00000001C0001C4C  cmp     edx, 1
00000001C0001C4F  jnz     short locret_1C0001C8D
00000001C0001C51  lea     r10d, [rdx+1Fh]
00000001C0001C55  jmp     short loc_1C0001C5D
00000001C0001C57  mov     r10d, 10h
00000001C0001C5D  cmp     byte ptr [r8+1Eh], 0     ; per-descriptor field
00000001C0001C62  jnz     short loc_1C0001C89
00000001C0001C64  cmp     byte ptr [r8+1Dh], 0
00000001C0001C69  jnz     short loc_1C0001C89
00000001C0001C6B  mov     eax, [r9]
00000001C0001C6E  test    al, 2
00000001C0001C70  jz      short locret_1C0001C8D
00000001C0001C72  mov     edx, eax
00000001C0001C74  xor     edx, ecx
00000001C0001C76  test    dl, 1
00000001C0001C79  jnz     short locret_1C0001C8D
00000001C0001C7B  mov     edx, r10d
00000001C0001C7E  or      edx, eax
00000001C0001C80  lock cmpxchg [r9], edx           ; same atomic CAS, now on *arg3
00000001C0001C85  jz      short locret_1C0001C8D
00000001C0001C87  jmp     short loc_1C0001C6E       ; retry
00000001C0001C89  lock or [r9], r10d               ; fast path
00000001C0001C8D  retn

The only functional difference is the state pointer source: cs:wil_details_featureDescriptors_a (a global) versus [r8] (*arg3, a caller-supplied per-descriptor pointer), plus the added lock or fast path gated on the per-descriptor bytes at +0x1d/+0x1e.

Finding 2 (cont.): wil_details_IsEnabledFallback — call sites

; ---- wil_details_IsEnabledFallback @ 0x1C0002850 ---- (UNPATCHED, excerpt)
00000001C0002879  lea     r8, wil_details_featureDescriptors_a
00000001C0002880  mov     rcx, cs:wil_details_featureDescriptors_a   ; hard-coded global
00000001C0002887  call    wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState
00000001C00028A9  call    wil_details_FeatureReporting_ReportUsageToService
00000001C00028BB  call    wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath

; ---- wil_details_IsEnabledFallback @ 0x1C0001BB0 ---- (PATCHED, excerpt)
00000001C0001BCA  mov     r14, r8                  ; save arg3
00000001C0001BDD  mov     rcx, [r8]                ; descriptor = *arg3
00000001C0001BE0  call    wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState
00000001C0001C02  mov     rcx, r14
00000001C0001C05  call    wil_details_FeatureReporting_ReportUsageToService
00000001C0001C12  mov     r8, r14
00000001C0001C1A  call    wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath

Finding 2 (cont.): Feature_4060024122__private_IsEnabledFallback — thunk

; ---- Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E00 ---- (UNPATCHED)
00000001C0002E00  sub     rsp, 28h
00000001C0002E04  call    wil_details_IsEnabledFallback      ; 2 args (inherits global)
00000001C0002E09  add     rsp, 28h
00000001C0002E0D  retn

; ---- Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E74 ---- (PATCHED)
00000001C0002E74  sub     rsp, 28h
00000001C0002E78  lea     r8, Feature_4060024122__private_descriptor  ; 3rd arg = this feature's descriptor
00000001C0002E7F  call    wil_details_IsEnabledFallback      ; 3 args
00000001C0002E84  add     rsp, 28h
00000001C0002E88  retn

5. Trigger Conditions

Triggering Finding 1: Uninitialized Memory Disclosure

  1. Open a handle to the AppID device interface (\Device\AppID or \??\AppID), or trigger evaluation indirectly via a file execution/creation operation on a system with AppLocker enforcement active.
  2. Cause a policy cache miss so that a new rule/cache buffer is allocated. This can be done by referencing a file path that has not been recently evaluated, or by causing cache eviction (flushing).
  3. Reach SmartlockerConstructOriginClaim @ 0x1C001F138 via the dispatch chain: SmartlockerVerifyProcess @ 0x1C0020344SmartlockerManagedInstallerCheck @ 0x1C001F25C or SmartlockerSmartscreenProcessTokenCheck @ 0x1C001F374SmartlockerConstructOriginClaim.
  4. The arg5 path determines the uninitialized region:
  5. If arg5 == NULL: allocation is 0x40 bytes; bytes 0x3C–0x3F are never initialized (4 bytes).
  6. If arg5 != NULL: allocation is *arg5 + 0x42 bytes; roughly 6 trailing bytes past the copied data are uninitialized.
  7. Path to observable output: the buffer is returned via *arg7 (pointer) and *arg6 (full size), then placed at full length into a process-token security attribute (SeSetSecurityAttributesToken) and a file extended attribute (AipSetEaFromOrigin), both of which are queryable from user mode via standard Windows APIs.
  8. Expected effect: No crash. A few bytes of paged-pool residue are carried into those objects. Under a debugger, dumping the buffer at the return point shows the uninitialized tail.

Triggering Finding 2

Finding 2 is not a security defect (see section 2). For reference, concurrent WIL feature-enablement checks for Feature_4060024122 reach Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E00wil_details_IsEnabledFallback @ 0x1C0002850wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C00028DC. Both callers perform a lock cmpxchg on the same global dword, but this is a correct lock-free CAS with a re-read-and-retry loop: the loser of a CAS re-reads the current value and retries, so no state is lost or torn. No inconsistent or corrupted state results, and no AppLocker allow/deny decision is involved.


6. Exploit Primitive & Development Notes

Finding 1: Uninitialized paged pool disclosure

  • Primitive: Disclosure of a small amount of stale kernel paged pool memory — 4 bytes (when arg5 == NULL) or roughly 6 trailing bytes (when arg5 != NULL) per allocation.
  • What is established from the binaries: The uninitialized tail is copied at the full allocation length into (a) a process-token security attribute via SmartlockerTagProcessTokenSeSetSecurityAttributesToken, and (b) a file extended attribute via AipSetEaFromOrigin. Both object types are queryable from user mode by design (NtQueryInformationToken(TokenSecurityAttributes), NtQueryEaFile), but that user-mode read is performed by the operating system, not by appid.sys; there is no IRP/IOCTL output-buffer copy of the buffer inside this driver.
  • Bounds on impact: The disclosed quantity is at most 4–6 bytes per allocation. The contents are whatever paged-pool residue previously occupied that block; the disassembly does not establish that these bytes contain any specific class of data (pointers, tokens, etc.). Claims of a KASLR bypass or a token/EPROCESS harvest are not supported by the binaries and are not made here.
  • Mitigation relevance: This is a read/disclosure of uninitialized memory (CWE-908), not a write or a code-execution vector; SMEP/SMAP/HVCI/CFG are not relevant.
  • Fix status: The patched memset closes the gap only when the WIL feature Feature_965997883 is enabled; with the feature disabled the patched driver behaves exactly as the unpatched one. This is a staged rollout, not an unconditional fix.

Finding 2: WIL feature-state cache refactor (no exploit primitive)

  • No primitive. The unpatched atomic compare-and-swap is a correct lock-free operation (re-read and retry on failure); there is no genuine race to exploit. The state involved is WIL feature enablement/usage telemetry, never an AppLocker allow/deny decision. The global-to-per-descriptor change is a library refactor with no security impact.

7. Debugger PoC Playbook

For Finding 1: Uninitialized Pool Allocation

Breakpoints:

bp appid!SmartlockerConstructOriginClaim (sub_1C001F138)+0x52   ; = 0x1c001f18a — ExAllocatePoolWithTag call
bp appid!SmartlockerConstructOriginClaim (sub_1C001F138)+0xed   ; = 0x1c001f225 — "and dword [rbx+0x38], 0" (NULL arg5 path, bytes 0x3C-0x3F leaked)
bp appid!SmartlockerConstructOriginClaim (sub_1C001F138)+0x100  ; = 0x1c001f238 — STATUS_SUCCESS return (buffer fully populated)

If symbols are not available, use raw offsets from the module base:

bp <appid_base>+0x1f18a   ; ExAllocatePoolWithTag call
bp <appid_base>+0x1f225   ; NULL-arg5 uninit path
bp <appid_base>+0x1f238   ; return point (inspect buffer here)

What to inspect:

Breakpoint Register/Memory Purpose
+0x1f18a (pre-alloc) edx = allocation size (esi); ecx = 1 (PagedPool); r8d = 'smtA' tag Confirm allocation parameters
+0x1f196 (post-alloc) rax/rbx = allocated buffer pointer Record the pool address for later inspection
+0x1f225 (NULL-arg5 path) rbx+0x3C through rbx+0x3F These 4 bytes should contain stale pool data
+0x1f238 (return) rbx = final buffer; inspect dq rbx L8 Dump full buffer to see all uninitialized gaps

Commands to run at the return breakpoint:

; Dump the full buffer (0x40 bytes = 8 qwords)
dq rbx L8

; Focus on bytes 0x3C-0x3F (last dword)
dd rbx+0x3c L1

; Check if the leaked content looks like a kernel pointer
!pool rbx

Key instruction offsets:

Offset Instruction Significance
+0x52 (0x1C001F18A) call cs:__imp_ExAllocatePoolWithTag The allocation; no zeroing follows in the unpatched build
Between +0x52 and the NULL check (absent) No zeroing call. The patched build inserts a memset at 0x1C001F1C2, but only on the feature-enabled path (Feature_965997883)
+0xED (0x1C001F225) and dword ptr [rbx+38h], 0 Only zeroes [+0x38]; [+0x3C..0x3F] left uninitialized

Trigger setup:

  1. Enable AppLocker policy enforcement on the target system (or use WDAC).
  2. From a user-mode test harness, open \Device\\AppID or simply execute a file that triggers policy evaluation: c CreateProcessW(L"C:\\Windows\\System32\\notepad.exe", ...);
  3. The call chain is: file operation → kernel AppID interception → SmartlockerVerifyProcess @ 0x1C0020344 (dispatcher) → SmartlockerManagedInstallerCheck @ 0x1C001F25C / SmartlockerSmartscreenProcessTokenCheck @ 0x1C001F374SmartlockerConstructOriginClaim @ 0x1C001F138.

Expected observation:

  • At the return breakpoint, dq rbx L8 will show the buffer. Bytes at offset 0x3C–0x3F (the last dword) may contain non-zero residue from a previously freed pool block when arg5 == NULL — these are the uninitialized bytes.
  • Use !pool rbx to inspect the pool block.
  • No crash — this is a silent disclosure of a few bytes.

Struct/offset notes:

Offset (from buffer base) Size Initialized by Notes
0x00 dword constant 1 Magic/type
0x04 dword arg1 (event type)
0x08 dword constant 0
0x0C dword constant 1
0x10 16 bytes BCryptGenRandom Random bytes
0x20 16 bytes BCryptGenRandom or copy of 0x10 Depends on arg1 == 0
0x30 dword arg4
0x34 dword global_version (data_1c0016660)
0x38 dword *arg5 or 0
0x3C–0x3F 4 bytes UNINITIALIZED (when arg5 == NULL) Leaked pool data

For Finding 2: WIL feature-state cache refactor (not a security issue)

Finding 2 is a WIL feature-staging refactor with no security impact, so there is no exploit to reproduce. For inspection of the relevant code:

Breakpoints (RVAs from the image base):

bp appid!wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath   ; unpatched entry @ 0x1C00028DC
bp <appid_base>+0x290e   ; lock cmpxchg dword ptr [r9], edx (0x1C000290E)  the atomic CAS

The instruction at 0x1C00028DC (mov r9, cs:wil_details_featureDescriptors_a) loads the global feature descriptor pointer, and the CAS at 0x1C000290E is followed by jnz 0x1C00028FC (retry). Observing this loop under contention shows a correct lock-free compare-and-swap: on failure it re-reads the current value and retries, so no update is lost. This is expected, correct behavior, not a race, and no security-relevant state can be corrupted.

Expected observation:

  • The write breakpoint on the global state dword will fire from multiple thread contexts.
  • The CAS loop (lock cmpxchg) may occasionally fail and retry under contention. This is the expected, correct behavior of a lock-free CAS: the retry re-reads the current value, so no update is lost and no state is corrupted.
  • No inconsistent or torn state results; there is no security-relevant effect to observe.

Struct/offset notes:

Symbol Reference Description
wil_details_featureDescriptors_a mov r9, cs:wil_details_featureDescriptors_a at 0x1C00028DC Global feature descriptor pointer (unpatched)
State bit 0x10 Set for event type 3
State bit 0x20 Set for event type 4

8. Changed Functions — Full Triage

Security-Relevant Changes

Function Similarity Change Type Summary
SmartlockerConstructOriginClaim @ 0x1C001F138 0.8129 Security-relevant Pool allocation without zeroing → adds a memset that zeroes the buffer, gated behind Feature_965997883; the size base for the arg5 != NULL case is 0x40 when the feature is on and 0x42 (original) when off. Feature-off path is identical to the unpatched code (staged rollout).

WIL Feature-Staging Refactor (not security-relevant)

The following four functions form a single WIL (Windows Implementation Library) feature-staging refactor: the callee stops hard-coding the single global feature descriptor and instead receives a per-descriptor context pointer as a parameter. The underlying atomic compare-and-swap is already correct in both builds, and none of these functions touch an AppLocker allow/deny decision.

Function Similarity Change Type Summary
wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C00028DC 0.8176 Refactor (non-security) Atomic CAS retargeted from the global descriptor to *arg3; adds a lock or fast path when per-descriptor bytes [arg3+0x1d]/[arg3+0x1e] are set. The unpatched CAS is already a correct lock-free loop.
wil_details_IsEnabledFallback @ 0x1C0002850 0.9535 Refactor (non-security) Takes new arg3 (descriptor pointer); passes it to child functions instead of using the global descriptor.
Feature_4060024122__private_IsEnabledFallback @ 0x1C0002E00 0.6736 Refactor (non-security), thunk Adds a 3rd arg (lea r8, Feature_4060024122__private_descriptor); relocated to 0x1C0002E74 in the patched binary.
wil_details_FeatureReporting_ReportUsageToServiceDirect @ 0x1C00022FC 0.9765 Refactor (non-security) Hardcoded feature id and global config table → reads them from the context pointer (*(arg1+0x18), *(arg1+8)).
wil_details_FeatureReporting_ReportUsageToService @ 0x1C00023EC 0.9897 Refactor (non-security) Hardcoded id and global dispatch table → reads id from *(arg1+0x18), config from *(arg1+0x10), callback from dword_1C0016230. Same event-type mapping logic.

The differences in the two ReportUsage* functions are limited to register reallocation and the substitution of immediate constants with context-pointer-relative loads; the control flow and arithmetic logic are identical.


9. Unmatched Functions

No functions were added or removed in the patched binary (unmatched_unpatched: 0, unmatched_patched: 0). The patch is entirely in-place — all changes are modifications to existing functions, not additions of new security checks or removal of vulnerable helpers.

This is notable because it means the zeroing memset was inserted inline into SmartlockerConstructOriginClaim @ 0x1C001F138 rather than factored into a new wrapper function, and the WIL per-descriptor context refactor was accomplished by rethreading parameters through the existing call chain rather than introducing new dispatch functions.


10. Confidence & Caveats

Confidence: High (for Finding 1), High (for Finding 2 being a non-issue)

Finding 1 (uninitialized memory, Low): - The unpatched assembly shows ExAllocatePoolWithTag followed by partial initialization with no intervening zeroing call, leaving 4 bytes uninitialized when arg5 == NULL and roughly 6 trailing bytes otherwise. This is unambiguous. - The patched build adds a zeroing memset, but only on the path taken when Feature_965997883 is enabled; the feature-disabled path allocates without zeroing (identical to unpatched). The fix is a staged rollout. - The uninitialized tail is carried at full length into a process-token security attribute (SeSetSecurityAttributesToken) and a file extended attribute (AipSetEaFromOrigin). Both object types are user-mode-queryable by Windows design, but that final read is performed by the OS; the driver itself does not copy the buffer to a user IRP/IOCTL output. The disclosed quantity is small (4–6 bytes), which keeps the practical severity Low.

Finding 2 (WIL feature-state refactor, not a security issue): - Both builds use the same lock-free atomic compare-and-swap with a re-read-and-retry loop; there is no genuine data race. The only change is that the descriptor is passed as a per-descriptor context parameter (arg3) rather than read from the single global descriptor. - These functions manipulate only WIL feature enablement/usage telemetry bits; none participate in an AppLocker allow/deny decision. There is no security impact.

What a researcher should verify manually:

  1. Finding 1: Confirm whether Feature_965997883 is enabled by default in the shipped configuration; if disabled, the patched driver leaves the uninitialized-allocation path in place.
  2. Finding 1: Determine, on a live system, whether the residue placed into the token security attribute and file extended attribute is in practice queryable by an unprivileged process (NtQueryInformationToken(TokenSecurityAttributes), NtQueryEaFile), and whether the 4–6 uninitialized bytes ever carry sensitive content.
  3. Finding 1: Driver Verifier with special pool can be used to confirm the use of uninitialized memory.
  4. All: The 0x1C00xxxx addresses in this analysis are RVAs from the preferred image base, not absolute runtime addresses; confirm the loaded module base at runtime.