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}