internal: avoid potential info leaks via sse/avx registers
What changed, and why it matters
This commit fixes a potential information leak in a cryptocurrency wallet library. When the library is built as a shared library on x86/x86_64 systems, optimized memory-copy functions can leave secret data (private keys, seed phrases, chain codes) inside special CPU registers (SSE/AVX). If the program then makes a call through the dynamic linker before those registers are reused, the CPU spills the register contents onto the stack, where they may remain uncleared. The patch replaces many ordinary memory copies with a simple, unoptimized byte-by-byte copy function for sensitive data, preventing the data from being left in those registers. The commit message explicitly calls this an 'info leak' and thanks an external contributor for finding the cause and suggesting the fix.
Users building libwally-core as a shared library on x86/x86_64 Linux should upgrade to a revision containing this commit. Developers should ensure wally_memcpy is used for any new code paths that copy secret material, and should run the test_clear regression test in shared-library builds to confirm the leak is mitigated. Consider also auditing other cryptographic libraries in the same process for similar register-spill issues.
Security signals we found
Information disclosure via CPU register spill to stack
Shared-library/PLT-specific side channel
Use of SSE/AVX registers by optimized libc memcpy
Replacement of memcpy with unoptimized wally_memcpy for secret data
Hardening of BIP32 private key and chain code handling
Hardening of BIP39 mnemonic/seed handling
Hardening of WIF private key import/export
Updated test_clear regression test to detect stack leaks
Evidence from the diff
The patch introduces wally_memcpy(), a WALLY_NO_OPTIMIZE byte-by-byte copy routine used on x86/x86_64 non-Windows builds, and replaces memcpy() calls that handle secret or potentially secret material in src/aes.c, src/bip32.c, src/bip39.c, src/internal.c, src/mnemonic.c, and src/wif.c. The rationale, stated in src/internal.c, is that glibc’s optimized memcpy can use SSE/AVX registers (up to 512 bits). In shared-library builds, a subsequent PLT call can spill those registers to the stack before they are overwritten, and the spilled secret bytes are not later cleared. The test_clear test is updated to use a non-optimized memcmp and to detect such stack leaks more reliably. The commit does not change public APIs or behavior; it is a defensive hardening measure against side-channel information disclosure.
Changed components
src/internal.csrc/internal.hsrc/aes.csrc/bip32.csrc/bip39.csrc/mnemonic.csrc/wif.csrc/ctest/test_clear.csrc/Makefile.amsrc/ctest/_CMakeLists.txtInspect captured patch +96 / −36
diff --git a/src/Makefile.am b/src/Makefile.am
index 977593a..150f7a5 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 f4ea03e..291946f 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 b39a3b8..a52a7c8 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 73d3045..9fdc2d0 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 a8214bd..2b0a9e8 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} $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/src> $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/src/secp256k1/include>)
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 0432396..4a2441e 100644
--- a/src/ctest/test_clear.c
+++ b/src/ctest/test_clear.c
@@ -3,6 +3,9 @@
#ifdef HAVE_ASM_PAGE_H
# include <asm/page.h>
#endif
+#include "internal.h"
+#undef malloc
+#undef free
#include <wally_bip32.h>
#include <wally_bip39.h>
#include <pthread.h>
@@ -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 451fdf4..4d3c582 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 2e26e02..7a33499 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 <secp256k1.h>
#include <secp256k1_recovery.h>
#include <secp256k1_extrakeys.h>
@@ -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 0679315..67adb5c 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 4e487ac..9f4e232 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;
Why this scored 68/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.