#10952·mbedtls

LMS: defensive output zeroization added by 9b3051fb10 covers one of five equivalent call sites

Author: iivielCreated Sep 14, 2026Updated Sep 16, 2026
Labelsbugcomponent-cryptosize-spriority-low

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:

c
/* 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 yeslms.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 :386
  • library/lms.c:408 → checked at :410
  • library/lmots.c:485 → checked at :488
  • library/lmots.c:492 → checked at :495
  • library/lmots.c:499 → checked at :502
  • library/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 use MBEDTLS_LMOTS_N_HASH_LEN(params->type) / MBEDTLS_LMS_M_NODE_BYTES(params->type) rather than sizeof, so it stays correct if a parameter set with n < MAX is ever added (cf. PR #6584).
  • public_key_from_hashed_digit_array() at library/lmots.c:354-358 places its exit: label inside the body of if (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.