Skip to content

passphrase_protect() leaves the plaintext passphrase on the stack, unzeroed and outside CONFIDENTIAL #476

Description

@BitHighlander

Noticed while fixing #428(a) in passphrase_sm.c. Pre-existing, not introduced by that change.

The defect

bool passphrase_protect(void) {
  bool ret = false;
  PassphraseInfo passphrase_info;          // <- stack, holds the plaintext

  if (storage_getPassphraseProtected() && !session_isPassphraseCached()) {
    if (passphrase_request(&passphrase_info)) {
      session_cachePassphrase(passphrase_info.passphrase);
      ret = true;
    }
  } else {
    ret = true;
  }
  return (ret);                            // <- no memzero on any path
}

wait_for_passphrase_ack() copies the host-supplied passphrase into passphrase_info->passphrase with strlcpy (passphrase_sm.c:67). passphrase_protect() then hands it to session_cachePassphrase() and returns, leaving the plaintext in its stack frame on every path — success, host cancel, and the early-out where passphrase protection is off.

grep -n "memzero\|CONFIDENTIAL" lib/firmware/passphrase_sm.c returns nothing: the buffer is neither zeroed nor placed in the CONFIDENTIAL section that the rest of the firmware uses for secret-bearing objects (compare confirm_sm.c:47's static CONFIDENTIAL char strbuf[...], and ethereum.c's static CONFIDENTIAL uint8_t privkey[32]).

Why it matters

The passphrase is a wallet-selecting secret: it is the difference between the visible wallet and a hidden one. Anything that later reads uninitialised stack in that region — a deep call path, an unrelated buffer over-read, a fault handler dumping stack — can surface it. It is the same class the firmware already defends against elsewhere by zeroing key material immediately after use.

Severity is bounded by needing another defect to read it, so this is defence-in-depth rather than a directly reachable disclosure.

Fix

memzero(&passphrase_info, sizeof(passphrase_info)) before every return in passphrase_protect(), and mark the type or the local CONFIDENTIAL so it lands in the section the rest of the firmware uses for secrets. Worth checking session_cachePassphrase()'s own copy discipline at the same time.

Not proposed for 7.14.2: it is untestable at the unit layer for the same reason #428(a) is, and it is unchanged from the shipped base.

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity-relevant defect or hardening

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions