The library currently tries to avoid leaking sensitive data (secret keys, nonces etc.) in memory by individually clearing relevant variables and buffers on the stack, with a special cleanse function that avoids being optimized out by the compiler (first proposed in #448, later revived in #636 and #1579). Doing this consistently without forgetting instances for non-obvious derived values is relatively hard and takes a lot of developer and maintenance time, as an increasing number of PRs show, e.g.: #1967, #1966, #1934, #1933, #1885, #1861, #1849, #1731...
Moreover, it's still unclear how deep down we should really go, see the IRC discussion from 2026-07-28 (+2026-07-29):
23:26 < real_or_random> There's a lot of developer and maintenance work going into the stack cleaning stuff
23:29 < real_or_random> And I wonder where we should stop. It seems like a good idea to clean entire secret keys, but when we go deeper, that's a huge rabbit hole. ecmult_gen and ecmult_const boil down to adding ges, which boils down to doing operations on fes. And I don't think we want to clear every local variable in the fe code. Even if we manage to do so, at some point it will hurt performance.
23:31 < real_or_random> So there's some concern that if an attacker sees these internal temporary values, they can reconstruct the full secret key anyway.
23:32 < real_or_random> On the other hand: The deeper you go, the more you find loop. And these loops / repeated function calls leave only the data from the last iteration on the stack. Moreover, the deeper you go, maybe the more likely is that variables will end up in registers. But seems only a very rough rule.
23:34 < real_or_random> So there's hope that this entire business of cleaning the stuff is not useless. But it would be good to have a rule of thumb that tells us how deep we want to go.
23:36 < real_or_random> We could, of course, play the attacker and look at the stack. But that's a ton of work. ^^
23:37 < real_or_random> I wonder what others think about this.
10:05 < theStack> concept ack on defining a rule of thumb / stopping point when it comes to stack clearing of values that depend on secrets. looking e.g. at [#1885](/bitcoin-core-secp256k1/1885/), i feel the first commit is clearly within the stopping point, while for the second commit (clearing values that only allow partial reconstruction of the secret) i'm a bit less convinced
A simple alternative to manual clearing could be to simply clear all of the stack memory that an API function used before returning, by a construct like the following:
#define SECP256K1_MAX_STACK_USAGE 16384
SECP256K1_NOINLINE void secp256k1_stack_scrub() {
unsigned char stack[SECP256K1_MAX_STACK_USAGE];
secp256k1_memclear_explicit(stack, sizeof(stack));
}
int secp256k1_some_api_function_internal(...) {
/* actual logic */
}
int secp256k1_some_api_function(...) {
int ret = secp256k1_some_api_function_internal(...);
secp256k1_stack_scrub();
return ret;
}
This seems attractive at first sight if it worked, as we wouldn't have to answer the "where to stop" question in the first place and ideally never have to deal with internal memory clearing again. Open questions though:
- Does this work consistently for all platforms and compilers (that we care), can we rely on the assumption that stack memory is contiguous?
- Is the performance penalty acceptable? (my gut feeling here would be yes, as I'd assume memory clearing even of large regions should be orders of magnitudes faster than the point multiplication(s) usually happening in the relevant functions; to be benchmarked)
- Can we determine a maximum stack usage constant value that is safe across all platforms/compilers/OSes (see also issue #188)
- Is it okay to rely on the
noinlineattribute (without that, the approach just doesn't work AFAIU), which is not part of the C standard
I'm not actively proposing this approach yet (even if the answer to all of the questions above is "yes", it has a kind of hacky vibe), but I find it interesting and potentially attractive enough to be discussed.
Thanks to Rodrigo Gomes and Tiago Oliveira for making us aware of this approach. The idea is certainly not new, e.g. libsodium has an utility function sodium_stackzero, though its not used internally. Note that GCC has a special stack scrubbing feature, that I didn't look closer yet, but it seems to need cooperation between all involved functions, and relying on a feature that only a single compiler offers wouldn't be sufficient anyways.