Skip to content

bip39: scrub vector registers after mnemonic decoding#534

Open
odudex wants to merge 1 commit into
ElementsProject:masterfrom
odudex:bip39-scrub-pr
Open

bip39: scrub vector registers after mnemonic decoding#534
odudex wants to merge 1 commit into
ElementsProject:masterfrom
odudex:bip39-scrub-pr

Conversation

@odudex

@odudex odudex commented Jun 30, 2026

Copy link
Copy Markdown

During the work proposed on #533 I separated this out of scope, yet likely relevant commit from @pythcoiner, so it can be evaluated individually:

"test_clear finds plaintext secret material ~11KB deep on the stack after bip39_mnemonic_validate returns. The residue comes from XMM registers xsaved by the dynamic linker on subsequent PLT calls, not from a source-level local buffer. Adding a zeroed stack scratch in bip39_mnemonic_to_bytes forces glibc memset to issue SSE/AVX zero stores that clobber the vector registers before the function returns, so later xsave operations no longer spill secret bytes."

test_clear finds plaintext secret material ~11KB deep on the stack
after bip39_mnemonic_validate returns. The residue comes from XMM
registers xsaved by the dynamic linker on subsequent PLT calls, not
from a source-level local buffer. Adding a zeroed stack scratch in
bip39_mnemonic_to_bytes forces glibc memset to issue SSE/AVX zero
stores that clobber the vector registers before the function
returns, so later xsave operations no longer spill secret bytes.
@jgriffiths

jgriffiths commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

OK, I'm looking at this and having some trouble verifying it.

Running locally, I see the same leak after bip39_mnemonic_validate, but although the reasoning given in the comment looks reasonable, this patch doesn't fix it or change the behaviour in any way. I've always assumed the leak is due to the optimizer seeing though the calls and eliding them (despite the asm constraints telling it not to, the compiler can see that the cleared result is never used in the test code).

I added __attribute__((always_inline)) to ensure the function is always inlined at the call site, and additionally added a call to it in wally_clear since that's always called after copying secret data around. I then make wally_clear an explicitly hidden symbol so that calls to it don't go through the PLT (i.e. nothing can be spilled via the mechanism discussed). Still no dice - the test fails in exactly the same way.

Did this change resolve test_clear failing on your machine?

@pythcoiner

Copy link
Copy Markdown

yes it do on 2 different machines:

~/c/libwally-core master *1 ❯ make -C src test_clear    
make: Entering directory '/home/pyth/c/libwally-core/src'
cd .. && make  am--refresh
[...]
  CCLD     libwallycore.la
  CCLD     test_clear
make: Leaving directory '/home/pyth/c/libwally-core/src'
~/c/libwally-core master *1 ❯ ./src/test_clear
Found bip39_mnemonic_validate secret at stack position 10912 and base 0x56291be39010
7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 7f, 
test_bip39 wally_clear() test failed!
~/c/libwally-core master *1 ❯ log 1
00013739 build: bump dso version for 1.5.3 (+2 -2)
~/c/libwally-core master *1 ❯ git checkout pr534  
M       src/secp256k1
Switched to branch 'pr534'
~/c/libwally-core pr534 *1 !1 ❯ make -C src test_clear
make: Entering directory '/home/pyth/c/libwally-core/src'
cd .. && make  am--refresh
[...]
  CCLD     libwallycore.la
  CCLD     test_clear
make: Leaving directory '/home/pyth/c/libwally-core/src'
~/c/libwally-core pr534 *1 !1 ❯ ./src/test_clear
~/c/libwally-core pr534 *1 !1 ❯ log 1                 
8b8f67bf bip39: scrub vector registers after mnemonic decoding (+123 -0)

@jgriffiths

Copy link
Copy Markdown
Contributor

Hmm, what is your full build line (configure args and initial make)?

@pythcoiner

pythcoiner commented Jul 8, 2026

Copy link
Copy Markdown
Details
~/c/libwally-core pr534 *1 !1 ❯ ./tools/autogen.sh            
libtoolize: putting auxiliary files in AC_CONFIG_AUX_DIR, 'tools/build-aux'.
libtoolize: copying file 'tools/build-aux/ltmain.sh'
libtoolize: putting macros in AC_CONFIG_MACRO_DIRS, 'tools/build-aux/m4'.
libtoolize: copying file 'tools/build-aux/m4/libtool.m4'
libtoolize: copying file 'tools/build-aux/m4/ltoptions.m4'
libtoolize: copying file 'tools/build-aux/m4/ltsugar.m4'
libtoolize: copying file 'tools/build-aux/m4/ltversion.m4'
libtoolize: copying file 'tools/build-aux/m4/lt~obsolete.m4'
configure.ac:38: installing 'tools/build-aux/compile'
configure.ac:34: installing 'tools/build-aux/missing'
fuzz/Makefile.am: installing 'tools/build-aux/depcomp'
libtoolize: putting auxiliary files in AC_CONFIG_AUX_DIR, 'build-aux'.
libtoolize: copying file 'build-aux/ltmain.sh'
libtoolize: putting macros in AC_CONFIG_MACRO_DIRS, 'build-aux/m4'.
libtoolize: copying file 'build-aux/m4/libtool.m4'
libtoolize: copying file 'build-aux/m4/ltoptions.m4'
libtoolize: copying file 'build-aux/m4/ltsugar.m4'
libtoolize: copying file 'build-aux/m4/ltversion.m4'
libtoolize: copying file 'build-aux/m4/lt~obsolete.m4'
configure.ac:37: installing 'build-aux/compile'
configure.ac:27: installing 'build-aux/missing'
Makefile.am: installing 'build-aux/depcomp'
~/c/libwally-core pr534 *1 !1 ❯ ./configure --enable-elements                                                                               7s
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking for a BSD-compatible install... /usr/bin/install -c
checking whether sleep supports fractional seconds... yes
checking filesystem timestamp resolution... 0.01
checking whether build environment is sane... yes
checking for a race-free mkdir -p... /usr/bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking xargs -n works... yes
checking whether UID '1000' is supported by ustar format... yes
checking whether GID '1000' is supported by ustar format... yes
checking how to create a ustar tar archive... gnutar
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables... 
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether the compiler supports GNU C... yes
checking whether gcc accepts -g... yes
checking for gcc option to enable C23 features... none needed
checking whether gcc understands -c and -o together... yes
checking whether make supports the include directive... yes (GNU style)
checking dependency style of gcc... gcc3
checking for ar... ar
checking the archiver (ar) interface... ar
checking how to print strings... printf
checking for a sed that does not truncate output... /usr/bin/sed
checking for grep that handles long lines and -e... /usr/bin/grep
checking for egrep... /usr/bin/grep -E
checking for fgrep... /usr/bin/grep -F
checking for ld used by gcc... /usr/bin/ld
checking if the linker (/usr/bin/ld) is GNU ld... yes
checking for BSD- or MS-compatible name lister (nm)... /usr/bin/nm -B
checking the name lister (/usr/bin/nm -B) interface... BSD nm
checking whether ln -s works... yes
checking the maximum length of command line arguments... 1572864
checking how to convert x86_64-pc-linux-gnu file names to x86_64-pc-linux-gnu format... func_convert_file_noop
checking how to convert x86_64-pc-linux-gnu file names to toolchain format... func_convert_file_noop
checking for /usr/bin/ld option to reload object files... -r
checking for file... file
checking for objdump... objdump
checking how to recognize dependent libraries... pass_all
checking for dlltool... no
checking how to associate runtime and link libraries... printf %s\n
checking for ranlib... ranlib
checking for archiver @FILE support... @
checking for strip... strip
checking command to parse /usr/bin/nm -B output from gcc object... ok
checking for sysroot... no
checking for a working dd... /usr/bin/dd
checking how to truncate binary pipes... /usr/bin/dd bs=4096 count=1
checking for mt... no
checking if : is a manifest tool... no
checking for stdio.h... yes
checking for stdlib.h... yes
checking for string.h... yes
checking for inttypes.h... yes
checking for stdint.h... yes
checking for strings.h... yes
checking for sys/stat.h... yes
checking for sys/types.h... yes
checking for unistd.h... yes
checking for dlfcn.h... yes
checking which variant of shared library versioning to provide... (default) aix
checking for objdir... .libs
checking if gcc supports -fno-rtti -fno-exceptions... no
checking for gcc option to produce PIC... -fPIC -DPIC
checking if gcc PIC flag -fPIC -DPIC works... yes
checking if gcc static flag -static works... yes
checking if gcc supports -c -o file.o... yes
checking if gcc supports -c -o file.o... (cached) yes
checking whether the gcc linker (/usr/bin/ld -m elf_x86_64) supports shared libraries... yes
checking whether -lc should be explicitly linked in... no
checking dynamic linker characteristics... GNU/Linux ld.so
checking how to hardcode library paths into programs... immediate
checking whether stripping libraries is possible... yes
checking if libtool supports shared libraries... yes
checking whether to build shared libraries... yes
checking whether to build static libraries... no
checking for gsed... no
checking whether byte ordering is bigendian... no
checking for inline... inline
checking for size_t... yes
checking for uint64_t... yes
checking for uint32_t... yes
checking for uint16_t... yes
checking for uint8_t... yes
checking whether C compiler accepts -O0... yes
checking whether C compiler accepts -O2... yes
checking for _FORTIFY_SOURCE defined... no
checking whether C compiler accepts -D_FORTIFY_SOURCE=2... yes
checking whether C compiler accepts -fstack-protector-strong... yes
checking whether C compiler accepts -DNDEBUG=1... yes
checking whether the linker accepts -O2... yes
checking whether the linker accepts -Wl,-z,relro... yes
checking whether C compiler accepts -DBUILD_ELEMENTS=1... yes
checking whether C compiler accepts -flax-vector-conversions... yes
checking whether C compiler accepts -fno-strict-aliasing... yes
checking whether C compiler accepts -fno-builtin... yes
checking whether C compiler accepts -Wformat-nonliteral... yes
checking whether C compiler accepts -Wformat-security... yes
checking whether C compiler accepts -Wformat... yes
checking whether C compiler accepts -Wstrict-prototypes... yes
checking whether C compiler accepts -Wshadow... yes
checking whether C compiler accepts -Wnested-externs... yes
checking whether C compiler accepts -Wcast-align... yes
checking whether C compiler accepts -Wpedantic... yes
checking whether C compiler accepts -Wextra... yes
checking whether C compiler accepts -Wall... yes
checking whether C compiler accepts -D_DEFAULT_SOURCE=1... yes
checking whether C compiler accepts -Wno-unused-function... yes
checking whether C compiler accepts -Wno-long-long... yes
checking whether C compiler accepts -Wno-overlength-strings... yes
checking whether C compiler accepts -Wno-variadic-macros... yes
checking whether C compiler accepts -Wno-unused-parameter... yes
checking whether C compiler accepts -Wno-shadow... yes
checking whether C compiler accepts -Wno-pedantic... yes
checking whether C compiler accepts -Wno-missing-field-initializers... yes
checking whether C compiler accepts -fvisibility=hidden... yes
checking for memset_s... no
checking for explicit_bzero... yes
checking for explicit_memset... no
checking whether we can use inline asm code... yes
checking for byteswap.h... yes
checking for sys/mman.h... yes
checking how to run the C preprocessor... gcc -E
checking for egrep -e... (cached) /usr/bin/grep -E
checking whether gcc is Clang... no
checking whether pthreads work with "-pthread" and "-lpthread"... yes
checking for joinable pthread attribute... PTHREAD_CREATE_JOINABLE
checking whether more special flags are required for pthreads... no
checking for PTHREAD_PRIO_INHERIT... yes
checking for asm/page.h... no
checking for pkg-config... /usr/bin/pkg-config
checking pkg-config is at least version 0.9.0... yes
checking for python... /usr/bin/python
checking for a version of Python >= '2.1.0'... yes
checking for a version of Python >= '3.9.0'... yes
checking for the sysconfig Python package... yes
checking for Python include path... -I/usr/include/python3.14
checking for Python library path... -L/usr/lib -lpython3.14
checking for Python site-packages path... /usr/local/lib/python3.14/site-packages
checking for Python platform specific site-packages path... /usr/local/lib/python3.14/site-packages
checking python extra libraries... -ldl -lm
checking python extra linking flags... -Xlinker -export-dynamic
checking consistency of all components of python development environment... yes
checking for swig... no
configure: WARNING: cannot find 'swig' program. You should look at http://www.swig.org
checking for java... no
checking for javac... no
checking for jar... no
checking that generated files are newer than configure... done
=== configuring in src/secp256k1 (/home/pyth/c/libwally-core/src/secp256k1)
configure: running /bin/sh ./configure '--prefix=/usr/local' --disable-option-checking  '--enable-elements' '--disable-shared' '--enable-static' '--with-pic' '--enable-experimental' '--enable-module-ecdh' '--enable-module-recovery' '--enable-module-extrakeys' '--enable-module-schnorrsig' '--enable-module-generator' '--enable-module-rangeproof' '--enable-module-surjectionproof' '--enable-module-whitelist' '--enable-module-ecdsa-s2c' '--disable-tests' '--enable-exhaustive-tests=no' '--enable-benchmark=no' '--disable-dependency-tracking' '--with-asm=auto' '--srcdir=.' --cache-file=/dev/null
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking for a BSD-compatible install... /usr/bin/install -c
checking whether sleep supports fractional seconds... yes
checking filesystem timestamp resolution... 0.01
checking whether build environment is sane... yes
checking for a race-free mkdir -p... /usr/bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking xargs -n works... yes
checking whether UID '1000' is supported by ustar format... yes
checking whether GID '1000' is supported by ustar format... yes
checking how to create a ustar tar archive... gnutar
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables... 
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether the compiler supports GNU C... yes
checking whether gcc accepts -g... yes
checking for gcc option to enable C23 features... none needed
checking whether gcc understands -c and -o together... yes
checking whether make supports the include directive... yes (GNU style)
checking dependency style of gcc... none
checking dependency style of gcc... none
checking the archiver (ar) interface... ar
checking how to print strings... printf
checking for a sed that does not truncate output... /usr/bin/sed
checking for grep that handles long lines and -e... /usr/bin/grep
checking for egrep... /usr/bin/grep -E
checking for fgrep... /usr/bin/grep -F
checking for ld used by gcc... /usr/bin/ld -m elf_x86_64
checking if the linker (/usr/bin/ld -m elf_x86_64) is GNU ld... yes
checking for BSD- or MS-compatible name lister (nm)... /usr/bin/nm -B
checking the name lister (/usr/bin/nm -B) interface... BSD nm
checking whether ln -s works... yes
checking the maximum length of command line arguments... 1572864
checking how to convert x86_64-pc-linux-gnu file names to x86_64-pc-linux-gnu format... func_convert_file_noop
checking how to convert x86_64-pc-linux-gnu file names to toolchain format... func_convert_file_noop
checking for /usr/bin/ld -m elf_x86_64 option to reload object files... -r
checking for file... file
checking for objdump... objdump
checking how to recognize dependent libraries... pass_all
checking for dlltool... no
checking how to associate runtime and link libraries... printf %s\n
checking for ranlib... ranlib
checking for archiver @FILE support... @
checking for strip... strip
checking command to parse /usr/bin/nm -B output from gcc object... ok
checking for sysroot... no
checking for a working dd... /usr/bin/dd
checking how to truncate binary pipes... /usr/bin/dd bs=4096 count=1
checking for mt... no
checking if : is a manifest tool... no
checking for stdio.h... yes
checking for stdlib.h... yes
checking for string.h... yes
checking for inttypes.h... yes
checking for stdint.h... yes
checking for strings.h... yes
checking for sys/stat.h... yes
checking for sys/types.h... yes
checking for unistd.h... yes
checking for dlfcn.h... yes
checking which variant of shared library versioning to provide... (default) aix
checking for objdir... .libs
checking if gcc supports -fno-rtti -fno-exceptions... no
checking for gcc option to produce PIC... -fPIC -DPIC
checking if gcc PIC flag -fPIC -DPIC works... yes
checking if gcc static flag -static works... yes
checking if gcc supports -c -o file.o... yes
checking if gcc supports -c -o file.o... (cached) yes
checking whether the gcc linker (/usr/bin/ld -m elf_x86_64 -m elf_x86_64) supports shared libraries... yes
checking dynamic linker characteristics... GNU/Linux ld.so
checking how to hardcode library paths into programs... immediate
checking whether stripping libraries is possible... yes
checking if libtool supports shared libraries... yes
checking whether to build shared libraries... no
checking whether to build static libraries... yes
checking if gcc supports -Werror... yes
checking if gcc supports -std=c89 -pedantic -Wno-long-long -Wnested-externs -Wshadow -Wstrict-prototypes -Wundef... yes
checking if gcc supports -Wno-overlength-strings... yes
checking if gcc supports -Wall... yes
checking if gcc supports -Wno-unused-function... yes
checking if gcc supports -Wextra... yes
checking if gcc supports -Wcast-align... yes
checking if gcc supports -Wcast-align=strict... yes
checking if gcc supports -Wconditional-uninitialized... no
checking if gcc supports -Wreserved-identifier... no
checking if gcc supports -fvisibility=hidden... yes
checking for valgrind support... yes
checking for __builtin_popcount... no
checking for  __builtin_clzll... no
checking for x86_64 assembly availability... yes
configure: ******
configure: WARNING: experimental build
configure: Experimental features do not have stable APIs or properties, and may not be safe for production use.
configure: ******
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating libsecp256k1.pc
config.status: executing depfiles commands
config.status: executing libtool commands

Build Options:
  with external callbacks = no
  with benchmarks         = no
  with tests              = no
  with ctime tests        = yes
  with coverage           = no
  with examples           = no
  module ecdh             = yes
  module recovery         = yes
  module extrakeys        = yes
  module schnorrsig       = yes
  module ellswift         = yes
  module generator        = yes
  module rangeproof       = yes
  module surjectionproof  = yes
  module whitelist        = yes
  module musig            = no
  module ecdsa-s2c        = yes
  module ecdsa-adaptor    = no
  module bppp             = no
  module schnorrsig-halfagg = no

  asm                       = x86_64
  ecmult window size        = 15
  ecmult gen prec. bits     = 4

  valgrind                  = yes
  CC                        = gcc
  CPPFLAGS                  = 
  SECP_CFLAGS               = -O2  -std=c89 -pedantic -Wno-long-long -Wnested-externs -Wshadow -Wstrict-prototypes -Wundef -Wno-overlength-strings -Wall -Wno-unused-function -Wextra -Wcast-align -Wcast-align=strict -fvisibility=hidden 
  CFLAGS                    = -g -O2
  LDFLAGS                   = -O2  -Wl,-z,relro
configure: creating ./config.status
config.status: creating Makefile
config.status: creating src/Makefile
config.status: creating src/wallycore.pc
config.status: creating src/config.h
config.status: src/config.h is unchanged
config.status: executing depfiles commands
config.status: executing libtool commands
~/c/libwally-core pr534 *1 !1 ❯ make -C src test_clear                                                                                      6s
make: Entering directory '/home/pyth/c/libwally-core/src'
make: Leaving directory '/home/pyth/c/libwally-core/src'
~/c/libwally-core pr534 *1 !1 ❯ ./src/test_clear             
~/c/libwally-core pr534 *1 !1 ❯

@jgriffiths

jgriffiths commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

OK so there are a few things going on here:

  1. Internal functions are going through the PLT for shared builds which causes internal register spills
  2. My box supports AVX512 which saves registers that this patch doesn't address
  3. AVX (and variants) are only set if explicitly targeting a given platform when compiling via CFLAGS, but
  4. The library can be compiled without targeting, and then the glibc provided functions are 'optimized' to use whatever extended register set results in the fastest performance - leaving data in registers that will spill, and
  5. You can't build a generic x86_64 binary that clears these registers since you can't guarantee that the box it ends up running on has them.

Note that this issue disappears entirely if static linking, only dynamic linking is affected.

I've addressed (1), I suspect for the remainder the answer is probably to replace memcpy/cmp/set with unoptimizable variants that prevent the "optimized" libc versions from being called, and therefore don't require clearing the registers at the end :(

@jgriffiths

Copy link
Copy Markdown
Contributor

@pythcoiner @odudex Can you please try my attempt at a fix https://github.com/ElementsProject/libwally-core/tree/multi_updates and let me know if this allows test_clear to pass for you? Thanks

@odudex

odudex commented Jul 9, 2026

Copy link
Copy Markdown
Author

@pythcoiner @odudex Can you please try my attempt at a fix https://github.com/ElementsProject/libwally-core/tree/multi_updates and let me know if this allows test_clear to pass for you? Thanks

I can confirm all tests, python and C, including test_clear are green for me in branch multi_updates @ 78499f0

@pythcoiner

Copy link
Copy Markdown

@jgriffiths I can confirm test_clear passes at 78499f00 on my machine, and dropping 91b3b6d6 make the test fail as expected

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.

3 participants