Skip to main content

hermit/
kvm_address_space.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//! Refuse to start the KVM backend under a finite address-space limit.
10//!
11//! reverie-kvm reserves host address space for guest memory (its README,
12//! "Host address space"): the guest memory size Hermit gives it for every
13//! live guest process, once more while an exec validates its new image, and,
14//! for an exited child, until the child's host thread releases it, which can
15//! be after the parent has gone on. `RLIMIT_AS` counts every reserved byte,
16//! though only the pages a guest touches use memory. The peak therefore
17//! follows the guest's process tree and the host's thread scheduling, and a
18//! run under a finite limit stops wherever those put it: on the same command
19//! one run completes and the next does not
20//! (<https://github.com/rrnewton/reverie/issues/1225>). Hermit refuses such a
21//! run before the guest starts instead, by name and on every run.
22//!
23//! This is launch policy, about Hermit's own process. It never raises the
24//! limit, and it does not touch the guest's view of `RLIMIT_AS`, which
25//! Detcore models as before. The backend still refuses, by name, an exec
26//! whose memory a host refuses for another reason (a limit lowered after
27//! launch, `vm.max_map_count`), and the run ends on that refusal.
28
29use std::fmt;
30
31/// A named, pre-guest refusal of a finite `RLIMIT_AS` on the KVM backend.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct KvmAddressSpaceRefusal {
34    soft: libc::rlim_t,
35    hard: libc::rlim_t,
36    guest_memory_bytes: usize,
37}
38
39impl KvmAddressSpaceRefusal {
40    /// The refusal for `limit`, the `RLIMIT_AS` Hermit would run the backend
41    /// under, when the backend reserves `guest_memory_bytes` for each guest
42    /// memory. `None` for an unlimited soft limit: the kernel enforces the
43    /// soft limit, and only an unlimited one refuses no reservation however
44    /// large the guest's process tree grows.
45    pub fn for_limit(limit: libc::rlimit, guest_memory_bytes: usize) -> Option<Self> {
46        (limit.rlim_cur != libc::RLIM_INFINITY).then_some(Self {
47            soft: limit.rlim_cur,
48            hard: limit.rlim_max,
49            guest_memory_bytes,
50        })
51    }
52}
53
54/// A size in bytes as the refusal prints it: in GiB, with the exact byte
55/// count, for example `3 GiB (3221225472 bytes)`.
56fn size_text(bytes: u64) -> String {
57    const GIB: u64 = 1 << 30;
58    if bytes.is_multiple_of(GIB) {
59        format!("{} GiB ({bytes} bytes)", bytes / GIB)
60    } else {
61        format!("{:.2} GiB ({bytes} bytes)", bytes as f64 / GIB as f64)
62    }
63}
64
65/// An `RLIMIT_AS` value as the refusal prints it.
66fn limit_text(value: libc::rlim_t) -> String {
67    if value == libc::RLIM_INFINITY {
68        "unlimited".to_owned()
69    } else {
70        size_text(value)
71    }
72}
73
74impl fmt::Display for KvmAddressSpaceRefusal {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        let guest_memory = size_text(self.guest_memory_bytes as u64);
77        write!(
78            f,
79            "kvm_address_space_limit: --backend=kvm refuses to start under a finite RLIMIT_AS \
80             (soft {}, hard {}): the KVM backend reserves {guest_memory} of host address space \
81             for every guest process, plus {guest_memory} more for each exec in flight, and \
82             RLIMIT_AS counts every reserved byte although only the pages a guest touches use \
83             memory, so no finite limit fits every process tree, and under one a run would stop \
84             wherever the guest's process tree and host thread scheduling put the peak; Hermit \
85             does not raise the limit itself; run Hermit with an unlimited address-space limit \
86             (`ulimit -v unlimited`, which needs an unlimited hard limit or privilege) and bound \
87             its memory with a cgroup instead (memory.max, which charges the memory the run \
88             uses, not the address space it reserves); the guest has not started",
89            limit_text(self.soft),
90            limit_text(self.hard),
91        )
92    }
93}
94
95impl std::error::Error for KvmAddressSpaceRefusal {}
96
97/// Refuses ([`KvmAddressSpaceRefusal`]) when this process, which is about to
98/// run the KVM backend, has a finite soft `RLIMIT_AS`; the backend would
99/// reserve `guest_memory_bytes` for each guest memory.
100pub fn admit_kvm_address_space(guest_memory_bytes: usize) -> anyhow::Result<()> {
101    let mut limit = libc::rlimit {
102        rlim_cur: 0,
103        rlim_max: 0,
104    };
105    // SAFETY: getrlimit writes only `limit`.
106    if unsafe { libc::getrlimit(libc::RLIMIT_AS, &mut limit) } != 0 {
107        return Err(anyhow::Error::new(std::io::Error::last_os_error())
108            .context("failed to read RLIMIT_AS before starting the KVM backend"));
109    }
110    match KvmAddressSpaceRefusal::for_limit(limit, guest_memory_bytes) {
111        Some(refusal) => Err(anyhow::Error::new(refusal)),
112        None => Ok(()),
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    const GUEST_MEMORY: usize = 3 << 30;
121
122    /// Only an unlimited soft limit starts the backend; every finite one is
123    /// refused, whatever the hard limit, and the refusal names both limits
124    /// (in GiB and bytes), the reservations behind the refusal, and both
125    /// remedies.
126    #[test]
127    fn kvm_starts_only_under_an_unlimited_soft_address_space_limit() {
128        let limit = |soft, hard| libc::rlimit {
129            rlim_cur: soft,
130            rlim_max: hard,
131        };
132        let unlimited = libc::RLIM_INFINITY;
133        assert_eq!(
134            KvmAddressSpaceRefusal::for_limit(limit(unlimited, unlimited), GUEST_MEMORY),
135            None
136        );
137        for (soft, hard, soft_text, hard_text) in [
138            (
139                16 << 30,
140                16 << 30,
141                "16 GiB (17179869184 bytes)",
142                "16 GiB (17179869184 bytes)",
143            ),
144            (
145                16 << 30,
146                unlimited,
147                "16 GiB (17179869184 bytes)",
148                "unlimited",
149            ),
150            (
151                1 << 40,
152                unlimited,
153                "1024 GiB (1099511627776 bytes)",
154                "unlimited",
155            ),
156            (
157                3537227776,
158                unlimited,
159                "3.29 GiB (3537227776 bytes)",
160                "unlimited",
161            ),
162            (0, 0, "0 GiB (0 bytes)", "0 GiB (0 bytes)"),
163        ] {
164            let refusal = KvmAddressSpaceRefusal::for_limit(limit(soft, hard), GUEST_MEMORY)
165                .expect("a finite soft limit is refused");
166            let text = refusal.to_string();
167            assert!(
168                text.starts_with(&format!(
169                    "kvm_address_space_limit: --backend=kvm refuses to start under a finite \
170                     RLIMIT_AS (soft {soft_text}, hard {hard_text}): "
171                )),
172                "{text}"
173            );
174            // The reason, in GiB: 3 GiB per guest process, plus 3 GiB per
175            // exec in flight.
176            assert!(
177                text.contains(
178                    "reserves 3 GiB (3221225472 bytes) of host address space for every guest \
179                     process, plus 3 GiB (3221225472 bytes) more for each exec in flight"
180                ),
181                "{text}"
182            );
183            assert!(
184                text.contains("Hermit does not raise the limit itself"),
185                "{text}"
186            );
187            // Both remedies: an unlimited limit, and a cgroup bound instead.
188            assert!(text.contains("`ulimit -v unlimited`"), "{text}");
189            assert!(
190                text.contains("bound its memory with a cgroup instead (memory.max"),
191                "{text}"
192            );
193            assert!(text.ends_with("the guest has not started"), "{text}");
194        }
195    }
196}