seq_lock/lib.rs
1// Copyright 2023 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 starnix_logging::with_zx_name;
6use std::arch::asm;
7use std::marker::PhantomData;
8use std::mem::{align_of, size_of};
9use std::sync::Arc;
10use std::sync::atomic::AtomicU32;
11use zerocopy::{Immutable, IntoBytes};
12
13const SEQUENCE_SIZE: usize = size_of::<AtomicU32>();
14
15/// Byte size to use when incrementally writing out T in [`set_value()`]. Determined
16/// by the params in T.
17/// Four -> write in u32 chunks.
18/// Eight -> write in u64 chunks, although the first 8 bytes may be two u32s (one
19/// of which is the `sequence`).
20#[derive(PartialEq)]
21pub enum WriteSize {
22 Four,
23 Eight,
24}
25
26/// Types that are safe to be synchronized across address spaces using a Seqlock.
27///
28/// A type implementing this trait can optionally include the sequence as
29/// its first field, indicated by `HAS_INLINE_SEQUENCE`. If it does not, [`SeqLock`]
30/// will place a u32 atomic sequence number in between the header and value.
31///
32/// # Safety
33///
34/// Types implementing this trait guarantee that they can be safely written
35/// to shared memory in chunks of `WRITE_SIZE` without introducing undefined
36/// behavior for concurrent readers in other address spaces.
37pub unsafe trait SeqLockable: IntoBytes + Immutable {
38 /// The chunk size to use when writing to memory, either 4 or 8 bytes.
39 const WRITE_SIZE: WriteSize;
40
41 /// Indicates whether the type includes the u32 sequence as its first field.
42 const HAS_INLINE_SEQUENCE: bool;
43
44 /// Name used to identify the VMO for debugging.
45 const VMO_NAME: &'static [u8];
46}
47
48/// Declare an instance of [`SeqLock`] by supplying header([`H`]) and value([`T`]) types,
49/// which should be configured with C-style layout & alignment.
50/// The value T can optionally include the sequence param as its first field (HAS_INLINE_SEQUENCE).
51/// If you choose not to do that, [`SeqLock`] will place a u32 atomic sequence number
52/// in between the header and value, in a VMO, shifting the value payload by `SEQUENCE_SIZE`.
53pub struct SeqLock<H: IntoBytes + Immutable, T: SeqLockable> {
54 map_addr: usize,
55 readonly_vmo: Arc<zx::Vmo>,
56 _phantom_data: PhantomData<(H, T)>,
57}
58
59impl<H: IntoBytes + Default + Immutable, T: SeqLockable + Default> SeqLock<H, T> {
60 pub fn new_default() -> Result<Self, zx::Status> {
61 Self::new(H::default(), T::default())
62 }
63}
64
65/// Points to the sequence (lock) address.
66/// This is always right after the H struct.
67const fn sequence_offset<H>() -> usize {
68 let offset = size_of::<H>();
69 assert!(offset % align_of::<AtomicU32>() == 0, "Sequence must be correctly aligned");
70 offset
71}
72
73impl<H: IntoBytes + Immutable, T: SeqLockable> SeqLock<H, T> {
74 /// Points to the value address, adding any required padding if `sequence` is not inline.
75 ///
76 /// Example with inline sequence (HAS_INLINE_SEQUENCE = true):
77 /// H: 0
78 /// H: 4
79 /// T: 8 <-- points here, because `sequence` is the first param of T.
80 /// T: 12
81 ///
82 /// Example without inline sequence (HAS_INLINE_SEQUENCE = false):
83 /// H: 0
84 /// H: 4
85 /// [sequence]: 8
86 /// T: 12 <-- points here, after the added sequence.
87 ///
88 /// Some implementations (SeLinuxStatusValue) rely on SeqLock to track `sequence`, while
89 /// some others (PerfMetadataValue) track `sequence` in T so that they can refer to it.
90 const fn value_offset() -> usize {
91 let offset = sequence_offset::<H>();
92 assert!(
93 offset % align_of::<T>() == 0,
94 "Value alignment must allow packing without padding"
95 );
96 offset + if T::HAS_INLINE_SEQUENCE { 0 } else { SEQUENCE_SIZE }
97 }
98
99 /// Returns the total size of the VMO required to store the header, value, and sequence.
100 const fn vmo_size() -> usize {
101 Self::value_offset() + size_of::<T>()
102 }
103
104 /// Returns an instance with initial values and a read-only VMO handle.
105 /// May fail if the VMO backing the structure cannot be created, duplicated
106 /// read-only, or mapped.
107 pub fn new(header: H, value: T) -> Result<Self, zx::Status> {
108 // Create a VMO sized to hold the header H, value T, and sequence number.
109 let vmo_size = Self::vmo_size();
110 let writable_vmo = with_zx_name(zx::Vmo::create(vmo_size as u64)?, T::VMO_NAME);
111
112 // SAFETY: This is ok because there are no other references to this memory.
113 return unsafe { Self::new_from_vmo(header, value, writable_vmo) };
114 }
115
116 /// Same as new() except that we can pass in an existing Vmo. This means that the
117 /// first part of the Vmo is a SeqLock.
118 ///
119 /// # Safety
120 ///
121 /// Callers must guarantee that any other references to this memory will
122 /// only make aligned atomic accesses to the sequence offset within the memory
123 /// or to fields of H or T.
124 pub unsafe fn new_from_vmo(
125 header: H,
126 value: T,
127 writable_vmo: zx::Vmo,
128 ) -> Result<Self, zx::Status> {
129 const {
130 let write_size = match T::WRITE_SIZE {
131 WriteSize::Four => size_of::<u32>(),
132 WriteSize::Eight => size_of::<u64>(),
133 };
134 assert!(align_of::<T>() >= write_size, "T must be aligned to the write size");
135 assert!(size_of::<T>() % write_size == 0, "size of T must be a multiple of write size");
136 assert!(
137 Self::value_offset() % write_size == 0,
138 "value_offset must be aligned to the write size"
139 );
140 }
141 let value_offset = Self::value_offset();
142 let vmo_size = Self::vmo_size();
143 // Populate the initial default values.
144 writable_vmo.write(header.as_bytes(), 0)?;
145 writable_vmo.write(value.as_bytes(), value_offset as u64)?;
146
147 // Create a readonly handle to the VMO.
148 let writable_rights = writable_vmo.basic_info()?.rights;
149 let readonly_rights = writable_rights.difference(zx::Rights::WRITE);
150 let readonly_vmo = Arc::new(writable_vmo.duplicate_handle(readonly_rights)?);
151
152 // Map the VMO writable by this object, and populate it.
153 let flags = zx::VmarFlags::PERM_READ
154 | zx::VmarFlags::ALLOW_FAULTS
155 | zx::VmarFlags::REQUIRE_NON_RESIZABLE
156 | zx::VmarFlags::PERM_WRITE;
157
158 let status = Self {
159 map_addr: fuchsia_runtime::vmar_root_self().map(
160 0,
161 &writable_vmo,
162 0,
163 vmo_size,
164 flags,
165 )?,
166 readonly_vmo: readonly_vmo,
167 _phantom_data: PhantomData,
168 };
169 Ok(status)
170 }
171
172 /// Returns a read-only handle to the VMO containing the header, atomic
173 /// sequence number, and value.
174 pub fn get_readonly_vmo(&self) -> Arc<zx::Vmo> {
175 self.readonly_vmo.clone()
176 }
177
178 /// Returns a read-only copy of the value as a T struct object.
179 /// This read occurs with a sequence check to ensure that:
180 /// 1. Someone else is not already in the middle of writing the data
181 /// 2. The data had not been modified during the read
182 pub fn get(&self) -> T {
183 let mut value = std::mem::MaybeUninit::<T>::uninit();
184 let value_ptr = value.as_mut_ptr();
185 let starting_addr = self.map_addr + Self::value_offset();
186 let sequence_addr = self.map_addr + sequence_offset::<H>();
187
188 loop {
189 // Read sequence (lock) value.
190 // SAFETY: We know sequence is u32 hardcoded to sequence_addr.
191 let sequence = unsafe { atomic_load_u32_acquire(sequence_addr as *mut u32) };
192 if sequence % 2 != 0 {
193 std::hint::spin_loop();
194 continue;
195 }
196
197 // Read data in chunks of u32 or u64 depending on the WriteSize for T.
198 if T::WRITE_SIZE == WriteSize::Four {
199 for i in 0..(size_of::<T>() / size_of::<u32>()) {
200 let addr = starting_addr + i * size_of::<u32>();
201 // SAFETY: User stated via WriteSize that T is made of u32s.
202 let val = unsafe { atomic_load_u32_acquire(addr as *mut u32) };
203 // SAFETY: We know value_ptr points to a T struct param.
204 unsafe { (value_ptr as *mut u32).add(i).write(val) };
205 }
206 } else if T::WRITE_SIZE == WriteSize::Eight {
207 for i in 0..(size_of::<T>() / size_of::<u64>()) {
208 let addr = starting_addr + i * size_of::<u64>();
209 // SAFETY: User stated via WriteSize that T is made of u64s.
210 let val = unsafe { atomic_load_u64_acquire(addr as *mut u64) };
211 // SAFETY: We know value_ptr points to a T struct param.
212 unsafe { (value_ptr as *mut u64).add(i).write(val) };
213 }
214 }
215
216 // Read sequence again to compare with earlier sequence value.
217 // SAFETY: We know sequence is u32 hardcoded to sequence_addr.
218 let current_sequence = unsafe { atomic_load_u32_acquire(sequence_addr as *mut u32) };
219 if sequence != current_sequence {
220 continue;
221 }
222 break;
223 }
224 // Only return after sequence checks are valid, otherwise loops to check again.
225 // SAFETY: By this point the value should be synced and valid. Also we know the
226 // data starting at the offset is a T struct.
227 unsafe { value.assume_init() }
228 }
229
230 /// Updates the value directly. Uses Seqlock pattern.
231 pub fn set_value(&self, value: T) {
232 // All data in <T> must be stored with some form of atomic write.
233 // Given two consecutive writes W1 and W2, it is technically possible for a
234 // client to observe the data written by W2 before observing the
235 // start-increment for W2. The reader observes the same post-W1/pre-W2
236 // sequence number at both start and end of the read, so thinks everything
237 // is consistent, but gets some mix of W1 and W2's data.
238 // In order to synchronize correctly we must either:
239 //
240 // 1) Store all the data with any atomic ordering (i.e. relaxed)
241 // 2) Store all the data with atomic-release
242 // We've chosen to do the second.
243 let starting_addr = self.map_addr + Self::value_offset();
244
245 // Convert T to u8s so that we can process in u32 or u64 chunks.
246 const { assert!(size_of::<T>() % 4 == 0) };
247 let value_as_u8_bytes = value.as_bytes();
248 let value_ptr_in_u32 = value.as_bytes().as_ptr().cast::<u32>();
249
250 // Lock prior to writing.
251 let sequence_addr = (self.map_addr + sequence_offset::<H>()) as *mut u32;
252 // Don't use AtomicU32 fetch_add because it is undefined behavior to
253 // access across mutually distrusting address spaces, which happens for the seq lock.
254 // SAFETY: sequence_addr is a valid pointer because `map_addr` is sized to fit
255 // `H` and `T` and unmapped when `self` is dropped.
256 let old_sequence = unsafe { atomic_fetch_add_u32_acq_rel(sequence_addr, 1) };
257 // Old `sequence` value must always be even (i.e. unlocked) before writing.
258 assert!((old_sequence % 2) == 0, "expected sequence to be unlocked");
259
260 // Process and write to memory in u32 or u64 chunks.
261 const { assert!(align_of::<T>() == 4 || align_of::<T>() == 8) };
262 // If T included the sequence number, we shouldn't write to it
263 // (overwrite it) here. We should just skip it.
264 let mut start_index = 0;
265 if T::HAS_INLINE_SEQUENCE {
266 start_index = 1;
267 }
268
269 if T::WRITE_SIZE == WriteSize::Four {
270 assert!(align_of::<T>() == 4);
271 for i in start_index..(value_as_u8_bytes.len() / size_of::<u32>()) {
272 let current_value_addr = starting_addr + (i * size_of::<u32>());
273 // SAFETY: We checked alignment and size above so we know that this points to
274 // the valid current u32 value.
275 let current_value = unsafe { *value_ptr_in_u32.add(i) };
276
277 // Use asm to write u32 chunk so that the values are being written
278 // atomically between address spaces. Don't use std::sync::atomic because that
279 // only syncs writes within the Rust abstract machine.
280 // SAFETY: Caller has verified that no one else is writing to this exact memory, and
281 // that both currrent_value_addr and value_as_u64 are valid.
282 unsafe { atomic_store_u32_release(current_value_addr as *mut u32, current_value) };
283 }
284 } else if T::WRITE_SIZE == WriteSize::Eight {
285 assert!(align_of::<T>() == 8 && size_of::<T>() % 8 == 0);
286
287 // When `WRITE_SIZE` is `Eight`, the memory is 8-byte aligned.
288 // If `HAS_INLINE_SEQUENCE` is true, the 4-byte sequence lock occupies the
289 // first half of an 8-byte block. We must skip that 4-byte sequence, perform a
290 // 4-byte store for the remainder of that block, and then proceed with 8-byte stores.
291 let mut offset_index = 0;
292
293 if start_index == 1 {
294 // Skip first u32 (sequence). Write next u32.
295 let addr = starting_addr + (start_index * size_of::<u32>());
296 // SAFETY: As a `SeqLockable`, the caller guarantees via `HAS_INLINE_SEQUENCE` that
297 // the u32 sequence spans the first half of the 8-byte aligned block. This means that
298 // getting the next u32 value (to sum up to a complete u64) is safe.
299 let value = unsafe { *value_ptr_in_u32.add(start_index) };
300 // SAFETY: Caller has verified that no one else is writing to this exact memory, and
301 // that both addr and value are valid.
302 unsafe { atomic_store_u32_release(addr as *mut u32, value) };
303
304 offset_index += 1;
305 }
306
307 // Write the rest of the data using 8-byte stores.
308 let value_ptr_in_u64 = value.as_bytes().as_ptr().cast::<u64>();
309 for i in offset_index..(value_as_u8_bytes.len() / size_of::<u64>()) {
310 let addr = starting_addr + (i * size_of::<u64>());
311 // SAFETY: We checked alignment and size above so we know that this points to
312 // the valid current u64 value.
313 let value = unsafe { *value_ptr_in_u64.add(i) };
314
315 // Use asm to write u64 chunk so that the values are being written
316 // atomically between address spaces. Don't use std::sync::atomic because that
317 // only syncs writes within the Rust abstract machine.
318 // SAFETY: Caller has verified that no one else is writing to this exact memory, and
319 // that both addr and value are valid.
320 unsafe { atomic_store_u64_release(addr as *mut u64, value) };
321 }
322 }
323
324 // Unlock after all writing is done.
325 // SAFETY: sequence_addr is a valid pointer as per above SAFETY comment.
326 let _ = unsafe { atomic_fetch_add_u32_acq_rel(sequence_addr, 1) };
327 }
328
329 /// Retrieves the memory address of the beginning of the handle part of the VMO.
330 /// You can use this to point to a param you want to edit (e.g. with an offset).
331 pub fn get_map_address(&mut self) -> *const T {
332 let address = self.map_addr;
333 return std::ptr::with_exposed_provenance::<T>(address);
334 }
335}
336
337/// This performs an atomic store-release of a 32-bit value to `addr`.
338/// Use this if you have a u32 or your struct is align(4).
339///
340/// Rust's memory model defines how atomics work across threads, but
341/// doesn't account for the way Starnix handles access across mutually distrusting
342/// address spaces.
343/// This Seqlock is intended to be mapped and read by different address spaces. Rust's
344/// guarantees do not apply and reading across these address spaces is undefined behavior.
345/// Theoretically the Rust compiler could determine that the atomic is never read
346/// from within the process and optimize out the store. We work around this by directly
347/// including the assembly an atomic would generate to prevent the compiler from
348/// "helpfully" optimizing it away.
349///
350/// # Safety
351///
352/// 1. The caller must ensure `addr` points to an address ptr that is valid and 4-byte
353/// aligned. The `addr` must be writable by the current process.
354/// 2. The caller must ensure that no other non-atomic operations are
355/// occurring on this memory address simultaneously.
356pub unsafe fn atomic_store_u32_release(addr: *mut u32, value: u32) {
357 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64")))]
358 compile_error!("This architecture is not supported");
359
360 // SAFETY: Caller must provide a valid `addr` and `value` as defined in the # Safety
361 // section above. The asm directly stores the value to that ptr. The original value
362 // may not have been a u32 (e.g. it's a SeLinuxStatusValue struct); caller is
363 // responsible to break struct into valid u32 chunks.
364 unsafe {
365 #[cfg(target_arch = "x86_64")]
366 {
367 asm!(
368 "mov [{addr}], {val:e}",
369 addr = in(reg) addr,
370 val = in(reg) value,
371 options(nostack, preserves_flags)
372 );
373 }
374 #[cfg(target_arch = "aarch64")]
375 {
376 asm!(
377 "stlr {val:w}, [{addr}]",
378 addr = in(reg) addr,
379 val = in(reg) value,
380 options(nostack, preserves_flags)
381 );
382 }
383 #[cfg(target_arch = "riscv64")]
384 {
385 asm!(
386 "fence rw, w",
387 "sw {val}, 0({addr})",
388 addr = in(reg) addr,
389 val = in(reg) value,
390 options(nostack, preserves_flags)
391 );
392 }
393 }
394}
395
396/// This performs an atomic fetch-add with Acquire and Release ordering of `val`
397/// to a 32-bit value at `addr`. Use this to update the u32 lock.
398///
399/// Rust's memory model defines how atomics work across threads, but
400/// doesn't account for the way Starnix handles access across mutually distrusting
401/// address spaces.
402/// This Seqlock is intended to be mapped and read by different address spaces. Rust's
403/// guarantees do not apply and reading across these address spaces is undefined behavior.
404/// Theoretically the Rust compiler could determine that the atomic is never read
405/// from within the process and optimize out the store. We work around this by directly
406/// including the assembly an atomic would generate to prevent the compiler from
407/// "helpfully" optimizing it away.
408///
409/// # Safety
410/// The caller must ensure `addr` is valid. The `addr` must be writable by the current process.
411pub unsafe fn atomic_fetch_add_u32_acq_rel(addr: *mut u32, value: u32) -> u32 {
412 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64")))]
413 compile_error!("This architecture is not supported");
414
415 let old_value: u32;
416 // SAFETY: Caller must provide a valid `addr` and `value`. The asm directly
417 // updates the value at that ptr.
418 unsafe {
419 #[cfg(target_arch = "x86_64")]
420 {
421 asm!(
422 "lock xadd [{addr}], {val:e}",
423 addr = in(reg) addr,
424 val = inout(reg) value => old_value,
425 options(nostack),
426 );
427 }
428 #[cfg(target_arch = "aarch64")]
429 {
430 asm!(
431 "1:",
432 "ldaxr {old:w}, [{addr}]",
433 "add {tmp:w}, {old:w}, {val:w}",
434 "stlxr {status:w}, {tmp:w}, [{addr}]",
435 "cbnz {status:w}, 1b",
436 addr = in(reg) addr,
437 val = in(reg) value,
438 old = out(reg) old_value,
439 tmp = out(reg) _,
440 status = out(reg) _,
441 options(nostack),
442 );
443 }
444 #[cfg(target_arch = "riscv64")]
445 {
446 asm!(
447 "amoadd.w.aqrl {old}, {val}, ({addr})",
448 addr = in(reg) addr,
449 val = in(reg) value,
450 old = out(reg) old_value,
451 options(nostack),
452 );
453 }
454 }
455 old_value
456}
457
458/// This performs an atomic store-release of a 64-bit value to `addr`.
459/// Use this if you have a u64 or your struct is align(8).
460///
461/// Rust's memory model defines how atomics work across threads, but
462/// doesn't account for the way Starnix handles access across mutually distrusting
463/// address spaces.
464/// This Seqlock is intended to be mapped and read by different address spaces. Rust's
465/// guarantees do not apply and reading across these address spaces is undefined behavior.
466/// Theoretically the Rust compiler could determine that the atomic is never read
467/// from within the process and optimize out the store. We work around this by directly
468/// including the assembly an atomic would generate to prevent the compiler from
469/// "helpfully" optimizing it away.
470///
471/// # Safety
472///
473/// 1. The caller must ensure `addr` points to an address ptr that is valid and 8-byte
474/// aligned. The `addr` must be writable by the current process.
475/// 2. The caller must ensure that no other non-atomic operations are
476/// occurring on this memory address simultaneously.
477pub unsafe fn atomic_store_u64_release(addr: *mut u64, value: u64) {
478 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64")))]
479 compile_error!("This architecture is not supported");
480
481 // SAFETY: Caller must provide a valid `addr` and `value` as defined in the # Safety
482 // section above. The asm directly stores the value to that ptr. The original value
483 // may not have been a u64 (e.g. it's a PerfMetadataValue struct); caller is
484 // responsible to break struct into valid u64 chunks.
485 unsafe {
486 #[cfg(target_arch = "x86_64")]
487 {
488 asm!(
489 "mov [{addr}], {val}",
490 addr = in(reg) addr,
491 val = in(reg) value,
492 options(nostack, preserves_flags)
493 );
494 }
495 #[cfg(target_arch = "aarch64")]
496 {
497 asm!(
498 // Add memory barrier.
499 "dmb ishst",
500 // Use str instead of stlr to explicitly write only.
501 // Otherwise stlr attempts to read first and we don't have permissions.
502 "str {val}, [{addr}]",
503 addr = in(reg) addr,
504 val = in(reg) value,
505 options(nostack, preserves_flags)
506 );
507 }
508 #[cfg(target_arch = "riscv64")]
509 {
510 asm!(
511 "fence rw, w",
512 "sd {val}, 0({addr})",
513 addr = in(reg) addr,
514 val = in(reg) value,
515 options(nostack, preserves_flags)
516 );
517 }
518 }
519}
520
521/// Performs an atomic acquire (load, or read) of a u32 from `addr`.
522/// You can use this to read the `sequence` or `lock` value.
523///
524/// # Safety
525/// `addr` must point to a valid address and be 4-byte aligned.
526pub unsafe fn atomic_load_u32_acquire(addr: *mut u32) -> u32 {
527 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64")))]
528 compile_error!("This architecture is not supported");
529
530 let value: u32;
531 // SAFETY: addr must be a valid pointer and 4-byte aligned.
532 unsafe {
533 #[cfg(target_arch = "x86_64")]
534 {
535 asm!(
536 "mov {val:e}, [{ptr}]",
537 ptr = in(reg) addr,
538 val = out(reg) value,
539 options(nostack, preserves_flags)
540 );
541 }
542 #[cfg(target_arch = "aarch64")]
543 {
544 asm!(
545 "ldar {val:w}, [{ptr}]",
546 ptr = in(reg) addr,
547 val = out(reg) value,
548 options(nostack, preserves_flags)
549 );
550 }
551 #[cfg(target_arch = "riscv64")]
552 {
553 asm!(
554 "lw {val}, 0({ptr})",
555 "fence r, rw",
556 ptr = in(reg) addr,
557 val = out(reg) value,
558 options(nostack, preserves_flags)
559 );
560 }
561 }
562 value
563}
564
565/// Performs an atomic acquire (load, or read) of a u64 from `addr`.
566///
567/// # Safety
568/// `addr` must point to a valid address and be 8-byte aligned.
569pub unsafe fn atomic_load_u64_acquire(addr: *mut u64) -> u64 {
570 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64")))]
571 compile_error!("This architecture is not supported");
572
573 let value: u64;
574 // SAFETY: addr must be a valid pointer and 8-byte aligned.
575 unsafe {
576 #[cfg(target_arch = "x86_64")]
577 {
578 asm!(
579 "mov {val}, [{ptr}]",
580 ptr = in(reg) addr,
581 val = out(reg) value,
582 options(nostack, preserves_flags)
583 );
584 }
585 #[cfg(target_arch = "aarch64")]
586 {
587 asm!(
588 "ldar {val}, [{ptr}]",
589 ptr = in(reg) addr,
590 val = out(reg) value,
591 options(nostack, preserves_flags)
592 );
593 }
594 #[cfg(target_arch = "riscv64")]
595 {
596 asm!(
597 "ld {val}, 0({ptr})",
598 "fence r, rw",
599 ptr = in(reg) addr,
600 val = out(reg) value,
601 options(nostack, preserves_flags)
602 );
603 }
604 }
605 value
606}
607
608impl<H: IntoBytes + Immutable, T: SeqLockable> Drop for SeqLock<H, T> {
609 fn drop(&mut self) {
610 // SAFETY: `self` owns the mapping, and does not dispense any references
611 // to it.
612 unsafe {
613 fuchsia_runtime::vmar_root_self()
614 .unmap(self.map_addr, Self::vmo_size())
615 .expect("failed to unmap SeqLock");
616 }
617 }
618}
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use zerocopy::KnownLayout;
623
624 // Example struct that mirrors PerfMetadataValue.
625 #[repr(C)]
626 #[derive(IntoBytes, Immutable, KnownLayout, Copy, Clone, Debug, PartialEq, Default)]
627 struct WriteSizeEightStruct {
628 lock: u32,
629 val1: u32,
630 val2: u64,
631 val3: u64,
632 }
633
634 // SAFETY: This struct is composed of fields that are safe to write
635 // in 8-byte chunks (two u32s and u64s). It is only used for testing.
636 // It emulates a perf_event_value struct.
637 unsafe impl SeqLockable for WriteSizeEightStruct {
638 const WRITE_SIZE: WriteSize = WriteSize::Eight;
639 const HAS_INLINE_SEQUENCE: bool = true;
640 const VMO_NAME: &'static [u8] = b"test:write_size_eight";
641 }
642
643 #[test]
644 fn test_seqlock_gets_align_eight_with_sequence() {
645 let seqlock = SeqLock::<u64, WriteSizeEightStruct>::new(0, WriteSizeEightStruct::default())
646 .expect("failed to create seqlock");
647
648 let val = WriteSizeEightStruct {
649 lock: 0,
650 val1: 42,
651 val2: 123_456_789_012_345_678,
652 val3: 987_654_321_098_765_432,
653 };
654 seqlock.set_value(val);
655
656 let data = seqlock.get();
657 // The 'lock' field was incremented twice by set_value(),
658 // and not incremented for get().
659 assert_eq!(data.lock, 2);
660 assert_eq!(data.val1, val.val1);
661 assert_eq!(data.val2, val.val2);
662 assert_eq!(data.val3, val.val3);
663 }
664
665 // Example struct that mirrors SeLinuxStatusValue.
666 #[repr(C)]
667 #[derive(IntoBytes, Immutable, KnownLayout, Copy, Clone, Debug, PartialEq, Default)]
668 struct WriteSizeFourStruct {
669 val1: u32,
670 val2: u32,
671 val3: u32,
672 }
673
674 // SAFETY: This struct is composed of u32 fields, making it safe
675 // to write in 4-byte chunks. It is only used for testing.
676 // It emulates a SeLinuxStatusValue struct.
677 unsafe impl SeqLockable for WriteSizeFourStruct {
678 const WRITE_SIZE: WriteSize = WriteSize::Four;
679 const HAS_INLINE_SEQUENCE: bool = false;
680 const VMO_NAME: &'static [u8] = b"test:write_size_four";
681 }
682
683 #[test]
684 fn test_seqlock_gets_align_four() {
685 let seqlock = SeqLock::<u32, WriteSizeFourStruct>::new(0, WriteSizeFourStruct::default())
686 .expect("failed to create seqlock");
687
688 let val = WriteSizeFourStruct { val1: 42, val2: 123_456_789, val3: 987_654_321 };
689 seqlock.set_value(val);
690
691 let data = seqlock.get();
692 assert_eq!(data.val1, val.val1);
693 assert_eq!(data.val2, val.val2);
694 assert_eq!(data.val3, val.val3);
695 }
696
697 // Stress test for get() and set_value().
698 // For two threads, get() and set_value() should work on the same piece of memory.
699 // One thread tries to read a lot, and another writes a lot. This test verifies that,
700 // thanks to the seqlock, the data read is correct (didn't get overwritten mid-read).
701 // TODO(https://fxbug.dev/460246292): Handle cases for more than 1 writer thread.
702 #[test]
703 fn test_seqlock_handles_concurrent_gets_and_sets() {
704 let seqlock = std::sync::Arc::new(
705 SeqLock::<u64, WriteSizeEightStruct>::new(0, WriteSizeEightStruct::default())
706 .expect("failed to create seqlock"),
707 );
708
709 let seqlock_clone = std::sync::Arc::clone(&seqlock);
710 let seqlock_clone_2 = std::sync::Arc::clone(&seqlock);
711
712 let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
713 let barrier_clone = std::sync::Arc::clone(&barrier);
714
715 // Spawn 2 threads that run concurrently.
716 let writer_thread = std::thread::spawn(move || {
717 barrier.wait();
718 let start = std::time::Instant::now();
719 let mut i = 0u32;
720 while start.elapsed() < std::time::Duration::from_millis(200) {
721 let val = WriteSizeEightStruct { lock: 0, val1: i, val2: i as u64, val3: i as u64 };
722 seqlock_clone.set_value(val);
723 i += 1;
724 }
725 });
726 let reader_thread = std::thread::spawn(move || {
727 let mut reads = 0;
728 let mut last_valid_read = 0;
729 barrier_clone.wait();
730 let start = std::time::Instant::now();
731 while start.elapsed() < std::time::Duration::from_millis(200) {
732 let data = seqlock_clone_2.get();
733 // All fields are the same (no mid-read writes).
734 assert_eq!(data.val1 as u64, data.val2);
735 assert_eq!(data.val2, data.val3);
736
737 // The sequence (lock) should be even (completed writes).
738 assert_eq!(data.lock % 2, 0);
739
740 // get() returns the latest value. The latest value might not increment exactly
741 // by 1 each time because the writer thread might have written zero or multiple
742 // times since we last read. So, we just verify that the latest value is higher
743 // than the previous value.
744 assert!(data.val1 >= last_valid_read);
745 last_valid_read = data.val1;
746 reads += 1;
747 }
748 reads
749 });
750
751 // Wait for both threads to finish.
752 writer_thread.join().unwrap();
753 let total_reads = reader_thread.join().unwrap();
754
755 // Check that reading actually happened.
756 assert!(total_reads > 1, "Expected threads to run concurrently");
757
758 // Check that writes actually happened.
759 let final_data = seqlock.get();
760 assert!(final_data.val1 > 0, "Expected some writes to happen");
761 assert_eq!(final_data.val1 as u64, final_data.val2);
762 assert_eq!(final_data.val2, final_data.val3);
763 assert_eq!(final_data.lock % 2, 0, "Sequence lock should be unlocked");
764 }
765}