Fix: WolfCrypt Fenrir - 12 fixes#10786
Conversation
9046e08 to
8968849
Compare
|
retest this please |
|
|
Jenkins retest this please |
f0db333 to
60b22a5
Compare
|
Jenkins retest this please |
|
Jenkins retest this please |
1 similar comment
|
Jenkins retest this please |
60b22a5 to
06d9993
Compare
|
rebased branch on to master |
7a668b2 to
f475fe0
Compare
|
Jenkins retest this please. |
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 2 total — 2 posted, 0 skipped
Posted findings
- [High] wc_Sha256Copy frees a zero-initialized dst and closes fd 0 (devcrypto) —
wolfcrypt/src/port/devcrypto/devcrypto_hash.c:231 - [Medium] wc_Sha256Copy leaks the just-opened session on XMALLOC failure —
wolfcrypt/src/port/devcrypto/devcrypto_hash.c:238-241
Review generated by Skoll via Claude/Codex
|
|
||
| wc_InitSha256_ex(dst, src->heap, 0); | ||
| #ifdef WOLFSSL_DEVCRYPTO_HASH_KEEP | ||
| wc_Sha256Free(dst); |
There was a problem hiding this comment.
🟠 [High] wc_Sha256Copy frees a zero-initialized dst and closes fd 0 (devcrypto)
🚫 BLOCK bug
The PR adds wc_Sha256Free(dst) at the very top of the WOLFSSL_DEVCRYPTO_HASH_KEEP path of wc_Sha256Copy to fix a leak of a pre-existing session in dst. However wc_Sha256Free() -> wc_DevCryptoFree() closes ctx.cfd whenever ctx.cfd >= 0 (see wc_devcrypto.c:191-196). The generic (software) wc_Sha256Copy contract does NOT require dst to be pre-initialized — it simply overwrites dst. Public callers therefore legitimately pass a dst that is zero-initialized, giving cfd == 0. Freeing it runs ioctl(0, CIOCFSESSION, ...) on a garbage session and then close(0) — closing the process's stdin. The internal caller wc_Sha256GetHash was patched to set cpy.ctx.cfd = -1 before calling Copy precisely to avoid this hazard (see the new comment at devcrypto_hash.c:198-201), but external callers were not. Concrete example: ExpectedResumptionSecret() (src/tls13.c:12779-12785) does XMEMSET(&digest, 0, sizeof(Digest)) then wc_Sha256Copy(&ssl->hsHashes->hashSha256, &digest.sha256) — digest.sha256.ctx.cfd == 0 -> close(0). The HMAC copy path in src/ssl_crypto.c:1464 has the same shape.
Suggestion:
| wc_Sha256Free(dst); | |
| /* Only tear down an existing session; never touch fd 0 for a | |
| * caller-zeroed dst. Match the software backend which does not free dst. */ | |
| if (dst->ctx.cfd > 0) { | |
| wc_Sha256Free(dst); | |
| } | |
| if ((ret = wc_InitSha256_ex(dst, src->heap, 0)) != 0) { | |
| dst->ctx.cfd = -1; | |
| return ret; | |
| } |
There was a problem hiding this comment.
Re-opening — this is only half-fixed.
The internal wc_Sha256GetHash path is safe now (it sets cpy.ctx.cfd = -1 before wc_Sha256Copy), and wc_InitSha256_ex sets the -1 sentinel. But the external, zero-initialized dst path this finding named is still live at head fdb0743:
wc_Sha256Copystill callswc_Sha256Free(dst)unconditionally (devcrypto_hash.c:233) — the comment there says "Sha256Free checks that ctx.cfd is >= 0".wc_DevCryptoFreeguards onctx->cfd >= 0(wc_devcrypto.c:191) —>= 0includes0— so for a caller-zeroeddstit runsioctl(0, CIOCFSESSION, …)thenclose(0).ExpectedResumptionSecret()doesXMEMSET(&digest, 0, sizeof(Digest))thenwc_Sha256Copy(&ssl->hsHashes->hashSha256, &digest.sha256)(tls13.c:12860/:12866) →digest.sha256.ctx.cfd == 0→close(0)(stdin) underWOLFSSL_DEVCRYPTO_HASH_KEEP.
The software wc_Sha256Copy contract doesn't require dst to be pre-initialized, and this in-tree TLS 1.3 caller doesn't initialize it — so setting the sentinel only in wc_InitSha256_ex / wc_Sha256GetHash doesn't cover it. Minimal fix is the originally-suggested guard:
/* Only tear down a real session; never touch fd 0 for a caller-zeroed dst */
if (dst->ctx.cfd > 0) {
wc_Sha256Free(dst);
}(AFALG's free already uses > 0; devcrypto's >= 0 is the outlier, so tightening the guard in wc_DevCryptoFree would also close this.)
5fea5ea to
7947f01
Compare
Frauschi
left a comment
There was a problem hiding this comment.
Follow-up review — my three earlier wc_Sha256Copy findings are all fixed, thanks. A few adjacent items surfaced while re-checking the touched code:
- [Med] The
wc_fspsm_hw_unlock()fix for finding 5418 was only applied towc_fspsm_AesGcmEncrypt; the identicalreturn MEMORY_Ewith the lock held still exists in thewc_fspsm_AesGcmDecryptsibling. - [Med] The alloc-failure block frees the plain/cipher/aTag buffers but not the session-key buffers on a partial allocation.
- [Low]
wc_Sha256Finalstill has one error path that leaks the session/msg buffer. - [Nit]
cfd > 0vs the documented-1sentinel.
Details inline. #1 (the missing Decrypt unlock) is the one worth fixing before merge — it re-introduces the exact lock leak this PR set out to fix.
0795ecb to
fdb0743
Compare
|
retest this please |
37b7051 to
ae38cf9
Compare
|
This PR also fixes a separate bug in the devcrypto AES-GCM path (found by a failing test): the auth tag was being appended past the end of the caller's ciphertext buffer. The encrypt/decrypt now stage the ciphertext || tag in a correctly-sized temporary buffer so nothing is written out of bounds. Tested with ./configure --enable-devcrypto=all + make check (all suites passing) against a loaded cryptodev module. |
|
retest this please |
dgarske
left a comment
There was a problem hiding this comment.
Skoll Multi-Scan Review
Modes: review + review-securityOverall recommendation: COMMENT
Findings: 8 total — 5 posted, 3 skipped
5 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [Medium] [review+review-security] Incomplete alignment fix: RDRAND generator still writes through (word64)output cast* —
wolfcrypt/src/random.c:3812 - [Low] [review] Residual lazy-init race window in wc_DrbgState_MutexInit —
wolfcrypt/src/random.c:397-408 - [Low] [review] Stale comment above wc_AesGcmEncrypt after temp-buffer rewrite —
wolfcrypt/src/port/devcrypto/devcrypto_aes.c:373 - [Low] [review] wc_DrbgState_MutexFree clears atomic flag with a plain store, inconsistent with init —
wolfcrypt/src/random.c:420 - [Info] [review-security] AES-GCM devcrypto path now allocates a full-message-size heap buffer that can fail —
wolfcrypt/src/port/devcrypto/devcrypto_aes.c:319-323
Skipped findings
- [Medium]
Rewritten devcrypto AES-GCM path lacks explicit coverage for short-tag and NULL in/out cases - [Low]
devcrypto AES-GCM temporary buffer holding plaintext freed without zeroization - [Info]
wc_Sha256Copy now returns NOT_COMPILED_IN for devcrypto builds without WOLFSSL_DEVCRYPTO_HASH_KEEP
Review generated by Skoll
| return -1; | ||
| } | ||
|
|
||
| for (; (sz / sizeof(word64)) > 0; sz -= sizeof(word64), |
There was a problem hiding this comment.
🟠 [Medium] Incomplete alignment fix: RDRAND generator still writes through (word64)output cast* · Logic / Unaligned write
The PR (finding 5412) states it fixes both the Intel RDSEED and RDRAND generators to stop writing arbitrary output buffers through word64 pointer casts, using a temporary value plus writeUnalignedWord64. However, only the RDSEED path (wc_GenerateSeed_IntelRD) was changed. The sibling RDRAND path in wc_GenerateRand_IntelRD still does ret = IntelRDrand64_r((word64 *)output);, casting a potentially-unaligned byte* output to word64*. This is the exact unaligned-write / strict-aliasing pattern the PR set out to remove; it is UB and will be flagged by UBSan. On x86 (the only platform where HAVE_INTEL_RDRAND compiles) it does not fault, so this is a pre-existing issue not made worse by the diff, but it is left unfixed despite the PR description implying RDRAND was addressed, and it is inconsistent with the RDSEED loop immediately above (which now zeroizes its temporary on error). Severity view differs across scans: the review mode rates this Medium (bug), while the security mode rates it Info (pre-existing, x86-only, low blast radius); the stricter Medium is retained here.
Fix: Apply the same temp-value + writeUnalignedWord64 treatment used for the RDSEED fix: read into a local word64, store with writeUnalignedWord64(output, tmp), and ForceZero the temporary on the error path so RDRAND matches the RDSEED fix and the PR's stated scope.
| #ifndef SINGLE_THREADED | ||
| #ifndef WOLFSSL_MUTEX_INITIALIZER | ||
| if (!drbgStateMutex_inited) { | ||
| int expected = 0; |
There was a problem hiding this comment.
🔵 [Low] Residual lazy-init race window in wc_DrbgState_MutexInit · Concurrency
The atomic compare-exchange correctly prevents two threads from both calling wc_InitMutex (the reported double-init bug). But as the new comment acknowledges, the flag is set to 1 before the mutex is actually initialized. A second thread whose CAS fails returns 0 (success) immediately and its caller may proceed to LockDrbgState() -> wc_LockMutex(&drbgStateMutex) while the first thread is still inside wc_InitMutex, i.e. locking a not-yet-initialized mutex. LockDrbgState() does not itself call the init routine, so it relies entirely on init having completed. In practice this is only reachable if two threads enter wolfCrypt_Init concurrently (the sole caller), so severity is low, but the window is real and cannot be closed with a mark-before-init flag alone.
Fix: Consider a two-state scheme (e.g. 0=uninit, 1=in-progress, 2=ready) where callers that lose the CAS wait until the state reaches 'ready', or document that wc_DrbgState_MutexInit must complete before any concurrent DRBG use. At minimum note the residual window so it is not assumed fully thread-safe.
There was a problem hiding this comment.
Added state machine and force all threads to wait for success or each take a turn trying to init the mutex if the winner fails. I performed the exact same behavior to the mutexFree function.
| XFREE(buf, aes->heap, DYNAMIC_TYPE_AES_BUFFER); | ||
| return 0; | ||
| } | ||
|
|
There was a problem hiding this comment.
🔵 [Low] Stale comment above wc_AesGcmEncrypt after temp-buffer rewrite · Documentation
The comment /* it is assumed that "out" buffer has enough room for cipher text + tag */ predates the rewrite. The new implementation writes only sz ciphertext bytes into out and delivers the tag separately via authTag (using an internal temp buffer), so out no longer needs room for the tag. The comment now misdescribes the contract and could mislead callers/reviewers.
Fix: Update the stale comment to reflect that out holds only ciphertext (sz bytes) and the tag is returned separately in authTag.
There was a problem hiding this comment.
fixed by reverting function behavior and changing the test to make the assumption true.
| * from the end of the input. The caller's in/out buffers only hold "sz" | ||
| * bytes, so use a temporary buffer with room for the tag to avoid writing | ||
| * past their bounds. */ | ||
| bufSz = sz + WC_AES_BLOCK_SIZE; |
There was a problem hiding this comment.
⚪ [Info] AES-GCM devcrypto path now allocates a full-message-size heap buffer that can fail · Resource Leak
BEFORE, wc_DevCrypto_AesGcm used only a fixed 16-byte stack scratch buffer and processed data in place. AFTER, every GCM encrypt/decrypt allocates sz + WC_AES_BLOCK_SIZE bytes on the heap and returns MEMORY_E if the allocation fails. This is functionally correct (and fixes the prior out-of-bounds writes into caller buffers), but it introduces a new failure mode and per-call allocation/copy overhead for large messages where the previous code could not fail on memory. For typical TLS record sizes this is bounded and acceptable; noted for awareness of the behavioral/performance change.
Fix: Acceptable as-is; optionally retain a small-stack fast path for small sz to avoid an allocation per operation. No action required for correctness.
There was a problem hiding this comment.
This was found because a test did not allocated a buffer with enough space for the aes authTag to be appended to it. I was looking through the rest of the tests and they do provide a large enough buffer to hold the tag. I decided to fix the test by allocating enough space for the tag to be appended and revert my changes to this function. I could not find a way to not make this assumption without ~40% encryption slow down in the worst case without making major changes. Should we make major changes to the behavior of the function or keep the assumption?
| } | ||
| } | ||
| #endif | ||
| #endif |
There was a problem hiding this comment.
🔵 [Low] wc_DrbgState_MutexFree clears atomic flag with a plain store, inconsistent with init · Convention
wc_DrbgState_MutexInit now uses wolfSSL_Atomic_Int_CompareExchange / wolfSSL_Atomic_Int_Exchange on drbgStateMutex_inited, but wc_DrbgState_MutexFree still reads it with a plain if (drbgStateMutex_inited) and clears it with drbgStateMutex_inited = 0;. Since the variable is now wolfSSL_Atomic_Int when WOLFSSL_ATOMIC_OPS is defined, the direct load/store is still atomic and compiles, but using the atomic helper here would be symmetric and clearer. Free is normally a shutdown/single-threaded path, so this is purely stylistic.
Fix: Optionally use wolfSSL_Atomic_Int_Exchange in MutexFree for symmetry with the atomic init; not required for correctness.
Note: Referenced line (
wolfcrypt/src/random.c:420) is outside the diff hunk. Comment anchored to nearest changed region.
There was a problem hiding this comment.
Fixed matches new MutexInit
https://fenrir.wolfssl.com/finding/5384 https://fenrir.wolfssl.com/finding/4432 https://fenrir.wolfssl.com/finding/5392 https://fenrir.wolfssl.com/finding/5392 skoll fixes Changed type for keys for CAAM in ecc so it matches assignment with out cast to never truncate Added check to see if CAAM_ADDRESS is defined before using in ecc.h https://fenrir.wolfssl.com/finding/5994 https://fenrir.wolfssl.com/finding/4445 Fixed memory leaks for dev crypto and fixed https://fenrir.wolfssl.com/finding/4446 https://fenrir.wolfssl.com/finding/5418 https://fenrir.wolfssl.com/finding/5420 https://fenrir.wolfssl.com/finding/5411 https://fenrir.wolfssl.com/finding/5412 https://fenrir.wolfssl.com/finding/5413 Skoll Fixes github comment fix github review fixes skoll fixes skoll fixes spelling fix
…nited. Also fixed bug in aes where the authTag was appended past the end of the cypher text
ae38cf9 to
2541f5e
Compare
|
retest this please |
Description
https://fenrir.wolfssl.com/finding/6145
wc_DsaVerify/wc_DsaVerify_exleave*answeruninitialized on all error paths, unlike sibling ECC/ECCSI verify APIs that default to "not verified".answerparameter to zero so that on early exit the output parameter is defined as false.https://fenrir.wolfssl.com/finding/5384
CAAM secure-memory addresses are truncated to 32 bits in ECC keys.
ecc_keyfields toCAAM_ADDRESSwhen it is defined. This allows the address width to expand with the platform so addresses are not truncated.https://fenrir.wolfssl.com/finding/4432
wc_DrbgState_MutexInitunsafe lazy mutex initialization withoutWOLFSSL_MUTEX_INITIALIZER.int, we use atomic operations when they are present to initialize the mutex. This ensures that no two threads can initialize the same mutex.https://fenrir.wolfssl.com/finding/5392
DES key schedule branches on secret key bits.
ifstatement, we use a mask to set bits inks.https://fenrir.wolfssl.com/finding/5994
Invalid free / use-after-free of embedded X509 NAME in the ESP32 cert-bundle verify callback on a lookup miss.
esp_crt_bundle.c.https://fenrir.wolfssl.com/finding/4445
devcrypto
wc_Sha256Copyproduces a non-functional hash copy whenWOLFSSL_DEVCRYPTO_HASH_KEEPis disabled.https://fenrir.wolfssl.com/finding/4446
devcrypto
wc_Sha256Finalleaks the kernel hash session whenGetDigestfails.https://fenrir.wolfssl.com/finding/5418
FSPSM AES-GCM TLS key allocation failures return without unlocking hardware.
https://fenrir.wolfssl.com/finding/5420
FSPSM hash
Final/GetHashsilently succeeds when hardware hash initialization fails.retto an error value so that on return the error is no longer silent.https://fenrir.wolfssl.com/finding/5411
SipHash assembly paths load the caller key through
word64pointer casts.byte*toword64*casts to use theGET_U64()helper macro to protect against alignment issues.https://fenrir.wolfssl.com/finding/5412
Intel RDSEED/RDRAND generators write arbitrary output buffers as
word64.word64value, then usedwriteUnalignedWord64to transfer it into the output without alignment issues.https://fenrir.wolfssl.com/finding/5413
ML-KEM AArch64 noise helpers cast byte buffers and seeds to
word64pointers.writeUnalignedWord64instead of abyte*->word64*cast.