LMS: defensive output zeroization added by 9b3051fb10 covers one of five equivalent call sites
Summary
Commit 9b3051fb10 ("Built-in lms driver: always zeroize output-buffer in
create_merkle_leaf_value"), part of the June 2025 LMS fix batch, added a
defensive memset() of the output buffer on entry:
/* library/lms.c:104-105 */
/* Always zeroize the output buffer because it may contain data from the previous invocation */
memset(out, 0, MBEDTLS_LMS_M_NODE_BYTES(params->type));Four other functions in lms.c / lmots.c have the identical shape — a
caller-supplied output buffer filled by a fallible PSA hash chain, where an
early goto exit leaves the buffer holding whatever the caller's stack held —
and none of them got the equivalent treatment.
To be clear up front: this is not a vulnerability, and I am not reporting it as one. Every one of the four is called with its return value captured and tested, so a failure cannot be mistaken for success. This is about the defence-in-depth measure being applied inconsistently, which weakens it as a regression barrier. Nothing here involves memory corruption or a crash — see "Actual behavior" below.
System information
Mbed TLS version (number or commit id): mbedtls-3.6 @ 08e82477d8; also TF-PSA-Crypto development @ fa93e8fa6 (extras/lms.c, extras/lmots.c — same code)
Operating system and version: n/a — source-level consistency issue
Configuration (if not default, please attach mbedtls_config.h): default (MBEDTLS_LMS_C is default-on)
Compiler and options (if you used a pre-built binary, please indicate how you obtained it): n/a
Additional environment information: n/a
Expected behavior
The entry-condition zeroization introduced by 9b3051fb10 applied consistently
across all five functions that fill a caller-supplied output buffer from a
fallible PSA hash chain.
Actual behavior
One of the five zeroizes; four do not. No crash, no memory corruption, no sanitizer finding — there is no path today on which a stale buffer is consumed, because every one of the four is reached only through call sites that check the return value. The report is about the asymmetry, not a fault.
Steps to reproduce
Static; no runtime reproduction. See the table and the call-site enumeration below.
Additional information
The five sites
| Function | Definition | Output buffer it fills | Zeroizes on entry? |
|---|---|---|---|
create_merkle_leaf_value |
library/lms.c:94 |
Tc_candidate_root_node |
yes — lms.c:105 |
create_merkle_internal_value |
library/lms.c:170 |
Tc_candidate_root_node |
no |
create_digit_array_with_checksum |
library/lmots.c:119 |
tmp_digit_array |
no |
hash_digit_array |
library/lmots.c:208 |
y_hashed_digits |
no |
public_key_from_hashed_digit_array |
library/lmots.c:313 |
Kc_candidate_ots_pub_key |
no |
The two buffers reaching mbedtls_lms_verify() from outside are both
uninitialised stack: Kc_candidate_ots_pub_key at library/lms.c:309 and
Tc_candidate_root_node at library/lms.c:310.
Why it is currently harmless
The four unzeroized functions are reached only through call sites that check the return:
library/lms.c:380→ checked at:386library/lms.c:408→ checked at:410library/lmots.c:485→ checked at:488library/lmots.c:492→ checked at:495library/lmots.c:499→ checked at:502library/lms.c:453/:468→ checked at:458/:476(MBEDTLS_LMS_PRIVATE)
I enumerated every status-returning call site in both files: 73 total, 65 capture
and test the result, and the 8 that do not are all psa_hash_abort() cleanup
calls (lms.c:143, lms.c:223, lmots.c:174, :284, :292, :357, :650,
:656), whose return cannot influence a verification verdict. So there is no
path today on which a stale buffer is consumed.
Why it still seems worth closing
The scenario 9b3051fb10 was hardening against is CVE-2025-49600's: a
Tc_candidate_root_node left holding a valid root from a previous successful
verification, so that a later failed hash makes a bogus signature verify. The
protection against that is now twofold at one site — checked return and
zeroized buffer — and single-layered at the other four. Since the CVE was caused
by exactly the layer that remains single at those four sites (a missing return
check), the asymmetry is the wrong way round: the extra barrier sits where the
bug was found rather than where an equivalent bug could next appear.
Two smaller notes in the same area, if a sweep is done:
create_merkle_leaf_value()is the only one of the five whose zeroization uses the parameter-set length. Any fix should useMBEDTLS_LMOTS_N_HASH_LEN(params->type)/MBEDTLS_LMS_M_NODE_BYTES(params->type)rather thansizeof, so it stays correct if a parameter set withn < MAXis ever added (cf. PR #6584).public_key_from_hashed_digit_array()atlibrary/lmots.c:354-358places itsexit:label inside the body ofif (status != PSA_SUCCESS) { … }. It is correct as written — I traced all three paths — but it means the error path depends on control falling out of a block it never structurally entered. Anyone touching this function to add a zeroization would be well advised to straighten that out first.
Suggested change
Add the same entry-condition memset() to the four remaining functions, sized
from the parameter set, and ideally a one-line comment recording why (so the
next reader knows it is deliberate defence-in-depth and not dead code).
Happy to send a PR if that is wanted.
Source: Mbed-TLS/mbedtls