Hi,
Currently we have 2 instances of operations on uninitialized values, in the cmov implementation.
all the _cmov
implementations read from the resulting variable(ie r->n[0] & mask0
) so if r
points to uninitialized values this is undefined behavior [1] [2]
Unlike the memcpy
debate, actual user operations and conditions on uninit values are assumed to not happen in the compiler and are used for optimizations (especially in clang)
Instance 1:
In ecdsa_sign, if noncefp
returns 0 then we get to:
https://github.com/bitcoin-core/secp256k1/blob/41fc78560223aa035d9fe2cbeedb3ee632c740e2/src/secp256k1.c#L517-L518
with both r
and s
being unitialized.
Instance 2:
In ecdh, we pass &tmpa
here: https://github.com/bitcoin-core/secp256k1/blob/41fc78560223aa035d9fe2cbeedb3ee632c740e2/src/ecmult_const_impl.h#L186 to the ECMULT_CONST_TABLE_GET_GE
in an uninit state, which become the r
value in the macro.
Then in:
https://github.com/bitcoin-core/secp256k1/blob/41fc78560223aa035d9fe2cbeedb3ee632c740e2/src/ecmult_const_impl.h#L31-L32
we cmov into &(r)->x
which will perform operations on uninit values.
notice that under VERIFY
we actually do clear those before:
https://github.com/bitcoin-core/secp256k1/blob/41fc78560223aa035d9fe2cbeedb3ee632c740e2/src/ecmult_const_impl.h#L26-L27
Possible solutions: The only solution I can currently think of is default initializing those arguments.
Mitigations:
We could add VALGRIND_CHECK_MEM_IS_DEFINED(r, sizeof(*r));
to all the cmovs but we only enable valgrind(-DVALGRIND
) under tests, which use the VERIFY macro too, so that will not catch the second case.
[1] http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_451.htm [2] https://markshroyer.com/2012/06/c-both-true-and-false/