duende_platform/
memory.rs1use std::io;
32
33use crate::{PlatformError, Result};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum MlockResult {
38 Success,
40 Disabled,
42 Failed(i32),
44}
45
46#[cfg(target_os = "linux")]
76#[allow(unsafe_code)]
77pub fn lock_daemon_memory(required: bool) -> Result<MlockResult> {
78 use tracing::{info, warn};
79
80 info!("Locking daemon memory to prevent swap deadlock (DT-007)...");
81
82 let result = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
86
87 if result == 0 {
88 info!("Memory locked successfully - daemon pages will not be swapped");
89 Ok(MlockResult::Success)
90 } else {
91 let errno = io::Error::last_os_error().raw_os_error().unwrap_or(-1);
92 let err_msg = match errno {
93 libc::ENOMEM => "insufficient memory or resource limits (check RLIMIT_MEMLOCK)",
94 libc::EPERM => "insufficient privileges (need CAP_IPC_LOCK or root)",
95 libc::EINVAL => "invalid flags",
96 _ => "unknown error",
97 };
98
99 if required {
100 Err(PlatformError::Resource(format!(
101 "mlockall() failed: {} (errno={}). \
102 Cannot safely run as swap device without mlock(). \
103 Either run as root, add CAP_IPC_LOCK, or set lock_memory_required=false",
104 err_msg, errno
105 )))
106 } else {
107 warn!(
108 "mlockall() failed: {} (errno={}). \
109 Daemon may deadlock under memory pressure when used as swap device. \
110 Set lock_memory_required=true to make this fatal.",
111 err_msg, errno
112 );
113 Ok(MlockResult::Failed(errno))
114 }
115 }
116}
117
118#[cfg(target_os = "macos")]
120#[allow(unsafe_code)]
121pub fn lock_daemon_memory(required: bool) -> Result<MlockResult> {
122 use tracing::{info, warn};
123
124 info!("Attempting memory lock on macOS...");
125
126 let result = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
129
130 if result == 0 {
131 info!("Memory locked successfully on macOS");
132 Ok(MlockResult::Success)
133 } else {
134 let errno = io::Error::last_os_error().raw_os_error().unwrap_or(-1);
135 let err_msg = match errno {
136 libc::ENOMEM => "insufficient memory or resource limits",
137 libc::EPERM => {
138 "insufficient privileges (may need com.apple.security.cs.allow-jit entitlement)"
139 }
140 libc::EINVAL => "invalid flags",
141 libc::EAGAIN => "system resources temporarily unavailable",
142 _ => "unknown error",
143 };
144
145 if required {
146 Err(PlatformError::Resource(format!(
147 "mlockall() failed on macOS: {} (errno={})",
148 err_msg, errno
149 )))
150 } else {
151 warn!("mlockall() failed on macOS: {} (errno={})", err_msg, errno);
152 Ok(MlockResult::Failed(errno))
153 }
154 }
155}
156
157#[cfg(not(any(target_os = "linux", target_os = "macos")))]
159pub fn lock_daemon_memory(_required: bool) -> Result<MlockResult> {
160 use tracing::debug;
161 debug!("Memory locking not supported on this platform");
162 Ok(MlockResult::Disabled)
163}
164
165#[cfg(target_os = "linux")]
169pub fn is_memory_locked() -> bool {
170 if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
171 for line in status.lines() {
172 if line.starts_with("VmLck:") {
173 let parts: Vec<&str> = line.split_whitespace().collect();
174 if parts.len() >= 2 && let Ok(kb) = parts[1].parse::<u64>() {
175 return kb > 0;
176 }
177 }
178 }
179 }
180 false
181}
182
183#[cfg(not(target_os = "linux"))]
185pub fn is_memory_locked() -> bool {
186 false
188}
189
190#[cfg(any(target_os = "linux", target_os = "macos"))]
197#[allow(unsafe_code)]
198pub fn unlock_daemon_memory() -> Result<()> {
199 let result = unsafe { libc::munlockall() };
201 if result == 0 {
202 Ok(())
203 } else {
204 Err(PlatformError::Resource("munlockall() failed".to_string()))
205 }
206}
207
208#[cfg(not(any(target_os = "linux", target_os = "macos")))]
210pub fn unlock_daemon_memory() -> Result<()> {
211 Ok(())
212}
213
214pub fn apply_memory_config(config: &duende_core::ResourceConfig) -> Result<()> {
237 if config.lock_memory {
238 let result = lock_daemon_memory(config.lock_memory_required)?;
239 tracing::info!("Memory lock result: {:?}", result);
240 } else {
241 tracing::debug!("Memory locking disabled (lock_memory=false)");
242 }
243 Ok(())
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use duende_core::ResourceConfig;
250
251 #[test]
252 fn test_mlock_result_variants() {
253 let success = MlockResult::Success;
255 let disabled = MlockResult::Disabled;
256 let failed = MlockResult::Failed(1);
257
258 assert_eq!(success, MlockResult::Success);
259 assert_eq!(disabled, MlockResult::Disabled);
260 assert_eq!(failed, MlockResult::Failed(1));
261 assert_ne!(success, disabled);
262 assert_ne!(success, failed);
263
264 let _ = format!("{:?}", success);
266 let _ = format!("{:?}", disabled);
267 let _ = format!("{:?}", failed);
268
269 let cloned = success;
271 assert_eq!(cloned, success);
272 }
273
274 #[test]
275 fn test_mlock_disabled_when_not_required() {
276 let result = lock_daemon_memory(false);
279 assert!(result.is_ok());
280 let mlock_result = result.expect("should succeed");
282 assert!(matches!(
283 mlock_result,
284 MlockResult::Success | MlockResult::Failed(_) | MlockResult::Disabled
285 ));
286 }
287
288 #[test]
289 fn test_is_memory_locked_returns_bool() {
290 let _ = is_memory_locked();
292 }
293
294 #[test]
295 fn test_unlock_daemon_memory() {
296 let result = unlock_daemon_memory();
298 let _ = result; }
301
302 #[test]
303 fn test_apply_memory_config_disabled() {
304 let config = ResourceConfig {
305 lock_memory: false,
306 lock_memory_required: false,
307 ..ResourceConfig::default()
308 };
309
310 let result = apply_memory_config(&config);
311 assert!(result.is_ok());
312 }
313
314 #[test]
315 fn test_apply_memory_config_enabled_not_required() {
316 let config = ResourceConfig {
317 lock_memory: true,
318 lock_memory_required: false,
319 ..ResourceConfig::default()
320 };
321
322 let result = apply_memory_config(&config);
323 assert!(result.is_ok());
325 }
326
327 #[test]
328 #[cfg(target_os = "linux")]
329 fn test_mlock_with_privileges() {
330 let result = lock_daemon_memory(false);
333 assert!(result.is_ok());
334
335 match result.expect("mlock result") {
336 MlockResult::Success => {
337 let _ = unlock_daemon_memory();
343 }
344 MlockResult::Failed(errno) => {
345 assert!(
347 errno == libc::EPERM || errno == libc::ENOMEM,
348 "Unexpected errno: {}",
349 errno
350 );
351 }
352 MlockResult::Disabled => {
353 panic!("Should not be disabled on Linux");
354 }
355 }
356 }
357
358 #[test]
359 #[cfg(target_os = "linux")]
360 fn test_mlock_required_may_fail() {
361 let result = lock_daemon_memory(true);
363 match result {
365 Ok(MlockResult::Success) => {
366 let _ = unlock_daemon_memory();
368 }
369 Err(_) => {
370 }
372 Ok(MlockResult::Failed(_)) => {
373 panic!("Should not return Failed when required=true");
374 }
375 Ok(MlockResult::Disabled) => {
376 panic!("Should not be disabled on Linux");
377 }
378 }
379 }
380}