clock_bound/shm/reader.rs
1#![expect(clippy::cast_ptr_alignment, reason = "TODO COME BACK TO THIS")]
2
3use errno::{Errno, errno};
4use std::ffi::{CStr, c_void};
5use std::mem::size_of;
6use std::ptr;
7use std::sync::atomic::{self, AtomicU16};
8
9use crate::shm::shm_header::{CLOCKBOUND_SHM_LATEST_VERSION, ShmHeader};
10use crate::{
11 shm::{
12 ClockErrorBound, ClockErrorBoundGeneric, ClockErrorBoundLayoutVersion, ClockErrorBoundV2,
13 ClockErrorBoundV3, ShmError,
14 },
15 syserror,
16};
17
18/// A guard tracking an open file descriptor.
19///
20/// Creating the `FdGuard` opens the file with read-only permission.
21/// The file descriptor is closed when the guard is dropped.
22struct FdGuard(i32);
23
24impl FdGuard {
25 /// Create a new `FdGuard`.
26 ///
27 /// Open a file at `path` and store the open file descriptor
28 fn new(path: &CStr) -> Result<Self, ShmError> {
29 // SAFETY: `path` is a valid C string.
30 let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
31 if fd < 0 {
32 return syserror!(format!("Faild to open file at {:?}", path));
33 }
34
35 Ok(FdGuard(fd))
36 }
37}
38
39impl Drop for FdGuard {
40 /// Drop the `FdGuard` and close the file descriptor it holds.
41 fn drop(&mut self) {
42 // SAFETY: Unsafe because this is a call into a C API, but this particular
43 // call is always safe.
44 unsafe {
45 let ret = libc::close(self.0);
46 assert!(ret == 0 || errno() == Errno(libc::EINTR));
47 }
48 }
49}
50
51/// A guard tracking an memory mapped file.
52///
53/// Creating the `MmapGuard` maps an open file descriptor.
54/// The file is unmap'ed when the guard is dropped.
55#[derive(Debug)]
56struct MmapGuard {
57 /// A pointer to the head of the segment
58 segment: *mut c_void,
59
60 /// The size of the segment mapped into memory
61 segsize: usize,
62}
63
64impl MmapGuard {
65 /// Create a new `MmapGuard`.
66 ///
67 /// Map the open file descriptor held in the `FdGuard`.
68 fn new(fdguard: &FdGuard) -> Result<Self, ShmError> {
69 // Read the header so we know how much to map in memory.
70 let header = ShmHeader::read(fdguard.0)?;
71
72 // This consumes the segsize, but we only needed the header for validation and extracting
73 // the segment size. So the move is fine here.
74 let segsize = header.segsize.into_inner() as usize;
75
76 // SAFETY: We're calling into a C function, but this particular call is always safe.
77 let segment: *mut c_void = unsafe {
78 libc::mmap(
79 ptr::null_mut(),
80 segsize,
81 libc::PROT_READ,
82 libc::MAP_SHARED,
83 fdguard.0,
84 0,
85 )
86 };
87
88 if segment == libc::MAP_FAILED {
89 return syserror!(String::from("Failed to mmap the SHM segment"));
90 }
91
92 Ok(MmapGuard { segment, segsize })
93 }
94}
95
96impl Drop for MmapGuard {
97 /// Drop the `MmapGuard` and unmap the file it tracks.
98 fn drop(&mut self) {
99 // SAFETY: `segment` was previously returned from `mmap`, and therefore
100 // when this destructor runs there are no more live references into
101 // it.
102 unsafe {
103 let ret = libc::munmap(self.segment, self.segsize);
104 assert_eq!(0, ret);
105 }
106 }
107}
108
109/// Reader for ClockBound daemon shared memory segment.
110///
111/// The Clockbound daemon shared memory segment consists of a `ShmHeader` followed by a
112/// `ClockBoundError` struct. The segment is updated by a single producer (the clockbound daemon),
113/// but may be read by many clients. The shared memory segment does not implement a semaphore or
114/// equivalent to synchronize the single-producer / many-consumers processes. Instead, the
115/// mechanism is lock-free and relies on a `generation` number to ensure consistent reads (over
116/// retries).
117///
118/// The writer increments the generation field from even to odd before each update. It also
119/// increment it again, from odd to even, after finishing the update. Readers must check the
120/// `generation` field before and after each read, and verify that they obtain the same, even,
121/// value. Otherwise, the read was dirty and must be retried.
122#[derive(Debug)]
123pub struct ShmReader {
124 // Explicitly make the ShmReader be !Send and !Sync, since it is not thread safe. A bit ugly to
125 // use a phantom raw pointer, but effective and free at runtime.
126 _marker: std::marker::PhantomData<*const ()>,
127
128 // Drop guard to unmap the shared memory segment
129 _guard: MmapGuard,
130
131 // A raw pointer into the shared memory segment, pointing to the version member of the ShmHeader
132 // section. The version number defines the shared memory segment content and layout. This is a
133 // bit less flexible than a series of TLV but simpler (and not mutually exclusive).
134 version: *const atomic::AtomicU16,
135
136 // A raw pointer into the shared memory segment, pointing to the generation member of the
137 // ShmHeader section. The generation number is used to read consistent snapshots of the shared
138 // memory segment (that is outside of an update event by the writer). This is expected to roll
139 // over as a function of the rate of update from the writer (eg. every ~9 hours if updating
140 // once a second). A generation number equals to 0 signals the shared memory segment has not
141 // been initialized.
142 generation: *const atomic::AtomicU16,
143
144 // A raw pointer into the shared memory segment, pointing to the ClockErrorBound section. Note
145 // that the structured reference by this pointer may not be consistent, and reading it requires
146 // to assert the generation value.
147 ceb_shm: *const ClockErrorBound,
148
149 // The last snapshot of ClockErrorBound taken. This acts as a cache to avoid waiting for the
150 // writer to complete an update and allow to share a reference to this memory location
151 // (avoiding some memory copy). Keeping a state here and sharing it with the caller makes the
152 // ShmReader not thread safe.
153 snapshot_ceb: ClockErrorBound,
154
155 // The value of the writer generation when the ceb snapshot was taken.
156 snapshot_gen: u16,
157}
158
159impl ShmReader {
160 /// Open a ClockBound shared memory segment for reading.
161 ///
162 /// On error, returns an appropriate `Errno`. If the content of the segment
163 /// is uninitialized, unparseable, or otherwise malformed, EPROTO will be
164 /// returned.
165 #[expect(clippy::missing_errors_doc, reason = "todo")]
166 pub fn new(path: &CStr) -> Result<ShmReader, ShmError> {
167 // Map the segment, with explicit pointers to fields
168 let (mmap_guard, version, generation, ceb_shm) = ShmReader::map_segment(path)?;
169
170 // Atomically read the current version in the shared memory segment
171 // SAFETY: `self.version` has been validated when creating the reader
172 let shm_version = unsafe { &*version };
173 let shm_version = shm_version.load(atomic::Ordering::Acquire);
174
175 // FIXME: the ShmHeader should be versioned behind an enum instead.
176 let min_version = shm_version >> 8;
177 let max_version = shm_version & 0x00ff;
178 if CLOCKBOUND_SHM_LATEST_VERSION < min_version
179 || CLOCKBOUND_SHM_LATEST_VERSION > max_version
180 {
181 let msg = format!(
182 "Clockbound shared memory segment supports versions {min_version} to {max_version} which does not include this reader version {CLOCKBOUND_SHM_LATEST_VERSION}",
183 );
184 return Err(ShmError::SegmentVersionNotSupported(msg));
185 }
186 let shm_version = ClockErrorBoundLayoutVersion::try_from(CLOCKBOUND_SHM_LATEST_VERSION)?;
187
188 Ok(ShmReader {
189 _marker: std::marker::PhantomData,
190 _guard: mmap_guard,
191 version,
192 generation,
193 ceb_shm,
194 snapshot_ceb: ClockErrorBoundGeneric::builder().build(shm_version),
195 snapshot_gen: 0,
196 })
197 }
198
199 /// Open a ClockBound shared memory segment for reading.
200 ///
201 /// On error, returns an appropriate `Errno`. If the content of the segment
202 /// is uninitialized, unparseable, or otherwise malformed, EPROTO will be
203 /// returned.
204 #[expect(clippy::missing_errors_doc, reason = "todo")]
205 pub fn new_with_max_version_unchecked(path: &CStr) -> Result<(ShmReader, u16), ShmError> {
206 // Map the segment, with explicit pointers to fields
207 let (mmap_guard, version, generation, ceb_shm) = ShmReader::map_segment(path)?;
208
209 // Atomically read the current version from the shared memory segment
210 // SAFETY: `self.version` has been validated when creating the reader
211 let shm_version = unsafe { &*version };
212 let shm_version = shm_version.load(atomic::Ordering::Acquire);
213 let max_version = shm_version & 0x00ff;
214
215 let current_version =
216 ClockErrorBoundLayoutVersion::try_from(CLOCKBOUND_SHM_LATEST_VERSION)?;
217
218 Ok((
219 ShmReader {
220 _marker: std::marker::PhantomData,
221 _guard: mmap_guard,
222 version,
223 generation,
224 ceb_shm,
225 snapshot_ceb: ClockErrorBoundGeneric::builder().build(current_version),
226 snapshot_gen: 0,
227 },
228 max_version,
229 ))
230 }
231
232 /// Open and map the ClockBound shared memory segment.
233 ///
234 /// Make sure the file can be open, and that the raw pointers into memory are set.
235 ///
236 /// # Errors
237 ///
238 /// Returns `ShmError` if the path is not found, the file cannot be open or sanity checks fail.
239 fn map_segment(
240 path: &CStr,
241 ) -> Result<
242 (
243 MmapGuard,
244 *const AtomicU16,
245 *const AtomicU16,
246 *const ClockErrorBound,
247 ),
248 ShmError,
249 > {
250 let fdguard = FdGuard::new(path)?;
251 let mmap_guard = MmapGuard::new(&fdguard)?;
252
253 // Create a cursor to pick the addresses of the various elements of interest in the shared
254 // memory segment.
255 let mut cursor: *const u8 = mmap_guard.segment.cast();
256
257 // Pick fields from the ShmHeader
258 // SAFETY: `cursor` is aligned to the start of the memory segment and the MmapGuard has
259 // validated the memory segment is large enough to contain the header.
260 let version = unsafe { ptr::addr_of!((*cursor.cast::<ShmHeader>()).version) };
261 let generation = unsafe { ptr::addr_of!((*cursor.cast::<ShmHeader>()).generation) };
262
263 // Atomically read the current version in the shared memory segment
264 // SAFETY: `self.version` has been validated when creating the reader
265 let shm_version = unsafe { &*version };
266 let shm_version = shm_version.load(atomic::Ordering::Acquire);
267
268 // FIXME: temporary workaround waiting for https://github.com/aws/private-clock-bound-staging/pull/150
269 // to be pulled in
270 let shm_version = ClockErrorBoundLayoutVersion::try_from(shm_version & 0x00ff)?;
271
272 // Move to the end of the header and map the ClockErrorBound data, but only if the segment
273 // size allows it and matches our expectation.
274 let layout_size = match shm_version {
275 ClockErrorBoundLayoutVersion::V2 => size_of::<ClockErrorBoundV2>(),
276 ClockErrorBoundLayoutVersion::V3 => size_of::<ClockErrorBoundV3>(),
277 };
278
279 if mmap_guard.segsize < size_of::<ShmHeader>() + layout_size {
280 let msg = format!(
281 "Clockbound segment size is smaller than expected [{} < {}].",
282 mmap_guard.segsize,
283 size_of::<ShmHeader>() + layout_size
284 );
285 return Err(ShmError::SegmentMalformed(msg));
286 }
287
288 // SAFETY: segment size has been checked to ensure `cursor` move leads to a valid cast
289 cursor = unsafe { cursor.add(size_of::<ShmHeader>()) };
290 let ceb_shm = ptr::addr_of!(*cursor.cast::<ClockErrorBound>());
291
292 Ok((mmap_guard, version, generation, ceb_shm))
293 }
294
295 /// Return a consistent snapshot of the shared memory segment.
296 ///
297 /// Taking a snapshot consists in reading the memory segment while confirming the generation
298 /// number in the header has not changed (which would indicate an update from the writer
299 /// occurred while reading). If an update is detected, the read is retried.
300 ///
301 /// This function returns a reference to the `ClockErrorBound` snapshot stored by the reader, and
302 /// not an owned value. This make the `ShmReader` NOT thread-safe: the data pointed to could be
303 /// updated without one of the thread knowing, leading to a incorrect clock error bond. The
304 /// advantage are in terms of performance: less data copied, but also no locking, yielding or
305 /// excessive retries.
306 #[expect(clippy::missing_errors_doc, reason = "todo")]
307 pub fn snapshot(&mut self) -> Result<&ClockErrorBound, ShmError> {
308 // Atomically read the current version in the shared memory segment
309 // SAFETY: `self.version` has been validated when creating the reader
310 let version = unsafe { &*self.version };
311 let version = version.load(atomic::Ordering::Acquire);
312
313 // The version number is checked when the reader is created to not be 0. If we now see a
314 // version equal to 0, the writer has restarted, wiped the segment clean, but has not
315 // defined the layout yet. Choose to return the last snapshot. If the writer died in the
316 // middle of restarting, the snapshot will eventually be stale. Enough information is
317 // returned to the caller to take appropriate action (e.g. assert clock status).
318 if version == 0 {
319 return Ok(&self.snapshot_ceb);
320 }
321
322 // It is possible the daemon has been upgraded and restarted. It is meant to be backward
323 // compatible when writing to the default path to the shared memory segment. That is, the
324 // current version written by the daemon may have incremented, but the minimum version
325 // supported must ours or lower. In the other direction, if the clockbound daemon was
326 // downgraded, also have to report errors, since expected features may be missing.
327 let min_version = version >> 8;
328 let max_version = version & 0x00ff;
329 if CLOCKBOUND_SHM_LATEST_VERSION < min_version
330 || CLOCKBOUND_SHM_LATEST_VERSION < max_version
331 {
332 let msg = format!(
333 "Clockbound shared memory segment supports versions {min_version} to {max_version} which does not include this reader version {CLOCKBOUND_SHM_LATEST_VERSION}",
334 );
335 return Err(ShmError::SegmentVersionNotSupported(msg));
336 }
337
338 // Atomically read the current generation in the shared memory segment
339 // SAFETY: `self.generation` has been validated when creating the reader
340 let generation = unsafe { &*self.generation };
341 let mut first_gen = generation.load(atomic::Ordering::Acquire);
342
343 // The generation number is checked when the reader is created to not be 0. If we now see a
344 // generation equals to 0, the writer has restarted, wiped the segment clean, but has not
345 // initialized it with valid data yet. Choose to return the last snapshot. If the writer
346 // died in the middle of restarting, the snapshot will eventually be stale. Enough
347 // information is returned to the caller to take appropriate action (e.g. assert clock
348 // status).
349 if first_gen == 0 {
350 return Ok(&self.snapshot_ceb);
351 }
352
353 // Quick optimization, if the generation number matches the last one recorded, the shared
354 // memory segment has not been updated since last read. No need to read more of the memory
355 // segment, instead return the reference to the snapshot. This is useful in cases where the
356 // rate of clockbound read is much higher than the rate of write to the shared memory
357 // segment.
358 //
359 // Note that the generation number DOES roll over, but never take a value of 0 once the
360 // segment is initialized. It is still possible that the generation number matches although
361 // the counter has rolled over. Assuming one update per sec, this leaves a collision
362 // probability of 1 / 2^16, and a rollover once every 18 hours. Although the risk is very
363 // small it exists, but the `void_after` member on the ClockErrorBound struct can be used
364 // to provide an additional layer of protection.
365 if first_gen == self.snapshot_gen {
366 return Ok(&self.snapshot_ceb);
367 }
368
369 // If the generation is an odd number, the shared memory segment is in the process of being
370 // updated by the writer. Instead of waiting, yielding or busy looping, simply return the
371 // last snapshot taken. It is fine for the reader to return a bound on clock error based on
372 // the previously updated shared memory segment. The bound on clock error returned would be
373 // larger than it could have been, but still correct. If the writer died in the middle of
374 // an update, the snapshot will eventually be stale. The caller is returned enough
375 // information to act accordingly.
376 if first_gen & 0x0001 == 1 {
377 return Ok(&self.snapshot_ceb);
378 }
379
380 // The generation number is an even number, and has changed since the last snapshot. Loop
381 // until we obtain a consistent read of the clock error bound data. This relies on reading
382 // the generation value twice, making sure they are identical and an even number.
383 //
384 // The writer could die in the middle of the update. This could lead to not making any
385 // progress hence capping the number of retries.
386 let mut retries = 1_000_000;
387 while retries > 0 {
388 // Read the ClockErrorBound data from the shared memory
389 // SAFETY: `ceb_at` has been checked to be valid while creating the ShmReader
390 let snapshot = unsafe { self.ceb_shm.read_volatile() };
391
392 // Confirm no update occurred during the read
393 let second_gen = generation.load(atomic::Ordering::Acquire);
394 if first_gen == second_gen {
395 self.snapshot_gen = first_gen;
396 self.snapshot_ceb = snapshot;
397 return Ok(&self.snapshot_ceb);
398 }
399 // Only track complete updates indicated by an even generation number.
400 if second_gen & 0x0001 == 0 {
401 first_gen = second_gen;
402 }
403 retries -= 1;
404 }
405
406 // Attempts to read the snapshot have failed.
407 Err(ShmError::SegmentNotInitialized(String::from(
408 "Failed to read the SHM segment after all attempts",
409 )))
410 }
411}
412
413#[cfg(test)]
414mod t_reader {
415 use super::*;
416 use crate::shm::ClockStatus;
417 use byteorder::{NativeEndian, WriteBytesExt};
418 use nix::sys::time::TimeSpec;
419 use std::ffi::CString;
420 use std::fs::OpenOptions;
421 use std::io::Seek;
422 use std::io::Write;
423 /// We make use of tempfile::NamedTempFile to ensure that
424 /// local files that are created during a test get removed
425 /// afterwards.
426 use tempfile::NamedTempFile;
427
428 macro_rules! write_memory_segment {
429 ($file:ident,
430 $magic_0:literal,
431 $magic_1:literal,
432 $segsize:literal,
433 $version:literal,
434 $generation:literal,
435 ($as_of_sec:literal, $as_of_nsec:literal),
436 ($void_after_sec:literal, $void_after_nsec:literal),
437 $bound_nsec:literal,
438 $max_drift: literal) => {
439 // Build the bound on clock error data
440 let ceb = ClockErrorBoundGeneric::builder()
441 .as_of(TimeSpec::new($as_of_sec, $as_of_nsec))
442 .void_after(TimeSpec::new($void_after_sec, $void_after_nsec))
443 .bound_nsec($bound_nsec)
444 .disruption_marker(0)
445 .max_drift_ppb($max_drift)
446 .clock_status(ClockStatus::Synchronized)
447 .clock_disruption_support_enabled(true)
448 .build(ClockErrorBoundLayoutVersion::V2);
449
450 // Convert the ceb struct into a slice so we can write it all out, fairly magic.
451 // Definitely needs the #[repr(C)] layout.
452 let slice = unsafe {
453 ::core::slice::from_raw_parts(
454 (&ceb as *const ClockErrorBound) as *const u8,
455 ::core::mem::size_of::<ClockErrorBound>(),
456 )
457 };
458
459 $file
460 .write_u32::<NativeEndian>($magic_0)
461 .expect("Write failed magic_0");
462 $file
463 .write_u32::<NativeEndian>($magic_1)
464 .expect("Write failed magic_1");
465 $file
466 .write_u32::<NativeEndian>($segsize)
467 .expect("Write failed segsize");
468 $file
469 .write_u16::<NativeEndian>($version)
470 .expect("Write failed version");
471 $file
472 .write_u16::<NativeEndian>($generation)
473 .expect("Write failed generation");
474 $file
475 .write_all(slice)
476 .expect("Write failed ClockErrorBound");
477 $file.sync_all().expect("Sync to disk failed");
478 };
479 }
480
481 /// Assert that the reader can map a file.
482 #[test]
483 fn test_reader_new_shm_v2() {
484 let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
485 let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
486 let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
487 let mut clockbound_shm_file = OpenOptions::new()
488 .write(true)
489 .open(clockbound_shm_path)
490 .expect("open clockbound file failed");
491 write_memory_segment!(
492 clockbound_shm_file,
493 0x414D5A4E,
494 0x43420200,
495 400,
496 0x0002,
497 10,
498 (0, 0),
499 (0, 0),
500 123,
501 0
502 );
503 let path = CString::new(clockbound_shm_path).expect("CString failed");
504
505 // This should fail with a version mismatch error
506 let res = ShmReader::new(&path);
507 assert!(res.is_err());
508 }
509
510 /// Assert that the reader can map a file.
511 #[test]
512 fn test_reader_new_shm_v3() {
513 let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
514 let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
515 let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
516 let mut clockbound_shm_file = OpenOptions::new()
517 .write(true)
518 .open(clockbound_shm_path)
519 .expect("open clockbound file failed");
520 write_memory_segment!(
521 clockbound_shm_file,
522 0x414D5A4E,
523 0x43420200,
524 400,
525 0x0303,
526 10,
527 (0, 0),
528 (0, 0),
529 123,
530 0
531 );
532
533 let path = CString::new(clockbound_shm_path).expect("CString failed");
534 let reader = ShmReader::new(&path).expect("Failed to create ShmReader");
535
536 let version = unsafe { &*reader.version };
537 let generation = unsafe { &*reader.generation };
538 let ceb = unsafe { *reader.ceb_shm };
539
540 assert_eq!(version.load(atomic::Ordering::Relaxed), 0x0303);
541 assert_eq!(generation.load(atomic::Ordering::Relaxed), 10);
542 assert_eq!(ceb.bound_nsec(), 123);
543 }
544
545 /// Assert that creating a reader when the
546 /// shared memory segment has an unsupported version causes an Err result.
547 #[test]
548 fn test_reader_new_of_unsupported_shm_version() {
549 let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
550 let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
551 let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
552 let mut clockbound_shm_file = OpenOptions::new()
553 .write(true)
554 .open(clockbound_shm_path)
555 .expect("open clockbound file failed");
556 write_memory_segment!(
557 clockbound_shm_file,
558 0x414D5A4E,
559 0x43420200,
560 400,
561 9999,
562 10,
563 (0, 0),
564 (0, 0),
565 123,
566 0
567 );
568
569 let path = CString::new(clockbound_shm_path).expect("CString failed");
570 let result = ShmReader::new(&path);
571
572 // Assert that creating a reader on an unsupported shared memory segment version
573 // returns Err(ShmError::SegmentVersionNotSupported).
574 assert!(matches!(
575 result.unwrap_err(),
576 ShmError::SegmentVersionNotSupported(_)
577 ));
578 }
579
580 /// Assert that creating a reader and taking a snapshot when the
581 /// shared memory segment has an unsupported version causes an Err result.
582 #[test]
583 fn test_reader_snapshot_of_unsupported_shm_version() {
584 let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
585 let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
586 let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
587 let mut clockbound_shm_file = OpenOptions::new()
588 .write(true)
589 .open(clockbound_shm_path)
590 .expect("open clockbound file failed");
591 // Initially, write the current supported version.
592 write_memory_segment!(
593 clockbound_shm_file,
594 0x414D5A4E,
595 0x43420200,
596 400,
597 0x0303,
598 10,
599 (0, 0),
600 (0, 0),
601 123,
602 0
603 );
604
605 let path = CString::new(clockbound_shm_path).expect("CString failed");
606 let mut reader = ShmReader::new(&path).expect("Failed to create ShmReader");
607 let version = unsafe { &*reader.version };
608 assert_eq!(version.load(atomic::Ordering::Relaxed), 0x0303);
609
610 // Assert that snapshot works without an error with this supported version.
611 let result = reader.snapshot();
612 assert!(result.is_ok());
613
614 // Update the shared memory segment so that the version number is an
615 // unsupported number (e.g. 9999).
616 let _ = clockbound_shm_file.rewind();
617 write_memory_segment!(
618 clockbound_shm_file,
619 0x414D5A4E,
620 0x43420200,
621 400,
622 9999,
623 10,
624 (0, 0),
625 (0, 0),
626 123,
627 0
628 );
629
630 let version = unsafe { &*reader.version };
631 assert_eq!(version.load(atomic::Ordering::Relaxed), 9999);
632
633 // Assert that taking a snapshot of an unsupported shared memory segment version
634 // returns Err(ShmError::SegmentVersionNotSupported).
635 let result = reader.snapshot();
636 assert!(
637 matches!(result.unwrap_err(), ShmError::SegmentVersionNotSupported(msg) if msg.starts_with("Clockbound shared memory segment supports versions"))
638 );
639 }
640}