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