1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
//! Memory locking operations to prevent pages from being swapped out.
use crate::errors::{MmapIoError, Result};
use crate::mmap::MemoryMappedFile;
use crate::utils::slice_range;
impl MemoryMappedFile {
/// Lock memory pages to prevent them from being swapped to disk.
///
/// This operation requires appropriate permissions (typically root/admin).
/// Locked pages count against system limits.
///
/// # Platform-specific behavior
///
/// - **Unix**: Uses `mlock` system call
/// - **Windows**: Uses `VirtualLock`
///
/// # Errors
///
/// Returns `MmapIoError::OutOfBounds` if the range exceeds file bounds.
/// Returns `MmapIoError::LockFailed` if the lock operation fails (often due to permissions).
#[cfg(feature = "locking")]
pub fn lock(&self, offset: u64, len: u64) -> Result<()> {
if len == 0 {
return Ok(());
}
let total = self.current_len()?;
let (start, end) = slice_range(offset, len, total)?;
let length = end - start;
// Get the base pointer for the mapping
let ptr = match &self.inner.map {
crate::mmap::MapVariant::Ro(m) => m.as_ptr(),
crate::mmap::MapVariant::Rw(lock) => {
let guard = lock.read();
guard.as_ptr()
}
crate::mmap::MapVariant::Cow(m) => m.as_ptr(),
};
// SAFETY: `start` satisfies `start + length <= total` per
// `slice_range` above, where `total` is the current mapped
// length owned by `self.inner.map`. `ptr.add(start)` therefore
// remains within the same allocated object (the OS mapping).
// We never form a Rust reference to the memory at `addr`; only
// the kernel reads it (via `mlock`/`VirtualLock`), which
// operates on the address range itself.
let addr = unsafe { ptr.add(start) };
#[cfg(unix)]
{
// SAFETY: POSIX `mlock` requires:
// 1. `[addr, addr + length)` lies within a mapped region
// of the process. Established by the
// `slice_range`/`ensure_in_bounds` check above.
// 2. `length > 0` (we early-return on `len == 0` at the
// top of this method, and the bounds check guarantees
// `length == end - start > 0` reaches here).
// The call locks the resident pages into RAM, preventing
// them from being paged out. It does not access the memory
// contents and does not retain `addr` after the call. On
// failure (typically EPERM without CAP_IPC_LOCK, or ENOMEM
// exceeding RLIMIT_MEMLOCK) it returns -1 and we surface
// that as `LockFailed`. No UB is reachable from any failure
// mode.
// Reference: https://man7.org/linux/man-pages/man2/mlock.2.html
let result = unsafe { libc::mlock(addr as *const libc::c_void, length) };
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(MmapIoError::LockFailed(format!(
"mlock failed: {err}. This operation typically requires elevated privileges."
)));
}
}
#[cfg(windows)]
{
extern "system" {
fn VirtualLock(lpAddress: *const core::ffi::c_void, dwSize: usize) -> i32;
}
// SAFETY: `VirtualLock` (kernel32.dll) requires:
// 1. `lpAddress` points within a committed region of the
// caller's address space, and
// `[lpAddress, lpAddress + dwSize)` does not cross a
// region boundary. The mmap-io mapping is a single
// committed region of length `total`, and our bounds
// check guarantees the range is inside it.
// 2. `dwSize > 0` (guaranteed by the early-return on
// `len == 0` and the bounds-check arithmetic).
// Like `mlock`, the function operates on the address range
// without accessing the memory contents or retaining the
// pointer. A return of 0 indicates failure; we read the
// last OS error and return `LockFailed`.
// Reference: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtuallock
let result = unsafe { VirtualLock(addr as *const core::ffi::c_void, length) };
if result == 0 {
let err = std::io::Error::last_os_error();
return Err(MmapIoError::LockFailed(format!(
"VirtualLock failed: {err}. This operation may require elevated privileges."
)));
}
}
Ok(())
}
/// Unlock previously locked memory pages.
///
/// This allows the pages to be swapped out again if needed.
///
/// # Platform-specific behavior
///
/// - **Unix**: Uses `munlock` system call
/// - **Windows**: Uses `VirtualUnlock`
///
/// # Errors
///
/// Returns `MmapIoError::OutOfBounds` if the range exceeds file bounds.
/// Returns `MmapIoError::UnlockFailed` if the unlock operation fails.
#[cfg(feature = "locking")]
pub fn unlock(&self, offset: u64, len: u64) -> Result<()> {
if len == 0 {
return Ok(());
}
let total = self.current_len()?;
let (start, end) = slice_range(offset, len, total)?;
let length = end - start;
// Get the base pointer for the mapping
let ptr = match &self.inner.map {
crate::mmap::MapVariant::Ro(m) => m.as_ptr(),
crate::mmap::MapVariant::Rw(lock) => {
let guard = lock.read();
guard.as_ptr()
}
crate::mmap::MapVariant::Cow(m) => m.as_ptr(),
};
// SAFETY: same justification as in `lock`: `start + length`
// is within the mapping per the prior `slice_range` check, so
// `ptr.add(start)` is in-bounds of the underlying allocated
// object. The resulting pointer is only handed to a kernel
// syscall below; no Rust reference is formed.
let addr = unsafe { ptr.add(start) };
#[cfg(unix)]
{
// SAFETY: POSIX `munlock` requires the same range
// preconditions as `mlock` (range lies within a mapped
// region, length > 0). Both are established above.
// `munlock` does not access the memory contents; it removes
// the lock that prevented paging. If the range was not
// previously locked, the syscall is still well-defined and
// simply succeeds (or returns ENOMEM on Linux, which we
// surface as `UnlockFailed` rather than treating as UB).
// Reference: https://man7.org/linux/man-pages/man2/mlock.2.html
let result = unsafe { libc::munlock(addr as *const libc::c_void, length) };
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(MmapIoError::UnlockFailed(format!("munlock failed: {err}")));
}
}
#[cfg(windows)]
{
extern "system" {
fn VirtualUnlock(lpAddress: *const core::ffi::c_void, dwSize: usize) -> i32;
}
// SAFETY: `VirtualUnlock` (kernel32.dll) requires the same
// range preconditions as `VirtualLock`. The function does
// not read or write the memory; it operates on the locked-
// pages bookkeeping for the address range. If the range
// was not previously locked, the function returns 0 with
// `GetLastError() == ERROR_NOT_LOCKED` (158), which we
// detect below and treat as a soft success.
// Reference: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualunlock
let result = unsafe { VirtualUnlock(addr as *const core::ffi::c_void, length) };
if result == 0 {
let err = std::io::Error::last_os_error();
// VirtualUnlock can fail if pages weren't locked, which is often not an error
let err_code = err.raw_os_error().unwrap_or(0);
if err_code != 158 {
// ERROR_NOT_LOCKED
return Err(MmapIoError::UnlockFailed(format!(
"VirtualUnlock failed: {err}"
)));
}
}
}
Ok(())
}
/// Lock all pages of the memory-mapped file.
///
/// Convenience method that locks the entire file.
///
/// # Errors
///
/// Returns `MmapIoError::LockFailed` if the lock operation fails.
#[cfg(feature = "locking")]
pub fn lock_all(&self) -> Result<()> {
let len = self.current_len()?;
self.lock(0, len)
}
/// Unlock all pages of the memory-mapped file.
///
/// Convenience method that unlocks the entire file.
///
/// # Errors
///
/// Returns `MmapIoError::UnlockFailed` if the unlock operation fails.
#[cfg(feature = "locking")]
pub fn unlock_all(&self) -> Result<()> {
let len = self.current_len()?;
self.unlock(0, len)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::create_mmap;
use std::fs;
use std::path::PathBuf;
fn tmp_path(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("mmap_io_lock_test_{}_{}", name, std::process::id()));
p
}
#[test]
#[cfg(feature = "locking")]
fn test_lock_unlock_operations() {
let path = tmp_path("lock_ops");
let _ = fs::remove_file(&path);
let mmap = create_mmap(&path, 8192).expect("create");
// Note: These operations may fail without appropriate privileges
// We test that they at least don't panic
// Test locking a range
let lock_result = mmap.lock(0, 4096);
if lock_result.is_ok() {
// If we successfully locked, we should be able to unlock
mmap.unlock(0, 4096)
.expect("unlock should succeed after lock");
} else {
// Expected on systems without privileges
println!("Lock failed (expected without privileges): {lock_result:?}");
}
// Test empty range (should be no-op)
mmap.lock(0, 0).expect("empty lock");
mmap.unlock(0, 0).expect("empty unlock");
// Test out of bounds
assert!(mmap.lock(8192, 1).is_err());
assert!(mmap.unlock(8192, 1).is_err());
// Test lock_all/unlock_all
let lock_all_result = mmap.lock_all();
if lock_all_result.is_ok() {
mmap.unlock_all()
.expect("unlock_all should succeed after lock_all");
}
fs::remove_file(&path).expect("cleanup");
}
#[test]
#[cfg(feature = "locking")]
fn test_lock_with_different_modes() {
let path = tmp_path("lock_modes");
let _ = fs::remove_file(&path);
// Create and test with RW mode
let mmap = create_mmap(&path, 4096).expect("create");
let _ = mmap.lock(0, 1024); // May fail without privileges
drop(mmap);
// Test with RO mode
let mmap = MemoryMappedFile::open_ro(&path).expect("open ro");
let _ = mmap.lock(0, 1024); // May fail without privileges
#[cfg(feature = "cow")]
{
// Test with COW mode
let mmap = MemoryMappedFile::open_cow(&path).expect("open cow");
let _ = mmap.lock(0, 1024); // May fail without privileges
}
fs::remove_file(&path).expect("cleanup");
}
#[test]
#[cfg(all(feature = "locking", unix))]
fn test_multiple_lock_regions() {
let path = tmp_path("multi_lock");
let _ = fs::remove_file(&path);
let mmap = create_mmap(&path, 16384).expect("create");
// Try to lock multiple non-overlapping regions
// These may fail without privileges, but shouldn't panic
let _ = mmap.lock(0, 4096);
let _ = mmap.lock(4096, 4096);
let _ = mmap.lock(8192, 4096);
// Unlock in different order
let _ = mmap.unlock(4096, 4096);
let _ = mmap.unlock(0, 4096);
let _ = mmap.unlock(8192, 4096);
fs::remove_file(&path).expect("cleanup");
}
}