Skip to main content

starnix_core/task/
thread_lockup_detector.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
5//! This module implements a lockup detector for Starnix kernel threads.
6//! It tracks the operations running on threads and reports threads that run for too long
7//! without pausing or stopping the operation.
8//!
9//! It uses a global registry to track active operations across all threads.
10
11use pin_project::pin_project;
12use starnix_sync::{LockDepRwLock, ThreadLockupDetectorRegistryLock};
13use std::borrow::Borrow;
14use std::cell::{Cell, RefCell};
15use std::collections::{HashMap, HashSet};
16use std::sync::LazyLock;
17use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering};
18
19#[derive(Default)]
20pub struct ThreadLockupDetector;
21
22/// Thread-local state that registers the thread in the global registry on creation
23/// and removes it on drop.
24struct ThreadState {
25    /// The KOID of the thread, used as the key for removal in `Drop`.
26    koid: zx::Koid,
27}
28
29impl ThreadState {
30    /// Creates a new `ThreadState`, registering the current thread in the global `REGISTRY`.
31    fn new() -> Self {
32        let handle = fuchsia_runtime::with_thread_self(|thread| thread.raw_handle());
33        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
34        let ptr = OPERATION_GENERATION.with(|atomic| atomic as *const AtomicU64);
35
36        let mut rcu_nesting_level = std::ptr::null();
37        let mut rcu_counter_index = std::ptr::null();
38        fuchsia_rcu::with_thread_block_counters(|nesting_ptr, counter_ptr| {
39            rcu_nesting_level = nesting_ptr;
40            rcu_counter_index = counter_ptr;
41        });
42
43        let registered = RegisteredThread {
44            // SAFETY: The handle is valid as long as the thread is registered.
45            thread: unsafe { zx::Unowned::from_raw_handle(handle) },
46            koid,
47            atomic: ptr,
48            rcu_nesting_level,
49            rcu_counter_index,
50        };
51        REGISTRY.write().insert(registered);
52        Self { koid }
53    }
54}
55
56impl Drop for ThreadState {
57    /// Removes the thread from the global `REGISTRY` when the thread exits.
58    fn drop(&mut self) {
59        REGISTRY.write().remove(&self.koid);
60        IS_REGISTERED.set(false);
61    }
62}
63
64thread_local! {
65    static THREAD_STATE: RefCell<Option<ThreadState>> = const { RefCell::new(None) };
66
67    /// The generation of the operation running on the current thread, or 0 if no operation is
68    /// running.
69    ///
70    /// This is a `const`-initialized thread-local without a destructor, so accessing it is cheap and
71    /// its storage remains valid until the thread exits. `ThreadState::new` registers its address
72    /// in `REGISTRY`, and dropping the `ThreadState` removes it again.
73    static OPERATION_GENERATION: AtomicU64 = const { AtomicU64::new(0) };
74
75    /// Last generation published by the current thread (never 0 once an operation started).
76    static LAST_GENERATION: Cell<u64> = const { Cell::new(0) };
77
78    /// Whether the current thread's `OPERATION_GENERATION` is registered in `REGISTRY`.
79    static IS_REGISTERED: Cell<bool> = const { Cell::new(false) };
80}
81
82/// The information stored in the global registry for each tracked thread.
83#[derive(Clone)]
84struct RegisteredThread {
85    /// An unowned handle to the thread, used for inspection.
86    thread: zx::Unowned<'static, zx::Thread>,
87    /// The KOID of the thread.
88    koid: zx::Koid,
89    /// Pointer to the thread's `OPERATION_GENERATION` thread-local.
90    atomic: *const AtomicU64,
91    /// Pointer to the RCU nesting level.
92    rcu_nesting_level: *const AtomicUsize,
93    /// Pointer to the RCU counter index.
94    rcu_counter_index: *const AtomicU8,
95}
96
97// We only hash and compare by `koid` to allow lookup and removal by `koid`
98// in the `HashSet`.
99impl std::hash::Hash for RegisteredThread {
100    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
101        self.koid.hash(state);
102    }
103}
104
105impl PartialEq for RegisteredThread {
106    fn eq(&self, other: &Self) -> bool {
107        self.koid == other.koid
108    }
109}
110
111impl Eq for RegisteredThread {}
112
113impl Borrow<zx::Koid> for RegisteredThread {
114    fn borrow(&self) -> &zx::Koid {
115        &self.koid
116    }
117}
118
119// SAFETY: Access to the pointers in the global REGISTRY is protected by a LockDepRwLock,
120// ensuring that a thread cannot free its data while another thread is reading it.
121unsafe impl Send for RegisteredThread {}
122// SAFETY: Same as above.
123unsafe impl Sync for RegisteredThread {}
124
125#[derive(Clone, Debug)]
126pub struct ThreadLockupInfo {
127    pub thread: zx::Unowned<'static, zx::Thread>,
128    pub koid: zx::Koid,
129    /// When `get_long_running_threads` first observed the operation. The operation started at or
130    /// before this time.
131    pub start_time: zx::MonotonicInstant,
132}
133
134/// For each thread running an operation, the generation of that operation and the time at which
135/// `get_long_running_threads` first observed it.
136static FIRST_SEEN: LazyLock<fuchsia_sync::Mutex<HashMap<zx::Koid, (u64, zx::MonotonicInstant)>>> =
137    LazyLock::new(|| Default::default());
138
139/// Global registry of all tracked threads.
140static REGISTRY: LazyLock<
141    LockDepRwLock<HashSet<RegisteredThread>, ThreadLockupDetectorRegistryLock>,
142> = LazyLock::new(|| Default::default());
143
144impl ThreadLockupDetector {
145    /// Starts an operation by publishing a new, non-zero operation generation in the
146    /// thread-local atomic.
147    ///
148    /// This runs on every syscall, so it avoids reading the monotonic clock, which is a
149    /// significant part of the cost of a trivial syscall. Instead, `get_long_running_threads`
150    /// timestamps each operation the first time it observes it.
151    #[inline]
152    fn start_operation() {
153        if !IS_REGISTERED.get() {
154            Self::register_current_thread();
155        }
156        let next = LAST_GENERATION.get().wrapping_add(1).max(1);
157        LAST_GENERATION.set(next);
158        OPERATION_GENERATION.with(|gen_atomic| gen_atomic.store(next, Ordering::Relaxed));
159    }
160
161    /// Registers the current thread in `REGISTRY` the first time it starts an operation.
162    #[cold]
163    #[inline(never)]
164    fn register_current_thread() {
165        // `try_with` fails once `THREAD_STATE` has been destroyed during thread exit. The thread
166        // is no longer tracked from that point on.
167        let _ = THREAD_STATE.try_with(|state| {
168            state.borrow_mut().get_or_insert_with(ThreadState::new);
169            IS_REGISTERED.set(true);
170        });
171    }
172
173    /// Stops an operation by storing 0 in the thread-local atomic.
174    #[inline]
175    fn stop_operation() {
176        OPERATION_GENERATION.with(|gen_atomic| gen_atomic.store(0, Ordering::Relaxed));
177    }
178
179    /// Iterates over the registry, finds threads that have been running an operation for at least
180    /// the threshold, and returns their `ThreadLockupInfo`.
181    ///
182    /// Operations are timestamped by the first call that observes them, not when they start: an
183    /// operation is reported once the call that first observed it is at least `threshold` old.
184    /// When called periodically, this delays reports by at most one period.
185    pub fn get_long_running_threads(threshold: zx::MonotonicDuration) -> Vec<ThreadLockupInfo> {
186        let now = zx::MonotonicInstant::get();
187        let registry = REGISTRY.read();
188        let mut first_seen = FIRST_SEEN.lock();
189        // Forget threads that are idle or gone.
190        first_seen.retain(|koid, _| {
191            registry.get(koid).is_some_and(|registered| {
192                // SAFETY: We hold the read lock on REGISTRY, so the thread-local is still alive
193                // (see below).
194                unsafe { &*registered.atomic }.load(Ordering::Relaxed) != 0
195            })
196        });
197        registry
198            .iter()
199            .filter_map(|registered| {
200                // SAFETY: We hold the read lock on REGISTRY. Any thread exiting must
201                // acquire the write lock to remove its pointer before freeing the memory.
202                // So the pointer is valid as long as we hold the read lock.
203                let atomic = unsafe { &*registered.atomic };
204                let generation = atomic.load(Ordering::Relaxed);
205                if generation == 0 {
206                    return None;
207                }
208                // The operation started no later than the first scan that observed this
209                // generation.
210                let entry = first_seen.entry(registered.koid).or_insert((generation, now));
211                if entry.0 != generation {
212                    *entry = (generation, now);
213                }
214                let start_time = entry.1;
215                if now - start_time >= threshold {
216                    Some(ThreadLockupInfo {
217                        thread: registered.thread.clone(),
218                        koid: registered.koid,
219                        start_time,
220                    })
221                } else {
222                    None
223                }
224            })
225            .collect()
226    }
227
228    /// Starts tracking the current operation on the current thread.
229    /// Returns a guard that stops tracking when dropped.
230    pub fn track() -> LockupDetectorGuard {
231        LockupDetectorGuard::new()
232    }
233
234    /// Pauses tracking for the current operation on the current thread.
235    /// Returns a guard that resumes tracking when dropped.
236    pub fn pause_tracking() -> LockupDetectorWaitingGuard {
237        LockupDetectorWaitingGuard::new()
238    }
239
240    /// Wraps a future to track its execution when polled.
241    pub fn track_future<F>(inner: F) -> LockupDetectorFuture<F> {
242        LockupDetectorFuture::new(inner)
243    }
244
245    /// Creates a channel where the receiver pauses tracking while waiting for messages.
246    pub fn tracked_channel<T>() -> (std::sync::mpsc::Sender<T>, LockupDetectorReceiver<T>) {
247        let (sender, receiver) = std::sync::mpsc::channel();
248        (sender, LockupDetectorReceiver::new(receiver))
249    }
250
251    pub fn active_rcu_read_locks<F>(mut check: F)
252    where
253        F: FnMut(&zx::Thread, zx::Koid, u8),
254    {
255        let registry = REGISTRY.read();
256        for registered in registry.iter() {
257            if registered.rcu_nesting_level.is_null() || registered.rcu_counter_index.is_null() {
258                continue;
259            }
260            // SAFETY: The pointers point to thread-local storage of the registered thread.
261            // Before the thread exits, its `ThreadState` is dropped, which acquires a write
262            // lock on `REGISTRY` before the thread-local storage is destroyed. Since we hold the
263            // read lock on `REGISTRY` here, the thread cannot complete its cleanup and destroy the
264            // TLS until we release the lock, ensuring the pointers remain valid.
265            let (nesting_level, counter_index) = unsafe {
266                (
267                    (*registered.rcu_nesting_level).load(Ordering::Relaxed),
268                    (*registered.rcu_counter_index).load(Ordering::Relaxed),
269                )
270            };
271            if nesting_level > 0 {
272                check(&registered.thread, registered.koid, counter_index);
273            }
274        }
275    }
276}
277
278pub struct LockupDetectorGuard;
279
280impl LockupDetectorGuard {
281    fn new() -> Self {
282        ThreadLockupDetector::start_operation();
283        Self
284    }
285}
286
287impl Drop for LockupDetectorGuard {
288    fn drop(&mut self) {
289        ThreadLockupDetector::stop_operation();
290    }
291}
292
293pub struct LockupDetectorWaitingGuard;
294
295impl LockupDetectorWaitingGuard {
296    fn new() -> Self {
297        ThreadLockupDetector::stop_operation();
298        Self
299    }
300}
301
302impl Drop for LockupDetectorWaitingGuard {
303    fn drop(&mut self) {
304        ThreadLockupDetector::start_operation();
305    }
306}
307
308#[pin_project]
309pub struct LockupDetectorFuture<F> {
310    #[pin]
311    inner: F,
312}
313
314impl<F> LockupDetectorFuture<F> {
315    fn new(inner: F) -> Self {
316        Self { inner }
317    }
318}
319
320impl<F: std::future::Future> std::future::Future for LockupDetectorFuture<F> {
321    type Output = F::Output;
322
323    fn poll(
324        self: std::pin::Pin<&mut Self>,
325        cx: &mut std::task::Context<'_>,
326    ) -> std::task::Poll<Self::Output> {
327        let _guard = LockupDetectorGuard::new();
328        let this = self.project();
329        this.inner.poll(cx)
330    }
331}
332
333pub struct LockupDetectorReceiver<T> {
334    inner: std::sync::mpsc::Receiver<T>,
335}
336
337impl<T> LockupDetectorReceiver<T> {
338    fn new(inner: std::sync::mpsc::Receiver<T>) -> Self {
339        Self { inner }
340    }
341
342    pub fn recv(&self) -> Result<T, std::sync::mpsc::RecvError> {
343        let _guard = LockupDetectorWaitingGuard::new();
344        self.inner.recv()
345    }
346
347    pub fn try_iter(&self) -> std::sync::mpsc::TryIter<'_, T> {
348        self.inner.try_iter()
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    fn get_long_running_koids() -> Vec<zx::Koid> {
357        ThreadLockupDetector::get_long_running_threads(zx::MonotonicDuration::from_nanos(0))
358            .iter()
359            .map(|r| r.koid)
360            .collect()
361    }
362
363    #[test]
364    fn test_lockup_detector() {
365        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
366
367        {
368            let _guard = ThreadLockupDetector::track();
369
370            // Exceed threshold immediately with zero duration.
371            assert!(get_long_running_koids().contains(&koid));
372
373            // After triggering, it still contains it (we don't reset).
374            assert!(get_long_running_koids().contains(&koid));
375        }
376
377        // Guard dropped.
378        assert!(get_long_running_koids().is_empty());
379    }
380
381    #[test]
382    fn test_guard() {
383        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
384
385        {
386            let _guard = ThreadLockupDetector::track();
387            assert!(get_long_running_koids().contains(&koid));
388        }
389
390        // Guard dropped.
391        assert!(get_long_running_koids().is_empty());
392    }
393
394    #[test]
395    fn test_start_time_is_recorded() {
396        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
397
398        let _guard = ThreadLockupDetector::track();
399        let before = zx::MonotonicInstant::get();
400        let first =
401            ThreadLockupDetector::get_long_running_threads(zx::MonotonicDuration::from_nanos(0))
402                .into_iter()
403                .find(|info| info.koid == koid)
404                .expect("current thread is tracked");
405        let after = zx::MonotonicInstant::get();
406        assert!(before <= first.start_time && first.start_time <= after);
407
408        // Subsequent scans of the same operation generation preserve the first-seen timestamp.
409        std::thread::sleep(std::time::Duration::from_millis(1));
410        let second =
411            ThreadLockupDetector::get_long_running_threads(zx::MonotonicDuration::from_nanos(0))
412                .into_iter()
413                .find(|info| info.koid == koid)
414                .expect("current thread is still tracked");
415        assert_eq!(first.start_time, second.start_time);
416    }
417
418    #[test]
419    fn test_new_operation_resets_start_time() {
420        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
421        let start_time = || {
422            ThreadLockupDetector::get_long_running_threads(zx::MonotonicDuration::from_nanos(0))
423                .into_iter()
424                .find(|info| info.koid == koid)
425                .expect("current thread is tracked")
426                .start_time
427        };
428
429        let first = {
430            let _guard = ThreadLockupDetector::track();
431            start_time()
432        };
433        std::thread::sleep(std::time::Duration::from_millis(1));
434
435        // A new operation does not inherit the timestamp of the previous one.
436        let _guard = ThreadLockupDetector::track();
437        assert!(start_time() > first);
438    }
439
440    #[test]
441    fn test_thread_exit_unregisters_thread() {
442        let koid = std::thread::spawn(|| {
443            let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
444            // Leak the guard so that the operation is still active when the thread exits.
445            std::mem::forget(ThreadLockupDetector::track());
446            assert!(REGISTRY.read().contains(&koid));
447            koid
448        })
449        .join()
450        .unwrap();
451
452        // Exiting the thread must remove the pointer to its thread-local from the registry.
453        assert!(!REGISTRY.read().contains(&koid));
454        assert!(!get_long_running_koids().contains(&koid));
455    }
456
457    #[test]
458    fn test_waiting_guard() {
459        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
460
461        let _guard = ThreadLockupDetector::track();
462
463        {
464            let _waiting_guard = ThreadLockupDetector::pause_tracking();
465            // Operation stopped during wait.
466            assert!(get_long_running_koids().is_empty());
467        }
468
469        // Guard dropped, operation restarted.
470        assert!(get_long_running_koids().contains(&koid));
471    }
472
473    #[test]
474    fn test_track_future() {
475        let (koid_tx, koid_rx) = std::sync::mpsc::channel();
476        let (signal_tx, signal_rx) = futures::channel::oneshot::channel::<()>();
477
478        let t = std::thread::spawn(move || {
479            let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
480            koid_tx.send(koid).unwrap();
481
482            let fut = ThreadLockupDetector::track_future(async move {
483                signal_rx.await.unwrap();
484            });
485
486            fuchsia_async::LocalExecutor::default().run_singlethreaded(fut);
487
488            koid
489        });
490
491        let spawned_koid = koid_rx.recv().unwrap();
492
493        // Wait a bit to ensure it entered the future and is waiting.
494        std::thread::sleep(std::time::Duration::from_millis(100));
495
496        // Check that spawned_koid is NOT in long running koids.
497        assert!(!get_long_running_koids().contains(&spawned_koid));
498
499        // Now signal to unblock it.
500        signal_tx.send(()).unwrap();
501
502        t.join().unwrap();
503    }
504
505    #[test]
506    fn test_track_future_polling() {
507        let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
508
509        // Before polling, should not be found.
510        assert!(!get_long_running_koids().contains(&koid));
511
512        let fut = ThreadLockupDetector::track_future(async {
513            assert!(get_long_running_koids().contains(&koid));
514        });
515
516        fuchsia_async::LocalExecutor::default().run_singlethreaded(fut);
517
518        // After polling, should not be found.
519        assert!(!get_long_running_koids().contains(&koid));
520    }
521
522    #[test]
523    fn test_track_channel() {
524        let (koid_tx, koid_rx) = std::sync::mpsc::channel();
525        let (tx, rx) = ThreadLockupDetector::tracked_channel();
526
527        let t = std::thread::spawn(move || {
528            let koid = fuchsia_runtime::with_thread_self(|thread| thread.koid()).unwrap();
529            koid_tx.send(koid).unwrap();
530
531            let _guard = ThreadLockupDetector::track();
532
533            // This will block.
534            rx.recv().unwrap();
535
536            koid
537        });
538
539        let spawned_koid = koid_rx.recv().unwrap();
540
541        // Wait a bit to ensure it entered rx.recv()
542        std::thread::sleep(std::time::Duration::from_millis(100));
543
544        // Check that spawned_koid is NOT in long running koids.
545        assert!(!get_long_running_koids().contains(&spawned_koid));
546
547        // Now send data to unblock it.
548        tx.send(()).unwrap();
549
550        t.join().unwrap();
551    }
552}