psa_cipher_update() with ECB_NO_PADDING accepts input == output when a partial block is pending #891
Open
opened 2026-09-01 07:54:49 -05:00 by Pranav1904
·
1 comment
No Branch/Tag specified
development
tf-psa-crypto-1.1
archive/20261005T153337Z-force/pull/927
archive/20261001T141055Z-force/pull/927
archive/20261005T153337Z-force/pull/931
archive/20261001T083616Z-force/pull/927
archive/20260930T232327Z-force/pull/927
archive/20260930T154618Z-force/pull/927
archive/20260929T231919Z-rescue/rohankl-arm/mldsa-pubkey-899
archive/20260929T231919Z-rescue/sigvartmh/pr/spake2p-9342-public-key-import-export
archive/20260929T231919Z-rescue/ManoharPaturi/fix-pem-ct-padding
archive/20260929T231919Z-rescue/ManoharPaturi/fix-asn1write-null-guard
archive/20260929T231919Z-rescue/ManoharPaturi/fix-pbes2-params-validation
archive/20260929T231919Z-rescue/valeriosetti/blake2
archive/20260929T231919Z-rescue/jfdev001/v1.1.1_bump-cmake-minimum-version
archive/20260929T231919Z-rescue/jfdev001/bump-cmake-minimum-version
archive/20260929T231919Z-rescue/ronald-cron-arm/sm3-base
archive/20260929T231919Z-rescue/valeriosetti/blake2-hmac
archive/20260929T231919Z-rescue/v1rtu3x/enforce-hash-length-sign-verify
archive/20260929T231919Z-rescue/bjwtaylor/file-backed-keystore-doc
archive/20260929T231919Z-rescue/sigvartmh/pr/spake2p-9344-set-context
archive/20260929T231919Z-rescue/sigvartmh/pr/spake2p-9343-key-pair-import-export-depends-on-9342
archive/20260929T231919Z-rescue/romart/development
archive/20260929T231919Z-rescue/sweetlilmre/constant-time-embedded-perf
archive/20260929T231919Z-rescue/JulioLoayzaM/add-wycheproof-rsa-pss
archive/20260929T231919Z-rescue/JulioLoayzaM/add-wycheproof-ecdsa
archive/20260929T231919Z-rescue/JulioLoayzaM/add-wycheproof-tests
archive/20260929T231919Z-rescue/oubaid-a/dev_psa_driver_wrapper
archive/20260929T231919Z-rescue/gilles-peskine-arm/keystoreless-prototype-1
archive/20260929T231919Z-rescue/valeriosetti/issue9580
archive/20260929T231919Z-rescue/LeonieReichertABB/hpke-functionality
archive/20260929T231919Z-rescue/gilles-peskine-arm/psa-hash-no-buffer-copy-1.1
archive/20260929T231919Z-rescue/vanhauser-thc/development
archive/20260929T231919Z-rescue/tpambor/key-wrap
archive/20260929T231919Z-rescue/ruiliio/psa_xts
archive/20261011T063515Z-force/pull/538
archive/20260929T231919Z-rescue/gilles-peskine-arm/fix-broken-links-1.1
archive/20260929T231919Z-rescue/valeriosetti/issue669
archive/20260929T231919Z-rescue/gilles-peskine-arm/prepare-release-test
archive/20260929T231919Z-rescue/krish2718/wpa3-sae-design-note
archive/20260929T231919Z-rescue/nzzlinh/fix_iar_endianness_1.1
archive/20260929T231919Z-rescue/nzzlinh/fix-iar-endianness
archive/20260929T231919Z-rescue/abtom87/fix-pbkdf2-iter-count
archive/20260929T231919Z-rescue/minosgalanakis/tools/prepare_release_lite
archive/20260929T231919Z-rescue/Harshal5/fix/aes-xts-in-place-cts
archive/20260929T231919Z-rescue/sigvartmh/pr/spake2p-9347-prover-depends-on-9342-9343-9344
archive/20260929T231919Z-rescue/crabel99/async-hardware-support-c35df962
tf-psa-crypto-1.2.0_mergeback
tf-psa-crypto-1.1.1_mergeback
archive/20260929T231919Z-rescue/asmellby/feature/dispatch-library
tf-psa-crypto-release-sync
archive/20260929T231919Z-rescue/ChakshuGupta13/fix/ecdsa-zeroize-stack-key
archive/20260929T231919Z-rescue/gilles-peskine-arm/security-md-option-names
archive/20260929T231919Z-rescue/parmi93/patch-2
archive/20260929T231919Z-rescue/bjwtaylor/remove-mldsa-sign-from-build
archive/20260929T231919Z-rescue/bjwtaylor/tf-m-build-issues
archive/20260929T231919Z-rescue/bjwtaylor/test-context-population
archive/20260929T231919Z-rescue/gilles-peskine-arm/iar-1.1.0
archive/20260929T231919Z-rescue/gilles-peskine-arm/fork-helper-unit-tests-create-crypto
archive/20260929T231919Z-rescue/bjwtaylor/redundant-declarations
archive/20260929T231919Z-rescue/hasheddan/docs/fix-broken-links
main
archive/20260929T231919Z-rescue/bjwtaylor/reintroduce-dh
archive/20260929T231919Z-rescue/bjwtaylor/cipher-update-incorrect-error
archive/20260929T231919Z-rescue/Marandil/fix/mbedtls-4314-parse-explicit-curve
archive/20260929T231919Z-rescue/schneider-electric/dev/plaj-se/parse-rsassa-pss
archive/20260929T231919Z-rescue/minosgalanakis/bugfix/program-fuzz-mscv
archive/20260929T231919Z-rescue/daverodgman/aesgcm
archive/20260929T231919Z-rescue/gilles-peskine-arm/sha3-coverity-20260224
archive/20260929T231919Z-rescue/bensze01/undef
archive/20260929T231919Z-rescue/valeriosetti/issue523
archive/20260929T231919Z-rescue/dannytsen/ppc-dev
archive/20260929T231919Z-rescue/daverodgman/aes12
archive/20260929T231919Z-rescue/gilles-peskine-arm/ascon-prototype
archive/20260929T231919Z-rescue/daverodgman/chacha10
archive/20260929T231919Z-rescue/kusumitg/implement_psa_pake_set_context_for_spake2p
archive/20260929T231919Z-rescue/davidhorstmann-arm/add-code-size-target
archive/20260929T231919Z-rescue/bjwtaylor/simplify-sign_func
archive/20260929T231919Z-rescue/mfil/feature/aes_ni_parallel
archive/20260929T231919Z-rescue/ivv19041994/patch-1
archive/20260929T231919Z-rescue/gilles-peskine-arm/psa-verify-policy-relax
public_2/main
archive/20260929T231919Z-rescue/mpg/pk-sign-proto
release/tf-psa-crypto1.0.0-documentation
archive/20260929T231919Z-rescue/gilles-peskine-arm/0e-plans-july-updates
archive/20260929T231919Z-rescue/davidhorstmann-arm/guard-context-types-in-client-only
archive/20260929T231919Z-rescue/gilles-peskine-arm/psa-driver-interface-spec-remove
archive/20260929T231919Z-rescue/gilles-peskine-arm/ecp-self-test-race-crypto
archive/20260929T231919Z-rescue/KiruthikPrakash/feature/9342_add-support-for-spake2p-public-key-import-export
archive/20260929T231919Z-rescue/ruiliio/chacha20_16
archive/20260929T231919Z-rescue/irwir/winsock2
archive/20260929T231919Z-rescue/aphroteus/development
archive/20260929T231919Z-rescue/amjoul01/amjoul01/psa-mac-out-of-bound-fix
archive/20260929T231919Z-rescue/gilles-peskine-arm/everest-submodule-crypto
archive/20260929T231919Z-rescue/yamt/xtensa-ccount
archive/20260929T231919Z-rescue/gilles-peskine-arm/all.sh-generate-only-before-checks-crypto
archive/20260929T231919Z-rescue/Harshal5/feat/builtin_cmac_driver_use_psa_crypto_interface
archive/20260929T231919Z-rescue/gilles-peskine-arm/test-data_t-empty_is_empty-crypto
archive/20260929T231919Z-rescue/jean-baptisteboric-eaton/oids-pkcs7-authenticated-attributes
dev/bensze01/public-only-commit
archive/20260929T231919Z-rescue/waleed-elmelegy-arm/psa-key-wrap-aes-kw
archive/20260929T231919Z-rescue/tfloch/psa_add_xts_algorithm
archive/20260929T231919Z-rescue/gilles-peskine-arm/msancf-clang16-crypto
archive/20260929T231919Z-rescue/DemiMarie/document-unexpected-tags
archive/20260929T231919Z-rescue/DemiMarie/strict-len
archive/20260929T231919Z-rescue/valeriosetti/issue9255-tfpsacrypto
archive/20260929T231919Z-rescue/daantimmer/use-compiler-frontend
archive/20260929T231919Z-rescue/benmcollins/enable-rsassa-pss-pkparse
dev/paul-elliott-arm/add_iop_key_agreement_negative_tests
add-iop-export-pub-key-get-num-ops
dev/rc/new-2-d635ea1
dev/bensze01/jenkins
v1.2.0
tf-psa-crypto-1.2.0
v1.1.1
tf-psa-crypto-1.1.1
v1.1.0
tf-psa-crypto-1.1.0
v1.0.0
tf-psa-crypto-1.0.0
v1.0.0-beta
tf-psa-crypto-1.0.0-beta
Labels
Clear labels
api-break
This issue/PR breaks the API and must wait for a new major version
approved
Design and code approved - may be waiting for CI or backports
bug
Something isn't working
component-docs
Docs / website issues filed here for tracking
component-platform
Portability layer and build scripts
component-test
Test framework and CI scripts
dependencies
Pull requests that update a dependency file
deprecation
needs deprecation announcement
DO-NOT-MERGE
enhancement
New feature or request
good first issue
Good for newcomers
help wanted
This issue is not being actively worked on, but PRs welcome.
needs-adoption
stalled PR that someone should pick up and complete
needs-backports
Backports are missing or are pending review and approval.
needs-ci
Needs to pass CI tests
needs-design-approval
Needs design discussion / approval
needs-info
An issue or PR which needs further info from the reporter / author
needs-preceding-pr
Requires another PR to be merged first
needs-review
Every commit must be reviewed by at least two team members
needs-reviewer
This PR needs someone to pick it up for review
needs-work
priority-high
High priority - will be reviewed soon
priority-low
Low priority - this may not receive review soon
priority-medium
Medium priority - this can be reviewed as time permits
priority-scheduled
This PR is big - it will require time to be scheduled for review
priority-very-high
Highest priority - prioritise this over other review work
resolved-after-1.1
The issue is resolved in development, but still present in 1.1 LTS.
single-reviewer
This PR qualifies for having only one reviewer
size-l
Estimated task size: large (2w+)
size-m
Estimated task size: medium (~1w)
size-optimisation
size-s
Estimated task size: small (~2d)
size-xs
Estimated task size: extra small (a few hours at most)
No labels
api-break
approved
bug
component-docs
component-platform
component-test
dependencies
deprecation
DO-NOT-MERGE
enhancement
good first issue
help wanted
needs-adoption
needs-backports
needs-ci
needs-design-approval
needs-info
needs-preceding-pr
needs-review
needs-reviewer
needs-work
priority-high
priority-low
priority-medium
priority-scheduled
priority-very-high
resolved-after-1.1
single-reviewer
size-l
size-m
size-optimisation
size-s
size-xs
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
gh-Mbed-TLS/TF-PSA-Crypto#891
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
psa_cipher_update()withPSA_ALG_ECB_NO_PADDINGacceptsinput == outputwhile a partial block from an earlier call is pending.mbedtls_cipher_update()rejects that exact combination withMBEDTLS_ERR_CIPHER_BAD_INPUT_DATA(drivers/builtin/src/cipher.c:422), but the check never fires, becausepsa_cipher_update_ecb()moves the two pointers one byte apart before calling into it. Every call returnsPSA_SUCCESSand the resulting ciphertext is wrong.The call is accepted in both configurations, and I have included a step below that shows it in a default build as well, so this is not specific to one build option:
MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS, the write lands on the caller'sown buffer and destroys input that has not been read yet, so the blocks that follow encrypt data that was never supplied.
plaintext intact, so the ciphertext blocks are correct and only their position is wrong. The call is still accepted and the result is still not the expected ciphertext.
I am aware that the documentation of
MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERSstates that overlap between input and output buffers is not supported when it is enabled, and that a caller who reuses the input offset as the output pointer has made a mistake. My request is not that the operation be made to work. It is that it be rejected rather than silently corrupted, which is what the legacy API already does for the same condition, and what some PSA drivers already do.The practical problem this causes is that the outcome depends on the driver. I work on the Mbed TLS integration in ESP-IDF, where our hardware AES driver rejects this buffer layout and the built-in software driver silently accepts it. The same PSA call, with the same key and the same data, therefore either returns a clean error or produces wrong ciphertext depending on which driver is compiled in. That is not something an application can be written against, and it is what led me to this report.
System information
TF-PSA-Crypto version (number or commit id):
ec56e6a6ffccf6f8982d271d2d3ddac2de84906f(development, 2026-08-27), as the submodule of Mbed TLS522e2e4651db0758dcc7f5da6737239ce45cab22Operating system and version: macOS 26.4 (Darwin 25.4.0, arm64), and Ubuntu 22.04 (x86-64); reproduces identically on both Configuration (if not default, please attachcrypto_config.h): defaultcrypto_config.h; the call is accepted both with and withoutMBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS, and overwrites unread input when it is enabled Compiler and options (if you used a pre-built binary, please indicate how you obtained it): Apple clang 21.0.0 and gcc 11.4.0,cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-DMBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS -g -O0"Additional environment information: reproduces on the plain C AES path and on the AESNI path, and on an Espressif target through the Mbed TLS port in ESP-IDF, so it is not specific to one host, compiler or AES backend.Expected behavior
psa_cipher_update()should return an error wheninput == outputand a partial block is pending, asmbedtls_cipher_update()does. Any error is better than the current behaviour;PSA_ERROR_INVALID_ARGUMENTseems the natural choice.The combination cannot be executed correctly, because completing a pending block writes a full block of output while consuming fewer than a full block of input, so the write necessarily lands on input that has not been read. There is nothing to preserve here, and no caller can be relying on the current results.
Actual behavior
All four
psa_cipher_update()calls returnPSA_SUCCESSand report plausible output lengths, and the output does not match the ciphertext that the same key and plaintext produce.psa_cipher_finish()also succeeds. Pass 3 of the reproducer below, on the unfixed library withMBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS:Comparing the two hex strings the program prints, with spaces added here for alignment:
0xc5at offset 64 is the original plaintext byte, which was never overwritten. The block1c63ed...f1c1that belongs there follows one byte late. From the next block onwards the output is unrelated to the plaintext:282ead2e...is not the encryption of anything the caller supplied.What happens inside the call
The second call supplies 33 bytes, so one byte is left pending in
ctx->unprocessed_dataandbuf[64]is left untouched in the buffer. The third call is entered withinput == output == buf+65and does two things inpsa_cipher_update_ecb():buf[65..79]and writes 16 bytes tobuf[65..80].buf[80]held the plaintext byte0x36; it now holds0xc1, the last byte of that ciphertext block. 2. It advancesinputby 15 tobuf+80andoutputby 16 tobuf+81, then callsmbedtls_cipher_update(input=buf+80, ilen=16, output=buf+81)for the remaining full blocks.buf[80]was destroyed in step 1, so this block encrypts a byte the caller never supplied, and every later block inherits the damage.At step 2 the pointers are one byte apart, so the
input == outputtest inmbedtls_cipher_update()is false and the existing guard cannot fire. The check is correct; it is simply no longer shown the aliasing by the time it runs.In-place operation itself is not affected
This matters for the shape of the fix, so I checked it rather than assuming it. A caller that advances its output offset by the returned
output_length, instead of reusing the input offset, gets the correct ciphertext on the unfixed library. Pass 2 of the reproducer does this.It also holds in general, not just for one chunking. Under that idiom the invariant
in_off - out_off == unprocessed_lenholds, so when a pending block is completed the write ends atout_off + block_sizeand the read ends atin_off + (block_size - unprocessed_len), which is the same address. The write never overtakes the read. I confirmed this over 3000 random chunkings on the unfixed library, in both configurations, with no failures.So the check I am proposing rejects only the combination that is already broken, and no working caller is affected by it.
Behaviour by configuration
output == inputMBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERSPSA_SUCCESS, unread input destroyed, later blocks encrypt data never suppliedPSA_SUCCESS, blocks correct but displaced by one byteTo be precise about the second row: in a default build the library writes the correct ciphertext at the pointer it was given, so the displaced result there follows from the caller's own offset choice rather than from memory being overwritten, and I am not reporting it as memory corruption. I include it because it shows the call is accepted in every configuration, which is the part I would like to see changed.
Steps to reproduce
Everything below is against
ec56e6a6ffccf6f8982d271d2d3ddac2de84906fondevelopment.1. Get the sources.
The last line is needed; without Jinja2 the build fails while generating
core/psa_crypto_driver_wrappers.h.2. Build the library with
MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS.3. Derive the reference ciphertext independently, with OpenSSL.
The reproducer compares against a vector produced outside this library, so that a failure cannot be attributed to the reference. The plaintext is
plain[i] = i * 7 + 1 + i / 16for 112 bytes; the key is the FIPS-197 AES-128 test key.This prints the following, which is what the
openssl_ctarray in the program below contains:4. Build and run the reproducer.
It exits 0 if the library behaves and 1 if the defect is present.
ecb_inplace.c5. Result with
MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERS.Passes 1 and 2 match the OpenSSL vector, pass 3 does not, and the program exits 1 with
DEFECT REPRODUCED:6. The same in a default build, to show this is not specific to one configuration.
Rebuild both the library and the reproducer without the macro:
Pass 3 is still accepted, still returns
PSA_SUCCESSon every call, and still fails at byte 64. The program again exits 1:The difference from step 5 is in the tail of the buffer rather than in whether the call is accepted. Here the blocks are correct and merely displaced by one byte, because the dispatch layer's input copy protected the plaintext; with
MBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERSthe later blocks are unrelated to the plaintext. Passes 1 and 2 are correct in both builds.The chunk sizes matter throughout. The defect needs one call that leaves a partial block pending, which is the second call because 33 is not a multiple of the block size, followed by an in-place call that completes it, which is the third.
Additional information
I have a fix and a regression test ready and will open a pull request that references this issue. The fix rejects the call with
PSA_ERROR_INVALID_ARGUMENTwheninput == outputandunprocessed_len != 0, mirroring the existing check inmbedtls_cipher_update(). With it, pass 3 is refused on the third call, passes 1 and 2 are unaffected, and the 3000-chunking check above still passes.One question on placement, which I would like your view on before the pull request. I put the check in
psa_cipher_update_ecb(), which is reached only afterLOCAL_INPUT_ALLOCandLOCAL_OUTPUT_ALLOChave substituted internal copies in a default build. It therefore fires only whenMBEDTLS_PSA_ASSUME_EXCLUSIVE_BUFFERSis enabled, which is where the memory damage happens, but it leaves a default build returningPSA_SUCCESSfor the same call, as step 6 shows. If you would prefer the combination to be rejected consistently in every configuration, the test needs to move up topsa_cipher_update()incore/psa_crypto.c, before the local copies are made. I am happy to write it either way.A note on the reproducer: it passes the exact number of output bytes each call produces rather than a generous
output_size. A largeroutput_sizebrings in a second, unrelated effect in a default build that would obscure this report. I will raise that separately.Related to Mbed-TLS/mbedtls#3266, which tracks overlapping input and output buffers more generally.
I need to think about this. The problem with rejecting overlap is that it's practically impossible to do it in standard C. It's easy to check if
input == output, but what aboutinput == output + 1,input == output - 3, etc.? Partial overlap is common when the ciphertext starts with an IV or with some protocol header. For the specific case of ECB, partial overlap is likely not very common, but ECB itself is not very common either (the only reason we have it in the PSA API is to allow callers to make a custom mode that the library doesn't support). Some platforms allow comparisons likeinput < output + length, but we can't have that in our code, because there are platforms where this faults or can have false positives ifinputandoutputare in different regions of memory.