Fix stack buffer overflow in aes_ccm_encr for partial blocks - #11347
Conversation
📝 WalkthroughWalkthroughThe AES-CCM partial-block encryption path now uses a temporary keystream buffer and indexed XOR writes. The change removes a duplicate nonce copy and adds bounds and round-trip tests for partial payloads. ChangesAES-CCM encryption
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
caveman99
left a comment
There was a problem hiding this comment.
Fix itself looks right. I pulled it on top of develop and ran test_crypto and test_pki_admin_fallback, both green, output unchanged.
Three things before this goes in:
- The description oversells it. Nothing actually breaks on develop as it stands today, the buffers in the tests are big enough. You only get the ASan hit once the extra tests from #9749 are in. Please reword it as a latent bug. Still worth fixing though, I checked the decrypt path and it only just barely stays inside the buffer. A byte less headroom anywhere and it would be a real one.
- Add a small test for it, otherwise someone puts this straight back in a year.
- While you are in there, clean up the leftovers in CryptoEngine.cpp. The comment on line 250 saying it can write 15 bytes past the buffer is now simply wrong. And the extraNonce memcpy on line 233 was only ever there because the old code trampled over it, the one on line 251 already does the job.
| crypto->aesEncrypt(a, out); | ||
| crypto->aesEncrypt(a, tmp); | ||
| /* XOR zero-padded last block */ | ||
| for (i = 0; i < last; i++) |
There was a problem hiding this comment.
This part is fine, it is the same thing the two functions right below already do.
aes_ccm_encr() writes a full 16-byte AES block to the output before XOR-ing with the input, so a trailing partial block writes up to 15 bytes past the length the caller asked for. Every caller in the tree passes a buffer with enough slack, so nothing misbehaves today, but the decrypt path clears it by only a few bytes. Encrypt into a temporary block and XOR out of it, matching what aes_ccm_encr_auth() and aes_ccm_decr_auth() already do in this same file. The ciphertext is unchanged.
…rkarounds The guard bytes past the caller's buffer catch the overflow without relying on a sanitizer, so the test is meaningful in the native environment too. encryptCurve25519() no longer needs to write extraNonce before aes_ccm_ae(): the call stays inside numBytes now, so the copy after it is the only one required. The comment warning about the 15-byte overshoot no longer describes the code.
76eaa4d to
a04a26f
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Thanks for pulling it and for checking the decrypt path. All three addressed in a04a26f, on top of a rebase onto current Description reworded. You are right, I had it stated too strongly. Nothing on Regression test added — I went with guard bytes rather than an exactly-sized buffer plus ASan, for two reasons: it fails in 165 is the Leftovers cleaned. Both gone:
I ran
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/test_crypto/test_main.cpp (1)
339-346: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd fixed-vector assertions for
cryptandauth.
test_AES_CCM_partial_block_boundsonly checks guard bytes and successful decrypt-after-encrypt. Comparecrypt, or at least one non-multiple-of-16cryptand its 8-byteauth, against a known-good value so ciphertext compatibility cannot regress while the round trip still passes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/test_crypto/test_main.cpp` around lines 339 - 346, Add known-good fixed-vector assertions in test_AES_CCM_partial_block_bounds for the generated crypt ciphertext and 8-byte auth tag, using the existing key, nonce, and plaintext inputs. Keep the guard-byte checks and round-trip assertions, and compare the expected values before decryption so incompatible encryption output is detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/test_crypto/test_main.cpp`:
- Around line 339-346: Add known-good fixed-vector assertions in
test_AES_CCM_partial_block_bounds for the generated crypt ciphertext and 8-byte
auth tag, using the existing key, nonce, and plaintext inputs. Keep the
guard-byte checks and round-trip assertions, and compare the expected values
before decryption so incompatible encryption output is detected.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 86959e4d-7111-434b-b1a6-9b1b413133fe
📒 Files selected for processing (3)
src/mesh/CryptoEngine.cppsrc/mesh/aes-ccm.cpptest/test_crypto/test_main.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
- src/mesh/aes-ccm.cpp
caveman99
left a comment
There was a problem hiding this comment.
All good, ignore the overzealous nitpick comment from coderabbit. the ask is covered already.
aes_ccm_encr()writes the full 16-byte AES keystream block straight into the output buffer and only afterwards XORs the firstlastbytes in place:When the final block is partial, that
aesEncryptwrites up to 15 bytes past the length the caller asked for.This is latent — nothing on
developmisbehaves today. Every caller in the tree hands over a buffer with enough slack to absorb the overshoot, the existing tests included, so nothing trips. It is still worth fixing: the overshoot is invisible at the call sites, the decrypt path clears it by only a few bytes, and any future caller that sizes a buffer to the payload gets a silent out-of-bounds write. #9749 adds exactly such a caller, and AddressSanitizer flags it as astack-buffer-overflowincoverage:test_crypto.The fix encrypts into a temporary block and XORs out of it — the same pattern
aes_ccm_encr_auth()andaes_ccm_decr_auth()already use a few lines below in this same file:The ciphertext is unchanged.
Regression test
test_AES_CCM_partial_block_boundsdrivesaes_ccm_ae()/aes_ccm_ad()at lengths 5 (pure partial block) and 20 (one full block plus a partial one), with guard bytes laid down past the requested length in both the ciphertext and the plaintext buffer.It asserts on those guard bytes rather than leaning on a sanitizer, so it fails identically under
nativeandcoverage, and the test itself never actually goes out of bounds. Reverting the fix turns it red withExpected 165 Was 23.Leftovers removed
encryptCurve25519()copiedextraNonceintoauth + 8both before and after theaes_ccm_ae()call, because the call used to trample it. It no longer reaches that far, so the copy before it is gone and the one after does the job on its own. The comment on the call warning that it "can write up to 15 bytes longer than numbytes past bytesOut" no longer describes the code, so it is gone too.Verification
test_crypto12/12 onnativeand oncoverage(ASan)test_pki_admin_fallback6/6 oncoveragepio run -e nativeSUCCESSclang-formatcleanSplit out of #9749, where the overflow first surfaced.
Summary by CodeRabbit