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;
17use tracing::warn;
18
19use crate::Detcore;
20use crate::RecordOrReplay;
21
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
76        | libc::MADV_POPULATE_WRITE
77        | reverie::syscalls::linux_abi::MADV_COLLAPSE => MadviseAction::Reject(Errno::EINVAL),
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 page = crate::page::page_size() as usize;
89    let start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
90    if !start.is_multiple_of(page) {
91        return Err(Errno::EINVAL.into());
92    }
93    if call.len() == 0 {
94        return Ok(());
95    }
96
97    let end = start.checked_add(call.len()).ok_or(Errno::EINVAL)?;
98    end.checked_add(page - 1).ok_or(Errno::EINVAL)?;
99    Ok(())
100}
101
102impl<T: RecordOrReplay> Detcore<T> {
103    /// Apply a deterministic policy to madvise(2).
104    ///
105    /// Hints and advice with guest-visible semantics are forwarded to the
106    /// backend, on every backend. A backend that does not implement an advice
107    /// answers ENOSYS with the typed refusal `MemoryAdvice`, which is logged as a
108    /// warning naming the advice; the guest sees the ENOSYS.
109    /// Record/replay accepts pure hints as no-ops and passes guest-semantic advice
110    /// to the recorder and replayer, which record and restore the effects that
111    /// differ because replay replaces file mappings with anonymous mappings.
112    /// Reclaim and asynchronous VM-policy advice receives fixed success without exposing host memory pressure. Resource-
113    /// dependent, backing-store, and hardware-failure operations receive fixed errors.
114    // AUTONOMOUS-BOT-IMPLEMENTED
115    // TODO-HUMAN-REVIEW(#548): Recheck advice policy and record/replay boundaries.
116    pub async fn handle_madvise<G: Guest<Self>>(
117        &self,
118        guest: &mut G,
119        call: syscalls::Madvise,
120    ) -> Result<i64, Error> {
121        let advice = call.advice();
122        let action = madvise_action(advice);
123        validate_common_args(call)?;
124
125        if call.len() == 0 {
126            return match action {
127                MadviseAction::Unknown => Err(Errno::EINVAL.into()),
128                _ => Ok(0),
129            };
130        }
131        if self.cfg.recordreplay_modes && action == MadviseAction::ForwardHint {
132            crate::detlog!(
133                "[dtid {}] madvise hint {} accepted as record/replay no-op",
134                guest.thread_state().dettid,
135                advice,
136            );
137            return Ok(0);
138        }
139
140        match action {
141            MadviseAction::ForwardHint | MadviseAction::ForwardSemantic => {
142                let result = self.record_or_replay(guest, call).await;
143                if result.is_err()
144                    && let Some(refusal) = guest.take_unsupported_refusal().filter(|refusal| {
145                        refusal.operation == reverie::UnsupportedOperation::MemoryAdvice
146                    })
147                {
148                    warn!(
149                        "[dtid {}] madvise advice {} refused by the backend: {}",
150                        guest.thread_state().dettid,
151                        advice,
152                        refusal.diagnostic,
153                    );
154                }
155                Ok(result?)
156            }
157            MadviseAction::Ignore => {
158                crate::detlog!(
159                    "[dtid {}] madvise advice {} accepted as deterministic no-op",
160                    guest.thread_state().dettid,
161                    advice,
162                );
163                Ok(0)
164            }
165            MadviseAction::Reject(errno) => {
166                crate::detlog!(
167                    "[dtid {}] madvise advice {} rejected with {}",
168                    guest.thread_state().dettid,
169                    advice,
170                    errno,
171                );
172                Err(errno.into())
173            }
174            MadviseAction::Unknown => {
175                crate::detlog!(
176                    "[dtid {}] unknown madvise advice {} rejected with EINVAL",
177                    guest.thread_state().dettid,
178                    advice,
179                );
180                Err(Errno::EINVAL.into())
181            }
182        }
183    }
184
185    /// Deterministic `mincore(2)`.
186    ///
187    /// Real page residency reflects host memory pressure and is therefore
188    /// nondeterministic, which is why mincore was previously classified as an
189    /// unsupported syscall. GNU grep (and other glibc consumers) invoke mincore
190    /// under the KVM backend on a code path the ptrace backend does not take, so
191    /// leaving it unsupported aborts the guest under `--strict`.
192    ///
193    /// Inject the call first so the backend preserves Linux pointer and mapping
194    /// validation, then report every mapped page as resident. The residency
195    /// vector is only an advisory hint, so replacing those nondeterministic bits
196    /// with a constant answer remains bitwise-identical across runs.
197    // AUTONOMOUS-BOT-IMPLEMENTED
198    // TODO-HUMAN-REVIEW(#775): Review deterministic mincore residency emulation.
199    pub async fn handle_mincore<G: Guest<Self>>(
200        &self,
201        guest: &mut G,
202        call: syscalls::Mincore,
203    ) -> Result<i64, Error> {
204        // Linux rejects a start address that is not page-aligned with EINVAL.
205        let page = crate::page::page_size() as usize;
206        let start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
207        if !start.is_multiple_of(page) {
208            return Err(Errno::EINVAL.into());
209        }
210        let len = call.len();
211        if len == 0 {
212            return Ok(0);
213        }
214        // Match madvise's overflow guard on the requested range.
215        start.checked_add(len).ok_or(Errno::EINVAL)?;
216        let page_count = len.div_ceil(page);
217        let vec = call.vec().ok_or(Errno::EFAULT)?;
218        guest.inject(syscalls::Syscall::from(call)).await?;
219        let residency = vec![1u8; page_count];
220        guest.memory().write_exact(vec, &residency)?;
221        Ok(0)
222    }
223}
224
225/// `address` rounded up to a `page`-byte boundary, as `brk(2)` rounds the
226/// break (`PAGE_ALIGN`).
227fn brk_page_align(address: u64, page: u64) -> Option<u64> {
228    Some(address.checked_add(page - 1)? & !(page - 1))
229}
230
231/// Linux's default `stack_guard_gap` with `page`-byte pages: 256 pages kept
232/// free below a mapping that grows down.
233fn stack_guard_gap(page: u64) -> u64 {
234    256 * page
235}
236
237/// One mapping of a `/proc/<pid>/smaps` text: its range, and the gap Linux
238/// keeps free below it (`stack_guard_start_gap`): `stack_guard_gap` below a
239/// mapping that grows down (`gd` among its `VmFlags`), a page below a
240/// shadow stack (`ss`), none below any other.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242struct Vma {
243    start: u64,
244    end: u64,
245    start_gap: u64,
246}
247
248/// The mappings of a `/proc/<pid>/smaps` text, in address order, parsed as
249/// they are asked for: nothing is allocated, so where the tool shares the
250/// guest's process its allocator maps nothing between the read and the
251/// injected call that relies on it.
252fn smaps_vmas(smaps: &[u8], page: u64) -> SmapsVmas<'_> {
253    SmapsVmas { rest: smaps, page }
254}
255
256#[derive(Clone)]
257struct SmapsVmas<'a> {
258    rest: &'a [u8],
259    /// The host's page size, which sizes the guard gaps.
260    page: u64,
261}
262
263impl<'a> SmapsVmas<'a> {
264    /// The next line, without consuming it.
265    fn peek(&self) -> Option<&'a [u8]> {
266        (!self.rest.is_empty()).then(|| {
267            self.rest
268                .split(|&byte| byte == b'\n')
269                .next()
270                .unwrap_or_default()
271        })
272    }
273
274    fn advance(&mut self) {
275        self.rest = match self.rest.iter().position(|&byte| byte == b'\n') {
276            Some(newline) => &self.rest[newline + 1..],
277            None => &[],
278        };
279    }
280}
281
282/// A mapping's range, from the first field of its header line; `None` for
283/// any other line. Bytes, not text: the pathname that follows is whatever
284/// bytes the file was named with.
285fn smaps_range(line: &[u8]) -> Option<(u64, u64)> {
286    let range = line.split(|&byte| byte == b' ').next()?;
287    let (start, end) = std::str::from_utf8(range).ok()?.split_once('-')?;
288    Some((
289        u64::from_str_radix(start, 16).ok()?,
290        u64::from_str_radix(end, 16).ok()?,
291    ))
292}
293
294impl Iterator for SmapsVmas<'_> {
295    type Item = Vma;
296
297    fn next(&mut self) -> Option<Vma> {
298        let (start, end) = loop {
299            let line = self.peek()?;
300            self.advance();
301            if let Some(range) = smaps_range(line) {
302                break range;
303            }
304        };
305        let mut vma = Vma {
306            start,
307            end,
308            start_gap: 0,
309        };
310        // The mapping's fields, up to the next mapping's header.
311        while let Some(line) = self.peek() {
312            if smaps_range(line).is_some() {
313                break;
314            }
315            self.advance();
316            if let Some(flags) = line.strip_prefix(b"VmFlags:") {
317                let mut flags = flags.split(|byte| byte.is_ascii_whitespace());
318                vma.start_gap = if flags.clone().any(|flag| flag == b"gd") {
319                    stack_guard_gap(self.page)
320                } else if flags.any(|flag| flag == b"ss") {
321                    self.page
322                } else {
323                    0
324                };
325            }
326        }
327        Some(vma)
328    }
329}
330
331/// Whether Linux's `brk` grows a break whose page-aligned end is `old_end`
332/// to `new_end`: no mapping may meet `[old_end, new_end + PAGE)`, nor a
333/// mapping that grows down have its guard gap there
334/// (`vma_find(&vmi, newbrk + PAGE_SIZE + stack_guard_gap)` and
335/// `vm_start_gap`).
336fn brk_may_grow(
337    mut vmas: impl Iterator<Item = Vma>,
338    old_end: u64,
339    new_end: u64,
340    page: u64,
341) -> bool {
342    let Some(limit) = new_end.checked_add(page) else {
343        return false;
344    };
345    let search_end = limit.saturating_add(stack_guard_gap(page));
346    let Some(next) = vmas.find(|vma| vma.end > old_end && vma.start < search_end) else {
347        return true;
348    };
349    // `vm_start_gap`, which gives 0 where the gap would pass it.
350    let gap_start = next.start.saturating_sub(next.start_gap);
351    limit <= gap_start
352}
353
354/// Whether Linux's `brk` shrinks a break whose page-aligned end is `old_end`
355/// to `new_end`: only where a mapping meets `[new_end, old_end)`
356/// (`vma_find(&vmi, oldbrk)` from `newbrk`); otherwise it keeps the old
357/// break.
358fn brk_may_shrink(mut vmas: impl Iterator<Item = Vma>, new_end: u64, old_end: u64) -> bool {
359    vmas.any(|vma| vma.end > new_end && vma.start < old_end)
360}
361
362// Bounded DEBUG diagnostic: no new procfs read and no formatting between
363// the metadata capture and a mapping operation. Only the first rejected moving
364// request in this process is retained. Fork inherits the diagnostic latch.
365struct BrkDiagnostic<'a> {
366    dettid: i32,
367    model: &'a crate::memory::BrkModel,
368    requested: u64,
369}
370
371struct BrkDiagnosticHex<'a>(&'a [u8]);
372
373impl std::fmt::Display for BrkDiagnosticHex<'_> {
374    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        for byte in self.0 {
376            write!(formatter, "{byte:02x}")?;
377        }
378        Ok(())
379    }
380}
381
382impl BrkDiagnostic<'_> {
383    fn rejected(
384        &self,
385        branch: &'static str,
386        limit: Option<u64>,
387        smaps: Option<&[u8]>,
388        kernel_result: &dyn std::fmt::Debug,
389    ) {
390        use std::sync::atomic::AtomicBool;
391        use std::sync::atomic::Ordering;
392        static CAPTURED: AtomicBool = AtomicBool::new(false);
393        const MAX_BYTES: usize = 256 * 1024;
394        if self.requested == 0
395            || self.requested == self.model.current
396            || !tracing::enabled!(target: "detcore::brk_diagnostic", tracing::Level::DEBUG)
397            || CAPTURED.swap(true, Ordering::Relaxed)
398        {
399            return;
400        }
401        let complete = smaps.is_some_and(|bytes| bytes.len() <= MAX_BYTES);
402        tracing::debug!(
403            target: "detcore::brk_diagnostic",
404            dettid = self.dettid,
405            requested = self.requested,
406            start = self.model.start,
407            current = self.model.current,
408            data_span = self.model.data_span,
409            launcher_break = self.model.launcher_break,
410            ?limit,
411            branch,
412            ?kernel_result,
413            smaps_len = ?smaps.map(<[u8]>::len),
414            complete,
415            "BRK_DENIAL_V1 begin",
416        );
417        if complete {
418            for (index, chunk) in smaps.unwrap().chunks(1024).enumerate() {
419                tracing::debug!(
420                    target: "detcore::brk_diagnostic",
421                    dettid = self.dettid,
422                    index,
423                    bytes = %BrkDiagnosticHex(chunk),
424                    "BRK_DENIAL_V1 chunk",
425                );
426            }
427        }
428        tracing::debug!(
429            target: "detcore::brk_diagnostic",
430            dettid = self.dettid,
431            complete,
432            "BRK_DENIAL_V1 end",
433        );
434    }
435}
436
437/// The mapping-dependent part of a modeled program-break request. Endpoints
438/// are page-aligned; resource limits and the actual mapping operation stay
439/// with the caller, independently of where the smaps bytes were captured.
440#[derive(
441    Debug,
442    Clone,
443    Copy,
444    PartialEq,
445    Eq,
446    serde::Serialize,
447    serde::Deserialize
448)]
449pub enum BrkMappingCheck {
450    /// Check new pages and Linux's guard gap above the old break.
451    Grow { old_end: u64, new_end: u64 },
452    /// Check that some mapping overlaps the pages being removed.
453    Shrink { new_end: u64, old_end: u64 },
454}
455
456impl BrkMappingCheck {
457    pub(crate) fn allows(self, smaps: &[u8]) -> bool {
458        self.allows_with_page(smaps, crate::page::page_size())
459    }
460
461    /// [`Self::allows`] on a host whose pages are `page` bytes.
462    fn allows_with_page(self, smaps: &[u8], page: u64) -> bool {
463        match self {
464            Self::Grow { old_end, new_end } => {
465                brk_may_grow(smaps_vmas(smaps, page), old_end, new_end, page)
466            }
467            Self::Shrink { new_end, old_end } => {
468                brk_may_shrink(smaps_vmas(smaps, page), new_end, old_end)
469            }
470        }
471    }
472}
473
474impl<T: RecordOrReplay> Detcore<T> {
475    /// `brk(addr)` in a run that declares a launcher
476    /// (`Detcore::declares_launcher`), where the kernel's program break is the
477    /// launcher's: answered from Detcore's model of the guest's break
478    /// ([`crate::memory::BrkModel`]), as Linux's `brk` (mm/mmap.c) answers
479    /// from `mm->brk`. A request below the start, one past `RLIMIT_DATA`,
480    /// growth that would meet another mapping or its guard gap, or a shrink
481    /// over a range with no mapping leaves the break where it is; the call
482    /// returns the break in effect afterwards, never an error. Growth maps
483    /// exactly the new anonymous zero pages (`MAP_FIXED_NOREPLACE`, so an
484    /// existing mapping is never replaced) and shrinking unmaps them.
485    ///
486    /// Refused by name, where an answer would be a plausible wrong one: a
487    /// brk without a model for the image (with the reason there is none),
488    /// and a request at or above the launcher's break but below the model's
489    /// start. Only a libc that cached the launcher's break before Detcore's
490    /// first callback asks for that, and it would take Linux's answer, the
491    /// break in effect above its request, for a grant at its stale address.
492    pub(crate) async fn handle_brk<G: Guest<Self>>(
493        &self,
494        guest: &mut G,
495        call: syscalls::Syscall,
496    ) -> Result<i64, Error> {
497        let (model, unmodeled) = {
498            let metadata = guest
499                .thread_state()
500                .memory_metadata
501                .lock()
502                .expect("memory metadata mutex poisoned");
503            (metadata.brk_model, metadata.brk_unmodeled.clone())
504        };
505        let Some(model) = model else {
506            let reason = format!(
507                "unsupported: brk where the kernel's program break is the launcher's and Detcore has no model of the guest's: {}",
508                unmodeled
509                    .as_deref()
510                    .unwrap_or("the image was never modeled"),
511            );
512            return self
513                .refuse_launched_exe_because(guest, syscalls::Sysno::brk, reason)
514                .await;
515        };
516        let requested = syscalls::SyscallInfo::into_parts(call).1.arg0 as u64;
517        let granted = match self.granted_brk(guest, &model, requested).await {
518            Ok(granted) => granted,
519            Err(reason) => {
520                return self
521                    .refuse_launched_exe_because(guest, syscalls::Sysno::brk, reason.to_owned())
522                    .await;
523            }
524        };
525        guest
526            .thread_state()
527            .memory_metadata
528            .lock()
529            .expect("memory metadata mutex poisoned")
530            .brk_model = Some(crate::memory::BrkModel {
531            current: granted,
532            ..model
533        });
534        Ok(granted as i64)
535    }
536
537    /// The break `brk(requested)` leaves in effect, from `model`, mapping or
538    /// unmapping the pages between the old and the new break; an error where
539    /// Detcore could not keep the address space and the model in step (a
540    /// mapping it could neither place nor remove).
541    async fn granted_brk<G: Guest<Self>>(
542        &self,
543        guest: &mut G,
544        model: &crate::memory::BrkModel,
545        requested: u64,
546    ) -> Result<u64, &'static str> {
547        let current = model.current;
548        let diagnostic = BrkDiagnostic {
549            dettid: guest.thread_state().dettid.as_raw(),
550            model,
551            requested,
552        };
553        if requested < model.start {
554            if requested >= model.launcher_break {
555                diagnostic.rejected("below_start_launcher", None, None, &"not_injected");
556                return Err(
557                    "unsupported: brk from the launcher's break, which the guest's libc read before Detcore modeled the guest's",
558                );
559            }
560            diagnostic.rejected("below_start", None, None, &"not_injected");
561            return Ok(current);
562        }
563        let limit = guest
564            .thread_state()
565            .resource_limits
566            .lock()
567            .unwrap()
568            .get(reverie::syscalls::linux_abi::RLIMIT_DATA)
569            .map_or(libc::RLIM64_INFINITY, |limit| limit.current);
570        let used = (requested - model.start).saturating_add(model.data_span);
571        if limit != libc::RLIM64_INFINITY && used > limit {
572            diagnostic.rejected("rlimit_data", Some(limit), None, &"not_injected");
573            return Ok(current);
574        }
575        let page = crate::page::page_size();
576        let (Some(new_end), Some(old_end)) = (
577            brk_page_align(requested, page),
578            brk_page_align(current, page),
579        ) else {
580            diagnostic.rejected("page_overflow", Some(limit), None, &"not_injected");
581            return Ok(current);
582        };
583        if new_end == old_end {
584            return Ok(requested);
585        }
586        // The address space as Linux's brk inspects it, read with raw
587        // syscalls and no allocation between reads where the tool shares the
588        // guest's process; the guest's other threads are held by the
589        // sequentialized schedule.
590        let smaps = self
591            .guest_task_directory(guest)
592            .and_then(|directory| crate::util::raw_read_listing(&directory.join("smaps")).ok());
593        let check = if requested <= current {
594            BrkMappingCheck::Shrink { new_end, old_end }
595        } else {
596            BrkMappingCheck::Grow { old_end, new_end }
597        };
598        let allowed = match smaps.as_deref() {
599            Some(smaps) => Some(check.allows(smaps)),
600            None if guest
601                .thread_state()
602                .changed_root
603                .load(std::sync::atomic::Ordering::SeqCst) =>
604            {
605                crate::tool_global::check_brk_mappings(guest, check)
606            }
607            None => None,
608        }
609        .ok_or_else(|| {
610            diagnostic.rejected("smaps_unavailable", Some(limit), None, &"not_injected");
611            "unsupported: brk where Detcore cannot read the guest's mappings"
612        })?;
613        if !allowed {
614            diagnostic.rejected(
615                if requested <= current {
616                    "shrink_predicate"
617                } else {
618                    "grow_predicate"
619                },
620                Some(limit),
621                smaps.as_deref(),
622                &"not_injected",
623            );
624            return Ok(current);
625        }
626        if requested <= current {
627            // As Linux, which keeps the old break where the unmap fails.
628            let unmapped = guest
629                .inject_with_retry(syscalls::Syscall::Munmap(
630                    syscalls::Munmap::new()
631                        .with_addr(syscalls::Addr::from_raw(new_end as usize))
632                        .with_len((old_end - new_end) as usize),
633                ))
634                .await;
635            if unmapped.is_err() {
636                diagnostic.rejected("munmap_error", Some(limit), smaps.as_deref(), &unmapped);
637            }
638            return Ok(if unmapped.is_ok() { requested } else { current });
639        }
640        let length = new_end - old_end;
641        let mapped = guest
642            .inject_with_retry(syscalls::Syscall::Mmap(
643                syscalls::Mmap::new()
644                    .with_addr(syscalls::Addr::from_raw(old_end as usize))
645                    .with_len(length as usize)
646                    .with_prot(syscalls::ProtFlags::PROT_READ | syscalls::ProtFlags::PROT_WRITE)
647                    .with_flags(
648                        syscalls::MapFlags::MAP_PRIVATE
649                            | syscalls::MapFlags::MAP_ANONYMOUS
650                            | syscalls::MapFlags::MAP_FIXED_NOREPLACE,
651                    )
652                    .with_fd(-1)
653                    .with_offset(0),
654            ))
655            .await;
656        match &mapped {
657            Ok(address) if *address as u64 == old_end => Ok(requested),
658            // A kernel without MAP_FIXED_NOREPLACE treats the address as a
659            // hint and may map elsewhere: give that mapping back, or refuse
660            // where it cannot be.
661            Ok(address) => {
662                let removed = guest
663                    .inject_with_retry(syscalls::Syscall::Munmap(
664                        syscalls::Munmap::new()
665                            .with_addr(syscalls::Addr::from_raw(*address as usize))
666                            .with_len(length as usize),
667                    ))
668                    .await;
669                diagnostic.rejected(
670                    "mmap_wrong_address",
671                    Some(limit),
672                    smaps.as_deref(),
673                    &(&mapped, &removed),
674                );
675                if removed.is_ok() {
676                    Ok(current)
677                } else {
678                    Err(
679                        "unsupported: brk growth the kernel mapped elsewhere and Detcore could not remove",
680                    )
681                }
682            }
683            Err(_) => {
684                diagnostic.rejected("mmap_error", Some(limit), smaps.as_deref(), &mapped);
685                Ok(current)
686            }
687        }
688    }
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694
695    /// The page size the fixtures below are written for.
696    const PAGE: u64 = 4096;
697
698    #[test]
699    fn a_shared_brk_check_uses_the_existing_mapping_policy() {
700        let smaps = b"00500000-00501000 rw-p 00000000 00:00 0\nVmFlags: rd wr\n";
701        let grow = |new_end| BrkMappingCheck::Grow {
702            old_end: 0x400000,
703            new_end,
704        };
705        assert!(grow(0x4ff000).allows_with_page(smaps, PAGE));
706        assert!(!grow(0x500000).allows_with_page(smaps, PAGE));
707        assert!(!grow(u64::MAX - PAGE + 1).allows_with_page(smaps, PAGE));
708        assert!(
709            BrkMappingCheck::Shrink {
710                new_end: 0x500000,
711                old_end: 0x501000,
712            }
713            .allows_with_page(smaps, PAGE)
714        );
715        assert!(
716            !BrkMappingCheck::Shrink {
717                new_end: 0x400000,
718                old_end: 0x500000,
719            }
720            .allows_with_page(smaps, PAGE)
721        );
722        assert!(grow(0x500000).allows_with_page(b"", PAGE));
723        assert!(
724            !BrkMappingCheck::Shrink {
725                new_end: 0x500000,
726                old_end: 0x501000,
727            }
728            .allows_with_page(b"", PAGE)
729        );
730    }
731
732    #[test]
733    fn a_break_grows_and_shrinks_only_where_linuxs_brk_would() {
734        let smaps = "\
73500400000-00401000 r-xp 00000000 00:20 1234 /image
736Size:                  4 kB
737VmFlags: rd ex mr mw me
73800406000-00407000 r--p 00000000 00:00 0
739VmFlags: rd mr mw me
7407ffffff00000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
741VmFlags: rd wr mr mw me gd ac
742";
743        let vmas: Vec<Vma> = smaps_vmas(smaps.as_bytes(), PAGE).collect();
744        assert_eq!(vmas.len(), 3);
745        // A pathname that is not UTF-8 does not hide the mappings.
746        let mut named =
747            b"00500000-00501000 r--p 00000000 00:20 42 /n\xffme\nVmFlags: rd\n".to_vec();
748        named.extend_from_slice(smaps.as_bytes());
749        let named: Vec<Vma> = smaps_vmas(&named, PAGE).collect();
750        assert_eq!(named.len(), 4);
751        assert_eq!(named[1..], vmas[..]);
752        assert_eq!(
753            vmas[2],
754            Vma {
755                start: 0x7ffffff00000,
756                end: 0x7ffffffff000,
757                start_gap: stack_guard_gap(PAGE)
758            }
759        );
760        // A mapping at 0x406000: growth to 0x405000 needs 0x405000-0x406000
761        // free above it, which it is; growth to 0x406000 meets the mapping.
762        assert!(brk_may_grow(vmas.iter().copied(), 0x401000, 0x405000, PAGE));
763        assert!(!brk_may_grow(
764            vmas.iter().copied(),
765            0x401000,
766            0x406000,
767            PAGE
768        ));
769        // Below a stack, the guard gap is kept as well.
770        let below_stack = 0x7ffffff00000 - stack_guard_gap(PAGE);
771        assert!(brk_may_grow(
772            vmas.iter().copied(),
773            0x500000,
774            below_stack - PAGE,
775            PAGE
776        ));
777        assert!(!brk_may_grow(
778            vmas.iter().copied(),
779            0x500000,
780            below_stack,
781            PAGE
782        ));
783        // Below a shadow stack, one page.
784        let shadow: Vec<Vma> = smaps_vmas(
785            b"00410000-00411000 rw-p 00000000 00:00 0\nVmFlags: rd wr mr mw me ss\n",
786            PAGE,
787        )
788        .collect();
789        assert_eq!(shadow[0].start_gap, PAGE);
790        assert!(brk_may_grow(
791            shadow.iter().copied(),
792            0x401000,
793            0x40e000,
794            PAGE
795        ));
796        assert!(!brk_may_grow(
797            shadow.iter().copied(),
798            0x401000,
799            0x40f000,
800            PAGE
801        ));
802        // Shrinking needs a mapping to unmap in the range.
803        assert!(brk_may_shrink(vmas.iter().copied(), 0x405000, 0x407000));
804        assert!(!brk_may_shrink(vmas.iter().copied(), 0x402000, 0x406000));
805    }
806
807    /// With 64 KiB pages the break, the page above it and the stack guard
808    /// gap are each counted in 64 KiB pages.
809    #[test]
810    fn a_break_on_a_host_with_large_pages_keeps_large_page_gaps() {
811        const LARGE: u64 = 0x10000;
812        let smaps = "\
81300400000-00410000 r-xp 00000000 00:20 1234 /image
814VmFlags: rd ex mr mw me
81500460000-00470000 r--p 00000000 00:00 0
816VmFlags: rd mr mw me
8177ffffff00000-7fffffff0000 rw-p 00000000 00:00 0 [stack]
818VmFlags: rd wr mr mw me gd ac
819";
820        let vmas: Vec<Vma> = smaps_vmas(smaps.as_bytes(), LARGE).collect();
821        assert_eq!(vmas[2].start_gap, 256 * LARGE);
822        // The page above the new break must be free as well: growth to
823        // 0x450000 leaves 0x450000-0x460000 free, growth to 0x460000 does not.
824        assert!(brk_may_grow(
825            vmas.iter().copied(),
826            0x410000,
827            0x450000,
828            LARGE
829        ));
830        assert!(!brk_may_grow(
831            vmas.iter().copied(),
832            0x410000,
833            0x460000,
834            LARGE
835        ));
836        // A break requested at 0x45f000: with 4 KiB pages it ends there and
837        // fits; with 64 KiB pages it rounds up to 0x460000 and meets the
838        // mapping's page.
839        let requested = 0x45f000;
840        let small = brk_page_align(requested, PAGE).unwrap();
841        let large = brk_page_align(requested, LARGE).unwrap();
842        assert_eq!((small, large), (0x45f000, 0x460000));
843        assert!(brk_may_grow(vmas.iter().copied(), 0x410000, small, PAGE));
844        assert!(!brk_may_grow(vmas.iter().copied(), 0x410000, large, LARGE));
845        // Below the stack, the guard gap is 256 large pages.
846        let below_stack = 0x7ffffff00000 - 256 * LARGE;
847        assert!(brk_may_grow(
848            vmas.iter().copied(),
849            0x500000,
850            below_stack - LARGE,
851            LARGE
852        ));
853        assert!(!brk_may_grow(
854            vmas.iter().copied(),
855            0x500000,
856            below_stack,
857            LARGE
858        ));
859        assert_eq!(brk_page_align(0x421001, LARGE), Some(0x430000));
860        assert_eq!(brk_page_align(0x430000, LARGE), Some(0x430000));
861        assert_eq!(brk_page_align(0x421001, PAGE), Some(0x422000));
862    }
863
864    #[test]
865    fn madvise_known_linux_advice_has_an_explicit_policy() {
866        for advice in [
867            libc::MADV_NORMAL,
868            libc::MADV_RANDOM,
869            libc::MADV_SEQUENTIAL,
870            libc::MADV_WILLNEED,
871        ] {
872            assert_eq!(madvise_action(advice), MadviseAction::ForwardHint);
873        }
874
875        for advice in [
876            libc::MADV_DONTNEED,
877            libc::MADV_DONTFORK,
878            libc::MADV_DOFORK,
879            libc::MADV_DONTDUMP,
880            libc::MADV_DODUMP,
881            libc::MADV_WIPEONFORK,
882            libc::MADV_KEEPONFORK,
883            libc::MADV_DONTNEED_LOCKED,
884            MADV_GUARD_INSTALL,
885            MADV_GUARD_REMOVE,
886        ] {
887            assert_eq!(madvise_action(advice), MadviseAction::ForwardSemantic);
888        }
889
890        for advice in [
891            libc::MADV_FREE,
892            libc::MADV_MERGEABLE,
893            libc::MADV_UNMERGEABLE,
894            libc::MADV_HUGEPAGE,
895            libc::MADV_NOHUGEPAGE,
896            libc::MADV_COLD,
897            libc::MADV_PAGEOUT,
898        ] {
899            assert_eq!(madvise_action(advice), MadviseAction::Ignore);
900        }
901
902        for advice in [
903            libc::MADV_REMOVE,
904            libc::MADV_POPULATE_READ,
905            libc::MADV_POPULATE_WRITE,
906            reverie::syscalls::linux_abi::MADV_COLLAPSE,
907        ] {
908            assert_eq!(madvise_action(advice), MadviseAction::Reject(Errno::EINVAL));
909        }
910        for advice in [libc::MADV_HWPOISON, libc::MADV_SOFT_OFFLINE] {
911            assert_eq!(madvise_action(advice), MadviseAction::Reject(Errno::EPERM));
912        }
913        assert_eq!(madvise_action(i32::MAX), MadviseAction::Unknown);
914    }
915
916    #[test]
917    fn common_argument_validation_is_host_independent() {
918        let aligned = unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(0x1000) };
919        assert!(
920            validate_common_args(
921                syscalls::Madvise::new()
922                    .with_addr(Some(aligned))
923                    .with_len(0)
924                    .with_advice(libc::MADV_FREE),
925            )
926            .is_ok()
927        );
928
929        let unaligned = unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(0x1001) };
930        assert!(
931            validate_common_args(
932                syscalls::Madvise::new()
933                    .with_addr(Some(unaligned))
934                    .with_len(0)
935                    .with_advice(libc::MADV_FREE),
936            )
937            .is_err()
938        );
939
940        let page = crate::page::page_size() as usize;
941        let near_end =
942            unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(usize::MAX & !(page - 1)) };
943        assert!(
944            validate_common_args(
945                syscalls::Madvise::new()
946                    .with_addr(Some(near_end))
947                    .with_len(page)
948                    .with_advice(libc::MADV_FREE),
949            )
950            .is_err()
951        );
952    }
953}