• Unpatched Binary: cng_unpatched.sys
  • Patched Binary: cng_patched.sys
  • Overall Similarity Score: 0.9625
  • Diff Statistics: 1552 matched functions, 85 changed functions, 1467 identical functions, 0 unmatched functions in either direction.

Verdict: The patch adds a hard iteration cap to an ML-DSA (FIPS 204 module-lattice post-quantum signature) rejection-sampling loop in the statically-linked SymCrypt library. The unpatched loop only terminates when the accepted-coefficient counter reaches 256, and that counter advances only on accepted samples. If every sample were rejected the loop would spin forever. That code pattern (a loop whose exit condition depends solely on making progress) genuinely exists in the unpatched build and is genuinely bounded in the patched build. However, the reject condition is gated by the ML-DSA eta bound, which is a compile-time constant taken from one of three static parameter tables and is not attacker-controlled. The change is therefore a defensive robustness hardening (CWE-835, loop with a potentially unreachable exit condition), not the remotely triggerable high-severity kernel DoS originally posited. Severity is Low.

The patch applies the same CWE-835 iteration-cap pattern to a second SymCrypt function, SymCryptMlDsaRejNttPoly (unpatched @ 0x180096CEC, patched @ 0x18009AA84), which rejection-samples the NTT-domain coefficients of the public matrix A. This second instance is documented in section 7a below. Its reachability differs in one important way: the seed that keys the SHAKE-128 stream driving the loop is the ML-DSA rho value, which comes directly from an attacker-supplied public-key blob (via BCryptImportKeyPair), not from a fixed compile-time table. Even so, forcing pathological rejection would require biasing the SHAKE-128 output distribution, which is computationally infeasible, so this instance is also Low severity. Both instances share CWE-835 and the overall verdict remains a low-severity defensive fix.


2. Vulnerability Summary

  • Severity: Low (defensive hardening; not reachable with attacker-controlled input)
  • Vulnerability Class: CWE-835 — Loop with unreachable exit condition (potential infinite loop)
  • Affected Function: cng!SymCryptMlDsaRejBoundedPoly @ 0x180096B4C (Unpatched), @ 0x18009A8C0 (Patched)

Root Cause: SymCryptMlDsaRejBoundedPoly squeezes bytes from a SHAKE/Keccak sponge and performs rejection sampling to generate the 256 bounded coefficients of an ML-DSA secret polynomial. In the unpatched build the loop terminates only when the output coefficient counter (edi) reaches 0x100 (256), and that counter increments only when a sample is accepted (0x180096C7F and 0x180096CA4, each guarded by a != 0x80 test).

Acceptance is gated by the ML-DSA eta bound, a byte read at [r14+0x16] (offset +0x16 of the internal parameters structure, 0x180096BE5). Each squeezed byte is split into two 4-bit nibbles; each nibble is mapped to a centered coefficient in [-eta, eta], and out-of-range nibbles are set to the 0x80 sentinel (0x180096C2E for the first nibble, 0x180096C60 for the second) marking a rejected sample. The code paths compare eta only against 2 and 4 (0x180096BEE and the cmp al, bl at 0x180096C17, where bl = 4). If eta held any other value, both nibbles would be forced to 0x80 on every iteration, edi would never increment, and the while (edi < 0x100) loop (back-edge jb loc_180096BCB at 0x180096CAC) would spin without progress.

Why this is Low, not High — reachability: The eta byte is not attacker-controlled. SymCryptMlDsaExpandS (sub_180095F78) is called with an internal parameters pointer produced by SymCryptMlDsaGetInternalParamsFromParams (sub_180096074), which selects one of three static, read-only structures — SymCryptMlDsaInternalParams44, SymCryptMlDsaInternalParams65, or SymCryptMlDsaInternalParams87 — based on a validated parameter-set index. An unrecognized index returns a SymCrypt error (0x800E or 0x8011) before any sampling occurs. The only eta values that can reach the loop are the compile-time constants baked into those tables (2 for ML-DSA-44/87, 4 for ML-DSA-65). There is no BCrypt/CNG API path that lets a caller inject an arbitrary eta; reaching the non-terminating path would require memory corruption from an unrelated bug. The alternative of forcing every sampled nibble to reject with a valid eta (needing the SHAKE stream to yield only out-of-range nibbles for hundreds of bytes) is computationally infeasible.

The patch is a defensive iteration cap on the rejection-sampling loop, consistent with SymCrypt library robustness hardening. It introduces an independent iteration counter and hard-caps the loop at 481 (0x1E1) squeeze iterations, returning a SymCrypt error code (0x8008) if exceeded.

Call path (for the code pattern, not an exploit): 1. A CNG ML-DSA key operation is invoked (e.g., BCryptGenerateKeyPair / BCryptFinalizeKeyPair on an ML-DSA algorithm provider). 2. SymCryptMlDsaExpandS (sub_180095F78) expands the secret vectors s1/s2 and calls SymCryptMlDsaRejBoundedPoly (sub_180096B4C) once per polynomial (0x180095FF9, 0x18009602D). 3. The internal parameters pointer passed in comes from the validated static table selection in SymCryptMlDsaGetInternalParamsFromParams (sub_180096074), so eta is always 2 or 4 on any reachable path.


3. Pseudocode Diff

The loop exit depends solely on accepting samples; the patch adds an independent iteration counter as a hard bound.

// UNPATCHED SymCryptMlDsaRejBoundedPoly (sub_180096b4c) - key loop logic:
uint32_t edi = 0;                       // output index
do {
    SymCryptKeccakExtract(&sponge, &b, 1, 0);   // squeeze next SHAKE byte
    uint8_t cl = b;
    uint8_t al = *(uint8_t*)(params + 0x16);    // eta bound (constant: 2 or 4)
    uint8_t dl = cl & 0xf;                       // low nibble
    uint8_t r8;

    if (al == 2) {
        if (dl >= 0xf) { r8 = 0x80; }            // reject
        else { r8 = ((dl * 0xd) >> 6) * 5 - dl + 2; }
    } else if (al == 4 && dl < 9) {
        r8 = 4 - dl;
    } else {
        r8 = 0x80;                                // reject
    }
    // ... second nibble computed symmetrically into dl, else dl = 0x80 ...

    if (r8 != 0x80) { out[edi] = ...; edi++; }    // increment only on accept
    if (dl != 0x80) {
        if (edi >= 0x100) break;
        out[edi] = ...; edi++;                    // increment only on accept
    }
} while (edi < 0x100);
// Exit condition depends only on edi advancing; if every sample rejected, no progress.

// PATCHED SymCryptMlDsaRejBoundedPoly (sub_18009a8c0):
uint32_t ebx = 0;                        // output index (was edi)
uint32_t esi = 0;                        // ADDED: independent iteration counter
do {
    if (esi >= 0x1e1) {                  // ADDED: hard cap of 481 iterations
        ret = 0x8008;                    // SymCrypt error return
        break;
    }
    esi++;                               // increments unconditionally every iteration
    // ... identical sampling/accept logic, writing out[ebx], ebx++ on accept ...
} while (ebx < 0x100);

4. Assembly Analysis

Unpatched loop body (SymCryptMlDsaRejBoundedPoly @ 0x180096B4C), showing the missing iteration bound:

0000000180096BC8  lea     ebx, [rdi+4]              ; bl = 4 (edi==0 here) -> the eta==4 compare value
0000000180096BCB  xor     r9d, r9d                  ; loc_180096BCB: loop head (no iteration counter)
0000000180096BCE  lea     rdx, [rsp+140h+var_11F]
0000000180096BD3  lea     rcx, [rsp+140h+var_110]
0000000180096BD8  lea     r8d, [r9+1]
0000000180096BDC  call    SymCryptKeccakExtract     ; squeeze next SHAKE byte
0000000180096BE1  mov     cl, [rsp+140h+var_11F]    ; cl = current byte
0000000180096BE5  mov     al, [r14+16h]             ; al = eta bound (params+0x16)
0000000180096BE9  mov     dl, cl
0000000180096BEB  and     dl, 0Fh                   ; dl = low nibble
0000000180096BEE  cmp     al, 2
0000000180096BF0  jnz     short loc_180096C17       ; if eta != 2, test eta == 4
0000000180096BF2  cmp     dl, 0Fh
0000000180096BF5  jnb     short loc_180096C2B       ; nibble >= 0xf -> reject
;   ... compute r8 from nibble (eta==2 mapping) ...
0000000180096C17  cmp     al, bl                    ; loc_180096C17: eta == 4 ?
0000000180096C19  jnz     short loc_180096C2B       ; eta neither 2 nor 4 -> reject
0000000180096C1B  cmp     dl, 9
0000000180096C1E  jnb     short loc_180096C2B       ; nibble >= 9 -> reject
;   ... compute r8 = 4 - dl (eta==4 mapping) ...
0000000180096C2B  shr     cl, 4                     ; loc_180096C2B: move to high nibble
0000000180096C2E  mov     r8b, 80h                  ; first-nibble sentinel (reject)
;   ... second nibble computed symmetrically, else: ...
0000000180096C60  mov     dl, 80h                   ; loc_180096C60: second-nibble sentinel (reject)
0000000180096C62  cmp     r8b, 80h
0000000180096C66  jz      short loc_180096C81       ; skip store if rejected
;   ... store out[edi] ...
0000000180096C7F  inc     edi                       ; edi++ ONLY on accepted sample
0000000180096C81  cmp     dl, 80h
0000000180096C84  jz      short loc_180096CA6       ; skip store if rejected
0000000180096C86  cmp     edi, 100h
0000000180096C8C  jnb     short loc_180096CB2       ; break if edi >= 0x100
;   ... store out[edi] ...
0000000180096CA4  inc     edi                       ; edi++ ONLY on accepted sample
0000000180096CA6  cmp     edi, 100h
0000000180096CAC  jb      loc_180096BCB             ; loop back while edi < 0x100
;                                                    ; no progress => no exit (CWE-835)

Patched loop body (SymCryptMlDsaRejBoundedPoly @ 0x18009A8C0), showing the added cap:

000000018009A8FC  xor     edi, edi                  ; return value = 0
000000018009A93A  mov     ebx, edi                  ; ebx = output index (was edi in unpatched)
000000018009A93C  mov     esi, edi                  ; esi = 0 : ADDED iteration counter
000000018009A93E  cmp     esi, 1E1h                 ; loc_18009A93E: loop head
000000018009A944  jnb     loc_18009AA41             ; if esi >= 481 -> bail out
000000018009A94A  xor     r9d, r9d
;   ... call SymCryptKeccakExtract, read eta at [r15+16h], same sampling logic ...
000000018009A9E7  inc     esi                       ; esi++ EVERY iteration (unconditional)
;   ... on accept: inc ebx (0x18009AA05 / 0x18009AA2D), store out[ebx] ...
000000018009AA33  cmp     ebx, 100h
000000018009AA39  jb      loc_18009A93E             ; loop back while ebx < 0x100
000000018009AA3F  jmp     short loc_18009AA46
000000018009AA41  mov     edi, 8008h                ; loc_18009AA41: iteration cap hit -> error 0x8008
000000018009AA46  mov     edx, 0F0h                 ; loc_18009AA46: common cleanup, return edi

0x8008 is a SymCrypt error code, not an NTSTATUS. It is returned in edi/eax on the capped path.


5. Trigger Conditions

The non-terminating path is not reachable through the CNG/BCrypt API surface, because eta is a compile-time constant, not caller input:

  1. Obtain a CNG Handle: Open an ML-DSA algorithm provider via BCryptOpenAlgorithmProvider.
  2. Reach Key Generation: Invoke an ML-DSA key-pair operation so that SymCryptMlDsaExpandS calls SymCryptMlDsaRejBoundedPoly.
  3. eta is fixed: SymCryptMlDsaGetInternalParamsFromParams (sub_180096074) selects one of the static tables SymCryptMlDsaInternalParams44/65/87 by a validated parameter-set index; the reachable eta values are only 2 or 4. An unrecognized index returns 0x800E/0x8011 before any sampling.
  4. No API path to an invalid eta: With a valid eta, the rejection loop terminates with overwhelming probability well within 481 iterations. Forcing every nibble to reject with a valid eta would require controlling the SHAKE output stream for hundreds of bytes, which is computationally infeasible. Reaching the non-terminating branch would require an out-of-band memory corruption of the parameters structure.

Because the input that would drive the loop past its bound cannot be supplied through the intended interface, this is a defensive fix rather than an exploitable condition.


6. Exploit Primitive & Development Notes

  • Primitive: None reachable through the CNG API. The code pattern, if the eta byte were corrupted by an unrelated bug, would be a kernel-thread livelock (no memory corruption, no read/write/execute primitive).
  • Attacker control: The gating value (eta at params+0x16) originates from static, read-only parameter tables selected by a validated index, so an attacker cannot set it via BCryptGenerateKeyPair / BCryptImportKeyPair or any documented interface.
  • Nature of the fix: A bounded-iteration guard added to a rejection-sampling loop in the statically-linked SymCrypt ML-DSA implementation — standard library robustness hardening.

7. Debugger Playbook (verification of the code change)

Load the unpatched cng.sys and inspect the loop to confirm the pattern; load the patched build to confirm the added cap.

Unpatched breakpoints:

bp cng_unpatched+0x96b4c        ; SymCryptMlDsaRejBoundedPoly entry
bp cng_unpatched+0x96be5        ; mov al, [r14+16h] : reads eta
bp cng_unpatched+0x96cac        ; jb loc_180096BCB : loop back-edge

What to inspect: - Entry 0x180096B4C: rcx = params pointer; db rcx+0x16 l1 shows the eta byte. On any reachable call it is 02 or 04. - 0x180096BE5: al = eta. Only 2 or 4 occur on reachable paths. - 0x180096CAC: back-edge. edi (output index) advances on every accepted sample; with a valid eta it reaches 0x100 and the loop exits.

Patched confirmation: - 0x18009A93E: cmp esi, 1E1h — the added counter check. - 0x18009A9E7: inc esi — unconditional per-iteration increment. - 0x18009AA41: mov edi, 8008h — the bail-out error return when the cap is hit.

Struct/offset notes: - params + 0x16: ML-DSA eta bound, a constant field in SymCryptMlDsaInternalParams44/65/87 (2 for 44/87, 4 for 65).


7a. Second Instance — SymCryptMlDsaRejNttPoly (same CWE-835 hardening)

  • Severity: Low (defensive hardening; attacker-controlled seed but not practically triggerable)
  • Vulnerability Class: CWE-835 — Loop with unreachable exit condition (potential infinite loop)
  • Affected Function: cng!SymCryptMlDsaRejNttPoly @ 0x180096CEC (Unpatched), @ 0x18009AA84 (Patched)

Root Cause: SymCryptMlDsaRejNttPoly generates the 256 NTT-domain coefficients of one entry of the ML-DSA public matrix A by rejection sampling a SHAKE-128 (Keccak) stream. It appends a 34-byte seed (0x22 bytes = the 32-byte rho plus a 2-byte matrix index i||j) to the sponge (SymCryptKeccakAppend at 0x180096D5C), then repeatedly squeezes 3 bytes, masks the top bit of the third byte to form a 23-bit value (and byte ptr [...+2], 7Fh at 0x180096D84), and rejects any value >= 0x7FE001 (the ML-DSA prime q = 8380417, cmp eax, 7FE001h at 0x180096D8D).

In the unpatched build the loop has no iteration counter. The output count rbx is initialized to 0x100 (256, mov ebx, 100h at 0x180096D69) and decrements only on an accepted sample (sub rbx, 1 at 0x180096D9A, jnz short loc_180096D6E at 0x180096D9E). A rejected sample takes the back-edge jnb short loc_180096D6E at 0x180096D92 straight back to the squeeze without touching any counter. The only way to leave the loop is for rbx to reach 0, i.e. by accepting 256 coefficients. If the stream produced only out-of-range values the loop would not terminate (CWE-835).

The patched build adds an independent iteration counter r14d (initialized to 0 via mov r14d, edi at 0x18009AAC1), checks it at the loop head (cmp r14d, 12Ah at 0x18009AB01, jnb short loc_18009AB45 at 0x18009AB08) with a hard cap of 0x12A (298), and increments it unconditionally every iteration (inc r14d at 0x18009AB25, before the reject test). If the cap is hit it returns SymCrypt error 0x8008 (mov edi, 8008h at 0x18009AB45). 298 bounds the total squeeze iterations above the 256 accepted outputs needed, leaving margin for the small number of rejections expected under a valid stream.

Why this is Low — reachability: Unlike eta in SymCryptMlDsaRejBoundedPoly (a fixed table constant), the seed here is attacker-controlled. The call path from an untrusted surface is:

  1. BCryptImportKeyPair on an ML-DSA public-key blob reaches SymCryptMlDsakeySetValue (sub_18008a47c), which calls SymCryptMlDsaPkDecode at 0x18008A4E5.
  2. SymCryptMlDsaPkDecode (sub_18009632c) copies the 32-byte rho from the parsed public key into the key object at [rbx+56h] (movups xmmword ptr [r12], xmm0 at 0x1800963FD, r12 = rbx+56h) and calls SymCryptMlDsaExpandA at 0x18009653C with rcx = r12 (the rho pointer).
  3. SymCryptMlDsaExpandA (sub_180095dd8) copies those 32 bytes into a local buffer (movups from [rcx] / [rcx+10h] at 0x180095DF3 / 0x180095DFB), appends the 2-byte matrix index, and calls SymCryptMlDsaRejNttPoly at 0x180095E44 once per matrix cell.
  4. SymCryptMlDsaRejNttPoly appends rho||i||j to SHAKE-128 and rejection-samples as above.

So the rho seed that keys the SHAKE-128 stream is public-key material the attacker fully controls (e.g. before BCryptVerifySignature). However, triggering the non-terminating / cap-exhausting path requires the SHAKE-128 output — keyed by that rho — to land in the tiny reject window (0x800000 - 0x7FE001 = 0x1FFF, about 0.098% per sample) more than the ~42 times the 298-iteration cap tolerates over 256 accepts. Biasing a XOF's output distribution this way is a preimage-hardness problem and is computationally infeasible. Real-world severity therefore remains Low/informational, but this is a more attacker-adjacent code path than the covered function, and the direction is correct (patched strictly bounds the loop, unpatched is unbounded).

Assembly (unpatched SymCryptMlDsaRejNttPoly @ 0x180096CEC), no iteration bound:

0000000180096D5C  call    SymCryptKeccakAppend      ; append 0x22-byte seed (rho||i||j)
0000000180096D69  mov     ebx, 100h                 ; output count = 256
0000000180096D6E  xor     r9d, r9d                  ; loc_180096D6E: loop head (NO iteration counter)
0000000180096D7F  call    SymCryptKeccakExtract     ; squeeze 3 bytes
0000000180096D84  and     byte ptr [rsp+130h+var_110+2], 7Fh ; mask top bit -> 23-bit value
0000000180096D89  mov     eax, [rsp+130h+var_110]
0000000180096D8D  cmp     eax, 7FE001h              ; compare against q = 8380417
0000000180096D92  jnb     short loc_180096D6E       ; value >= q -> reject, loop back (no counter)
0000000180096D94  mov     [rdi], eax                ; store accepted coefficient
0000000180096D96  add     rdi, 4
0000000180096D9A  sub     rbx, 1                    ; decrement output count ONLY on accept
0000000180096D9E  jnz     short loc_180096D6E       ; loop back while count != 0
;                                                    ; only exit is count reaching 0 (CWE-835)

Assembly (patched SymCryptMlDsaRejNttPoly @ 0x18009AA84), added cap:

000000018009AAC1  mov     r14d, edi                 ; r14d = 0 : ADDED iteration counter
000000018009AAFF  mov     ebx, edi                  ; ebx = 0 : output index
000000018009AB01  cmp     r14d, 12Ah                ; loc_18009AB01: loop head, cap = 298
000000018009AB08  jnb     short loc_18009AB45       ; if r14d >= 0x12A -> bail out
000000018009AB1B  call    SymCryptKeccakExtract     ; squeeze 3 bytes
000000018009AB20  and     byte ptr [rsp+140h+var_120+2], 7Fh
000000018009AB25  inc     r14d                      ; r14d++ EVERY iteration (unconditional)
000000018009AB28  mov     eax, [rsp+140h+var_120]
000000018009AB2C  cmp     eax, 7FE001h              ; compare against q
000000018009AB31  jnb     short loc_18009AB01       ; value >= q -> reject, back to cap check
000000018009AB33  mov     [rsi], eax                ; store accepted coefficient
000000018009AB35  inc     ebx                       ; ebx++ on accept
000000018009AB37  add     rsi, 4
000000018009AB3B  cmp     ebx, 100h
000000018009AB41  jb      short loc_18009AB01       ; loop back while ebx < 0x100
000000018009AB45  mov     edi, 8008h                ; loc_18009AB45: cap hit -> SymCrypt error 0x8008
000000018009AB4A  mov     eax, edi                  ; return edi

0x8008 is a SymCrypt error code (not an NTSTATUS), returned in edi/eax on the capped path — the same error convention used by the SymCryptMlDsaRejBoundedPoly fix above.


8. Changed Functions — Full Triage

Note: Several changes represent register relocations and refactoring of validation routines.

  • SymCryptMlDsaRejBoundedPoly (sub_180096b4c) (0.9187 similarity): [Hardening] Added an independent iteration counter (esi) with a hard cap of 481 (0x1E1, cmp esi,1E1h at 0x18009A93E) to bound the rejection-sampling loop, returning error 0x8008 if exceeded (mov edi,8008h at 0x18009AA41). The bounded value (eta at params+0x16) is a static per-parameter-set constant, so this is defensive robustness, not an attacker-reachable fix. CWE-835, low severity.
  • SymCryptMlDsaRejNttPoly (sub_180096cec) (patched sub_18009aa84): [Hardening] Added an independent iteration counter (r14d) with a hard cap of 298 (0x12A, cmp r14d,12Ah at 0x18009AB01) to bound the NTT-domain rejection-sampling loop, returning error 0x8008 if exceeded (mov edi,8008h at 0x18009AB45). The unpatched loop (back-edge jnz loc_180096D6E at 0x180096D9E, reject back-edge jnb loc_180096D6E at 0x180096D92) has no iteration counter and can exit only by accepting 256 coefficients. The SHAKE-128 seed (rho) is attacker-controlled via imported public-key material, but pathological rejection is computationally infeasible to force. CWE-835, low severity. See section 7a.
  • SymCryptMlDsaPkDecode (sub_18009632c) (0.938 similarity): [Behavioral] Added validation of the count field at *(arg4+0xc0) prior to entering a multi-precision scalar multiplication loop, plus error-path propagation and cleanup for the SymCryptMlDsaExpandA call. Hardening against use of uninitialized crypto state. (This function is also the untrusted entry point that reaches SymCryptMlDsaRejNttPoly via SymCryptMlDsaExpandA; see section 7a.)
  • SymCryptMlDsaVerifyEx (sub_180089d9c) (0.9221 similarity): [Behavioral] Refactored ML-DSA signature-verification parameter validation. Centralized inline size checks into a dedicated signature-decode routine plus an infinity-norm bound check.
  • MSCryptMlDsaVerifySignature (sub_180074a60) / MSCryptMlDsaSignHash (sub_180074800) (0.7368 similarity): [Behavioral] Refactored dispatch: replaced string-based cipher/hash mode lookups with direct integer type codes and added an upper-bounds check for RSA key sizes.
  • GetBlobInfo (sub_180075114) (0.0937 similarity): [Cosmetic] Rewrite of algorithm type validation using magic-value comparisons instead of string table iteration.
  • SymCryptRsakeyGenerate (sub_1800860e8) (0.9933 similarity): [Cosmetic] Register/call-target relocation between builds. Passes an extra 0 argument to the SymCryptRsakeyCalculatePrivateFields call.
  • SymCryptRsakeyCalculatePrivateFields (sub_18002ce54) (0.7495 similarity): [Behavioral] Added a new flags parameter (arg7) to the RSA private-field computation, returning SymCrypt error 0x800E (invalid argument) if disallowed flag bits are set.

9. Unmatched Functions

  • Added: 0
  • Removed: 0

No functions were entirely added or removed; all modifications were applied inline to existing cryptographic routines.


10. Confidence & Caveats

  • Confidence: High on the mechanics. The unpatched loop (back-edge jb loc_180096BCB at 0x180096CAC, output counter edi that advances only on accepted samples) and the patched iteration cap (esi, cmp esi,1E1h, error return 0x8008) are both directly present in the binaries and the patch direction is correct (unpatched unbounded, patched bounded). The same is confirmed for the second instance, SymCryptMlDsaRejNttPoly (unpatched back-edges jnb loc_180096D6E/jnz loc_180096D6E, patched cmp r14d,12Ah at 0x18009AB01 with unconditional inc r14d at 0x18009AB25 and error return 0x8008 at 0x18009AB45); see section 7a.
  • Second instance (SymCryptMlDsaRejNttPoly): Same CWE-835 pattern, Low severity. Its rho seed is attacker-controlled through BCryptImportKeyPair -> SymCryptMlDsakeySetValue -> SymCryptMlDsaPkDecode -> SymCryptMlDsaExpandA -> SymCryptMlDsaRejNttPoly, so the "not caller-controlled" argument used for RejBoundedPoly does not apply here. The infeasibility rests instead on the impossibility of biasing the SHAKE-128 output distribution to force enough rejections to exhaust the 298-iteration budget, so real-world severity is still Low/informational.
  • Severity rationale: The gating eta value at params+0x16 is a compile-time constant sourced from the static tables SymCryptMlDsaInternalParams44/65/87 via the validated selection in SymCryptMlDsaGetInternalParamsFromParams (sub_180096074). No documented CNG/BCrypt path lets a caller supply an arbitrary eta, and the valid-eta loop terminates with overwhelming probability. The change is defensive hardening (CWE-835), classified Low rather than a remotely triggerable high-severity kernel DoS.
  • Caveat: Reaching the non-terminating branch would require an out-of-band corruption of the parameters structure from a separate defect; that scenario is not demonstrated by these binaries.