Skip to main content

detcore/syscalls/
memory.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Deterministic policies for memory-management advice.
10
11use reverie::Error;
12use reverie::Guest;
13use reverie::syscalls;
14use reverie::syscalls::AddrMut;
15use reverie::syscalls::Errno;
16use reverie::syscalls::MemoryAccess;
17
18use crate::Detcore;
19use crate::RecordOrReplay;
20
21const PAGE_SIZE: usize = 4096;
22// Added in Linux 6.13 and not yet exposed by the pinned libc crate.
23const MADV_GUARD_INSTALL: i32 = 102;
24const MADV_GUARD_REMOVE: i32 = 103;
25
26#[derive(Debug, Clone, Copy, Eq, PartialEq)]
27enum MadviseAction {
28    ForwardHint,
29    ForwardSemantic,
30    Ignore,
31    Reject(Errno),
32    Unknown,
33}
34
35const fn madvise_action(advice: i32) -> MadviseAction {
36    match advice {
37        // Pure access-pattern and prefetch hints have no required memory-content
38        // side effect. Backends without native madvise support accept them as no-ops.
39        libc::MADV_NORMAL | libc::MADV_RANDOM | libc::MADV_SEQUENTIAL | libc::MADV_WILLNEED => {
40            MadviseAction::ForwardHint
41        }
42
43        // Operations with guest-visible memory, fork, backing-store, dump, or guard
44        // semantics must reach a backend that implements native madvise behavior.
45        libc::MADV_DONTNEED
46        | libc::MADV_DONTFORK
47        | libc::MADV_DOFORK
48        | libc::MADV_DONTDUMP
49        | libc::MADV_DODUMP
50        | libc::MADV_WIPEONFORK
51        | libc::MADV_KEEPONFORK
52        | libc::MADV_DONTNEED_LOCKED
53        | MADV_GUARD_INSTALL
54        | MADV_GUARD_REMOVE => MadviseAction::ForwardSemantic,
55
56        // These are optional reclaim or asynchronous VM-policy controls. Their host
57        // effects depend on memory pressure, KSM, and THP activity. Hermit accepts
58        // them as fixed no-ops after deterministic argument validation; it deliberately
59        // does not reproduce each advice's host- and mapping-specific EINVAL cases.
60        libc::MADV_FREE
61        | libc::MADV_MERGEABLE
62        | libc::MADV_UNMERGEABLE
63        | libc::MADV_HUGEPAGE
64        | libc::MADV_NOHUGEPAGE
65        | libc::MADV_COLD
66        | libc::MADV_PAGEOUT => MadviseAction::Ignore,
67
68        // Hole punching mutates backing storage and every mapping alias. Refuse it
69        // until Detcore can update file resources and replay all affected aliases.
70        libc::MADV_REMOVE => MadviseAction::Reject(Errno::EINVAL),
71
72        // Successful population and collapse promise synchronous, resource-
73        // dependent work. Report the same deterministic error Linux uses when
74        // an advice value is unsupported so callers can take their fallback.
75        libc::MADV_POPULATE_READ | libc::MADV_POPULATE_WRITE | libc::MADV_COLLAPSE => {
76            MadviseAction::Reject(Errno::EINVAL)
77        }
78
79        // Never let a guest inject host memory failures, even if the container
80        // unexpectedly has enough privilege to make these operations succeed.
81        libc::MADV_HWPOISON | libc::MADV_SOFT_OFFLINE => MadviseAction::Reject(Errno::EPERM),
82
83        _ => MadviseAction::Unknown,
84    }
85}
86
87fn validate_common_args(call: syscalls::Madvise) -> Result<(), Error> {
88    let start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
89    if !start.is_multiple_of(PAGE_SIZE) {
90        return Err(Errno::EINVAL.into());
91    }
92    if call.len() == 0 {
93        return Ok(());
94    }
95
96    let end = start.checked_add(call.len()).ok_or(Errno::EINVAL)?;
97    end.checked_add(PAGE_SIZE - 1).ok_or(Errno::EINVAL)?;
98    Ok(())
99}
100
101impl<T: RecordOrReplay> Detcore<T> {
102    /// Apply a deterministic policy to madvise(2).
103    ///
104    /// Ptrace/DBT forward hints and supported advice with guest-visible semantics.
105    /// Record/replay accepts pure hints as no-ops and passes guest-semantic advice
106    /// to the recorder and replayer, which record and restore the effects that
107    /// differ because replay replaces file mappings with anonymous mappings.
108    /// Reclaim and asynchronous VM-policy advice receives fixed success without exposing host memory pressure. Resource-
109    /// dependent, backing-store, and hardware-failure operations receive fixed errors.
110    /// KVM accepts pure hints as no-ops and reports ENOSYS for guest-visible semantics
111    /// its executor cannot provide.
112    // AUTONOMOUS-BOT-IMPLEMENTED
113    // TODO-HUMAN-REVIEW(#548): Recheck advice policy and record/replay boundaries.
114    pub async fn handle_madvise<G: Guest<Self>>(
115        &self,
116        guest: &mut G,
117        call: syscalls::Madvise,
118    ) -> Result<i64, Error> {
119        let advice = call.advice();
120        let action = madvise_action(advice);
121        validate_common_args(call)?;
122
123        if call.len() == 0 {
124            return match action {
125                MadviseAction::Unknown => Err(Errno::EINVAL.into()),
126                _ => Ok(0),
127            };
128        }
129        if self.cfg.recordreplay_modes && action == MadviseAction::ForwardHint {
130            crate::detlog!(
131                "[dtid {}] madvise hint {} accepted as record/replay no-op",
132                guest.thread_state().dettid,
133                advice,
134            );
135            return Ok(0);
136        }
137
138        match action {
139            MadviseAction::ForwardHint if self.cfg.backend_supports_madvise => {
140                Ok(self.record_or_replay(guest, call).await?)
141            }
142            MadviseAction::ForwardHint => {
143                crate::detlog!(
144                    "[dtid {}] madvise hint {} accepted as backend no-op",
145                    guest.thread_state().dettid,
146                    advice,
147                );
148                Ok(0)
149            }
150            MadviseAction::ForwardSemantic if self.cfg.backend_supports_madvise => {
151                Ok(self.record_or_replay(guest, call).await?)
152            }
153            MadviseAction::ForwardSemantic => {
154                crate::detlog!(
155                    "[dtid {}] madvise advice {} is unsupported by this backend",
156                    guest.thread_state().dettid,
157                    advice,
158                );
159                Err(Errno::ENOSYS.into())
160            }
161            MadviseAction::Ignore => {
162                crate::detlog!(
163                    "[dtid {}] madvise advice {} accepted as deterministic no-op",
164                    guest.thread_state().dettid,
165                    advice,
166                );
167                Ok(0)
168            }
169            MadviseAction::Reject(errno) => {
170                crate::detlog!(
171                    "[dtid {}] madvise advice {} rejected with {}",
172                    guest.thread_state().dettid,
173                    advice,
174                    errno,
175                );
176                Err(errno.into())
177            }
178            MadviseAction::Unknown => {
179                crate::detlog!(
180                    "[dtid {}] unknown madvise advice {} rejected with EINVAL",
181                    guest.thread_state().dettid,
182                    advice,
183                );
184                Err(Errno::EINVAL.into())
185            }
186        }
187    }
188
189    /// Deterministic `mincore(2)`.
190    ///
191    /// Real page residency reflects host memory pressure and is therefore
192    /// nondeterministic, which is why mincore was previously classified as an
193    /// unsupported syscall. GNU grep (and other glibc consumers) invoke mincore
194    /// under the KVM backend on a code path the ptrace backend does not take, so
195    /// leaving it unsupported aborts the guest under `--strict`.
196    ///
197    /// Inject the call first so the backend preserves Linux pointer and mapping
198    /// validation, then report every mapped page as resident. The residency
199    /// vector is only an advisory hint, so replacing those nondeterministic bits
200    /// with a constant answer remains bitwise-identical across runs.
201    // AUTONOMOUS-BOT-IMPLEMENTED
202    // TODO-HUMAN-REVIEW(#775): Review deterministic mincore residency emulation.
203    pub async fn handle_mincore<G: Guest<Self>>(
204        &self,
205        guest: &mut G,
206        call: syscalls::Mincore,
207    ) -> Result<i64, Error> {
208        // Linux rejects a start address that is not page-aligned with EINVAL.
209        let start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
210        if !start.is_multiple_of(PAGE_SIZE) {
211            return Err(Errno::EINVAL.into());
212        }
213        let len = call.len();
214        if len == 0 {
215            return Ok(0);
216        }
217        // Match madvise's overflow guard on the requested range.
218        start.checked_add(len).ok_or(Errno::EINVAL)?;
219        let page_count = len.div_ceil(PAGE_SIZE);
220        let vec = call.vec().ok_or(Errno::EFAULT)?;
221        guest.inject(syscalls::Syscall::from(call)).await?;
222        let residency = vec![1u8; page_count];
223        guest.memory().write_exact(vec, &residency)?;
224        Ok(0)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn madvise_known_linux_advice_has_an_explicit_policy() {
234        for advice in [
235            libc::MADV_NORMAL,
236            libc::MADV_RANDOM,
237            libc::MADV_SEQUENTIAL,
238            libc::MADV_WILLNEED,
239        ] {
240            assert_eq!(madvise_action(advice), MadviseAction::ForwardHint);
241        }
242
243        for advice in [
244            libc::MADV_DONTNEED,
245            libc::MADV_DONTFORK,
246            libc::MADV_DOFORK,
247            libc::MADV_DONTDUMP,
248            libc::MADV_DODUMP,
249            libc::MADV_WIPEONFORK,
250            libc::MADV_KEEPONFORK,
251            libc::MADV_DONTNEED_LOCKED,
252            MADV_GUARD_INSTALL,
253            MADV_GUARD_REMOVE,
254        ] {
255            assert_eq!(madvise_action(advice), MadviseAction::ForwardSemantic);
256        }
257
258        for advice in [
259            libc::MADV_FREE,
260            libc::MADV_MERGEABLE,
261            libc::MADV_UNMERGEABLE,
262            libc::MADV_HUGEPAGE,
263            libc::MADV_NOHUGEPAGE,
264            libc::MADV_COLD,
265            libc::MADV_PAGEOUT,
266        ] {
267            assert_eq!(madvise_action(advice), MadviseAction::Ignore);
268        }
269
270        for advice in [
271            libc::MADV_REMOVE,
272            libc::MADV_POPULATE_READ,
273            libc::MADV_POPULATE_WRITE,
274            libc::MADV_COLLAPSE,
275        ] {
276            assert_eq!(madvise_action(advice), MadviseAction::Reject(Errno::EINVAL));
277        }
278        for advice in [libc::MADV_HWPOISON, libc::MADV_SOFT_OFFLINE] {
279            assert_eq!(madvise_action(advice), MadviseAction::Reject(Errno::EPERM));
280        }
281        assert_eq!(madvise_action(i32::MAX), MadviseAction::Unknown);
282    }
283
284    #[test]
285    fn common_argument_validation_is_host_independent() {
286        let aligned = unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(0x1000) };
287        assert!(
288            validate_common_args(
289                syscalls::Madvise::new()
290                    .with_addr(Some(aligned))
291                    .with_len(0)
292                    .with_advice(libc::MADV_FREE),
293            )
294            .is_ok()
295        );
296
297        let unaligned = unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(0x1001) };
298        assert!(
299            validate_common_args(
300                syscalls::Madvise::new()
301                    .with_addr(Some(unaligned))
302                    .with_len(0)
303                    .with_advice(libc::MADV_FREE),
304            )
305            .is_err()
306        );
307
308        let near_end =
309            unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(usize::MAX & !(PAGE_SIZE - 1)) };
310        assert!(
311            validate_common_args(
312                syscalls::Madvise::new()
313                    .with_addr(Some(near_end))
314                    .with_len(PAGE_SIZE)
315                    .with_advice(libc::MADV_FREE),
316            )
317            .is_err()
318        );
319    }
320}