[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:03 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:
Why? `nnan/ninf/nsz` are defined independently of the intrinsics. You can only ignore rewrite-based FMF, as `select/phi/fabs/copysign/fneg/fcmp` do.
https://github.com/llvm/llvm-project/pull/166702
More information about the llvm-commits
mailing list