ML-DSA implementation CVE CVE-2026-22705

601 views
Skip to first unread message

Loganaden Velvindron

unread,
Jan 23, 2026, 10:39:54 AM (4 days ago) Jan 23
to pqc-forum
It was found in rustcrypto implementation of ml-dsa. It's an
implementation issue.

https://github.com/RustCrypto/signatures/security/advisories/GHSA-hcp2-x6j4-29j7

Kind regards,
//Logan

Tony Arcieri

unread,
Jan 23, 2026, 11:50:56 AM (4 days ago) Jan 23
to pqc-forum, Loganaden Velvindron
Speaking as a maintainer of that library, after Kyberslash we tried to add automatic linting for this sort of issue (the authors of the popular Rust linter "clippy" actually added a lint to detect use of division and modulus operations for us after Kyberslash), but it didn't detect this problem.

It may because we have various lint exceptions configured I should go through and remove. I still need to investigate.

Tony Arcieri

unread,
Jan 24, 2026, 4:49:07 PM (2 days ago) Jan 24
to Daniel Apon, pqc-forum
On Sat, Jan 24, 2026 at 2:32 PM Daniel Apon <dapon....@gmail.com> wrote:
While memory-safety in programming languages is "important;" cf. https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE-CASE-FOR-MEMORY-SAFE-ROADMAPS-TLP-CLEAR.PDF ,
the underlying compiler technology -- LLVM -- of Rust seems to regularly exhibit terrible constant-timeness in practice; seemingly often requiring highly non-standard programming practices to ensure constant-time of cryptographic implementations.

Trying to produce constant-time implementations when using an optimizing compiler is certainly a challenge, especially as the compilers themselves give you almost no tools to accomplish that goal. This isn't even our first bug related to non-constant-time codegen this year.

I will definitely try to get the lint for division/remainder working, that seems like an easy first step.

Though our ML-DSA implementation isn't using it yet, we do maintain a library for performing constant-time predication and equality testing, which uses inline assembly to execute architecture-specific predication instructions like CMOV and CSEL, and on other platforms uses a portable implementation which also for several platforms performs mask generation using inline assembly: https://crates.io/crates/cmov

There is also an effort underway to add first-class support for such an intrinsic to LLVM itself which could lower to similar CPU-specific predication instructions when available: https://discourse.llvm.org/t/rfc-constant-time-coding-support/87781

Finally, the pie-in-the-sky solution would be to give LLVM first-class support for constant-time code generation in the form of a set of new integer types and special boolean type which respect the rules of constant-time code generation and simply don't allow you to use them in non-constant-time ways and will refuse to generate code if you do (e.g. division wouldn't be supported, but also your standard no branches, use in pointer calculations, etc): https://github.com/avadacatavra/rfcs/blob/6f967443/text/0000-secret-types.md

I have heard rumors that Google prototyped that internally on RISC-V, but I don't think the code ever saw the light of day.

--
Tony Arcieri

Daniel Apon

unread,
Jan 24, 2026, 10:27:42 PM (2 days ago) Jan 24
to Tony Arcieri, pqc-forum, Loganaden Velvindron
While these are normatively distinct issues, I'd like to link back (as a reference to the community) to a somewhat recent, especially important discussion on this forum, regarding Rust implementations of Kyber and timing issues:

"Compiler-introduced timing leak in Kyber reference implementation"
https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/hqbtIGFKIpU

Let me be clear:


While memory-safety in programming languages is "important;" cf. https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE-CASE-FOR-MEMORY-SAFE-ROADMAPS-TLP-CLEAR.PDF ,
the underlying compiler technology -- LLVM -- of Rust seems to regularly exhibit terrible constant-timeness in practice; seemingly often requiring highly non-standard programming practices to ensure constant-time of cryptographic implementations.

Comments from those working on this broader issue are certainly welcome.

On Fri, Jan 23, 2026 at 11:51 AM Tony Arcieri <bas...@gmail.com> wrote:
Speaking as a maintainer of that library, after Kyberslash we tried to add automatic linting for this sort of issue (the authors of the popular Rust linter "clippy" actually added a lint to detect use of division and modulus operations for us after Kyberslash), but it didn't detect this problem.

It may because we have various lint exceptions configured I should go through and remove. I still need to investigate.

--
You received this message because you are subscribed to the Google Groups "pqc-forum" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pqc-forum+...@list.nist.gov.
To view this discussion visit https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/686b8c53-7cba-4877-a30c-dc0c5ac42b0dn%40list.nist.gov.

Vijay S

unread,
Jan 25, 2026, 11:27:37 AM (2 days ago) Jan 25
to Tony Arcieri, Daniel Apon, pqc-forum, vijaykrish...@hotmail.com

Thank you for the detailed discussion — 

One observation as an outsider: issues like this suggest that “constant-time” is no longer a purely algorithmic or even implementation-level property, but an emergent property of the full compilation and execution pipeline (language → compiler → optimizer → microarchitecture).

In that sense, many of these findings are less about specific primitives (ML-DSA, Kyber, etc.) and more about the lack of runtime observability into behavioral deviations that do not violate functional correctness.

As PQC transitions from reference implementations into production systems, it seems likely that side-channel risk will increasingly manifest as gradual behavioral drift rather than immediate failure — particularly in highly optimized environments.

I don’t have a proposed fix here, but I think this framing may be useful when thinking about future tooling, testing, and assurance models for PQC deployments.

Thanks again to the maintainers for surfacing this so transparently.

Regards

Vijay S 

https://www.linkedin.com/in/vijay-krishna-10480210/


--
You received this message because you are subscribed to the Google Groups "pqc-forum" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pqc-forum+...@list.nist.gov.

Filippo Valsorda

unread,
Jan 25, 2026, 11:54:24 AM (2 days ago) Jan 25
to Vijay S, Tony Arcieri, Daniel Apon, pqc-forum, vijaykrish...@hotmail.com
As interesting as theoretical conversations on the risk of timing side-channels introduced at code generation time by optimizing compilers are... this does not look like such a case?

The issue was the following line of Rust code, which was simply a variable-time operation.
r1.0 /= TwoGamma2::U32;

I don't think this teaches us anything on Rust, LLVM, optimizing compilers, or the need for special intrinsics. The exact same code would have been variable-time in every other language and with every other compiler.

It appears the only things that would have avoided this systematically would have been static analysis to forbid division operations, special types that forbid all variable-time operations (which don't exist in any language IIUC), or maybe dynamic analysis like ctgrind.

I say maybe because I reported a similar issue in BoringSSL recently (a modulo operation instead of a division), and AFAIK BoringSSL applies ctgrind pretty systematically, but it failed to detect this, possibly because it does not consider division/modulo of "uninitialized" memory to be illegal. https://boringssl-review.googlesource.com/c/boringssl/+/83348

Greg Maxwell

unread,
Jan 25, 2026, 12:48:57 PM (2 days ago) Jan 25
to Filippo Valsorda, Vijay S, Tony Arcieri, Daniel Apon, pqc-forum, vijaykrish...@hotmail.com
FWIW. It's a more or less trivial patch to valgrind to make it consider division/mod an operation that needs defined inputs.


Matthias Kannwischer

unread,
Jan 25, 2026, 7:36:46 PM (2 days ago) Jan 25
to pqc-forum, Filippo Valsorda, Daniel Apon, pqc-forum, vijaykrish...@hotmail.com, Vijay S, Tony Arcieri
On Monday, 26 January 2026 at 00:54:24 UTC+8 Filippo Valsorda wrote:
As interesting as theoretical conversations on the risk of timing side-channels introduced at code generation time by optimizing compilers are... this does not look like such a case?

The issue was the following line of Rust code, which was simply a variable-time operation.
r1.0 /= TwoGamma2::U32;

I don't think this teaches us anything on Rust, LLVM, optimizing compilers, or the need for special intrinsics. The exact same code would have been variable-time in every other language and with every other compiler.

This vulnerability is very close to KyberSlash. I agree with Filippo here: You can't really blame the compiler for not optimizing away a division the developer intentionally put there.
There are cases where we have to try very hard to stop the compiler from messing with our code, but this is not one of them.
 

It appears the only things that would have avoided this systematically would have been static analysis to forbid division operations, special types that forbid all variable-time operations (which don't exist in any language IIUC), or maybe dynamic analysis like ctgrind.

I say maybe because I reported a similar issue in BoringSSL recently (a modulo operation instead of a division), and AFAIK BoringSSL applies ctgrind pretty systematically, but it failed to detect this, possibly because it does not consider division/modulo of "uninitialized" memory to be illegal. https://boringssl-review.googlesource.com/c/boringssl/+/83348


Indeed, stock valgrind only warns about branches and memory accesses depending on undefined (secret) data.
The KyberSlash paper actually proposes a patch to valgrind that would allow catching secret-dependent divisions with ctgrind, too. See Patches to Valgrind at [1]. 
It also contains a large scale study analyzing implementations in SUPERCOP demonstrating that this approach scales well.
If one wants to avoid such vulnerabilities, there are certainly ways. In fact, I'm using that patch to perform ctgrind-style tests in the CI of mlkem-native [2] and mldsa-native [3] testing 242 platform/compiler/optimization flag combinations.

Daniel Bernstein tried multiple times to get (a variant of) that patch merged into valgrind - so far without a positive response from the valgrind developers. The latest attempt is here [4]. 
If we would get that merged, everyone who is using ctgrind-style tests could just add a flag to the valgrind call to catch secret-dependent divisions.
If that would be useful for your project, consider speaking up in the thread, please.

Kind regards,
Matthias

Peter Schwabe

unread,
Jan 26, 2026, 1:19:07 AM (yesterday) Jan 26
to Greg Maxwell, Filippo Valsorda, Vijay S, Tony Arcieri, Daniel Apon, pqc-forum, vijaykrish...@hotmail.com
Dear all,

Let me just throw in here, that systematically avoiding variable-time
operations on secret-dependent data is something that Jasmin *does*
offer, even during speculative execution after a mispredicted
conditional branch; see
https://tches.iacr.org/index.php/TCHES/article/view/12229

A prerequisite is that the language offers a type system to distinguish
secret from public data and that the compiler preserves this information
through compilation. As far as I understand, the former can be done
fairly easily in Rust by using the secret_integers crate, but at the
moment, no mainstream compiler preserves (or uses) this knowledge.

All the best,

Peter


Greg Maxwell <gmax...@gmail.com> wrote:
> FWIW. It's a more or less trivial patch to valgrind to make it consider
> division/mod an operation that needs defined inputs.
>
>
> On Sun, Jan 25, 2026 at 4:54 PM Filippo Valsorda <fil...@ml.filippo.io>
> wrote:
>
> > As interesting as theoretical conversations on the risk of timing
> > side-channels introduced at code generation time by optimizing compilers
> > are... this does not look like such a case?
> >
> > The issue was the following line of Rust code, which was simply a
> > variable-time operation.
> >
> > r1.0 /= TwoGamma2::U32;
> >
> >
> > I don't think this teaches us anything on Rust, LLVM, optimizing
> > compilers, or the need for special intrinsics. The exact same code would
> > have been variable-time in every other language and with every other
> > compiler.
> >
> > It appears the only things that would have avoided this systematically
> > would have been static analysis to forbid division operations, special
> > types that forbid all variable-time operations (which don't exist in any
> > language IIUC), or *maybe* dynamic analysis like ctgrind.
> >
> > I say maybe because I reported a similar issue in BoringSSL recently (a
> > modulo operation instead of a division), and AFAIK BoringSSL applies
> > ctgrind pretty systematically, but it failed to detect this, possibly
> > because it does not consider division/modulo of "uninitialized" memory to
> > be illegal. https://boringssl-review.googlesource.com/c/boringssl/+/83348
> >
> > 2026-01-25 17:27 GMT+01:00 Vijay S <vijaykri...@gmail.com>:
> >
> > Thank you for the detailed discussion —
> >
> > One observation as an outsider: issues like this suggest that
> > “constant-time” is no longer a purely algorithmic or even
> > implementation-level property, but an emergent property of the full
> > compilation and execution pipeline (language → compiler → optimizer →
> > microarchitecture).
> >
> > In that sense, many of these findings are less about specific primitives
> > (ML-DSA, Kyber, etc.) and more about the lack of *runtime observability*
> > into behavioral deviations that do not violate functional correctness.
> >
> > As PQC transitions from reference implementations into production systems,
> > it seems likely that side-channel risk will increasingly manifest as
> > gradual behavioral drift rather than immediate failure — particularly in
> > highly optimized environments.
> >
> > I don’t have a proposed fix here, but I think this framing may be useful
> > when thinking about future tooling, testing, and assurance models for PQC
> > deployments.
> >
> > Thanks again to the maintainers for surfacing this so transparently.
> >
> > Regards
> >
> > Vijay S
> >
> > https://www.linkedin.com/in/vijay-krishna-10480210/
> >
> > On Sun, 25 Jan 2026 at 03:19, Tony Arcieri <bas...@gmail.com> wrote:
> >
> > On Sat, Jan 24, 2026 at 2:32 PM Daniel Apon <dapon....@gmail.com>
> > wrote:
> >
> > While memory-safety in programming languages is "important;" cf.
> > https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE-CASE-FOR-MEMORY-SAFE-ROADMAPS-TLP-CLEAR.PDF
> > ,
> > the underlying compiler technology -- LLVM -- of Rust seems to *regularly*
> > exhibit *terrible* constant-timeness in practice; seemingly often
> > requiring *highly non-standard programming practices* to ensure
> > <https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/CAHOTMVJSK7GHjrV7oDZjpXE3AyNFrt78Kx9NrJ4UKQjShPohow%40mail.gmail.com?utm_medium=email&utm_source=footer>
> > .
> >
> >
> > --
> > You received this message because you are subscribed to the Google Groups
> > "pqc-forum" group.
> > To unsubscribe from this group and stop receiving emails from it, send an
> > email to pqc-forum+...@list.nist.gov.
> > To view this discussion visit
> > https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/CALoHbGKmqPySGE6sE_Cn-FB2Fi23hJpPLJxg62vVYvoDqpOoYw%40mail.gmail.com
> > <https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/CALoHbGKmqPySGE6sE_Cn-FB2Fi23hJpPLJxg62vVYvoDqpOoYw%40mail.gmail.com?utm_medium=email&utm_source=footer>
> > .
> >
> >
> > --
> > You received this message because you are subscribed to the Google Groups
> > "pqc-forum" group.
> > To unsubscribe from this group and stop receiving emails from it, send an
> > email to pqc-forum+...@list.nist.gov.
> > To view this discussion visit
> > https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/9b881899-304c-414c-8c5d-e06c13d96f87%40app.fastmail.com
> > <https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/9b881899-304c-414c-8c5d-e06c13d96f87%40app.fastmail.com?utm_medium=email&utm_source=footer>
> > .
> >
>
> --
> You received this message because you are subscribed to the Google Groups "pqc-forum" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to pqc-forum+...@list.nist.gov.
> To view this discussion visit https://groups.google.com/a/list.nist.gov/d/msgid/pqc-forum/CAAS2fgQa-NdioqiR%3DMrr6YSozz%3DiO1YbsG%2BYwzv4YdgeRHKpLg%40mail.gmail.com.

Simon Hoerder

unread,
Jan 26, 2026, 2:02:54 AM (yesterday) Jan 26
to pqc-...@list.nist.gov
Hi,

On 1/24/26 10:48 PM, Tony Arcieri wrote:
[...]
> There is also an effort underway to add first-class support for such an
> intrinsic to LLVM itself which could lower to similar CPU-specific
> predication instructions when available: https://discourse.llvm.org/t/
> rfc-constant-time-coding-support/87781 <https://discourse.llvm.org/t/
> rfc-constant-time-coding-support/87781>

[...]
I thought this is already implemented? There was a press-release from
December
(https://blog.trailofbits.com/2025/12/02/introducing-constant-time-support-for-llvm-to-protect-cryptographic-code/)
and I heard excited talk about it.

Of course, I can understand that developers don't always switch to the
latest and greatest (and least tested) compiler version. What I'm
wondering though is whether this is simply too fresh or whether the
division vulnerability slipped through LLVM despite the new constant
time support.

Usually I'm more on the HW side so I'm definitely not on top of LLVM
developments but constant time PQC is an interesting challenge on both
sides of the HW/SW divide. Hence my curiosity.

Thanks,
Simon

Tony Arcieri

unread,
Jan 26, 2026, 9:43:18 AM (17 hours ago) Jan 26
to Simon Hoerder, pqc-...@list.nist.gov
On Mon, Jan 26, 2026 at 12:02 AM Simon Hoerder <si...@hoerder.net> wrote:
I thought this is already implemented? There was a press-release from
December
(https://blog.trailofbits.com/2025/12/02/introducing-constant-time-support-for-llvm-to-protect-cryptographic-code/)
and I heard excited talk about it.

I believe it landed upstream in LLVM but it still needs to be exposed in Rust, and their current plan is to expose it through `core::intrinsics`, which is both unsafe and not available in stable Rust. So after that, someone will need to write a safe API, and then that API needs to get stabilized before it will actually be useful for general consumption.

--
Tony Arcieri

D. J. Bernstein

unread,
Jan 26, 2026, 10:11:30 AM (16 hours ago) Jan 26
to pqc-...@list.nist.gov
Simon Hoerder writes:
> I thought this is already implemented? There was a press-release from
> December (https://blog.trailofbits.com/2025/12/02/introducing-constant-time-support-for-llvm-to-protect-cryptographic-code/)
> and I heard excited talk about it.

That's providing a constant-time if-then-else operation in the language
and the compiler. It isn't trying to protect divisions, comparison
operations, bit masks, etc. So it's not a replacement for a larger
language change that (1) provides a full spectrum of constant-time
arithmetic operations and (2) makes it hard to accidentally mix those
with non-constant-time operations.

The "constant_time_lookup" C function from the blog post illustrates the
difficulty: it uses the new __builtin_ct_select operation but also uses
i == secret_idx, which by itself can turn into variable-time code. As a
side note, the function also seems buggy given that the prototype has a
table of size 16 but the loop checks only 8 table entries.

C programmers _can_ avoid the i == secret_idx security risk in this
example. My cryptoint library provides constant-time replacements for
all of the usual comparison operations and some further operations such
as bit extractions (but not yet divisions):

https://cr.yp.to/papers.html#cryptoint

Specifically, crypto_int64_equal_01(i,secret_idx) has the same numerical
result as i == secret_idx but stops the compiler from realizing that the
result is 0 or 1. You can also replace "01" with "mask" to negate the
result. The full function can then be written as follows:

#include <inttypes.h>
#include "crypto_int64.h"

uint64_t
constant_time_lookup(const size_t secret_idx,const uint64_t table[16])
{
uint64_t result = 0;
for (int64_t i = 0;i < 16;i++)
result |= table[i] & crypto_int64_equal_mask(i,secret_idx);
return result;
}

The compiler doesn't know that the mask is 0 or -1, and won't turn the
logic operations into branches. See the cryptoint paper for details.

There are many more cryptoint examples in SUPERCOP. Currently SUPERCOP
has 4902 implementations of 1469 primitives, with 1755 implementations
marked as constant-time (goal-constbranch and goal-constindex); 13111
lines of code in 1095 implementations of 284 primitives call cryptoint.
Converting to cryptoint is generally easy within the operations that
cryptoint supports (unlike, say, divisions). On the other hand, this
conversion is certainly not complete, and there's always the risk of
some non-constant-time operation still being in the code.

For better assurance, the implementations marked as constant-time are
also continually run through TIMECOP 2, with results rapidly available
online (including results from some machines that check for divisions
with the valgrind patch that Matthias mentioned). TIMECOP 2 doesn't
guarantee path coverage, but it does achieve path coverage in many
examples; it has caught many branches that programmers missed; it was
the source of my April 2024 alert that gcc was sometimes turning pure
arithmetic operations (specifically (-x)>>31) into branches.

Some libraries (lib25519, libmceliece, etc.) share source code with
SUPERCOP, so they benefit from SUPERCOP's centralized testing, and at
the same time include TIMECOP-like tests in their own test suites, so
they're applying the tests to exactly the code that the user ends up
using---which could matter if the library is compiled with a newer
compiler than what's used on any of the SUPERCOP machines, or with
different compiler options.

> Of course, I can understand that developers don't always switch to the
> latest and greatest (and least tested) compiler version.

Yes, there's that issue too. Code using cryptoint works with older
compilers, whereas code using __builtin_ct_select needs a new compiler.

To be clear, I think there can be value in compiler modifications. I
wrote a clang patch to eliminate some of the worst branch-introducing
"optimizations"; Fil-C has adopted the patch (and, btw, also adds memory
safety to your C/C++ code).

None of the above comments about protecting C/C++ code should be taken
as recommendations to use C and/or C++.

---D. J. Bernstein
signature.asc
Reply all
Reply to author
Forward
0 new messages