fbl/conditional_select_nospec.rs
1// Copyright 2026 The Fuchsia Authors
2//
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT
6
7//! `conditional_select_nospec_*()` returns one of its two integral arguments based on whether its
8//! `predicate` argument is true or false, like a ternary expression. It uses branchless
9//! sequences on every architecture and is immune to speculative execution information leak bugs
10//! such as Spectre V1.
11//!
12//! # Use
13//! `conditional_select_nospec_eq()` returns `a` if `x` == `y`, `b` otherwise.
14//! `conditional_select_nospec_lt()` returns `a` if `x` < `y`, `b` otherwise.
15//! It does so even in wrong-path speculative executions.
16//!
17//! # Example (susceptible to bounds check bypass / Spectre V1)
18//! ```rust
19//! fn lookup(index: usize, stamp: u64, table: &[Thing]) -> &Thing {
20//! let safe_index = index & 0xff; // Masking ensures in-bounds (assuming table.len() >= 256)
21//! let thing = &table[safe_index];
22//! if thing.stamp == stamp { thing } else { &table[0] }
23//! }
24//! ```
25//!
26//! Hostile code can cause the CPU to speculate that `thing.stamp == stamp` is true,
27//! executing the true branch and using `thing` even if the stamp didn't match.
28//! If dependent code uses values derived from `thing` to look up values in other data
29//! structures, the lookup cache side effects may be observable and allow hostile
30//! code to infer values in a structure that it should not have had access to.
31//!
32//! Converted to use `conditional_select_nospec_eq`:
33//! ```rust
34//! fn lookup(index: usize, stamp: u64, table: &[Thing]) -> &Thing {
35//! let safe_index = index & 0xff;
36//! let thing = &table[safe_index];
37//!
38//! // Select the index without branching
39//! let selected_index = conditional_select_nospec_eq(
40//! thing.stamp as usize,
41//! stamp as usize,
42//! safe_index,
43//! 0, // Fallback to index 0
44//! );
45//!
46//! &table[selected_index]
47//! }
48//! ```
49//!
50//! To avoid Spectre V1-style attacks, a caller must also avoid branching on the return value of
51//! `conditional_select_nospec()`; it may do so by using a safe object or index for `b` (fallback).
52
53/// returns `a` if `x == y`, `b` otherwise.
54/// Immune to speculative execution information leak bugs such as Spectre V1.
55#[inline]
56pub fn conditional_select_nospec_eq(x: usize, y: usize, a: usize, b: usize) -> usize {
57 cfg_select! {
58 // No mitigations defined for RISC-V.
59 //
60 // Miri is not run in security-critical contexts and does not support inline assembly.
61 any(miri, target_arch = "riscv64") => {
62 if x == y { a } else { b }
63 }
64 target_arch = "aarch64" => {
65 let select: usize;
66 // SAFETY: CSDB barrier and conditional select are safe to use here to prevent
67 // speculative execution leaks.
68 // See "Cache Speculation Side-channels" whitepaper, section "Software Mitigation".
69 // "The combination of both a conditional select/conditional move and the new barrier are
70 // sufficient to address this problem on ALL Arm implementations..."
71 unsafe {
72 core::arch::asm!(
73 "cmp {x}, {y}",
74 "csel {select}, {a}, {b}, eq",
75 "csdb",
76 x = in(reg) x,
77 y = in(reg) y,
78 select = out(reg) select,
79 a = in(reg) a,
80 b = in(reg) b,
81 options(nostack, nomem)
82 );
83 }
84 select
85 }
86 target_arch = "x86_64" => {
87 let mut select = a;
88 // SAFETY: CMOVNZ has data dependency on CMP and is safe to use here to prevent
89 // speculative execution leaks.
90 // See "Software Techniques for Managing Speculation on AMD Processors", Mitigation V1-2.
91 // See "Analyzing potential bounds check bypass vulnerabilities", Revision 002,
92 // Section 5.2 Bounds clipping
93 unsafe {
94 core::arch::asm!(
95 "cmp {x}, {y}",
96 "cmovnz {select}, {b}",
97 x = in(reg) x,
98 y = in(reg) y,
99 select = inout(reg) select,
100 b = in(reg) b,
101 options(nostack, nomem)
102 );
103 }
104 select
105 }
106 _ => {
107 compile_error!("Provide implementations of conditional_select for your ARCH here");
108 0 // Needed to satisfy return type
109 }
110 }
111}
112
113/// returns `a` if `x < y`, `b` otherwise.
114/// Immune to speculative execution information leak bugs such as Spectre V1.
115#[inline]
116pub fn conditional_select_nospec_lt(x: usize, y: usize, a: usize, b: usize) -> usize {
117 cfg_select! {
118 // No mitigations defined for RISC-V.
119 //
120 // Miri is not run in security-critical contexts and does not support inline assembly.
121 any(miri, target_arch = "riscv64") => {
122 if x < y { a } else { b }
123 }
124 target_arch = "aarch64" => {
125 let select: usize;
126 // SAFETY: CSDB barrier and conditional select are safe to use here to prevent
127 // speculative execution leaks.
128 // See "Cache Speculation Side-channels" whitepaper, section "Software Mitigation".
129 // "The combination of both a conditional select/conditional move and the new barrier are
130 // sufficient to address this problem on ALL Arm implementations..."
131 unsafe {
132 core::arch::asm!(
133 "cmp {x}, {y}",
134 "csel {select}, {a}, {b}, lo",
135 "csdb",
136 x = in(reg) x,
137 y = in(reg) y,
138 select = out(reg) select,
139 a = in(reg) a,
140 b = in(reg) b,
141 options(nostack, nomem)
142 );
143 }
144 select
145 }
146 target_arch = "x86_64" => {
147 let mut select = a;
148 // SAFETY: CMOVAE has data dependency on CMP and is safe to use here to prevent
149 // speculative execution leaks.
150 // See "Software Techniques for Managing Speculation on AMD Processors", Mitigation V1-2.
151 // See "Analyzing potential bounds check bypass vulnerabilities", Revision 002,
152 // Section 5.2 Bounds clipping
153 unsafe {
154 core::arch::asm!(
155 "cmp {x}, {y}",
156 "cmovae {select}, {b}",
157 x = in(reg) x,
158 y = in(reg) y,
159 select = inout(reg) select,
160 b = in(reg) b,
161 options(nostack, nomem)
162 );
163 }
164 select
165 }
166 _ => {
167 compile_error!("Provide implementations of conditional_select for your ARCH here");
168 0 // Needed to satisfy return type
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_conditional_select_nospec() {
179 assert_eq!(1, conditional_select_nospec_eq(0, 1, 0, 1));
180 assert_eq!(0, conditional_select_nospec_eq(1, 1, 0, 1));
181 assert_eq!(6, conditional_select_nospec_eq(1, 1, 6, 1));
182 assert_eq!(1, conditional_select_nospec_eq(66, 66, 1, 0));
183 assert_eq!(0, conditional_select_nospec_eq(67, 66, 1, 0));
184
185 assert_eq!(1, conditional_select_nospec_lt(65, 66, 1, 0));
186 assert_eq!(0, conditional_select_nospec_lt(66, 66, 1, 0));
187 assert_eq!(0, conditional_select_nospec_lt(67, 66, 1, 0));
188 assert_eq!(0, conditional_select_nospec_lt(usize::MAX, 66, 1, 0));
189 }
190}