Skip to main content

fuchsia_rseq/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fuchsia_sync::Mutex;
6use std::mem;
7use std::ptr::NonNull;
8use std::sync::LazyLock;
9use zx_types::zx_rseq_t;
10
11/// The maximum number of threads supported by the RSEQ allocator.
12///
13/// We map the VMO linearly up front to keep the mappings contiguous in virtual memory.
14/// This limit guarantees we don't exceed a reasonable amount of virtual memory.
15const MAX_THREADS: u64 = 100_000;
16
17/// Represents a unique memory location within the global RSEQ VMO assigned to a thread.
18#[derive(Debug, Default, Clone, Copy)]
19struct ThreadSlot {
20    /// The offset in bytes from the start of the global RSEQ VMO mapping.
21    offset: u64,
22}
23
24/// A synchronized allocator that manages unique thread slots in a single shared VMO.
25struct Allocator {
26    /// The backing virtual memory object mapped for RSEQ slots.
27    vmo: zx::Vmo,
28    /// The memory address where the `vmo` starts in the current process's virtual address space.
29    mapped_addr: usize,
30    /// A list of returned slots that are available for reuse by registering threads.
31    free_list: Vec<ThreadSlot>,
32    /// The next monotonically increasing slot index to be allocated.
33    next_slot_index: u64,
34    /// The distance in bytes between thread RSEQ slots.
35    stride: u64,
36}
37
38/// Rounds up a given size to the next multiple of the operating system page size.
39fn round_up_to_page_size(size: usize) -> usize {
40    let page_size = zx::system_get_page_size() as usize;
41    ((size + page_size - 1) / page_size) * page_size
42}
43
44impl Allocator {
45    /// Creates a new `Allocator` mapping space for up to `MAX_THREADS` structures.
46    fn new() -> Self {
47        let stride = std::cmp::max(
48            zx::system_get_dcache_line_size() as u64,
49            mem::size_of::<zx_rseq_t>() as u64,
50        );
51        let needed_size = (MAX_THREADS as usize) * (stride as usize);
52        let map_size = round_up_to_page_size(needed_size);
53        let vmo = zx::Vmo::create(map_size as u64).expect("failed to create RSEQ VMO");
54        let flags = zx::VmarFlags::PERM_READ | zx::VmarFlags::PERM_WRITE;
55        let mapped_addr = fuchsia_runtime::vmar_root_self()
56            .map(0, &vmo, 0, map_size, flags)
57            .expect("failed to map RSEQ VMO");
58
59        Self { vmo, mapped_addr, free_list: Vec::new(), next_slot_index: 0, stride }
60    }
61
62    /// Allocates a unique `ThreadSlot` from the free list or the unmapped pool.
63    ///
64    /// # Panics
65    ///
66    /// Panics if the globally managed pool exceeds `MAX_THREADS` (100,000 threads).
67    fn allocate(&mut self) -> ThreadSlot {
68        if let Some(slot) = self.free_list.pop() {
69            return slot;
70        }
71
72        assert!(self.next_slot_index < MAX_THREADS, "RSEQ max thread count exceeded");
73        let index = self.next_slot_index;
74        self.next_slot_index += 1;
75        let offset = index * self.stride;
76
77        ThreadSlot { offset }
78    }
79
80    /// Returns an active thread slot's memory address back to the free list.
81    fn free(&mut self, abi: *mut zx_rseq_t) {
82        self.free_list.push(self.slot(abi));
83    }
84
85    /// Retrieves the raw VMO handle needed by the kernel to register threads.
86    fn vmo_handle(&self) -> zx::sys::zx_handle_t {
87        self.vmo.raw_handle()
88    }
89
90    /// Returns the `ThreadSlot` for the given pointer to `zx_rseq_t`.
91    fn slot(&self, abi: *mut zx_rseq_t) -> ThreadSlot {
92        let offset = (abi as usize - self.mapped_addr) as u64;
93        ThreadSlot { offset }
94    }
95
96    /// Returns a pointer to the `zx_rseq_t` for the given slot.
97    fn abi(&self, slot: &ThreadSlot) -> *mut zx_rseq_t {
98        (self.mapped_addr + slot.offset as usize) as *mut zx_rseq_t
99    }
100}
101
102static ALLOCATOR: LazyLock<Mutex<Allocator>> = LazyLock::new(|| Mutex::new(Allocator::new()));
103
104thread_local! {
105    /// The `zx_rseq_t` structure for the current thread, if any.
106    ///
107    /// This structure is used to store the restartable sequence information for the current thread.
108    /// It is registered with the kernel when the thread is created and unregistered when the thread
109    /// is destroyed.
110    static RSEQ: std::cell::Cell<*mut zx_rseq_t> =
111        const { std::cell::Cell::new(std::ptr::null_mut()) };
112}
113
114/// The restartable sequence for the current thread.
115///
116/// This structure is used to store the restartable sequence information for the current thread.
117/// It is registered with the kernel when the thread is created and unregistered when the thread
118/// is destroyed.
119#[derive(Debug, Clone, Copy)]
120pub struct Rseq {
121    abi: NonNull<zx_rseq_t>,
122}
123
124impl Rseq {
125    /// Returns the restartable sequence for the current thread.
126    ///
127    /// # Panics
128    ///
129    /// Panics if the thread has not been registered for restartable sequences.
130    /// See `rseq_register_thread()`.
131    ///
132    /// # Safety
133    ///
134    /// The returned object must not be used after the current thread calls
135    /// `rseq_unregister_thread()`.
136    #[inline]
137    pub unsafe fn get() -> Self {
138        let abi = NonNull::new(RSEQ.with(|rseq| rseq.get())).expect("thread not registered");
139        Self { abi }
140    }
141
142    /// Returns the restartable sequence for the current thread if registered.
143    ///
144    /// # Safety
145    ///
146    /// The returned object must not be used after the current thread calls
147    /// `rseq_unregister_thread()`.
148    #[inline]
149    pub unsafe fn try_get() -> Option<Self> {
150        let abi = NonNull::new(RSEQ.with(|rseq| rseq.get()))?;
151        Some(Self { abi })
152    }
153
154    /// Returns a pointer to the `zx_rseq_t` structure.
155    ///
156    /// The returned pointer is only valid until the current thread calls
157    /// `rseq_unregister_thread()`.
158    ///
159    /// Useful for accessing the `zx_rseq_t` structure from inline assembly.
160    #[inline]
161    pub fn as_ptr(&self) -> *mut zx_rseq_t {
162        self.abi.as_ptr()
163    }
164
165    /// Reads the current CPU ID.
166    ///
167    /// The current CPU can change at any time. Useful for preparing for a restartable sequence.
168    ///
169    /// # Safety
170    ///
171    /// This method cannot be used after the current thread calls `rseq_unregister_thread()`. That
172    /// invariant is required by the safety contract of `get()` as well.
173    #[inline]
174    pub unsafe fn current_cpu(&self) -> u32 {
175        let abi = self.as_ptr();
176        unsafe {
177            let cpu_id_ptr: *mut u32 = std::ptr::addr_of_mut!((*abi).cpu_id);
178            // The CPU ID is only written by the kernel on this thread, which means a volatile
179            // read is necessary and sufficient.
180            std::ptr::read_volatile(cpu_id_ptr)
181        }
182    }
183
184    /// Activate a critical section.
185    ///
186    /// The critical section is defined by the `critical_section` parameter. The critical section is
187    /// active until the returned `RseqScope` is dropped.
188    ///
189    /// # Safety
190    ///
191    /// The caller must ensure that the returned `RseqScope` is dropped before calling
192    /// `rseq_unregister_thread()`.
193    #[must_use = "The critical section is active only until the returned `RseqScope` is dropped."]
194    pub unsafe fn activate(&self, critical_section: RseqCriticalSection) -> RseqScope {
195        self.set_critical_section(critical_section);
196        RseqScope { rseq: *self }
197    }
198
199    fn set_critical_section(&self, critical_section: RseqCriticalSection) {
200        let abi = self.as_ptr();
201        unsafe {
202            let start_ip_ptr: *mut u64 = std::ptr::addr_of_mut!((*abi).start_ip);
203            let post_commit_offset_ptr: *mut u64 =
204                std::ptr::addr_of_mut!((*abi).post_commit_offset);
205            let abort_ip_ptr: *mut u64 = std::ptr::addr_of_mut!((*abi).abort_ip);
206
207            // The kernel reads these values on the current thread, which means a volatile
208            // write is necessary and sufficient.
209
210            // We first set the post-commit offset to 0, which prevents the kernel from entering
211            // the critical section. Then we set the start and abort IPs, and finally the
212            // post-commit offset, which enables the critical section.
213            std::ptr::write_volatile(post_commit_offset_ptr, 0);
214            std::ptr::write_volatile(start_ip_ptr, critical_section.start_ip);
215            std::ptr::write_volatile(abort_ip_ptr, critical_section.abort_ip);
216            std::ptr::write_volatile(post_commit_offset_ptr, critical_section.post_commit_offset);
217        }
218    }
219}
220
221/// The bounds of a critical section.
222#[derive(Debug, Default, Clone, Copy)]
223pub struct RseqCriticalSection {
224    /// The address of the start of the critical section.
225    start_ip: u64,
226
227    /// The offset from the start of the critical section to the post-commit label.
228    post_commit_offset: u64,
229
230    /// The offset from the start of the critical section to the abort label.
231    abort_ip: u64,
232}
233
234impl RseqCriticalSection {
235    /// Creates a new critical section.
236    ///
237    /// # Safety
238    ///
239    /// The caller must ensure that the `start_ip`, `post_commit_offset`, and `abort_ip`
240    /// parameters correctly define the critical section.
241    pub unsafe fn new(start_ip: u64, post_commit_offset: u64, abort_ip: u64) -> Self {
242        Self { start_ip, post_commit_offset, abort_ip }
243    }
244}
245
246/// A RAII guard that clears the critical section bounds when dropped.
247pub struct RseqScope {
248    rseq: Rseq,
249}
250
251impl Drop for RseqScope {
252    fn drop(&mut self) {
253        self.rseq.set_critical_section(RseqCriticalSection::default());
254    }
255}
256
257/// Register the current thread for restartable sequences.
258///
259/// Returns an [`Rseq`] handle for the newly registered thread.
260///
261/// # Panics
262///
263/// Panics if the thread is already registered, if the maximum number of supported
264/// threads (`MAX_THREADS`) has been exhausted, or if setting the thread RSEQ via syscall fails.
265pub fn rseq_register_thread() -> Rseq {
266    RSEQ.with(|rseq| {
267        assert!(rseq.get().is_null(), "thread already registered");
268    });
269
270    let (vmo_handle, slot, abi) = {
271        let mut allocator = ALLOCATOR.lock();
272
273        let vmo_handle = allocator.vmo_handle();
274        let slot = allocator.allocate();
275        let abi = allocator.abi(&slot);
276
277        (vmo_handle, slot, abi)
278    };
279
280    let status = unsafe {
281        zx::sys::zx_thread_set_rseq(vmo_handle, slot.offset, mem::size_of::<zx_rseq_t>() as u64)
282    };
283
284    zx::Status::ok(status).expect("failed to register thread for RSEQ");
285
286    RSEQ.with(|rseq| {
287        rseq.set(abi);
288    });
289
290    Rseq { abi: NonNull::new(abi).expect("RSEQ slot pointer must be non-null") }
291}
292
293/// Register the current thread for restartable sequences with an initial critical section.
294///
295/// Returns an [`Rseq`] handle for the newly registered thread.
296///
297/// # Panics
298///
299/// Panics if the thread is already registered, if the maximum number of supported
300/// threads (`MAX_THREADS`) has been exhausted, or if setting the thread RSEQ via syscall fails.
301pub fn rseq_register_thread_with_cs(critical_section: RseqCriticalSection) -> Rseq {
302    let rseq = rseq_register_thread();
303    rseq.set_critical_section(critical_section);
304    rseq
305}
306
307/// Unregister the current thread from the restartable sequence.
308///
309/// # Panics
310///
311/// Panics if the thread is not registered, or if unsetting the thread RSEQ via syscall fails.
312///
313/// # Safety
314///
315/// Any previously obtained `Rseq` handles for this thread must not be used after calling this
316/// function.
317pub unsafe fn rseq_unregister_thread() {
318    let abi = RSEQ.with(|rseq| rseq.take());
319    assert!(!abi.is_null(), "thread not registered");
320
321    // SAFETY: Passing zeroes clears the current thread's RSEQ registration in Zircon.
322    let status = unsafe { zx::sys::zx_thread_set_rseq(0, 0, 0) };
323    zx::Status::ok(status).expect("failed to unregister thread from RSEQ");
324
325    // Zero out the struct to let Zircon's zero-page scanner reclaim the page.
326    // SAFETY: `abi` was allocated for this thread's RSEQ slot and is non-null. The kernel
327    // has unregistered the thread, so no concurrent accesses to this memory remain.
328    unsafe {
329        std::ptr::write_volatile(abi, mem::zeroed());
330    }
331
332    ALLOCATOR.lock().free(abi);
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use std::arch::global_asm;
339
340    unsafe extern "C" {
341        fn rseq_test_critical_section(counters: *mut u64, rseq_ptr: *mut zx_rseq_t);
342        static rseq_test_start: u8;
343        static rseq_test_end: u8;
344    }
345
346    #[cfg(target_arch = "x86_64")]
347    global_asm!(
348        ".globl rseq_test_critical_section",
349        ".globl rseq_test_start",
350        ".globl rseq_test_end",
351        "rseq_test_critical_section:",
352        "rseq_test_start:",
353        // Arguments: rdi = counters, rsi = rseq_ptr
354        "mov ecx, [rsi]",      // READ cpu_id from rseq_ptr into ecx
355        "shl rcx, 3",          // multiply by 8
356        "add rcx, rdi",        // address of counter
357        "inc qword ptr [rcx]", // increment counter
358        "rseq_test_end:",
359        "ret",
360    );
361
362    #[cfg(target_arch = "aarch64")]
363    global_asm!(
364        ".globl rseq_test_critical_section",
365        ".globl rseq_test_start",
366        ".globl rseq_test_end",
367        "rseq_test_critical_section:",
368        "rseq_test_start:",
369        // x0 = counters, x1 = rseq_ptr
370        "ldr w2, [x1]",   // read cpu_id
371        "lsl x2, x2, #3", // multiply by 8
372        "add x2, x0, x2", // address of counter
373        "ldr x3, [x2]",
374        "add x3, x3, #1",
375        "str x3, [x2]",
376        "rseq_test_end:",
377        "ret",
378    );
379
380    #[cfg(target_arch = "riscv64")]
381    global_asm!(
382        ".globl rseq_test_critical_section",
383        ".globl rseq_test_start",
384        ".globl rseq_test_end",
385        "rseq_test_critical_section:",
386        "rseq_test_start:",
387        // a0 = counters, a1 = rseq_ptr
388        "lw t0, 0(a1)",   // read cpu_id
389        "slli t0, t0, 3", // multiply by 8
390        "add t0, a0, t0", // address of counter
391        "ld t1, 0(t0)",
392        "addi t1, t1, 1",
393        "sd t1, 0(t0)",
394        "rseq_test_end:",
395        "ret",
396    );
397
398    struct TestRegistrationGuard;
399
400    impl TestRegistrationGuard {
401        fn new() -> Self {
402            rseq_register_thread();
403            Self
404        }
405    }
406
407    impl Drop for TestRegistrationGuard {
408        fn drop(&mut self) {
409            RSEQ.with(|rseq| {
410                if !rseq.get().is_null() {
411                    // SAFETY: The test guard is dropped when the test finishes or cleans up,
412                    // and no Rseq handles are retained after this drop.
413                    unsafe {
414                        rseq_unregister_thread();
415                    }
416                }
417            });
418        }
419    }
420
421    #[test]
422    #[should_panic = "thread already registered"]
423    fn test_double_registration_panics() {
424        let _guard = TestRegistrationGuard::new();
425        rseq_register_thread();
426    }
427
428    #[test]
429    #[should_panic = "thread not registered"]
430    fn test_unregistered_unregister_panics() {
431        // SAFETY: Testing unregister on an unregistered thread panics before performing any unsafe operations.
432        unsafe {
433            rseq_unregister_thread();
434        }
435    }
436
437    #[test]
438    fn test_current_cpu() {
439        let _guard = TestRegistrationGuard::new();
440        unsafe {
441            let rseq = Rseq::get();
442            let cpu_id = rseq.current_cpu();
443            assert_ne!(cpu_id, zx_types::ZX_INFO_INVALID_CPU);
444        }
445    }
446
447    #[test]
448    fn test_activate_field_propagation() {
449        let _guard = TestRegistrationGuard::new();
450        let rseq = unsafe { Rseq::get() };
451        let cs = unsafe { RseqCriticalSection::new(10, 20, 30) };
452
453        let scope = unsafe { rseq.activate(cs) };
454        let abi = rseq.as_ptr();
455        unsafe {
456            assert_eq!((*abi).start_ip, 10);
457            assert_eq!((*abi).abort_ip, 30);
458            assert_eq!((*abi).post_commit_offset, 20);
459        }
460
461        drop(scope);
462        unsafe {
463            assert_eq!((*abi).start_ip, 0);
464            assert_eq!((*abi).abort_ip, 0);
465            assert_eq!((*abi).post_commit_offset, 0);
466        }
467    }
468
469    #[test]
470    fn test_rseq_per_cpu_counter() {
471        let _guard = TestRegistrationGuard::new();
472
473        let rseq = unsafe { Rseq::get() };
474        let cpu_count = zx::system_get_num_cpus();
475        let mut counters = vec![0u64; cpu_count as usize];
476
477        unsafe {
478            let start = &rseq_test_start as *const u8 as u64;
479            let end = &rseq_test_end as *const u8 as u64;
480
481            let cs = RseqCriticalSection::new(start, end - start, start);
482            let _scope = rseq.activate(cs);
483
484            // Execute the critical section
485            rseq_test_critical_section(counters.as_mut_ptr(), rseq.as_ptr());
486        }
487
488        let sum = counters.iter().sum::<u64>();
489        assert_eq!(sum, 1, "Sum of counters should be 1");
490    }
491
492    #[test]
493    fn test_rseq_packing_and_reuse() {
494        for _ in 0..10 {
495            let _guard = TestRegistrationGuard::new();
496        }
497
498        let threads: Vec<_> = (0..200)
499            .map(|_| {
500                std::thread::spawn(|| {
501                    let _guard = TestRegistrationGuard::new();
502                    let cpu_id = unsafe { Rseq::get().current_cpu() };
503                    assert_ne!(cpu_id, zx_types::ZX_INFO_INVALID_CPU);
504                })
505            })
506            .collect();
507
508        for t in threads {
509            t.join().unwrap();
510        }
511    }
512}