Skip to main content

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}