uqa_storage/native_file/shared_mapping.rs
1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Shared bytes of a local coordination file, accessed only under its external lock.
8
9use std::{fs::File, io, os::fd::AsRawFd, ptr::NonNull};
10
11mod allocation;
12
13/// A retained mapping, without a second file descriptor or borrowed Rust slices.
14/// Mapping ownership may move between threads; byte access requires external
15/// exclusion, extent validation and memory barriers at the lock boundary.
16pub struct SharedFileMapping {
17 address: NonNull<u8>,
18 length: usize,
19}
20
21// SAFETY: moving the mapping changes no address or ownership. Access is unsafe
22// and requires external exclusion; this type deliberately does not implement Sync.
23unsafe impl Send for SharedFileMapping {}
24
25impl SharedFileMapping {
26 /// Map an existing prefix, reserving sparse holes before writable access.
27 /// The caller must serialize this operation with file writes and resizing.
28 /// The existing file bytes and length are preserved, as is its seek position.
29 /// Unsupported filesystems or allocation failures should use positioned I/O.
30 pub fn new(file: &File, length: usize) -> io::Result<Self> {
31 let metadata = file.metadata()?;
32 if !metadata.is_file()
33 || length == 0
34 || length > isize::MAX as usize
35 || length as u64 > metadata.len()
36 {
37 return Err(io::Error::new(
38 io::ErrorKind::InvalidInput,
39 "invalid mapped file extent",
40 ));
41 }
42 require_local_filesystem(file)?;
43 allocation::reserve(file, length)?;
44 // SAFETY: mmap borrows an open read/write file, with a checked nonzero
45 // existing extent; it creates a new mapping without replacing any address.
46 let address = unsafe {
47 libc::mmap(
48 std::ptr::null_mut(),
49 length,
50 libc::PROT_READ | libc::PROT_WRITE,
51 libc::MAP_SHARED,
52 file.as_raw_fd(),
53 0,
54 )
55 };
56 if address == libc::MAP_FAILED {
57 return Err(io::Error::last_os_error());
58 }
59 let Some(address) = NonNull::new(address.cast::<u8>()) else {
60 // SAFETY: even a zero-address mapping must be released on rejection.
61 unsafe { libc::munmap(address, length) };
62 return Err(io::Error::other("mapped file has a null address"));
63 };
64 Ok(Self { address, length })
65 }
66
67 pub fn mapped_len(&self) -> usize {
68 self.length
69 }
70
71 pub fn contains(&self, offset: u64, length: usize) -> bool {
72 self.start(offset, length).is_some()
73 }
74
75 fn start(&self, offset: u64, length: usize) -> Option<usize> {
76 let start = usize::try_from(offset).ok()?;
77 (start.checked_add(length)? <= self.length).then_some(start)
78 }
79
80 /// Copy a mapped range, returning false if it is outside this mapping.
81 ///
82 /// # Safety
83 /// The underlying file must still cover the mapped extent. Exclude writes
84 /// and resizing in every process for the complete copy, with an acquire
85 /// barrier after taking the external lock. The destination must not alias
86 /// the mapping. No reference into the mapped memory escapes this call.
87 pub unsafe fn read(&self, offset: u64, bytes: &mut [u8]) -> bool {
88 let Some(start) = self.start(offset, bytes.len()) else {
89 return false;
90 };
91 // SAFETY: the caller maintains the live extent and exclusion; start
92 // bounds the complete copy, whose destination is separately owned.
93 unsafe {
94 std::ptr::copy_nonoverlapping(
95 self.address.as_ptr().add(start),
96 bytes.as_mut_ptr(),
97 bytes.len(),
98 );
99 }
100 true
101 }
102
103 /// Replace a mapped range, returning false if it is outside this mapping.
104 ///
105 /// # Safety
106 /// The underlying file must still cover the reserved mapped extent. Exclude
107 /// all other access and resizing until the copy completes, then issue a
108 /// release barrier before unlocking. The source must not alias the mapping.
109 /// This publishes coordination bytes; it does not provide crash durability.
110 pub unsafe fn write(&self, offset: u64, bytes: &[u8]) -> bool {
111 let Some(start) = self.start(offset, bytes.len()) else {
112 return false;
113 };
114 // SAFETY: the caller maintains the reserved extent and exclusive access;
115 // start bounds the complete copy from a separately owned source.
116 unsafe {
117 std::ptr::copy_nonoverlapping(
118 bytes.as_ptr(),
119 self.address.as_ptr().add(start),
120 bytes.len(),
121 );
122 }
123 true
124 }
125
126 /// Replace a record while publishing its state byte last. An interrupted
127 /// copy leaves `pending`, so a peer never decodes a partially written record.
128 /// Returns false without mutation when either range is invalid.
129 ///
130 /// # Safety
131 /// The exclusion, extent and source requirements of `write` apply. `pending`
132 /// must describe a valid unpublished record in the caller's format, regardless
133 /// of its remaining bytes. Its state byte must be independently writable.
134 pub unsafe fn write_published(
135 &self,
136 offset: u64,
137 bytes: &[u8],
138 state: usize,
139 pending: u8,
140 ) -> bool {
141 use std::sync::atomic::{AtomicU8, Ordering};
142 let Some(start) = self
143 .start(offset, bytes.len())
144 .filter(|_| state < bytes.len())
145 else {
146 return false;
147 };
148 // SAFETY: the validated state byte is aligned for AtomicU8. Exclusion
149 // prevents concurrent atomic/non-atomic access to the mapped bytes.
150 let publication = unsafe { AtomicU8::from_ptr(self.address.as_ptr().add(start + state)) };
151 // SeqCst also orders the following payload copies after invalidation.
152 publication.store(pending, Ordering::SeqCst);
153 unsafe {
154 self.write(offset, &bytes[..state]);
155 self.write(offset + state as u64 + 1, &bytes[state + 1..]);
156 }
157 #[cfg(test)]
158 assert!(
159 !INTERRUPT_PUBLICATION.replace(false),
160 "interrupted record publication"
161 );
162 publication.store(bytes[state], Ordering::Release);
163 true
164 }
165}
166
167#[cfg(test)]
168thread_local! {
169 static INTERRUPT_PUBLICATION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
170}
171
172impl Drop for SharedFileMapping {
173 fn drop(&mut self) {
174 // SAFETY: this object uniquely owns this mapping and no borrowed slice
175 // exists. Unmapping neither opens nor closes a coordination descriptor.
176 unsafe { libc::munmap(self.address.as_ptr().cast(), self.length) };
177 }
178}
179
180fn require_local_filesystem(file: &File) -> io::Result<()> {
181 let mut filesystem = std::mem::MaybeUninit::<libc::statfs>::uninit();
182 // SAFETY: the descriptor and correctly sized output are live for fstatfs.
183 if unsafe { libc::fstatfs(file.as_raw_fd(), filesystem.as_mut_ptr()) } != 0 {
184 return Err(io::Error::last_os_error());
185 }
186 let filesystem = unsafe { filesystem.assume_init() };
187 #[cfg(target_os = "linux")]
188 let supported = matches!(
189 filesystem.f_type as u64,
190 0xef53 | 0x5846_5342 | 0x9123_683e | 0x0102_1994 | 0x8584_58f6
191 );
192 #[cfg(target_os = "macos")]
193 let supported = {
194 let name = unsafe { std::ffi::CStr::from_ptr(filesystem.f_fstypename.as_ptr()) };
195 filesystem.f_flags & libc::MNT_LOCAL as u32 != 0
196 && matches!(name.to_bytes(), b"apfs" | b"hfs")
197 };
198 if supported {
199 Ok(())
200 } else {
201 Err(io::Error::new(
202 io::ErrorKind::Unsupported,
203 "file mapping requires a supported local filesystem",
204 ))
205 }
206}
207
208#[cfg(test)]
209mod tests;