strat9_kernel/sync/spinlock.rs
1//! Generic spinlock with configurable guard behaviour.
2//!
3//! # Choosing a guardian
4//!
5//! ```text
6//! SpinLock<T> => SpinLock<T, IrqDisabled> (default)
7//! SpinLock<T, IrqDisabled> => saves RFLAGS + clears IF (equiv. spin_lock_irqsave)
8//! SpinLock<T, PreemptDisabled> => disables preemption only, IRQs untouched
9//! ```
10//!
11//! **Use `IrqDisabled` (the default) for**:
12//! - Data shared across CPUs (heap, VFS, IPC queues, network rings, …)
13//! - Any data touched from interrupt handlers
14//!
15//! **Use `PreemptDisabled` for**:
16//! - Per-CPU data never accessed from interrupt handlers
17//! (scheduler run-queues, per-CPU frame caches, statistics counters …)
18//!
19//! All call sites that use `SpinLock<T>` without a type argument continue to
20//! compile unchanged : `IrqDisabled` is the default guardian.
21//!
22//! # Debug helpers
23//!
24//! `debug_set_watch_lock_addr` / `debug_clear_watch_lock_addr` emit a serial
25//! trace on every `drop` of a specific lock instance : useful when hunting
26//! deadlocks.
27
28use super::{
29 guardian::{Guardian, GuardianState, IrqDisabled},
30 IrqDisabledToken,
31};
32use core::{
33 cell::UnsafeCell,
34 marker::PhantomData,
35 mem::ManuallyDrop,
36 ops::{Deref, DerefMut},
37 sync::atomic::{AtomicBool, AtomicUsize, Ordering},
38};
39
40static DEBUG_WATCH_LOCK_ADDR: AtomicUsize = AtomicUsize::new(usize::MAX);
41
42// =========================== SpinLock ========================================
43
44/// A spinlock parameterised by a [`Guardian`].
45///
46/// The default guardian is [`IrqDisabled`], preserving existing call-site
47/// semantics with no source changes required.
48///
49/// Supports `T: ?Sized` for `SpinLock<dyn Trait>` and other DST types.
50/// The `data` field is last so that the struct can hold dynamically sized types.
51pub struct SpinLock<T: ?Sized, G: Guardian = IrqDisabled> {
52 locked: AtomicBool,
53 /// CPU index of the current lock holder (`usize::MAX` when unlocked).
54 /// Used for deadlock diagnostics only.
55 owner_cpu: AtomicUsize,
56 _guardian: PhantomData<G>,
57 /// Must be the last field: when `T: ?Sized`, this is a DST and Rust
58 /// requires the dynamically sized field to be last in the struct.
59 data: UnsafeCell<T>,
60}
61
62// SAFETY: The guardian ensures mutual exclusion; T must itself be Send.
63unsafe impl<T: ?Sized + Send, G: Guardian> Sync for SpinLock<T, G> {}
64unsafe impl<T: ?Sized + Send, G: Guardian> Send for SpinLock<T, G> {}
65
66impl<T, G: Guardian> SpinLock<T, G> {
67 /// Create a new, unlocked spinlock.
68 ///
69 /// `T` must be `Sized` here because we construct a new value.
70 pub const fn new(data: T) -> Self {
71 SpinLock {
72 locked: AtomicBool::new(false),
73 owner_cpu: AtomicUsize::new(usize::MAX),
74 _guardian: PhantomData,
75 data: UnsafeCell::new(data),
76 }
77 }
78}
79
80impl<T: ?Sized, G: Guardian> SpinLock<T, G> {
81 /// Acquire the lock, spinning until available.
82 ///
83 /// The guardian's `enter()` hook runs **before** the spin loop so that the
84 /// CPU is already in the protected mode while we wait. This closes the
85 /// window where an IRQ or preempt-switch could occur between protect and
86 /// acquire.
87 pub fn lock(&self) -> SpinLockGuard<'_, T, G> {
88 let state = G::enter();
89 let mut spins: usize = 0;
90 let this_cpu = crate::arch::percpu::current_cpu_index();
91
92 while self
93 .locked
94 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
95 .is_err()
96 {
97 spins = spins.saturating_add(1);
98 if spins == 5_000_000 {
99 let owner = self.owner_cpu.load(Ordering::Relaxed);
100 crate::serial_println!(
101 "[trace][spin] long-wait lock={:#x} cpu={} owner_cpu={}",
102 self as *const _ as *const () as usize,
103 this_cpu,
104 owner,
105 );
106 spins = 0;
107 }
108 core::hint::spin_loop();
109 }
110 self.owner_cpu.store(this_cpu, Ordering::Relaxed);
111 // Race/corruption diagnostic: E9 trace when watched locks are acquired.
112 emit_trace_e9(self as *const _ as *const () as usize, 0);
113
114 SpinLockGuard {
115 lock: self,
116 state: ManuallyDrop::new(state),
117 }
118 }
119
120 /// Try to acquire the lock without spinning.
121 pub fn try_lock(&self) -> Option<SpinLockGuard<'_, T, G>> {
122 let state = G::enter();
123 if self
124 .locked
125 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
126 .is_ok()
127 {
128 let this_cpu = crate::arch::percpu::current_cpu_index();
129 self.owner_cpu.store(this_cpu, Ordering::Relaxed);
130 emit_trace_e9(self as *const _ as *const () as usize, 0);
131 Some(SpinLockGuard {
132 lock: self,
133 state: ManuallyDrop::new(state),
134 })
135 } else {
136 G::exit(state);
137 None
138 }
139 }
140
141 /// Returns the owner CPU index (`usize::MAX` if unlocked).
142 pub fn owner_cpu(&self) -> usize {
143 self.owner_cpu.load(Ordering::Relaxed)
144 }
145
146 /// Returns a mutable reference to the underlying data.
147 ///
148 /// By holding `&mut self`, the compiler guarantees exclusive access to the
149 /// lock; no other reference exists, so the inner data can be accessed
150 /// without acquiring the lock.
151 pub fn get_mut(&mut self) -> &mut T {
152 self.data.get_mut()
153 }
154}
155
156// =========================== IrqDisabled-specific extensions
157
158impl<T: ?Sized> SpinLock<T, IrqDisabled> {
159 /// Try to acquire without touching RFLAGS.
160 ///
161 /// Returns `None` if IRQs are currently enabled (no `IrqDisabledToken` can
162 /// be produced) or the lock is already held. The caller must ensure that
163 /// IRQs remain disabled for the entire lifetime of the returned guard.
164 pub fn try_lock_no_irqsave(&self) -> Option<SpinLockGuard<'_, T, IrqDisabled>> {
165 let token = match IrqDisabledToken::verify() {
166 Some(token) => token,
167 None => {
168 // Diagnostic only: distinguish "IRQs enabled" from ordinary
169 // lock contention at call sites that intentionally treat both
170 // as a best-effort `None` return in hot paths.
171 // F10: ring-0 port I/O compiled out under kernel_l2_host.
172 #[cfg(not(kernel_l2_host))]
173 unsafe {
174 core::arch::asm!("mov al, 'V'; out 0xe9, al", out("al") _)
175 };
176 #[cfg(kernel_l2_host)]
177 let _ = 0u8;
178 return None;
179 }
180 };
181 if self
182 .locked
183 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
184 .is_ok()
185 {
186 let this_cpu = crate::arch::percpu::current_cpu_index();
187 self.owner_cpu.store(this_cpu, Ordering::Relaxed);
188 emit_trace_e9(self as *const _ as *const () as usize, 0);
189 Some(SpinLockGuard {
190 lock: self,
191 state: ManuallyDrop::new(GuardianState {
192 token,
193 saved_flags: 0,
194 restore_flags: false,
195 }),
196 })
197 } else {
198 None
199 }
200 }
201}
202
203// =========================== Debug helpers ====================================
204
205/// Register a lock address to trace on every drop (serial console output).
206pub fn debug_set_watch_lock_addr(addr: usize) {
207 DEBUG_WATCH_LOCK_ADDR.store(addr, Ordering::Relaxed);
208}
209
210/// Clear the watched lock address.
211pub fn debug_clear_watch_lock_addr() {
212 DEBUG_WATCH_LOCK_ADDR.store(usize::MAX, Ordering::Relaxed);
213}
214
215/// Maximum number of locks that can be traced simultaneously via E9 port.
216const DEBUG_TRACE_WATCH_SLOTS: usize = 8;
217
218/// Fixed-size array of watched lock addresses for E9 trace.
219/// Each slot emits a unique ASCII tag ('A'..'H') on acquire/release.
220static DEBUG_TRACE_WATCH_ADDRS: [AtomicUsize; DEBUG_TRACE_WATCH_SLOTS] =
221 [const { AtomicUsize::new(usize::MAX) }; DEBUG_TRACE_WATCH_SLOTS];
222
223/// Register a lock address for E9 trace. Returns the slot index (0..7),
224/// or `None` if all slots are in use.
225pub fn debug_set_trace_lock_addr(addr: usize) -> Option<usize> {
226 for (i, slot) in DEBUG_TRACE_WATCH_ADDRS.iter().enumerate() {
227 if slot.load(Ordering::Relaxed) == usize::MAX {
228 slot.store(addr, Ordering::Relaxed);
229 return Some(i);
230 }
231 }
232 None
233}
234
235/// Clear a specific trace slot by index.
236pub fn debug_clear_trace_slot(index: usize) {
237 if index < DEBUG_TRACE_WATCH_SLOTS {
238 DEBUG_TRACE_WATCH_ADDRS[index].store(usize::MAX, Ordering::Relaxed);
239 }
240}
241
242/// Legacy alias: register the first available trace slot.
243pub fn debug_set_trace_buddy_addr(addr: usize) {
244 let _ = debug_set_trace_lock_addr(addr);
245}
246
247/// Legacy alias: register the first available trace slot.
248pub fn debug_set_trace_slab_addr(addr: usize) {
249 let _ = debug_set_trace_lock_addr(addr);
250}
251
252/// Emit an E9 byte for each matching trace slot (acquire = uppercase, release = lowercase).
253#[inline]
254fn emit_trace_e9(lock_addr: usize, tag_offset: u8) {
255 for (i, slot) in DEBUG_TRACE_WATCH_ADDRS.iter().enumerate() {
256 if slot.load(Ordering::Relaxed) == lock_addr {
257 let ch = b'A' + tag_offset + (i as u8);
258 // L2 host harness (testing-findings.md F10): `out 0xe9` is ring-0
259 // port I/O; executing it from userspace faults with SIGSEGV.
260 // Compiled out only under the kernel-l2-tests harness cfg;
261 // production behavior is unchanged.
262 #[cfg(not(kernel_l2_host))]
263 unsafe {
264 core::arch::asm!("out 0xe9, al", in("al") ch)
265 };
266 #[cfg(kernel_l2_host)]
267 let _ = ch;
268 }
269 }
270}
271
272// =========================== SpinLockGuard ====================================
273
274/// RAII guard that holds the lock and carries the guardian state.
275pub struct SpinLockGuard<'a, T: ?Sized, G: Guardian = IrqDisabled> {
276 lock: &'a SpinLock<T, G>,
277 /// Wrapped in ManuallyDrop so that `Drop::drop` can move it into
278 /// `G::exit()` without triggering a compiler-generated second drop of
279 /// the field.
280 state: ManuallyDrop<GuardianState<G::Token>>,
281}
282
283impl<'a, T: ?Sized> SpinLockGuard<'a, T, IrqDisabled> {
284 /// Return the typed proof that IRQs are disabled.
285 #[inline]
286 pub fn token(&self) -> &IrqDisabledToken {
287 &self.state.token
288 }
289
290 #[inline]
291 pub(crate) fn with_mut_and_token<R>(
292 &mut self,
293 f: impl FnOnce(&mut T, &IrqDisabledToken) -> R,
294 ) -> R {
295 let token = &self.state.token;
296 // SAFETY: this guard owns the lock, guaranteeing exclusive access.
297 let data = unsafe { &mut *self.lock.data.get() };
298 f(data, token)
299 }
300}
301
302impl<'a, T: ?Sized, G: Guardian> Deref for SpinLockGuard<'a, T, G> {
303 type Target = T;
304
305 fn deref(&self) -> &T {
306 // SAFETY: we hold the lock.
307 unsafe { &*self.lock.data.get() }
308 }
309}
310
311impl<'a, T: ?Sized, G: Guardian> DerefMut for SpinLockGuard<'a, T, G> {
312 fn deref_mut(&mut self) -> &mut T {
313 // SAFETY: we hold the lock.
314 unsafe { &mut *self.lock.data.get() }
315 }
316}
317
318impl<'a, T: ?Sized, G: Guardian> Drop for SpinLockGuard<'a, T, G> {
319 fn drop(&mut self) {
320 let lock_addr = self.lock as *const _ as *const () as usize;
321 let watched = DEBUG_WATCH_LOCK_ADDR.load(Ordering::Relaxed);
322 let trace = watched == lock_addr;
323
324 if trace {
325 crate::serial_force_println!(
326 "[trace][spin] drop begin lock={:#x} owner_cpu={} saved_flags={:#x}",
327 lock_addr,
328 self.lock.owner_cpu.load(Ordering::Relaxed),
329 self.state.saved_flags,
330 );
331 }
332
333 self.lock.owner_cpu.store(usize::MAX, Ordering::Relaxed);
334 self.lock.locked.store(false, Ordering::Release);
335
336 if trace {
337 crate::serial_force_println!("[trace][spin] drop unlocked lock={:#x}", lock_addr);
338 }
339 // Race/corruption diagnostic: E9 trace when watched locks are released.
340 // Use lowercase tags to distinguish release from acquire.
341 for (i, slot) in DEBUG_TRACE_WATCH_ADDRS.iter().enumerate() {
342 if slot.load(Ordering::Relaxed) == lock_addr {
343 let ch = b'a' + (i as u8);
344 // F10: see acquire-side note above.
345 #[cfg(not(kernel_l2_host))]
346 unsafe {
347 core::arch::asm!("out 0xe9, al", in("al") ch)
348 };
349 #[cfg(kernel_l2_host)]
350 let _ = ch;
351 }
352 }
353
354 // SAFETY: `state` is valid and initialised. We move it out of its
355 // ManuallyDrop wrapper so that G::exit() can consume it. The compiler
356 // will NOT run a second destructor on the field because ManuallyDrop
357 // suppresses automatic drops.
358 let state = unsafe { ManuallyDrop::take(&mut self.state) };
359 G::exit(state);
360
361 if trace {
362 crate::serial_force_println!(
363 "[trace][spin] drop guardian-exit done lock={:#x}",
364 lock_addr
365 );
366 }
367 }
368}
369
370// The guardian's invariant (e.g. preemption depth, IF flag) is per-CPU.
371// Sending the guard to another CPU would violate it.
372impl<T: ?Sized, G: Guardian> !Send for SpinLockGuard<'_, T, G> {}