-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Instances of operations on uninitialized values #753
Comments
That's an interesting find. I'm curious how you found this.
To be fair, all these values have its address taken and from types guaranteed no to have trap representations, so (ignoring differences between C versions) these are unspecified values, and not immediate UB. But yes, if the result of the cmov is unspecified, that's not much better. This means that the result of the sign function is unspecified. Currently compilers don't exploit this, so this is not a vulnerability, but it should be fixed.
This seems sensible to me. It's certainly better than exposing unspecified values to the caller or foregoing constant-time code. If we want to make sure that valgrind will complain about uninitialized values in 1), we could alternatively initialize to zero but then call VALGRIND_MAKE_MEM_UNDEFINED, and keep the cmov.
Well, why not. Depending on your abstraction, it's unexpected that cmov functions read the target. I guess this was the reason why this was not caught by code review. We should also add a note to the docs of the cmov functions. |
Wow, great find @elichai! Did you find these by fuzzing under MSan? Very exciting to follow your great work! |
Note that the logic for ECMULT_CONST_TABLE_GET_GE would work fine still if the first loop iteration was an unconditional copy. The m == idx_n can be ignored because either the first value is the correct one, or else it will be replaced by the correct one in a later iteration. I suppose it should be verified that the table size is > 0, and that the index is definitely in bounds. |
Thanks. |
Mostly because it has overhead https://godbolt.org/z/MSTRKM. (scroll down to main) But also this interacts badly with |
Ah I see. It's on my list to try to new sanitizer in gcc 10. It has landed in Arch Linux now, so I could give it a try. Adding |
Oh, I thought about adding it under VALGRIND, but yeah under VERIFY probably makes more sense.
It trips valgrind in the cmov, although putting it under VERIFY solves this. I'll open a PR with those in separate commits so I can drop that if there's any resistance |
Any ideas why this wasn't caught by the existing tests run under valgrind, such as Line 5141 in 05d315a
|
@jonasnick I think the answer is just that Valgrind can only detect bugs that are present in the compiled code. x86 machine code does not have a requirement of initializing a register before you can use it, which a compiler can exploit. Specifically, I think Valgrind keeps track of definedness per bit of every register and memory byte, but will treat e.g. (x & 0) as 0, even when (bits of) x are undefined. That makes sense because at the machine code level, whatever value x has, the output will always be a well-defined 0. In other words: the fact that the Valgrind test does not detect this is evidence that (on the platform the Valgrind test is compiled for), this code does not result in non-constant time effects (but doesn't guarantee it's correct, of course). |
See https://valgrind.org/docs/manual/mc-manual.html#mc-manual.uninitvals "A complaint is issued only when your program attempts to make use of uninitialised data in a way that might affect your program's externally-visible behaviour." And as @sipa points out correctly, "uninit & 0" is always "0" for the compiled code that runs your real machine, but not the abstract C "machine" that we have in mind when talking about the semantics of C. |
Hi,
Currently we have 2 instances of operations on uninitialized values, in the cmov implementation.
all the
_cmov
implementations read from the resulting variable(ier->n[0] & mask0
) so ifr
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:secp256k1/src/secp256k1.c
Lines 517 to 518 in 41fc785
with both
r
ands
being unitialized.Instance 2:
In ecdh, we pass
&tmpa
here:secp256k1/src/ecmult_const_impl.h
Line 186 in 41fc785
ECMULT_CONST_TABLE_GET_GE
in an uninit state, which become ther
value in the macro.Then in:
secp256k1/src/ecmult_const_impl.h
Lines 31 to 32 in 41fc785
we cmov into
&(r)->x
which will perform operations on uninit values.notice that under
VERIFY
we actually do clear those before:secp256k1/src/ecmult_const_impl.h
Lines 26 to 27 in 41fc785
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/
The text was updated successfully, but these errors were encountered: