From 017d76eba15230a0dfc138b75aebee55c235ba87 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Mon, 6 Jul 2026 23:42:28 +1200 Subject: [PATCH 1/7] descriptor: avoid iterating multisig nodes twice node_get_child_count() iterates all keys which we then immediately do again below. Count the children as we iterate checking the keys instead. Co-authored-by: odudex --- src/descriptor.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/descriptor.c b/src/descriptor.c index 83a74fe7b..fcddd7987 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -725,23 +725,22 @@ static int verify_combo(ms_ctx *ctx, ms_node *node) static int verify_multi(ms_ctx *ctx, ms_node *node) { (void)ctx; - const int64_t count = node_get_child_count(node); - ms_node *top, *key; + const ms_node *top = node->child; + ms_node *key = top ? top->next : NULL; + int64_t key_count = 0; - if (count < 2 || count - 1 > MINISCRIPT_MULTI_MAX) + if (!top || !key || top->builtin || + top->kind != KIND_NUMBER || top->number <= 0) return WALLY_EINVAL; - top = node->child; - if (!top->next || top->builtin || top->kind != KIND_NUMBER || - top->number <= 0 || count < top->number) - return WALLY_EINVAL; - - key = top->next; while (key) { - if (key->builtin || !(key->kind & KIND_KEY)) + if (key->builtin || !(key->kind & KIND_KEY) || + ++key_count > MINISCRIPT_MULTI_MAX) return WALLY_EINVAL; key = key->next; } + if (top->number > key_count) + return WALLY_EINVAL; node->type_properties = builtin_get(node)->type_properties; return WALLY_OK; From 2d36c91d193c1e30949b58179eafae5c5cb1817a Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 9 Jul 2026 01:40:42 +1200 Subject: [PATCH 2/7] internal: mark internal functions as hidden visibility when possible Even when building with -fvisibility=hidden, some internal functions (those that aren't static and thus provably limited to a single compilation unit) are not marked hidden. As a result, calls to them from within the library are routed through the PLT. This can result in register data containing secrets/partial secrets being spilled to the stack. Mark such internal functions as hidden so internal calls to them avoid the PLT call mechanism. --- configure.ac | 2 + src/ccan_config.h | 4 ++ src/internal.h | 112 ++++++++++++++++++++++++++-------------------- src/mnemonic.h | 6 ++- src/wordlist.h | 9 ++-- 5 files changed, 78 insertions(+), 55 deletions(-) diff --git a/configure.ac b/configure.ac index 9dd4fa54e..7fa0a66d1 100644 --- a/configure.ac +++ b/configure.ac @@ -236,6 +236,8 @@ AM_CONDITIONAL([EXPORT_ALL], [test "x$export_all" = "xyes"]) if test "x$export_all" != "xyes"; then AX_CHECK_COMPILE_FLAG([-fvisibility=hidden], [AM_CFLAGS="$AM_CFLAGS -fvisibility=hidden"]) +else + AX_CHECK_COMPILE_FLAG([-DWALLY_EXPORT_ALL=1], [AM_CFLAGS="$AM_CFLAGS -DWALLY_EXPORT_ALL=1"]) fi # Assume we have no unaligned access if cross-compiling diff --git a/src/ccan_config.h b/src/ccan_config.h index c660937b6..db347ac2e 100644 --- a/src/ccan_config.h +++ b/src/ccan_config.h @@ -59,7 +59,11 @@ #define CCAN_CRYPTO_SHA512_USE_MBEDTLS 1 #endif +#if !defined(WALLY_EXPORT_ALL) && !defined(_WIN32) +void __attribute__ ((visibility ("hidden"))) wally_clear(void *p, size_t len); +#else void wally_clear(void *p, size_t len); +#endif #define CCAN_CLEAR_MEMORY(p, len) wally_clear(p, len) diff --git a/src/internal.h b/src/internal.h index 70a93d013..2e26e02b0 100644 --- a/src/internal.h +++ b/src/internal.h @@ -16,6 +16,14 @@ #endif /* __clang__/__GNUC__ */ #endif /* BUILD_ELEMENTS */ #endif /* WALLY_ABI_NO_ELEMENTS */ + +#if !defined(WALLY_EXPORT_ALL) && !defined(_WIN32) +/* Ensure local symbols are resolved internally */ +#define WALLY_INTERNAL_API __attribute__ ((visibility ("hidden"))) +#else +#define WALLY_INTERNAL_API +#endif + #include #include #include @@ -30,48 +38,52 @@ #include /* Fetch an internal secp context */ -const secp256k1_context *secp_ctx(void); +WALLY_INTERNAL_API const secp256k1_context *secp_ctx(void); #define secp256k1_context_destroy(c) _do_not_destroy_shared_ctx_pointers(c) /* secp pub/priv key functions */ #define pubkey_create secp256k1_ec_pubkey_create #define pubkey_tweak_add secp256k1_ec_pubkey_tweak_add -int pubkey_combine(secp256k1_pubkey *pubnonce, const secp256k1_pubkey *const *pubnonces, size_t n); -int pubkey_negate(secp256k1_pubkey *pubkey); -int pubkey_parse(secp256k1_pubkey *pubkey, const unsigned char *input, size_t input_len); -int pubkey_serialize(unsigned char *output, size_t *outputlen, const secp256k1_pubkey *pubkey, unsigned int flags); +WALLY_INTERNAL_API int pubkey_combine(secp256k1_pubkey *pubnonce, const secp256k1_pubkey *const *pubnonces, size_t n); +WALLY_INTERNAL_API int pubkey_negate(secp256k1_pubkey *pubkey); +WALLY_INTERNAL_API int pubkey_parse(secp256k1_pubkey *pubkey, const unsigned char *input, size_t input_len); +WALLY_INTERNAL_API int pubkey_serialize(unsigned char *output, size_t *outputlen, const secp256k1_pubkey *pubkey, unsigned int flags); /* Note xpubkey_parse accepts standard compressed pubkeys as well as x-only */ -int xpubkey_parse(secp256k1_xonly_pubkey *pubkey, const unsigned char *input, size_t input_len); -int xpubkey_tweak_add(secp256k1_pubkey *pubkey, const secp256k1_xonly_pubkey *xpubkey, const unsigned char *tweak); -int xpubkey_serialize(unsigned char *output, const secp256k1_xonly_pubkey *xpubkey); -int seckey_verify(const unsigned char *seckey); -int seckey_negate(unsigned char *seckey); -int seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak); -int seckey_tweak_mul(unsigned char *seckey, const unsigned char *tweak); -int keypair_create(secp256k1_keypair *keypair, const unsigned char *priv_key); -int keypair_xonly_pub(secp256k1_xonly_pubkey *xpubkey, const secp256k1_keypair *keypair); -int keypair_sec(unsigned char *output, const secp256k1_keypair *keypair); -int keypair_xonly_tweak_add(secp256k1_keypair *keypair, const unsigned char *tweak); +WALLY_INTERNAL_API int xpubkey_parse(secp256k1_xonly_pubkey *pubkey, const unsigned char *input, size_t input_len); +WALLY_INTERNAL_API int xpubkey_tweak_add(secp256k1_pubkey *pubkey, const secp256k1_xonly_pubkey *xpubkey, const unsigned char *tweak); +WALLY_INTERNAL_API int xpubkey_serialize(unsigned char *output, const secp256k1_xonly_pubkey *xpubkey); +WALLY_INTERNAL_API int seckey_verify(const unsigned char *seckey); +WALLY_INTERNAL_API int seckey_negate(unsigned char *seckey); +WALLY_INTERNAL_API int seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak); +WALLY_INTERNAL_API int seckey_tweak_mul(unsigned char *seckey, const unsigned char *tweak); +WALLY_INTERNAL_API int keypair_create(secp256k1_keypair *keypair, const unsigned char *priv_key); +WALLY_INTERNAL_API int keypair_xonly_pub(secp256k1_xonly_pubkey *xpubkey, const secp256k1_keypair *keypair); +WALLY_INTERNAL_API int keypair_sec(unsigned char *output, const secp256k1_keypair *keypair); +WALLY_INTERNAL_API int keypair_xonly_tweak_add(secp256k1_keypair *keypair, const unsigned char *tweak); #define PUBKEY_COMPRESSED SECP256K1_EC_COMPRESSED #define PUBKEY_UNCOMPRESSED SECP256K1_EC_UNCOMPRESSED -void wally_clear(void *p, size_t len); -void wally_clear_2(void *p, size_t len, void *p2, size_t len2); -void wally_clear_3(void *p, size_t len, void *p2, size_t len2, - void *p3, size_t len3); -void wally_clear_4(void *p, size_t len, void *p2, size_t len2, - void *p3, size_t len3, void *p4, size_t len4); +WALLY_INTERNAL_API void wally_clear(void *p, size_t len); +WALLY_INTERNAL_API void wally_clear_2(void *p, size_t len, + void *p2, size_t len2); +WALLY_INTERNAL_API void wally_clear_3(void *p, size_t len, + void *p2, size_t len2, + void *p3, size_t len3); +WALLY_INTERNAL_API void wally_clear_4(void *p, size_t len, + void *p2, size_t len2, + void *p3, size_t len3, + void *p4, size_t len4); -void clear_and_free(void *p, size_t len); -void clear_and_free_bytes(unsigned char **p, size_t *len); +WALLY_INTERNAL_API void clear_and_free(void *p, size_t len); +WALLY_INTERNAL_API void clear_and_free_bytes(unsigned char **p, size_t *len); -bool mem_is_zero(const void *mem, size_t len); +WALLY_INTERNAL_API bool mem_is_zero(const void *mem, size_t len); /* Fetch our internal operations function pointers */ -const struct wally_operations *wally_ops(void); +WALLY_INTERNAL_API const struct wally_operations *wally_ops(void); #ifndef BUILD_AMALGAMATION #define malloc(size) __use_wally_malloc_internally__ @@ -97,32 +109,34 @@ const struct wally_operations *wally_ops(void); if (!*output) return WALLY_ENOMEM /* Helpers for operating on byte buffers */ -bool clone_data(void **dst, const void *src, size_t len); -bool clone_bytes(unsigned char **dst, const unsigned char *src, size_t len); -int replace_bytes(const unsigned char *bytes, size_t bytes_len, - unsigned char **bytes_out, size_t *bytes_len_out); -void *array_realloc(const void *src, size_t old_n, size_t new_n, size_t size); +WALLY_INTERNAL_API bool clone_data(void **dst, const void *src, size_t len); +WALLY_INTERNAL_API bool clone_bytes(unsigned char **dst, const unsigned char *src, size_t len); +WALLY_INTERNAL_API int replace_bytes(const unsigned char *bytes, size_t bytes_len, + unsigned char **bytes_out, size_t *bytes_len_out); +WALLY_INTERNAL_API void *array_realloc(const void *src, size_t old_n, + size_t new_n, size_t size); -int array_grow(void **src, size_t num_items, size_t *allocation_len, - size_t item_size); +WALLY_INTERNAL_API int array_grow(void **src, size_t num_items, + size_t *allocation_len, size_t item_size); struct ext_key; /* Internal: Create a partial bip32 key from a private key (no chaincode, un-derivable) */ -int bip32_key_from_private_key(uint32_t version, const unsigned char *priv_key, - size_t priv_key_len, struct ext_key *output); +WALLY_INTERNAL_API int bip32_key_from_private_key(uint32_t version, + const unsigned char *priv_key, + size_t priv_key_len, + struct ext_key *output); struct wally_map; -int map_add(struct wally_map *map_in, - const unsigned char *key, size_t key_len, - const unsigned char *value, size_t value_len, - bool take_value, bool ignore_dups); -int map_add_preimage_and_hash(struct wally_map *map_in, - const unsigned char *key, size_t key_len, - const unsigned char *val, size_t val_len, - size_t type, bool skip_verify); -const struct wally_map_item *map_find_equal_integer(const struct wally_map *lhs, - const struct wally_map *rhs, - uint32_t key); +WALLY_INTERNAL_API int map_add(struct wally_map *map_in, + const unsigned char *key, size_t key_len, + const unsigned char *value, size_t value_len, + bool take_value, bool ignore_dups); +WALLY_INTERNAL_API int map_add_preimage_and_hash(struct wally_map *map_in, + const unsigned char *key, size_t key_len, + const unsigned char *val, size_t val_len, + size_t type, bool skip_verify); +WALLY_INTERNAL_API const struct wally_map_item *map_find_equal_integer( + const struct wally_map *lhs, const struct wally_map *rhs, uint32_t key); /* Clamp input/output allocation sizing to standard tx sizes for BTC. * Liquid numbers are smaller; we use the upper limit */ @@ -134,9 +148,9 @@ const struct wally_map_item *map_find_equal_integer(const struct wally_map *lhs, /* Allows allocating a larger witness for e.g deserializing */ struct wally_tx_witness_stack; -int tx_witness_stack_init_alloc(size_t allocation_len, - size_t max_allocation_len, - struct wally_tx_witness_stack **output); +WALLY_INTERNAL_API int tx_witness_stack_init_alloc(size_t allocation_len, + size_t max_allocation_len, + struct wally_tx_witness_stack **output); /* Absolute maximum number of inputs and outputs for BTC. * Liquid numbers are smaller; we use the upper limit */ diff --git a/src/mnemonic.h b/src/mnemonic.h index 19719f9a6..f8eccb244 100644 --- a/src/mnemonic.h +++ b/src/mnemonic.h @@ -1,6 +1,8 @@ #ifndef LIBWALLY_MNEMONIC_H #define LIBWALLY_MNEMONIC_H +#include "internal.h" + struct words; /** @@ -12,7 +14,7 @@ struct words; * * @bytes must be an even multiple of the number of bits in the wordlist used. */ -char *mnemonic_from_bytes( +WALLY_INTERNAL_API char * mnemonic_from_bytes( const struct words *w, const unsigned char *bytes, size_t len); @@ -26,7 +28,7 @@ char *mnemonic_from_bytes( * @len: The length of @bytes_out in bytes. * @written: Destination for the number of bytes written to ``bytes_out``. */ -int mnemonic_to_bytes( +WALLY_INTERNAL_API int mnemonic_to_bytes( const struct words *w, const char *mnemonic, unsigned char *bytes_out, diff --git a/src/wordlist.h b/src/wordlist.h index c8189672e..622c5fdba 100644 --- a/src/wordlist.h +++ b/src/wordlist.h @@ -3,6 +3,7 @@ #include #include +#include "internal.h" /** * struct words- structure representing a parsed list of words @@ -30,7 +31,7 @@ struct words { * * The returned structure contains a copy of @text. */ -struct words *wordlist_init(const char *text); +WALLY_INTERNAL_API struct words *wordlist_init(const char *text); /** * Find a word in a wordlist. @@ -41,7 +42,7 @@ struct words *wordlist_init(const char *text); * Returns 0 if not found, idx + 1 otherwise. * @see wordlist_init. */ -size_t wordlist_lookup_word( +WALLY_INTERNAL_API size_t wordlist_lookup_word( const struct words *w, const char *word); @@ -53,7 +54,7 @@ size_t wordlist_lookup_word( * * Returns NULL if not found, the word otherwise. */ -const char *wordlist_lookup_index( +WALLY_INTERNAL_API const char *wordlist_lookup_index( const struct words *w, size_t idx); @@ -61,6 +62,6 @@ const char *wordlist_lookup_index( * wordlist_free - Free a words structure. * @w: structure to free. */ -void wordlist_free(struct words *w); +WALLY_INTERNAL_API void wordlist_free(struct words *w); #endif /* LIBWALLY_WORDLIST_H */ From 91b3b6d692d1fbd197bc54b23a687d22d100af7c Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 9 Jul 2026 21:43:24 +1200 Subject: [PATCH 3/7] internal: avoid potential info leaks via sse/avx registers In particular when built as a shared library, calls through the PLT spill these registers onto the stack. Thanks to @pythcoiner for identifying the leak cause and suggesting a fix. --- src/Makefile.am | 2 +- src/aes.c | 2 +- src/bip32.c | 24 +++++++++--------- src/bip39.c | 8 +++--- src/ctest/_CMakeLists.txt | 2 +- src/ctest/test_clear.c | 51 ++++++++++++++++++++++++++++++--------- src/internal.c | 28 ++++++++++++++++++--- src/internal.h | 9 +++++++ src/mnemonic.c | 2 +- src/wif.c | 4 +-- 10 files changed, 96 insertions(+), 36 deletions(-) diff --git a/src/Makefile.am b/src/Makefile.am index 977593a51..150f7a545 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -254,7 +254,7 @@ if USE_PTHREAD TESTS += test_clear noinst_PROGRAMS += test_clear test_clear_SOURCES = ctest/test_clear.c -test_clear_CFLAGS = -I$(top_srcdir)/include $(PTHREAD_CFLAGS) $(AM_CFLAGS) $(NOOPT_CFLAGS) $(NOBUILTIN_CFLAGS) +test_clear_CFLAGS = -I$(top_srcdir)/include -I$(top_srcdir) -I$(top_srcdir)/src/secp256k1/include $(PTHREAD_CFLAGS) $(AM_CFLAGS) $(NOOPT_CFLAGS) $(NOBUILTIN_CFLAGS) test_clear_LIBS = $(PTHREAD_LIBS) test_clear_LDADD = $(lib_LTLIBRARIES) @CTEST_EXTRA_STATIC@ if PYTHON_MANYLINUX diff --git a/src/aes.c b/src/aes.c index f4ea03e6f..291946f26 100644 --- a/src/aes.c +++ b/src/aes.c @@ -297,7 +297,7 @@ int wally_aes_cbc_with_ecdh_key( if (is_encrypt) { /* Copy the IV to the start of the encrypted output */ - memcpy(bytes_out, iv, iv_len); + wally_memcpy(bytes_out, iv, iv_len); } else { /* The IV is the first AES_BLOCK_LEN bytes of the payload */ iv = bytes; diff --git a/src/bip32.c b/src/bip32.c index b39a3b851..a52a7c8b5 100644 --- a/src/bip32.c +++ b/src/bip32.c @@ -364,7 +364,7 @@ int bip32_key_from_private_key(uint32_t version, key_out->version = version; /* Copy the private key and set its prefix */ key_out->priv_key[0] = BIP32_FLAG_KEY_PRIVATE; - memcpy(key_out->priv_key + 1, priv_key, priv_key_len); + wally_memcpy(key_out->priv_key + 1, priv_key, priv_key_len); /* Compute the public key */ if (key_compute_pub_key(key_out) != WALLY_OK) return wipe_key_fail(key_out); @@ -400,7 +400,7 @@ int bip32_key_from_seed_custom(const unsigned char *bytes, size_t bytes_len, ret = bip32_key_from_private_key(version, sha.u.u8, EC_PRIVATE_KEY_LEN, key_out); if (ret == WALLY_OK) { /* Copy the chain code and set other members */ - memcpy(key_out->chain_code, sha.u.u8 + sizeof(sha) / 2, sizeof(sha) / 2); + wally_memcpy(key_out->chain_code, sha.u.u8 + sizeof(sha) / 2, sizeof(sha) / 2); key_out->depth = 0; /* Master key, depth 0 */ key_out->child_num = 0; if (!(flags & BIP32_FLAG_SKIP_HASH)) @@ -453,7 +453,7 @@ int bip32_key_from_seed_alloc(const unsigned char *bytes, size_t bytes_len, static unsigned char *copy_out(unsigned char *dest, const void *src, size_t len) { - memcpy(dest, src, len); + wally_memcpy(dest, src, len); return dest + len; } @@ -536,7 +536,7 @@ int bip32_key_serialize(const struct ext_key *hdkey, uint32_t flags, static const unsigned char *copy_in(void *dest, const unsigned char *src, size_t len) { - memcpy(dest, src, len); + wally_memcpy(dest, src, len); return src + len; } @@ -674,13 +674,13 @@ int bip32_key_from_parent(const struct ext_key *hdkey, uint32_t child_num, /* NB: We use the key_outs' priv_key+child_num to hold 'Data' here */ if (hardened) { /* Hardened: Data = 0x00 || ser256(kpar) || ser32(i)) */ - memcpy(key_out->priv_key, hdkey->priv_key, sizeof(hdkey->priv_key)); + wally_memcpy(key_out->priv_key, hdkey->priv_key, sizeof(hdkey->priv_key)); } else { /* Non Hardened Private: Data = serP(point(kpar)) || ser32(i) * Non Hardened Public : Data = serP(kpar) || ser32(i) * point(kpar) when par is private is the public key. */ - memcpy(key_out->priv_key, hdkey->pub_key, sizeof(hdkey->pub_key)); + wally_memcpy(key_out->priv_key, hdkey->pub_key, sizeof(hdkey->pub_key)); } /* This is the '|| ser32(i)' part of the above */ @@ -694,7 +694,7 @@ int bip32_key_from_parent(const struct ext_key *hdkey, uint32_t child_num, /* Split I into two 32-byte sequences, IL and IR * The returned chain code ci is IR (i.e. the 2nd half of our hmac sha512) */ - memcpy(key_out->chain_code, sha.u.u8 + sizeof(sha) / 2, + wally_memcpy(key_out->chain_code, sha.u.u8 + sizeof(sha) / 2, sizeof(key_out->chain_code)); if (we_are_private) { @@ -702,7 +702,7 @@ int bip32_key_from_parent(const struct ext_key *hdkey, uint32_t child_num, * In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid * (NOTE: seckey_tweak_add checks both conditions) */ - memcpy(key_out->priv_key, hdkey->priv_key, sizeof(hdkey->priv_key)); + wally_memcpy(key_out->priv_key, hdkey->priv_key, sizeof(hdkey->priv_key)); if (!seckey_tweak_add(key_out->priv_key + 1, sha.u.u8) || key_compute_pub_key(key_out) != WALLY_OK) goto fail; @@ -822,7 +822,7 @@ int bip32_key_from_parent_path(const struct ext_key *hdkey, } if (ret == WALLY_OK) - memcpy(key_out, hdkey, sizeof(*key_out)); + wally_memcpy(key_out, hdkey, sizeof(*key_out)); wally_clear(tmp, sizeof(tmp)); return ret; @@ -1010,9 +1010,9 @@ int bip32_key_init(uint32_t version, uint32_t depth, uint32_t child_num, key_out->depth = depth; key_out->child_num = child_num; - memcpy(key_out->chain_code, chain_code, key_size(chain_code)); + wally_memcpy(key_out->chain_code, chain_code, key_size(chain_code)); if (priv_key && version != BIP32_VER_MAIN_PUBLIC && version != BIP32_VER_TEST_PUBLIC) - memcpy(key_out->priv_key + 1, priv_key, key_size(priv_key) - 1); + wally_memcpy(key_out->priv_key + 1, priv_key, key_size(priv_key) - 1); else key_out->priv_key[0] = BIP32_FLAG_KEY_PUBLIC; if (pub_key) @@ -1135,7 +1135,7 @@ static int getb_impl(const struct ext_key *hdkey, { if (!hdkey || !bytes_out || len != src_len) return WALLY_EINVAL; - memcpy(bytes_out, src, len); + wally_memcpy(bytes_out, src, len); return WALLY_OK; } diff --git a/src/bip39.c b/src/bip39.c index 73d30450f..9fdc2d03e 100644 --- a/src/bip39.c +++ b/src/bip39.c @@ -127,7 +127,7 @@ int bip39_mnemonic_from_bytes(const struct words *w, if (w->bits != 11u || !(mask = len_to_mask(bytes_len))) return WALLY_EINVAL; - memcpy(tmp_bytes, bytes, bytes_len); + wally_memcpy(tmp_bytes, bytes, bytes_len); checksum = bip39_checksum(bytes, bytes_len, mask); tmp_bytes[bytes_len] = checksum & 0xff; if (mask > 0xff) @@ -190,7 +190,7 @@ int bip39_mnemonic_to_bytes(const struct words *w, const char *mnemonic, ret = WALLY_EINVAL; /* Bad checksum */ } else - memcpy(bytes_out, tmp_bytes, tmp_len); + wally_memcpy(bytes_out, tmp_bytes, tmp_len); } } } @@ -232,9 +232,9 @@ int bip39_mnemonic_to_seed(const char *mnemonic, const char *passphrase, if (!salt) return WALLY_ENOMEM; - memcpy(salt, prefix, prefix_len); + wally_memcpy(salt, prefix, prefix_len); if (passphrase_len) - memcpy(salt + prefix_len, passphrase, passphrase_len); + wally_memcpy(salt + prefix_len, passphrase, passphrase_len); ret = wally_pbkdf2_hmac_sha512((unsigned char *)mnemonic, strlen(mnemonic), salt, salt_len, 0, diff --git a/src/ctest/_CMakeLists.txt b/src/ctest/_CMakeLists.txt index a8214bdea..2b0a9e8d6 100644 --- a/src/ctest/_CMakeLists.txt +++ b/src/ctest/_CMakeLists.txt @@ -7,7 +7,7 @@ add_test(test_bech32 test_bech32) if(NOT WIN32) add_executable(test_clear test_clear.c) - target_include_directories(test_clear PRIVATE ${CMAKE_BINARY_DIR}) + target_include_directories(test_clear PRIVATE ${CMAKE_BINARY_DIR} $ $) target_link_libraries(test_clear PRIVATE wallycore pthread) add_test(test_clear test_clear) endif() diff --git a/src/ctest/test_clear.c b/src/ctest/test_clear.c index 0432396c5..4a2441e1e 100644 --- a/src/ctest/test_clear.c +++ b/src/ctest/test_clear.c @@ -3,6 +3,9 @@ #ifdef HAVE_ASM_PAGE_H # include #endif +#include "internal.h" +#undef malloc +#undef free #include #include #include @@ -57,11 +60,11 @@ static unsigned char *gstack; /* Global scratch buffer */ static unsigned char *gbytes; -static const char *BIP39_MNEMONIC = "legal winner thank year wave sausage worth " - "useful legal winner thank yellow"; +static const char *BIP39_MNEMONIC = "team hospital room inspire tenant almost " + "push rich year warfare jeans foil"; static const unsigned char BIP39_SECRET[16] = { - 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, - 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f + 0xde, 0xad, 0xbe, 0xef, 0xba, 0xad, 0xf0, 0x0d, + 0xab, 0xad, 0xca, 0xfe, 0xfe, 0xe1, 0xde, 0xad }; /* Useful for developing these tests */ @@ -86,15 +89,35 @@ static unsigned char *checked_malloc(size_t len) return ret; } +/* Non-optimized memcmp. + * On e.g. x86_64, does not leave search data in SSE/AVX registers + * where it may be spilled to the stack when a call through the PLT + * occurs. + * TODO: Move to src/internal.c if we need to (we currently do not + * memcmp() any secret data so this is only required here). + */ +WALLY_NO_OPTIMIZE static int wally_memcmp(const void *s1, const void *s2, size_t n) { + const unsigned char *p1 = s1; + const unsigned char *p2 = s2; + + for (size_t i = 0; i < n; i++) { + if (p1[i] != p2[i]) { + return p1[i] - p2[i]; + } + } + return 0; +} + static bool in_stack(const char *caller, volatile const void *search, size_t len) { static size_t i; for (i = 0; i < PTHREAD_STACK_MIN - len - 1; ++i) - if (!memcmp(gstack + i, (const void *)search, len)) { + if (!wally_memcmp(gstack + i, (const void *)search, len)) { if (caller) { printf("Found %s secret at stack position %ld and base %p\n", caller, (long)i, (void *)gstack); - dump_mem(search, len); + printf("raw pointer: %p\n", gstack + i); + dump_mem(gstack + i - 64, len + 128); } return true; /* Found */ } @@ -105,10 +128,10 @@ static bool in_stack(const char *caller, volatile const void *search, size_t len /* Test that searching for data on the stack actually works */ static bool test_search(void) { - unsigned char buf[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + char buf[8] = { 's', 'e', 'c', 'r', 'e', 't', '_', '\0' }; - /* Don't let the optimiser elide buf off the stack */ - buf[7] ^= (((size_t)gstack) & 0xff); + /* printf here doesn't let the optimiser elide buf off the stack */ + printf("Testing stack search with %s\n", buf); return in_stack(NULL, buf, sizeof(buf)); } @@ -145,9 +168,15 @@ static void *run_tests(void *passed_stack) /* Due to the nature of the test reading poisoned bytes off the custom stack will trigger ASAN */ ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); - RUN(test_search); + if (!test_search()) { + /* Usually means the optimizer has beaten our efforts to fight it, + * or the compiler doesn't support e.g. no-optimize attributes. In + * both cases the tests below will fail as memcmp alone will leak. + */ + printf("WARNING: clear tests unreliable, skipping\n"); + return NULL; /* Don't fail test runs where the optimizer has won */ + } - /* Due to the nature of the test reading poisoned bytes off the custom stack will trigger ASAN */ ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); RUN(test_bip39); diff --git a/src/internal.c b/src/internal.c index 451fdf4d0..4d3c58217 100644 --- a/src/internal.c +++ b/src/internal.c @@ -428,12 +428,34 @@ void wally_free(void *ptr) _ops.free_fn(ptr); } +#if (defined(__x86_64__) || defined(__i386__)) && !defined(_WIN32) +/* On x86, optimized memcpy in libc can use SSE/AVX registers, + * leaving copied data in registers of up to 512 bits in size. + * In shared library builds if a call though the PLT is made before + * something else overwites these registers, they are spilled to the + * stack and not later cleared. + * Use an unoptimized, byte-by-byte impl to prevent this (we only + * need this for secret/potentially secret data which is usually small). + */ +void wally_memcpy(void *dest, const void *src, size_t n) +{ + for (size_t i = 0; i < n; ++i) { + ((unsigned char*)dest)[i] = ((const unsigned char*)src)[i]; + } +} +#else +void wally_memcpy(void *dest, const void *src, size_t n) +{ + memcpy(dest, src, n); +} +#endif + char *wally_strdup_n(const char *str, size_t str_len) { char *new_str = (char *)wally_malloc(str_len + 1); if (new_str) { if (str_len) { - memcpy(new_str, str, str_len); + wally_memcpy(new_str, str, str_len); } new_str[str_len] = '\0'; } @@ -591,7 +613,7 @@ bool clone_data(void **dst, const void *src, size_t len) } *dst = wally_malloc(len); if (*dst) - memcpy(*dst, src, len); + wally_memcpy(*dst, src, len); return *dst != NULL; } @@ -626,7 +648,7 @@ void *array_realloc(const void *src, size_t old_n, size_t new_n, size_t size) if (!p) return NULL; if (src) - memcpy(p, src, old_n * size); + wally_memcpy(p, src, old_n * size); wally_clear(p + old_n * size, (new_n - old_n) * size); return p; } diff --git a/src/internal.h b/src/internal.h index 2e26e02b0..7a3349920 100644 --- a/src/internal.h +++ b/src/internal.h @@ -24,6 +24,12 @@ #define WALLY_INTERNAL_API #endif +#if defined(__clang__) || defined(__GNUC__) +#define WALLY_NO_OPTIMIZE __attribute__((optimize("O0"))) +#else +#define WALLY_NO_OPTIMIZE +#endif + #include #include #include @@ -95,6 +101,9 @@ WALLY_INTERNAL_API const struct wally_operations *wally_ops(void); #define strdup(ptr) __use_wally_strdup_internally__ #endif +/* Used for copying secret data to avoid leaks via registers */ +WALLY_INTERNAL_API WALLY_NO_OPTIMIZE void wally_memcpy(void *dest, const void *src, size_t n); + #define NUM_ELEMS(a) (sizeof(a) / sizeof(a[0])) /* Validity checking for input parameters */ diff --git a/src/mnemonic.c b/src/mnemonic.c index 0679315c8..67adb5c09 100644 --- a/src/mnemonic.c +++ b/src/mnemonic.c @@ -49,7 +49,7 @@ char *mnemonic_from_bytes(const struct words *w, const unsigned char *bytes, siz size_t idx = extract_index(w->bits, bytes, i); size_t mnemonic_len = strlen(w->indices[idx]); - memcpy(out, w->indices[idx], mnemonic_len); + wally_memcpy(out, w->indices[idx], mnemonic_len); out[mnemonic_len] = ' '; /* separator */ out += mnemonic_len + 1; } diff --git a/src/wif.c b/src/wif.c index 4e487acb1..9f4e23294 100644 --- a/src/wif.c +++ b/src/wif.c @@ -23,7 +23,7 @@ int wally_wif_from_bytes(const unsigned char *priv_key, return WALLY_EINVAL; buf[0] = (unsigned char) prefix & 0xff; - memcpy(&buf[1], priv_key, EC_PRIVATE_KEY_LEN); + wally_memcpy(&buf[1], priv_key, EC_PRIVATE_KEY_LEN); if (flags & WALLY_WIF_FLAG_UNCOMPRESSED) buf_len--; @@ -82,7 +82,7 @@ int wally_wif_to_bytes(const char *wif, return WALLY_EINVAL; /** Incorrect format, prefix does not match or inconsistent flag */ } - memcpy(bytes_out, &buf[1], EC_PRIVATE_KEY_LEN); + wally_memcpy(bytes_out, &buf[1], EC_PRIVATE_KEY_LEN); wally_clear(buf, sizeof(buf)); return WALLY_OK; From 9d05ca17a736bab6016af4ec91792bd3d8967855 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 8 Apr 2026 02:32:40 -0400 Subject: [PATCH 4/7] tests: add more test_clear tests --- src/ctest/test_clear.c | 67 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/ctest/test_clear.c b/src/ctest/test_clear.c index 4a2441e1e..ad3c72324 100644 --- a/src/ctest/test_clear.c +++ b/src/ctest/test_clear.c @@ -8,6 +8,7 @@ #undef free #include #include +#include #include #include #include @@ -157,6 +158,60 @@ static bool test_bip39(void) return true; } +/* Sentinel for non-bip39 secret-handling tests: 32 bytes of 0xa5. + * Distinct from BIP39_SECRET so test_search-style false positives can't + * mask a real leak. */ +static const unsigned char SECRET32[32] = { + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5 +}; + +static bool test_bip32_from_seed(void) +{ + /* Derive an extended key from an all-0xa5 seed. The seed bytes + * pass through libc memcpy when stored into the ext_key struct + * and the HMAC-SHA512 input. */ + struct ext_key key; + if (bip32_key_from_seed(SECRET32, sizeof(SECRET32), + BIP32_VER_MAIN_PRIVATE, 0, &key)) + return false; + return !in_stack("bip32_key_from_seed", SECRET32, sizeof(SECRET32)); +} + +static bool test_ec_private_key_verify(void) +{ + /* The privkey is fed through libc on its way to libsecp. */ + if (wally_ec_private_key_verify(SECRET32, sizeof(SECRET32))) + return false; + return !in_stack("wally_ec_private_key_verify", SECRET32, sizeof(SECRET32)); +} + +static bool test_ec_sig_from_bytes(void) +{ + static unsigned char msg[32] = { 0x11 }; + static unsigned char sig[EC_SIGNATURE_LEN]; + /* Privkey is the secret. */ + if (wally_ec_sig_from_bytes(SECRET32, sizeof(SECRET32), + msg, sizeof(msg), + EC_FLAG_ECDSA, + sig, sizeof(sig))) + return false; + return !in_stack("wally_ec_sig_from_bytes", SECRET32, sizeof(SECRET32)); +} + +static bool test_hmac_sha256(void) +{ + static unsigned char out[32]; + static unsigned char msg[32] = { 0x22 }; + if (wally_hmac_sha256(SECRET32, sizeof(SECRET32), + msg, sizeof(msg), + out, sizeof(out))) + return false; + return !in_stack("wally_hmac_sha256", SECRET32, sizeof(SECRET32)); +} + static void *run_tests(void *passed_stack) { if (passed_stack != gstack) { @@ -180,6 +235,18 @@ static void *run_tests(void *passed_stack) ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); RUN(test_bip39); + ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); + RUN(test_bip32_from_seed); + + ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); + RUN(test_ec_private_key_verify); + + ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); + RUN(test_ec_sig_from_bytes); + + ASAN_UNPOISON_MEMORY_REGION(passed_stack, PTHREAD_STACK_MIN); + RUN(test_hmac_sha256); + return NULL; } From 1009497b39d47eb2baa943cd6f2e388c43b6c9a5 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 9 Jul 2026 22:27:58 +1200 Subject: [PATCH 5/7] ci: re-enable clear tests --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4b6657efd..546b35c4b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ test_asan_ubsan_gcc: - CONFIGURE_ARGS: [ --enable-elements=no, --enable-elements=no --enable-minimal=yes, "", --enable-minimal=yes ] script: - ./tools/cleanup.sh && ./tools/autogen.sh - - CC=gcc CFLAGS="-O2 -fsanitize=address -fsanitize=bounds -fsanitize=undefined -fsanitize=alignment -fsanitize-address-use-after-scope -fno-sanitize-recover=all" ./configure --enable-export-all --enable-swig-python --enable-swig-java $CONFIGURE_ARGS --enable-shared --disable-static --disable-clear-tests --disable-asm + - CC=gcc CFLAGS="-O2 -fsanitize=address -fsanitize=bounds -fsanitize=undefined -fsanitize=alignment -fsanitize-address-use-after-scope -fno-sanitize-recover=all" ./configure --enable-export-all --enable-swig-python --enable-swig-java $CONFIGURE_ARGS --enable-shared --disable-static --disable-asm - sed -i 's/^PYTHON = /PYTHON = LD_PRELOAD=\/usr\/lib\/gcc\/x86_64-linux-gnu\/14\/libasan.so /g' src/Makefile - sed -i 's/^JAVA = /JAVA = LD_PRELOAD=\/usr\/lib\/gcc\/x86_64-linux-gnu\/14\/libasan.so /g' src/Makefile - make -j $(($(grep ^processor /proc/cpuinfo | wc -l) / 2)) @@ -50,7 +50,7 @@ test_scan_build_clang: - CONFIGURE_ARGS: [ --enable-elements=no, --enable-elements=no --enable-minimal=yes, "", --enable-minimal=yes ] script: - ./tools/cleanup.sh && ./tools/autogen.sh - - CC=clang scan-build-19 ./configure --enable-export-all --enable-swig-python --enable-swig-java --disable-clear-tests --disable-asm $CONFIGURE_ARGS + - CC=clang scan-build-19 ./configure --enable-export-all --enable-swig-python --enable-swig-java --disable-asm $CONFIGURE_ARGS - scan-build-19 --keep-cc --exclude src/secp256k1/ --status-bugs --keep-empty -o scan-build-output make -j $(($(grep ^processor /proc/cpuinfo | wc -l) / 2)) artifacts: expire_in: 3 days @@ -93,7 +93,7 @@ test_amalgamation: BUILD_ARGS: [ "", -DBUILD_MINIMAL, -DBUILD_ELEMENTS, -DBUILD_ELEMENTS -DBUILD_MINIMAL ] script: - touch config.h - - $CC $BUILD_ARGS -Wall -W -Wextra -Werror -I. -I./src -I./src/ccan -I./src/secp256k1/include src/ctest/amalgamation_compile_test.c + - $CC $BUILD_ARGS -Wall -W -Wextra -Werror -Wno-unknown-attributes -I. -I./src -I./src/ccan -I./src/secp256k1/include src/ctest/amalgamation_compile_test.c test_mingw_static_build: extends: .test_template @@ -107,7 +107,7 @@ test_no_elements_abi: artifacts: script: - ./tools/cleanup.sh && ./tools/autogen.sh - - CFLAGS='-Wall -W -Wextra -Werror' ./configure --disable-swig-python --disable-swig-java --disable-clear-tests --enable-elements=no --enable-elements-abi=no + - CFLAGS='-Wall -W -Wextra -Werror' ./configure --disable-swig-python --disable-swig-java --enable-elements=no --enable-elements-abi=no - make -j $(($(grep ^processor /proc/cpuinfo | wc -l) / 2)) build_wally_release_files: From d0ac03ba98ae20bdee8124efac1ac0f55306dc31 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 9 Jul 2026 22:30:32 +1200 Subject: [PATCH 6/7] update CHANGES.md for release 1.5.5 --- CHANGES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 35de08639..ce7bbda30 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## Version 1.5.5 + +### Fixed +- De-optimize some memcpy calls on x86 to prevent leaks via extended registers. + ## Version 1.5.4 ### Added From 78499f0048d97b09d3afb9ad31c7a76427075a78 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 9 Jul 2026 22:30:42 +1200 Subject: [PATCH 7/7] Bump version to 1.5.5 --- README.md | 2 +- _CMakeLists.txt | 2 +- configure.ac | 2 +- docs/source/conf.py | 2 +- include/wally_core.h | 4 ++-- setup.py | 2 +- src/wasm_package/package-lock.json | 4 ++-- src/wasm_package/package.json | 2 +- src/wasm_package/src/const.js | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d830eb97b..1b93d1425 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ installed. For non-development use, you can install wally from PyPI with `pip` as follows: ``` -pip install wallycore==1.5.4 +pip install wallycore==1.5.5 ``` For development, you can build and install wally using: diff --git a/_CMakeLists.txt b/_CMakeLists.txt index 978858c52..f12bf9e36 100644 --- a/_CMakeLists.txt +++ b/_CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.18) project( libwallycore - VERSION 1.5.4 + VERSION 1.5.5 DESCRIPTION "A collection of useful primitives for cryptocurrency wallets" LANGUAGES C ) diff --git a/configure.ac b/configure.ac index 7fa0a66d1..ea3d71077 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_PREREQ([2.60]) -AC_INIT([libwallycore],[1.5.4]) +AC_INIT([libwallycore],[1.5.5]) AC_CONFIG_AUX_DIR([tools/build-aux]) AC_CONFIG_MACRO_DIR([tools/build-aux/m4]) AC_CONFIG_SRCDIR([src/mnemonic.h]) diff --git a/docs/source/conf.py b/docs/source/conf.py index 72c7af06b..c30530e61 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -168,7 +168,7 @@ def extract_docs(infile, outfile): # built documents. # # The short X.Y version. -version = u'1.5.4' +version = u'1.5.5' # The full version, including alpha/beta/rc tags. release = version diff --git a/include/wally_core.h b/include/wally_core.h index cb8f7ca9c..e8c5c9897 100644 --- a/include/wally_core.h +++ b/include/wally_core.h @@ -31,8 +31,8 @@ extern "C" { /** Library version */ #define WALLY_MAJOR_VER 1 #define WALLY_MINOR_VER 5 -#define WALLY_PATCH_VER 4 -#define WALLY_BUILD_VER 0x10504 +#define WALLY_PATCH_VER 5 +#define WALLY_BUILD_VER 0x10505 /** * Initialize wally. diff --git a/setup.py b/setup.py index b76e8cc85..016fbdf34 100644 --- a/setup.py +++ b/setup.py @@ -172,7 +172,7 @@ def _call(args, cwd=ABS_PATH): kwargs = { 'name': 'wallycore', - 'version': '1.5.4', + 'version': '1.5.5', 'description': 'libwally Bitcoin library', 'long_description': 'Python bindings for the libwally Bitcoin library', 'url': 'https://github.com/ElementsProject/libwally-core', diff --git a/src/wasm_package/package-lock.json b/src/wasm_package/package-lock.json index 286974f1d..491fd8300 100644 --- a/src/wasm_package/package-lock.json +++ b/src/wasm_package/package-lock.json @@ -1,12 +1,12 @@ { "name": "wallycore", - "version": "1.5.4", + "version": "1.5.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wallycore", - "version": "1.5.4", + "version": "1.5.5", "license": "(MIT or BSD)", "devDependencies": { "buffer": "^6.0.3", diff --git a/src/wasm_package/package.json b/src/wasm_package/package.json index 8d9acb769..06b1a54b1 100644 --- a/src/wasm_package/package.json +++ b/src/wasm_package/package.json @@ -1,6 +1,6 @@ { "name": "wallycore", - "version": "1.5.4", + "version": "1.5.5", "description": "JavaScript bindings for libwally", "main": "src/index.js", "type": "module", diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index ead3617c4..6ea43fccd 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -109,7 +109,7 @@ export const WALLY_ADDRESS_VERSION_WIF_TESTNET = 0xEF; /** Wallet Import Format export const WALLY_BIP32_CHAIN_CODE_LEN = 32; export const WALLY_BIP32_TWEAK_SUM_LEN = 32; export const WALLY_BTC_MAX = 21000000; -export const WALLY_BUILD_VER = 0x10504; +export const WALLY_BUILD_VER = 0x10505; export const WALLY_CA_PREFIX_LIQUID = 0x0c; /** Liquid v1 confidential address prefix */ export const WALLY_CA_PREFIX_LIQUID_REGTEST = 0x04; /** Liquid v1 confidential address prefix for regtest */ export const WALLY_CA_PREFIX_LIQUID_TESTNET = 0x17; /** Liquid v1 confidential address prefix for testnet */ @@ -153,7 +153,7 @@ export const WALLY_NETWORK_LIQUID_TESTNET = 0x05; /** Liquid v1 testnet */ export const WALLY_NETWORK_NONE = 0x00; /** Used for miniscript parsing only */ export const WALLY_NO_CODESEPARATOR = 0xffffffff; /* No BIP342 code separator position */ export const WALLY_OK = 0; /** Success */ -export const WALLY_PATCH_VER = 4; +export const WALLY_PATCH_VER = 5; export const WALLY_PSBT_COMBINE_SIGS = 0x1; /* Combine the signatures from a signature-only PSBT */ export const WALLY_PSBT_EXTRACT_FINAL = 0x0; /* Extract a final transaction; fail if any inputs aren't finalized */ export const WALLY_PSBT_EXTRACT_NON_FINAL = 0x1; /* Extract without any final scriptsig and witness */