duende_mlock/lib.rs
1//! # duende-mlock
2//!
3//! Memory locking for swap-critical daemons.
4//!
5//! ## DT-007: Swap Deadlock Prevention
6//!
7//! When a daemon serves as a swap device (e.g., `trueno-ublk`), a deadlock occurs if:
8//!
9//! 1. Kernel needs memory → initiates swap-out to the daemon
10//! 2. Daemon needs memory to process I/O
11//! 3. Kernel tries to swap daemon's pages → to the same daemon
12//! 4. **Deadlock**: daemon blocked waiting for itself
13//!
14//! This crate provides `mlockall()` to pin daemon memory, preventing the kernel
15//! from swapping it out.
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use duende_mlock::{lock_all, MlockError};
21//!
22//! fn main() -> Result<(), MlockError> {
23//! // Lock all current and future memory allocations
24//! let status = lock_all()?;
25//! println!("Locked {} bytes", status.bytes_locked());
26//! Ok(())
27//! }
28//! ```
29//!
30//! ## Configuration
31//!
32//! ```rust,no_run
33//! use duende_mlock::{MlockConfig, lock_with_config};
34//!
35//! let config = MlockConfig::builder()
36//! .current(true) // Lock existing pages
37//! .future(true) // Lock future allocations
38//! .required(false) // Don't fail if mlock fails
39//! .build();
40//!
41//! match lock_with_config(config) {
42//! Ok(status) => println!("Locked: {}", status.is_locked()),
43//! Err(e) => eprintln!("Warning: {}", e),
44//! }
45//! ```
46//!
47//! ## Platform Support
48//!
49//! | Platform | Support | Notes |
50//! |----------|---------|-------|
51//! | Linux | Full | Requires `CAP_IPC_LOCK` or root |
52//! | macOS | Limited | Requires entitlements |
53//! | Others | None | Returns `MlockStatus::Unsupported` |
54//!
55//! ## Container Requirements
56//!
57//! ```bash
58//! # Docker
59//! docker run --cap-add=IPC_LOCK --ulimit memlock=-1:-1 ...
60//!
61//! # docker-compose.yml
62//! cap_add:
63//! - IPC_LOCK
64//! ulimits:
65//! memlock:
66//! soft: -1
67//! hard: -1
68//! ```
69
70#![forbid(unsafe_op_in_unsafe_fn)]
71#![warn(missing_docs, rust_2018_idioms)]
72
73mod config;
74mod error;
75mod status;
76
77#[cfg(unix)]
78mod unix;
79
80#[cfg(not(unix))]
81mod unsupported;
82
83pub use config::{MlockConfig, MlockConfigBuilder};
84pub use error::MlockError;
85pub use status::MlockStatus;
86
87/// Lock all current and future memory allocations.
88///
89/// This is the recommended function for daemon memory locking. It calls
90/// `mlockall(MCL_CURRENT | MCL_FUTURE)` to pin all existing pages and
91/// ensure future allocations are also locked.
92///
93/// # Errors
94///
95/// Returns [`MlockError`] if memory locking fails:
96///
97/// - [`MlockError::PermissionDenied`]: Need `CAP_IPC_LOCK` capability or root
98/// - [`MlockError::ResourceLimit`]: `RLIMIT_MEMLOCK` too low
99/// - [`MlockError::InvalidArgument`]: Invalid flags (shouldn't happen)
100///
101/// # Example
102///
103/// ```rust,no_run
104/// use duende_mlock::lock_all;
105///
106/// let status = lock_all()?;
107/// assert!(status.is_locked());
108/// # Ok::<(), duende_mlock::MlockError>(())
109/// ```
110///
111/// # Platform Behavior
112///
113/// - **Linux/macOS**: Calls `mlockall(MCL_CURRENT | MCL_FUTURE)`
114/// - **Others**: Returns `Ok(MlockStatus::Unsupported)`
115pub fn lock_all() -> Result<MlockStatus, MlockError> {
116 lock_with_config(MlockConfig::default())
117}
118
119/// Lock memory with custom configuration.
120///
121/// Use [`MlockConfig::builder()`] to create a configuration:
122///
123/// ```rust,no_run
124/// use duende_mlock::{MlockConfig, lock_with_config};
125///
126/// let config = MlockConfig::builder()
127/// .current(true)
128/// .future(true)
129/// .required(false) // Don't fail on error
130/// .build();
131///
132/// let status = lock_with_config(config)?;
133/// # Ok::<(), duende_mlock::MlockError>(())
134/// ```
135///
136/// # Non-Required Mode
137///
138/// When `required(false)` is set, mlock failures return `Ok(MlockStatus::Failed { .. })`
139/// instead of `Err`. This allows daemons to continue with a warning.
140///
141/// # Errors
142///
143/// Returns [`MlockError::PermissionDenied`] if the process lacks `CAP_IPC_LOCK`.
144/// Returns [`MlockError::ResourceLimit`] if `RLIMIT_MEMLOCK` is exceeded.
145/// Returns [`MlockError::Unsupported`] on non-Unix platforms.
146pub fn lock_with_config(config: MlockConfig) -> Result<MlockStatus, MlockError> {
147 #[cfg(unix)]
148 {
149 unix::lock_with_config(config)
150 }
151
152 #[cfg(not(unix))]
153 {
154 unsupported::lock_with_config(config)
155 }
156}
157
158/// Unlock all locked memory.
159///
160/// Calls `munlockall()` to release all memory locks. This is rarely needed
161/// in production since process exit automatically releases all locks.
162///
163/// # Example
164///
165/// ```rust,no_run
166/// use duende_mlock::{lock_all, unlock_all};
167///
168/// let _ = lock_all()?;
169/// // ... do work ...
170/// unlock_all()?;
171/// # Ok::<(), duende_mlock::MlockError>(())
172/// ```
173///
174/// # Errors
175///
176/// Returns [`MlockError::Unsupported`] if `munlockall()` fails.
177pub fn unlock_all() -> Result<(), MlockError> {
178 #[cfg(unix)]
179 {
180 unix::unlock_all()
181 }
182
183 #[cfg(not(unix))]
184 {
185 Ok(())
186 }
187}
188
189/// Check if process memory is currently locked.
190///
191/// On Linux, reads `/proc/self/status` and checks the `VmLck` field.
192/// On other platforms, returns `false`.
193///
194/// # Example
195///
196/// ```rust,no_run
197/// use duende_mlock::{lock_all, is_locked};
198///
199/// assert!(!is_locked());
200/// lock_all()?;
201/// assert!(is_locked());
202/// # Ok::<(), duende_mlock::MlockError>(())
203/// ```
204#[must_use]
205pub fn is_locked() -> bool {
206 #[cfg(unix)]
207 {
208 unix::is_locked()
209 }
210
211 #[cfg(not(unix))]
212 {
213 false
214 }
215}
216
217/// Get the number of bytes currently locked.
218///
219/// On Linux, reads the `VmLck` field from `/proc/self/status`.
220/// On other platforms, returns `0`.
221///
222/// # Example
223///
224/// ```rust,no_run
225/// use duende_mlock::{lock_all, locked_bytes};
226///
227/// lock_all()?;
228/// println!("Locked {} KB", locked_bytes() / 1024);
229/// # Ok::<(), duende_mlock::MlockError>(())
230/// ```
231#[must_use]
232pub fn locked_bytes() -> usize {
233 #[cfg(unix)]
234 {
235 unix::locked_bytes()
236 }
237
238 #[cfg(not(unix))]
239 {
240 0
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_default_config() {
250 let config = MlockConfig::default();
251 assert!(config.current());
252 assert!(config.future());
253 assert!(config.required());
254 assert!(!config.onfault());
255 }
256
257 #[test]
258 fn test_config_builder() {
259 let config = MlockConfig::builder()
260 .current(false)
261 .future(true)
262 .required(false)
263 .onfault(true)
264 .build();
265
266 assert!(!config.current());
267 assert!(config.future());
268 assert!(!config.required());
269 assert!(config.onfault());
270 }
271
272 #[test]
273 fn test_is_locked_returns_bool() {
274 // Should not panic regardless of privileges
275 let _ = is_locked();
276 }
277
278 #[test]
279 fn test_locked_bytes_returns_usize() {
280 // Should not panic regardless of privileges
281 let _ = locked_bytes();
282 }
283
284 #[test]
285 fn test_unlock_all_does_not_panic() {
286 // unlock_all should not panic even if nothing is locked
287 let result = unlock_all();
288 // Result depends on platform and privileges
289 let _ = result;
290 }
291
292 #[test]
293 fn test_lock_all_non_fatal() {
294 // Test that lock_all works (may succeed or fail based on privileges)
295 let config = MlockConfig::builder().required(false).build();
296 let result = lock_with_config(config);
297 assert!(result.is_ok());
298 // Clean up
299 let _ = unlock_all();
300 }
301
302 #[test]
303 fn test_lock_with_config_empty_flags() {
304 // With no flags, should succeed
305 let config = MlockConfig::builder()
306 .current(false)
307 .future(false)
308 .build();
309 let result = lock_with_config(config);
310 assert!(result.is_ok());
311 if let Ok(status) = result {
312 assert!(status.is_locked());
313 assert_eq!(status.bytes_locked(), 0);
314 }
315 }
316
317 #[test]
318 fn test_mlock_status_methods() {
319 // Test MlockStatus methods
320 let locked = MlockStatus::Locked { bytes_locked: 1024 };
321 assert!(locked.is_locked());
322 assert!(!locked.is_failed());
323 assert!(!locked.is_unsupported());
324 assert_eq!(locked.bytes_locked(), 1024);
325 assert_eq!(locked.failure_errno(), None);
326
327 let failed = MlockStatus::Failed { errno: 1 };
328 assert!(!failed.is_locked());
329 assert!(failed.is_failed());
330 assert!(!failed.is_unsupported());
331 assert_eq!(failed.bytes_locked(), 0);
332 assert_eq!(failed.failure_errno(), Some(1));
333
334 let unsupported = MlockStatus::Unsupported;
335 assert!(!unsupported.is_locked());
336 assert!(!unsupported.is_failed());
337 assert!(unsupported.is_unsupported());
338 assert_eq!(unsupported.bytes_locked(), 0);
339 assert_eq!(unsupported.failure_errno(), None);
340 }
341
342 #[test]
343 fn test_mlock_status_display() {
344 let locked = MlockStatus::Locked { bytes_locked: 1024 };
345 assert!(format!("{locked}").contains("locked"));
346
347 let failed = MlockStatus::Failed { errno: 12 };
348 assert!(format!("{failed}").contains("failed"));
349
350 let unsupported = MlockStatus::Unsupported;
351 assert!(format!("{unsupported}").contains("unsupported"));
352 }
353}