Skip to main content

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}