cng.sys — Defensive iteration caps on two ML-DSA rejection-sampling loops (SymCryptMlDsaRejBoundedPoly and SymCryptMlDsaRejNttPoly), CWE-835, low severity, fixed
KB5077179
- 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:
- Obtain a CNG Handle: Open an ML-DSA algorithm provider via
BCryptOpenAlgorithmProvider. - Reach Key Generation: Invoke an ML-DSA key-pair operation so that
SymCryptMlDsaExpandScallsSymCryptMlDsaRejBoundedPoly. - eta is fixed:
SymCryptMlDsaGetInternalParamsFromParams (sub_180096074)selects one of the static tablesSymCryptMlDsaInternalParams44/65/87by a validated parameter-set index; the reachableetavalues are only2or4. An unrecognized index returns0x800E/0x8011before any sampling. - 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 validetawould 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
etabyte 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 (
etaatparams+0x16) originates from static, read-only parameter tables selected by a validated index, so an attacker cannot set it viaBCryptGenerateKeyPair/BCryptImportKeyPairor 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:
BCryptImportKeyPairon an ML-DSA public-key blob reachesSymCryptMlDsakeySetValue (sub_18008a47c), which callsSymCryptMlDsaPkDecodeat0x18008A4E5.SymCryptMlDsaPkDecode (sub_18009632c)copies the 32-byterhofrom the parsed public key into the key object at[rbx+56h](movups xmmword ptr [r12], xmm0at0x1800963FD,r12 = rbx+56h) and callsSymCryptMlDsaExpandAat0x18009653Cwithrcx = r12(therhopointer).SymCryptMlDsaExpandA (sub_180095dd8)copies those 32 bytes into a local buffer (movupsfrom[rcx]/[rcx+10h]at0x180095DF3/0x180095DFB), appends the 2-byte matrix index, and callsSymCryptMlDsaRejNttPolyat0x180095E44once per matrix cell.SymCryptMlDsaRejNttPolyappendsrho||i||jto 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,1E1hat0x18009A93E) to bound the rejection-sampling loop, returning error0x8008if exceeded (mov edi,8008hat0x18009AA41). The bounded value (etaatparams+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)(patchedsub_18009aa84): [Hardening] Added an independent iteration counter (r14d) with a hard cap of 298 (0x12A,cmp r14d,12Ahat0x18009AB01) to bound the NTT-domain rejection-sampling loop, returning error0x8008if exceeded (mov edi,8008hat0x18009AB45). The unpatched loop (back-edgejnz loc_180096D6Eat0x180096D9E, reject back-edgejnb loc_180096D6Eat0x180096D92) 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 theSymCryptMlDsaExpandAcall. Hardening against use of uninitialized crypto state. (This function is also the untrusted entry point that reachesSymCryptMlDsaRejNttPolyviaSymCryptMlDsaExpandA; 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 extra0argument to theSymCryptRsakeyCalculatePrivateFieldscall.SymCryptRsakeyCalculatePrivateFields (sub_18002ce54)(0.7495 similarity): [Behavioral] Added a newflagsparameter (arg7) to the RSA private-field computation, returning SymCrypt error0x800E(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_180096BCBat0x180096CAC, output counteredithat advances only on accepted samples) and the patched iteration cap (esi,cmp esi,1E1h, error return0x8008) 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-edgesjnb loc_180096D6E/jnz loc_180096D6E, patchedcmp r14d,12Ahat0x18009AB01with unconditionalinc r14dat0x18009AB25and error return0x8008at0x18009AB45); see section 7a. - Second instance (
SymCryptMlDsaRejNttPoly): Same CWE-835 pattern, Low severity. Itsrhoseed is attacker-controlled throughBCryptImportKeyPair->SymCryptMlDsakeySetValue->SymCryptMlDsaPkDecode->SymCryptMlDsaExpandA->SymCryptMlDsaRejNttPoly, so the "not caller-controlled" argument used forRejBoundedPolydoes 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
etavalue atparams+0x16is a compile-time constant sourced from the static tablesSymCryptMlDsaInternalParams44/65/87via the validated selection inSymCryptMlDsaGetInternalParamsFromParams (sub_180096074). No documented CNG/BCrypt path lets a caller supply an arbitraryeta, and the valid-etaloop 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.