mmap_io/
lock.rs

1//! Memory locking operations to prevent pages from being swapped out.
2
3use crate::errors::{MmapIoError, Result};
4use crate::mmap::MemoryMappedFile;
5use crate::utils::slice_range;
6
7impl MemoryMappedFile {
8    /// Lock memory pages to prevent them from being swapped to disk.
9    ///
10    /// This operation requires appropriate permissions (typically root/admin).
11    /// Locked pages count against system limits.
12    ///
13    /// # Platform-specific behavior
14    ///
15    /// - **Unix**: Uses `mlock` system call
16    /// - **Windows**: Uses `VirtualLock`
17    ///
18    /// # Errors
19    ///
20    /// Returns `MmapIoError::OutOfBounds` if the range exceeds file bounds.
21    /// Returns `MmapIoError::LockFailed` if the lock operation fails (often due to permissions).
22    #[cfg(feature = "locking")]
23    pub fn lock(&self, offset: u64, len: u64) -> Result<()> {
24        if len == 0 {
25            return Ok(());
26        }
27
28        let total = self.current_len()?;
29        let (start, end) = slice_range(offset, len, total)?;
30        let length = end - start;
31
32        // Get the base pointer for the mapping
33        let ptr = match &self.inner.map {
34            crate::mmap::MapVariant::Ro(m) => m.as_ptr(),
35            crate::mmap::MapVariant::Rw(lock) => {
36                let guard = lock.read();
37                guard.as_ptr()
38            }
39            crate::mmap::MapVariant::Cow(m) => m.as_ptr(),
40        };
41
42        // SAFETY: We've validated the range is within bounds
43        let addr = unsafe { ptr.add(start) };
44
45        #[cfg(unix)]
46        {
47            // SAFETY: mlock is safe to call with validated parameters
48            let result = unsafe { libc::mlock(addr as *const libc::c_void, length) };
49
50            if result != 0 {
51                let err = std::io::Error::last_os_error();
52                return Err(MmapIoError::LockFailed(format!(
53                    "mlock failed: {err}. This operation typically requires elevated privileges."
54                )));
55            }
56        }
57
58        #[cfg(windows)]
59        {
60            use std::ptr;
61
62            extern "system" {
63                fn VirtualLock(lpAddress: *const core::ffi::c_void, dwSize: usize) -> i32;
64            }
65
66            // SAFETY: VirtualLock is safe with valid memory range
67            let result = unsafe { VirtualLock(addr as *const core::ffi::c_void, length) };
68
69            if result == 0 {
70                let err = std::io::Error::last_os_error();
71                return Err(MmapIoError::LockFailed(format!(
72                    "VirtualLock failed: {err}. This operation may require elevated privileges."
73                )));
74            }
75        }
76
77        Ok(())
78    }
79
80    /// Unlock previously locked memory pages.
81    ///
82    /// This allows the pages to be swapped out again if needed.
83    ///
84    /// # Platform-specific behavior
85    ///
86    /// - **Unix**: Uses `munlock` system call
87    /// - **Windows**: Uses `VirtualUnlock`
88    ///
89    /// # Errors
90    ///
91    /// Returns `MmapIoError::OutOfBounds` if the range exceeds file bounds.
92    /// Returns `MmapIoError::UnlockFailed` if the unlock operation fails.
93    #[cfg(feature = "locking")]
94    pub fn unlock(&self, offset: u64, len: u64) -> Result<()> {
95        if len == 0 {
96            return Ok(());
97        }
98
99        let total = self.current_len()?;
100        let (start, end) = slice_range(offset, len, total)?;
101        let length = end - start;
102
103        // Get the base pointer for the mapping
104        let ptr = match &self.inner.map {
105            crate::mmap::MapVariant::Ro(m) => m.as_ptr(),
106            crate::mmap::MapVariant::Rw(lock) => {
107                let guard = lock.read();
108                guard.as_ptr()
109            }
110            crate::mmap::MapVariant::Cow(m) => m.as_ptr(),
111        };
112
113        // SAFETY: We've validated the range is within bounds
114        let addr = unsafe { ptr.add(start) };
115
116        #[cfg(unix)]
117        {
118            // SAFETY: munlock is safe to call with validated parameters
119            let result = unsafe { libc::munlock(addr as *const libc::c_void, length) };
120
121            if result != 0 {
122                let err = std::io::Error::last_os_error();
123                return Err(MmapIoError::UnlockFailed(format!("munlock failed: {err}")));
124            }
125        }
126
127        #[cfg(windows)]
128        {
129            extern "system" {
130                fn VirtualUnlock(lpAddress: *const core::ffi::c_void, dwSize: usize) -> i32;
131            }
132
133            // SAFETY: VirtualUnlock is safe with valid memory range
134            let result = unsafe { VirtualUnlock(addr as *const core::ffi::c_void, length) };
135
136            if result == 0 {
137                let err = std::io::Error::last_os_error();
138                // VirtualUnlock can fail if pages weren't locked, which is often not an error
139                let err_code = err.raw_os_error().unwrap_or(0);
140                if err_code != 158 {
141                    // ERROR_NOT_LOCKED
142                    return Err(MmapIoError::UnlockFailed(format!(
143                        "VirtualUnlock failed: {err}"
144                    )));
145                }
146            }
147        }
148
149        Ok(())
150    }
151
152    /// Lock all pages of the memory-mapped file.
153    ///
154    /// Convenience method that locks the entire file.
155    ///
156    /// # Errors
157    ///
158    /// Returns `MmapIoError::LockFailed` if the lock operation fails.
159    #[cfg(feature = "locking")]
160    pub fn lock_all(&self) -> Result<()> {
161        let len = self.current_len()?;
162        self.lock(0, len)
163    }
164
165    /// Unlock all pages of the memory-mapped file.
166    ///
167    /// Convenience method that unlocks the entire file.
168    ///
169    /// # Errors
170    ///
171    /// Returns `MmapIoError::UnlockFailed` if the unlock operation fails.
172    #[cfg(feature = "locking")]
173    pub fn unlock_all(&self) -> Result<()> {
174        let len = self.current_len()?;
175        self.unlock(0, len)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::create_mmap;
183    use std::fs;
184    use std::path::PathBuf;
185
186    fn tmp_path(name: &str) -> PathBuf {
187        let mut p = std::env::temp_dir();
188        p.push(format!("mmap_io_lock_test_{}_{}", name, std::process::id()));
189        p
190    }
191
192    #[test]
193    #[cfg(feature = "locking")]
194    fn test_lock_unlock_operations() {
195        let path = tmp_path("lock_ops");
196        let _ = fs::remove_file(&path);
197
198        let mmap = create_mmap(&path, 8192).expect("create");
199
200        // Note: These operations may fail without appropriate privileges
201        // We test that they at least don't panic
202
203        // Test locking a range
204        let lock_result = mmap.lock(0, 4096);
205        if lock_result.is_ok() {
206            // If we successfully locked, we should be able to unlock
207            mmap.unlock(0, 4096)
208                .expect("unlock should succeed after lock");
209        } else {
210            // Expected on systems without privileges
211            println!("Lock failed (expected without privileges): {lock_result:?}");
212        }
213
214        // Test empty range (should be no-op)
215        mmap.lock(0, 0).expect("empty lock");
216        mmap.unlock(0, 0).expect("empty unlock");
217
218        // Test out of bounds
219        assert!(mmap.lock(8192, 1).is_err());
220        assert!(mmap.unlock(8192, 1).is_err());
221
222        // Test lock_all/unlock_all
223        let lock_all_result = mmap.lock_all();
224        if lock_all_result.is_ok() {
225            mmap.unlock_all()
226                .expect("unlock_all should succeed after lock_all");
227        }
228
229        fs::remove_file(&path).expect("cleanup");
230    }
231
232    #[test]
233    #[cfg(feature = "locking")]
234    fn test_lock_with_different_modes() {
235        let path = tmp_path("lock_modes");
236        let _ = fs::remove_file(&path);
237
238        // Create and test with RW mode
239        let mmap = create_mmap(&path, 4096).expect("create");
240        let _ = mmap.lock(0, 1024); // May fail without privileges
241        drop(mmap);
242
243        // Test with RO mode
244        let mmap = MemoryMappedFile::open_ro(&path).expect("open ro");
245        let _ = mmap.lock(0, 1024); // May fail without privileges
246
247        #[cfg(feature = "cow")]
248        {
249            // Test with COW mode
250            let mmap = MemoryMappedFile::open_cow(&path).expect("open cow");
251            let _ = mmap.lock(0, 1024); // May fail without privileges
252        }
253
254        fs::remove_file(&path).expect("cleanup");
255    }
256
257    #[test]
258    #[cfg(all(feature = "locking", unix))]
259    fn test_multiple_lock_regions() {
260        let path = tmp_path("multi_lock");
261        let _ = fs::remove_file(&path);
262
263        let mmap = create_mmap(&path, 16384).expect("create");
264
265        // Try to lock multiple non-overlapping regions
266        // These may fail without privileges, but shouldn't panic
267        let _ = mmap.lock(0, 4096);
268        let _ = mmap.lock(4096, 4096);
269        let _ = mmap.lock(8192, 4096);
270
271        // Unlock in different order
272        let _ = mmap.unlock(4096, 4096);
273        let _ = mmap.unlock(0, 4096);
274        let _ = mmap.unlock(8192, 4096);
275
276        fs::remove_file(&path).expect("cleanup");
277    }
278}