Skip to content

fix: ~ecdsa_preprocessing_data cleanses over live std::map/std::vector members (UB + leak)#57

Open
grecow wants to merge 1 commit into
fireblocks:mainfrom
grecow:fix/preprocessing-data-cleanse-size
Open

fix: ~ecdsa_preprocessing_data cleanses over live std::map/std::vector members (UB + leak)#57
grecow wants to merge 1 commit into
fireblocks:mainfrom
grecow:fix/preprocessing-data-cleanse-size

Conversation

@grecow

@grecow grecow commented Jun 25, 2026

Copy link
Copy Markdown

Problem

ecdsa_preprocessing_data (in include/cosigner/cmp_ecdsa_signing_service.h) has:

~ecdsa_preprocessing_data() { OPENSSL_cleanse(k.data, sizeof(ecdsa_preprocessing_data)); }

k is the first member, so k.data aliases the object base — but the size passed is
sizeof(ecdsa_preprocessing_data), i.e. the entire struct. That span covers the three
non-trivially-destructible members:

byte_vector_t                                  mta_request;   // std::vector
std::map<uint64_t, byte_vector_t>              G_proofs;      // std::map
std::map<uint64_t, ecdsa_signing_public_data>  public_data;   // std::map

A user-declared destructor body runs before the implicit member destructors, so the
OPENSSL_cleanse zeroes the live control blocks of the vector and the two maps
(begin/end/root pointers, node counts). The subsequent ~vector / ~map then run on those
zeroed internals — undefined behaviour, and in practice a heap leak of every map node
and vector buffer (these are populated per-signing from peer messages).

Fix

Cleanse each secret scalar individually and let the containers destruct normally. This mirrors
the correct pattern already used by the EdDSA analog eddsa_signature_data
(include/cosigner/eddsa_online_signing_service.h), which cleanses sizeof(k.data) rather than
the whole struct.

Notes

  • Behaviour-preserving for the intended goal (all secret scalars k, gamma, a, b, delta, chi
    are still wiped); only the erroneous over-write of the live container internals is removed.
  • This is a correctness / memory-hygiene fix — not a remotely exploitable issue (the write stays
    within the object; no OOB).
  • Suggested validation: build with -fsanitize=address and run ecdsa_online_test /
    ecdsa_offline_test; LeakSanitizer flags the orphaned allocations before this change and is
    clean after.

OPENSSL_cleanse(k.data, sizeof(ecdsa_preprocessing_data)) passes the size of
the WHOLE struct. Because k is the first member, k.data aliases the object
base, so the cleanse spans the non-trivial members mta_request (std::vector),
G_proofs and public_data (std::map). A user-declared destructor body runs
before the implicit member destructors, so this zeroes the live container
control blocks; the subsequent ~vector/~map then operate on zeroed internals
(undefined behaviour + heap leak of their allocations).

Cleanse each secret scalar individually instead, leaving the containers to
their own destructors. Mirrors the correct pattern already used by
eddsa_signature_data in eddsa_online_signing_service.h (sizeof(k.data)).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant