-
Notifications
You must be signed in to change notification settings - Fork 553
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
Test: Run a self-test for our valgrind setup #4182
Test: Run a self-test for our valgrind setup #4182
Conversation
// This mimicks what went wrong in Kyber's secret message expansion | ||
// that was found by PQShield in Kyber's reference implementation and | ||
// was fixed in https://github.com/randombit/botan/pull/4107. | ||
// | ||
// Certain versions of Clang, namely 15, 16, 17 and 18 (maybe more) with | ||
// specific optimization flags (-Os, -O1, -O2 -fno-vectorize, ...) do | ||
// realize that `poisoned_mask` can only ever be all-zero or all-one and | ||
// conditionally jump over the loop execution below. | ||
// | ||
// See: https://pqshield.com/pqshield-plugs-timing-leaks-in-kyber-ml-kem-to-improve-pqc-implementation-maturity/ | ||
const uint8_t poisoned_mask = -static_cast<uint8_t>(poisoned_byte & 1); | ||
for(size_t i = 0; i < sizeof(output_bytes); ++i) { | ||
output_bytes[i] &= poisoned_mask; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the right [*] compiler and flags, this actually triggers valgrind.
[*]
./configure.py --without-documentation --cc=clang --compiler-cache=ccache --build-tool=ninja --build-targets=static,valgrind_selftest --with-valgrind --minimized-build --enable-modules=auto_rng,hmac,sha2_64,system_rng --with-debug-info
ninja valgrind_selftest
valgrind --error-exitcode=2 --track-origins=yes ./botan_valgrind_selftest clang_vs_bare_metal_ct_mask
// Before the introduction of CT::value_barrier, this did generate a | ||
// conditional jump when compiled with clang using certain compiler | ||
// optimizations. See the test case above for further details. | ||
auto poisoned_mask = Botan::CT::Mask<uint8_t>::expand(poisoned_byte & 1); | ||
for(size_t i = 0; i < sizeof(output_bytes); ++i) { | ||
output_bytes[i] = poisoned_mask.select(output_bytes[i], 0); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you comment-out the gist of CT::value_barrier()
this also triggers valgrind under the same conditions explained above: 487eb50#r1666833485
# This is certainly not an exhaustive list of compiler configurations | ||
# that could be problematic for this specific test | ||
if cc_macro == "CLANG" and "-Os" in cc_compile_flags: | ||
test_list.append(ValgrindTest("clang_vs_bare_metal_ct_mask", True)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a sanity check, we could run ./botan_valgrind_selftest --list-special
and double-check that the "clang_vs_bare_metal_ct_mask" test is in this list. Also, we could take the info whether or not it is expected to fail from there, rather than hard-coding it.
Generally a good idea. I'll review fully later. For now, a bikeshed: This should be named something like |
Fair. Renamed the target (and the python steering script to |
How so? Oh I see, because we have to see if we're running under valgrind or not. Let's just fix that
Let's not make our abstractions leakier than they have to be :) there should be no need for this test to know how we've implemented the constant time checking. In particular this would (in the future) allow us to run this same test using multiple tools, which will be helpful to check that they are doing what we expect. |
9f37ff6
to
d4c8a12
Compare
Yeah, that's indeed a very valid point. We've implemented it as suggested, except that the define is called Edit: In order to reach |
a36cea0
to
78fa574
Compare
As said in #4202 (comment), let's keep this open so that we can add another test that puts the new |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good
This allows abstracting from the concrete implementation of the CT::poison() checks. Currently this can only be done with valgrind, but in the future we may want to use other tools for that. Co-Authored-By: Fabian Albert <[email protected]>
78fa574
to
e03e2a9
Compare
Rebased to master, solved conflicts with the newly introduced |
c6d4c3b
to
fed1ee1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is really helpful for improving assurance that our tests and CT techniques are doing what we expect
... this selftest deliberately creates negative signals for typical cases where CT::poison() should result in a warning from valgrind (or other tools). Co-Authored-By: Fabian Albert <[email protected]>
fed1ee1
to
005e17b
Compare
Thanks for the approval, @randombit. I was working on some more tests, though. Sorry. 😅
The low-level wrappers (for ranges and integers, as well as The higher-level abstractions ( Note that particularly the |
005e17b
to
30bf6d9
Compare
Squash plz |
7ee962e
to
2257d83
Compare
This runs some basic scenarios we'd like to find with valgrind and checks that they are detected as expected. Otherwise, to the best of our understanding, there wouldn't be any negative signal from valgrind in a typical CI run.
We introduced a new build target "valgrind_selftest" for that and added a python-based steering script that runs each of the C++ test cases individually. The "valgrind" CI job builds and runs the selftest before running the unit tests. This is built as a minimal framework to add more test cases over time.
One of the test cases isn't an obvious side channel, it is a reproduction of the latest Kyber issue found by PQShield and will trigger valgrind only in certain compiler configurations. In a follow-up, we might add a nightly valgrind run with such a configuration in the hope to increase the valgrind signal overall. The python-based steering script detects vulnerable compiler settings (from
build-config.json
) and skips this test otherwise.Another one is a de-facto regression test for the recently-introduced value-barrier. The regression is obviously not visible unless run with vulnerable compiler settings either. Then, with a disabled value-barrier, valgrind does detect a secret-dependent branch successfully.