[llvm] [ConstantTime][LLVM] Add llvm.ct.select intrinsic with generic SelectionDAG lowering (PR #166702)

Yingwei Zheng via llvm-commits llvm-commits at lists.llvm.org
Sat Aug 15 13:39:02 PDT 2026


================
@@ -15911,6 +15911,153 @@ catch:
     ret void
 ```
 
+### Constant-Time Intrinsics
+
+These intrinsics are provided to support constant-time operations for
+security-sensitive code. Constant-time operations are designed to execute in
+time independent of secret data values, preventing timing side-channel leaks.
+
+(int_ct_select)=
+
+#### '`llvm.ct.select.*`' Intrinsic
+
+##### Syntax:
+
+This is an overloaded intrinsic. You can use `llvm.ct.select` on any integer,
+floating-point, or pointer type, or on any vector of those types, including
+scalable vectors. The declarations below are a representative sample:
+
+```
+declare i8 @llvm.ct.select.i8(i1 <cond>, i8 <val1>, i8 <val2>)
+declare i32 @llvm.ct.select.i32(i1 <cond>, i32 <val1>, i32 <val2>)
+declare i64 @llvm.ct.select.i64(i1 <cond>, i64 <val1>, i64 <val2>)
+declare half @llvm.ct.select.f16(i1 <cond>, half <val1>, half <val2>)
+declare float @llvm.ct.select.f32(i1 <cond>, float <val1>, float <val2>)
+declare double @llvm.ct.select.f64(i1 <cond>, double <val1>, double <val2>)
+declare fp128 @llvm.ct.select.f128(i1 <cond>, fp128 <val1>, fp128 <val2>)
+declare ptr @llvm.ct.select.p0(i1 <cond>, ptr <val1>, ptr <val2>)
+declare <4 x i32> @llvm.ct.select.v4i32(i1 <cond>, <4 x i32> <val1>, <4 x i32> <val2>)
+declare <2 x double> @llvm.ct.select.v2f64(i1 <cond>, <2 x double> <val1>, <2 x double> <val2>)
+declare <2 x ptr> @llvm.ct.select.v2p0(i1 <cond>, <2 x ptr> <val1>, <2 x ptr> <val2>)
+declare <vscale x 4 x i32> @llvm.ct.select.nxv4i32(i1 <cond>, <vscale x 4 x i32> <val1>, <vscale x 4 x i32> <val2>)
+```
+
+##### Overview:
+
+The '`llvm.ct.select`' family of intrinsic functions selects one of two
+values based on a condition, like the standard {ref}`select <i_select>`
+instruction, but is lowered to branchless code whose execution time does not
+depend on the condition value. This keeps the condition from leaking through
+timing side channels; see the Semantics section for the exact guarantee and
+its platform requirements.
+
+##### Arguments:
+
+The '`llvm.ct.select`' intrinsic requires three arguments:
+
+1. The condition, which must be a scalar value of type 'i1'. Unlike
+   {ref}`select <i_select>` which accepts both scalar 'i1' and vector
+   '`<N x i1>`' conditions, `llvm.ct.select` only accepts a scalar 'i1'
+   condition. Vector conditions are not supported.
+2. The first value argument, which must be an integer, floating-point, or
+   pointer type, or a vector of those types. Other
+   {ref}`first class <t_firstclass>` types, such as aggregates, are not
+   supported.
+3. The second value argument, which must have the same type as the first
+   value argument.
+
+##### Semantics:
+
+If the condition evaluates to true, the intrinsic returns the first value
+argument; otherwise, it returns the second value argument.
+
+If any argument is poison, the result is poison. Unlike `select`, this
+includes the unselected value argument, since the lowering blends both
+arguments with bitwise operations. If any argument is undef, the result is
+undef; an undef condition can blend bits from both arguments, so the result
+is not necessarily one of them. `llvm.ct.select` therefore cannot be used as
+a poison barrier.
+
+The return value carries the `noundef` attribute. A call where the condition
+or either value argument is undef or poison would return undef or poison, and
+is therefore undefined behavior. Frontends that cannot rule this out must
+{ref}`freeze <i_freeze>` the operands first.
+
+The key semantic difference from {ref}`select <i_select>` is the constant-time
+code generation guarantee: the intrinsic must be lowered to machine code that:
+
+- Does not introduce data-dependent control flow based on the condition value
+- Executes the same sequence of instructions regardless of the condition value
+- Uses both value arguments unconditionally; the not-selected argument is
+  never skipped or branched around
+
+**Platform Requirements:** The constant-time guarantee is conditional on
+hardware support for data-independent execution timing. This may be a
+processor mode that the platform enables, such as Arm DIT (Data Independent
+Timing) or x86 DOIT (Data Operand Independent Timing), or a static
+architectural guarantee, such as the RISC-V `Zkt` and `Zvkt` extensions,
+which certify data-independent latency for the scalar and vector instructions
+emitted by the lowering. Without such hardware support, the generated code
+will still be free from data-dependent control flow, but microarchitectural
+timing variations may still occur.
+
+The typical implementation uses bitwise operations to blend the two values
+based on a mask derived from the condition:
+
+```
+mask = sext(cond)  ; sign-extend condition to all 1s or all 0s
+result = val2 ^ ((val1 ^ val2) & mask)
+```
+
+Targets with native constant-time select support lower the intrinsic to
+suitable target instructions instead. Targets without native support use the
+generic bitwise expansion shown above. In either case the selection is kept
+as a single opaque operation until late in code generation, so optimization
+passes never see a pattern they could rewrite into a select or a conditional
+branch.
+
+Optimizations must preserve the constant-time code generation semantics.
+Transforms that would introduce data-dependent control flow are not permitted.
+This includes converting to conditional branches, using predicated instructions
+with data-dependent timing, or, except as described below, optimizing away
+either value argument before the selection completes.
+
+The call may be folded to one of its value arguments only when the condition
+is a literal `i1` constant or both value arguments are the same SSA value;
+the unused argument then becomes ordinary dead code. Proving the condition
+constant by other means (known-bits, range, or dominating conditions) does
+not permit the fold. Rewrites that keep the `llvm.ct.select`, such as
+swapping the arguments to remove a negated condition, are allowed. Fast-math
+flags on a call to `llvm.ct.select` are ignored.
----------------
dtcxzyw wrote:

I mean, you don't need to propagate these flags during transformation. But if it is set by the frontend, the optimization pipeline may leverage them automatically, regardless of whether the return value comes from a ct.select or a normal select.


https://github.com/llvm/llvm-project/pull/166702


More information about the llvm-commits mailing list