Skip to main content

arcbox_hypervisor/
types.rs

1//! Common types used across the hypervisor crate.
2
3use serde::{Deserialize, Serialize};
4
5/// Returns the effective memory limit for the host process in bytes.
6///
7/// On macOS this queries `hw.memsize` via `sysctlbyname`.
8/// On Linux this returns `min(sysinfo.totalram, cgroup_memory_limit)` so
9/// that defaults are sensible inside containers, CI runners, or systemd
10/// units with `MemoryMax`.
11///
12/// Returns 0 if the query fails (should never happen on supported platforms).
13#[must_use]
14pub fn host_memory_size() -> u64 {
15    #[cfg(target_os = "macos")]
16    {
17        let mut size: u64 = 0;
18        let mut len = std::mem::size_of::<u64>();
19        // SAFETY: `sysctlbyname` writes at most `len` bytes into `size`,
20        // which is a correctly-sized `u64` buffer we own.
21        let ret = unsafe {
22            libc::sysctlbyname(
23                c"hw.memsize".as_ptr(),
24                std::ptr::addr_of_mut!(size).cast(),
25                &mut len,
26                std::ptr::null_mut(),
27                0,
28            )
29        };
30        if ret == 0 { size } else { 0 }
31    }
32
33    #[cfg(target_os = "linux")]
34    {
35        // SAFETY: `info` is zero-initialized and we pass a valid pointer.
36        // `sysinfo(2)` only writes to the provided struct; fields are
37        // plain integers that are valid for any bit pattern.
38        let physical = unsafe {
39            let mut info: libc::sysinfo = std::mem::zeroed();
40            if libc::sysinfo(&mut info) == 0 {
41                info.totalram * u64::from(info.mem_unit)
42            } else {
43                return 0;
44            }
45        };
46
47        // Respect cgroup memory limits so defaults are sensible inside
48        // containers, CI runners, or systemd units with MemoryMax.
49        let cgroup = cgroup_memory_limit();
50        if cgroup > 0 && cgroup < physical {
51            cgroup
52        } else {
53            physical
54        }
55    }
56}
57
58/// Reads the cgroup memory limit (bytes) for this process.
59///
60/// Resolves the actual cgroup path via `/proc/self/cgroup`, then reads
61/// `memory.max` (v2) or `memory.limit_in_bytes` (v1) from the correct
62/// directory. Falls back to root cgroup paths if resolution fails.
63/// Returns 0 if no limit is set or the files cannot be read.
64#[cfg(target_os = "linux")]
65fn cgroup_memory_limit() -> u64 {
66    // Try cgroup v2 first, then v1.
67    if let Some(v) = cgroup_v2_memory_limit() {
68        return v;
69    }
70    if let Some(v) = cgroup_v1_memory_limit() {
71        return v;
72    }
73    0
74}
75
76/// Reads memory.max from the process's cgroup v2 directory.
77#[cfg(target_os = "linux")]
78fn cgroup_v2_memory_limit() -> Option<u64> {
79    // Resolve current cgroup path from /proc/self/cgroup.
80    // cgroup v2 has a single "0::" line.
81    let cgroup_path = std::fs::read_to_string("/proc/self/cgroup")
82        .ok()?
83        .lines()
84        .find(|l| l.starts_with("0::"))
85        .map(|l| l.strip_prefix("0::").unwrap_or("/").to_string())?;
86
87    // Try the process's own cgroup directory first, fall back to root.
88    let paths = [
89        format!("/sys/fs/cgroup{cgroup_path}/memory.max"),
90        "/sys/fs/cgroup/memory.max".to_string(),
91    ];
92    for path in &paths {
93        if let Ok(s) = std::fs::read_to_string(path) {
94            let s = s.trim();
95            if s != "max" {
96                if let Ok(v) = s.parse::<u64>() {
97                    return Some(v);
98                }
99            }
100        }
101    }
102    None
103}
104
105/// Reads memory.limit_in_bytes from the process's cgroup v1 memory controller.
106#[cfg(target_os = "linux")]
107fn cgroup_v1_memory_limit() -> Option<u64> {
108    // Find the memory controller entry in /proc/self/cgroup.
109    // v1 lines look like "N:memory:/path".
110    let cgroup_path = std::fs::read_to_string("/proc/self/cgroup")
111        .ok()?
112        .lines()
113        .find_map(|l| {
114            let parts: Vec<&str> = l.splitn(3, ':').collect();
115            if parts.len() == 3 && parts[1].split(',').any(|c| c == "memory") {
116                Some(parts[2].to_string())
117            } else {
118                None
119            }
120        })?;
121
122    let paths = [
123        format!("/sys/fs/cgroup/memory{cgroup_path}/memory.limit_in_bytes"),
124        "/sys/fs/cgroup/memory/memory.limit_in_bytes".to_string(),
125    ];
126    for path in &paths {
127        if let Ok(s) = std::fs::read_to_string(path) {
128            let s = s.trim();
129            if let Ok(v) = s.parse::<u64>() {
130                // Kernel sets this to a huge sentinel (PAGE_COUNTER_MAX)
131                // when there is no limit; ignore values above 2^62.
132                if v < (1 << 62) {
133                    return Some(v);
134                }
135            }
136        }
137    }
138    None
139}
140
141/// Returns a sensible default VM memory size based on host physical memory.
142///
143/// The default is half of host RAM, clamped to `[512 MB, 16 GB]` and
144/// rounded down to a 1 MiB boundary (KVM and Virtualization.framework
145/// both require page-aligned memory sizes).
146/// Falls back to 4 GB if host memory detection fails.
147#[must_use]
148pub fn default_vm_memory_size() -> u64 {
149    const MIN_DEFAULT: u64 = 512 * 1024 * 1024; // 512 MB
150    const MAX_DEFAULT: u64 = 16 * 1024 * 1024 * 1024; // 16 GB
151    const FALLBACK: u64 = 4 * 1024 * 1024 * 1024; // 4 GB
152    const MIB: u64 = 1024 * 1024;
153
154    let host = host_memory_size();
155    if host == 0 {
156        return FALLBACK;
157    }
158
159    let size = (host / 2).clamp(MIN_DEFAULT, MAX_DEFAULT);
160    // Round down to 1 MiB boundary.
161    size & !(MIB - 1)
162}
163
164/// Returns a sensible default VM vCPU count: the host's logical core count.
165///
166/// Falls back to 4 if host CPU detection fails.
167#[must_use]
168pub fn default_vm_cpu_count() -> u32 {
169    std::thread::available_parallelism().map_or(4, |n| u32::try_from(n.get()).unwrap_or(u32::MAX))
170}
171
172/// Emits a warning if `memory_size` exceeds 50% of host RAM.
173///
174/// Shared by Darwin and KVM `validate_config()` to keep the threshold
175/// and message consistent.
176pub fn warn_memory_exceeds_host_half(memory_size: u64) {
177    let host_mem = host_memory_size();
178    if host_mem > 0 && memory_size > host_mem / 2 {
179        tracing::warn!(
180            "VM memory {}MB exceeds 50% of host RAM ({}MB total) — host may experience memory pressure",
181            memory_size / (1024 * 1024),
182            host_mem / (1024 * 1024),
183        );
184    }
185}
186
187/// CPU architecture.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
189pub enum CpuArch {
190    /// `x86_64` / AMD64
191    X86_64,
192    /// ARM64 / `AArch64`
193    Aarch64,
194}
195
196impl CpuArch {
197    /// Returns the native CPU architecture of the current system.
198    #[must_use]
199    pub const fn native() -> Self {
200        #[cfg(target_arch = "x86_64")]
201        {
202            Self::X86_64
203        }
204        #[cfg(target_arch = "aarch64")]
205        {
206            Self::Aarch64
207        }
208        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
209        {
210            compile_error!("Unsupported CPU architecture")
211        }
212    }
213}
214
215/// Platform capabilities reported by the hypervisor.
216#[derive(Debug, Clone)]
217pub struct PlatformCapabilities {
218    /// Supported CPU architectures.
219    pub supported_archs: Vec<CpuArch>,
220    /// Maximum number of vCPUs per VM.
221    pub max_vcpus: u32,
222    /// Maximum memory size in bytes.
223    pub max_memory: u64,
224    /// Whether nested virtualization is supported.
225    pub nested_virt: bool,
226    /// Whether Rosetta 2 translation is available (macOS only).
227    pub rosetta: bool,
228}
229
230impl Default for PlatformCapabilities {
231    fn default() -> Self {
232        Self {
233            supported_archs: vec![CpuArch::native()],
234            max_vcpus: 1,
235            max_memory: 1024 * 1024 * 1024, // 1GB default
236            nested_virt: false,
237            rosetta: false,
238        }
239    }
240}
241
242/// CPU register state.
243#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct Registers {
245    // General purpose registers (x86_64)
246    pub rax: u64,
247    pub rbx: u64,
248    pub rcx: u64,
249    pub rdx: u64,
250    pub rsi: u64,
251    pub rdi: u64,
252    pub rsp: u64,
253    pub rbp: u64,
254    pub r8: u64,
255    pub r9: u64,
256    pub r10: u64,
257    pub r11: u64,
258    pub r12: u64,
259    pub r13: u64,
260    pub r14: u64,
261    pub r15: u64,
262
263    // Instruction pointer and flags
264    pub rip: u64,
265    pub rflags: u64,
266}
267
268/// Reason for vCPU exit.
269#[derive(Debug, Clone)]
270pub enum VcpuExit {
271    /// VM halted.
272    Halt,
273    /// I/O port access.
274    IoOut {
275        port: u16,
276        size: u8,
277        data: u64,
278    },
279    IoIn {
280        port: u16,
281        size: u8,
282    },
283    /// Memory-mapped I/O.
284    MmioRead {
285        addr: u64,
286        size: u8,
287    },
288    MmioWrite {
289        addr: u64,
290        size: u8,
291        data: u64,
292    },
293    /// Hypercall.
294    Hypercall {
295        nr: u64,
296        args: [u64; 6],
297    },
298    /// System reset requested.
299    SystemReset,
300    /// Shutdown requested.
301    Shutdown,
302    /// Debug exception.
303    Debug,
304    /// Unknown exit reason.
305    Unknown(i32),
306}
307
308/// `VirtIO` device configuration for attaching to a VM.
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct VirtioDeviceConfig {
311    /// Device type.
312    pub device_type: VirtioDeviceType,
313    /// Device-specific configuration.
314    pub config: Vec<u8>,
315    /// Path to device (for block/fs devices).
316    pub path: Option<String>,
317    /// Whether the device is read-only.
318    pub read_only: bool,
319    /// Tag for filesystem devices.
320    pub tag: Option<String>,
321    /// File descriptor for file-handle-based network attachment.
322    #[serde(skip)]
323    pub net_fd: Option<i32>,
324    /// Optional MAC address for network devices.
325    pub mac_address: Option<String>,
326}
327
328impl VirtioDeviceConfig {
329    /// Creates a new block device configuration.
330    pub fn block(path: impl Into<String>, read_only: bool) -> Self {
331        Self {
332            device_type: VirtioDeviceType::Block,
333            config: Vec::new(),
334            path: Some(path.into()),
335            read_only,
336            tag: None,
337            net_fd: None,
338            mac_address: None,
339        }
340    }
341
342    /// Creates a new network device configuration with NAT attachment.
343    #[must_use]
344    pub const fn network() -> Self {
345        Self {
346            device_type: VirtioDeviceType::Net,
347            config: Vec::new(),
348            path: None,
349            read_only: false,
350            tag: None,
351            net_fd: None,
352            mac_address: None,
353        }
354    }
355
356    /// Creates a new network device configuration with NAT attachment and an explicit MAC address.
357    pub fn network_with_mac(mac_address: impl Into<String>) -> Self {
358        Self {
359            device_type: VirtioDeviceType::Net,
360            config: Vec::new(),
361            path: None,
362            read_only: false,
363            tag: None,
364            net_fd: None,
365            mac_address: Some(mac_address.into()),
366        }
367    }
368
369    /// Creates a network device configuration with file-handle attachment.
370    ///
371    /// The VZ framework side uses one connected datagram socket file descriptor
372    /// for bidirectional frame I/O.
373    #[must_use]
374    pub const fn network_file_handle(fd: i32) -> Self {
375        Self {
376            device_type: VirtioDeviceType::Net,
377            config: Vec::new(),
378            path: None,
379            read_only: false,
380            tag: None,
381            net_fd: Some(fd),
382            mac_address: None,
383        }
384    }
385
386    /// Creates a network device configuration with file-handle attachment and
387    /// an explicit MAC address.
388    ///
389    /// Use this when the MAC must match an external interface (e.g. vmnet) so
390    /// that bridge FDB lookups resolve correctly.
391    pub fn network_file_handle_with_mac(fd: i32, mac_address: impl Into<String>) -> Self {
392        Self {
393            device_type: VirtioDeviceType::Net,
394            config: Vec::new(),
395            path: None,
396            read_only: false,
397            tag: None,
398            net_fd: Some(fd),
399            mac_address: Some(mac_address.into()),
400        }
401    }
402
403    /// Creates a new console device configuration.
404    #[must_use]
405    pub const fn console() -> Self {
406        Self {
407            device_type: VirtioDeviceType::Console,
408            config: Vec::new(),
409            path: None,
410            read_only: false,
411            tag: None,
412            net_fd: None,
413            mac_address: None,
414        }
415    }
416
417    /// Creates a new filesystem device configuration.
418    pub fn filesystem(path: impl Into<String>, tag: impl Into<String>, read_only: bool) -> Self {
419        Self {
420            device_type: VirtioDeviceType::Fs,
421            config: Vec::new(),
422            path: Some(path.into()),
423            read_only,
424            tag: Some(tag.into()),
425            net_fd: None,
426            mac_address: None,
427        }
428    }
429
430    /// Creates a new vsock device configuration.
431    #[must_use]
432    pub const fn vsock() -> Self {
433        Self {
434            device_type: VirtioDeviceType::Vsock,
435            config: Vec::new(),
436            path: None,
437            read_only: false,
438            tag: None,
439            net_fd: None,
440            mac_address: None,
441        }
442    }
443
444    /// Creates a new entropy device configuration.
445    #[must_use]
446    pub const fn entropy() -> Self {
447        Self {
448            device_type: VirtioDeviceType::Rng,
449            config: Vec::new(),
450            path: None,
451            read_only: false,
452            tag: None,
453            net_fd: None,
454            mac_address: None,
455        }
456    }
457}
458
459/// `VirtIO` device types.
460#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
461pub enum VirtioDeviceType {
462    /// Block device.
463    Block,
464    /// Network device.
465    Net,
466    /// Console device.
467    Console,
468    /// Filesystem (9p/virtiofs).
469    Fs,
470    /// Socket device.
471    Vsock,
472    /// Entropy source.
473    Rng,
474    /// Balloon device.
475    Balloon,
476    /// GPU device.
477    Gpu,
478}
479
480/// Memory balloon statistics.
481#[derive(Debug, Clone, Default, Serialize, Deserialize)]
482pub struct BalloonStats {
483    /// Target memory size in bytes.
484    ///
485    /// This is the memory size the balloon is trying to achieve.
486    pub target_bytes: u64,
487
488    /// Current balloon size in bytes.
489    ///
490    /// This is how much memory the balloon has currently claimed.
491    /// `actual_guest_memory = configured_memory - current_balloon_size`
492    pub current_bytes: u64,
493
494    /// Configured VM memory size in bytes.
495    ///
496    /// This is the maximum memory available to the guest when
497    /// the balloon is fully deflated.
498    pub configured_bytes: u64,
499}
500
501impl BalloonStats {
502    /// Returns the effective memory available to the guest in bytes.
503    ///
504    /// This is `configured_bytes - current_bytes`.
505    #[must_use]
506    pub const fn effective_memory(&self) -> u64 {
507        self.configured_bytes.saturating_sub(self.current_bytes)
508    }
509
510    /// Returns the target memory as a percentage of configured memory.
511    #[must_use]
512    pub fn target_percent(&self) -> f64 {
513        if self.configured_bytes == 0 {
514            return 100.0;
515        }
516        (self.target_bytes as f64 / self.configured_bytes as f64) * 100.0
517    }
518}
519
520impl VirtioDeviceConfig {
521    /// Creates a new balloon device configuration.
522    ///
523    /// The balloon device allows dynamic memory management by inflating
524    /// (reclaiming memory from guest) or deflating (returning memory to guest).
525    #[must_use]
526    pub const fn balloon() -> Self {
527        Self {
528            device_type: VirtioDeviceType::Balloon,
529            config: Vec::new(),
530            path: None,
531            read_only: false,
532            tag: None,
533            net_fd: None,
534            mac_address: None,
535        }
536    }
537}
538
539/// ARM64 register state for snapshots.
540#[derive(Debug, Clone, Default, Serialize, Deserialize)]
541pub struct Arm64Registers {
542    /// General purpose registers X0-X30.
543    pub x: [u64; 31],
544    /// Stack pointer (SP).
545    pub sp: u64,
546    /// Program counter (PC).
547    pub pc: u64,
548    /// Processor state (PSTATE/CPSR).
549    pub pstate: u64,
550    /// Floating point control register.
551    pub fpcr: u64,
552    /// Floating point status register.
553    pub fpsr: u64,
554    /// Vector registers Q0-Q31 (128-bit each, stored as [u64; 2]).
555    pub v: [[u64; 2]; 32],
556}
557
558/// vCPU snapshot state.
559#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct VcpuSnapshot {
561    /// vCPU ID.
562    pub id: u32,
563    /// CPU architecture.
564    pub arch: CpuArch,
565    /// `x86_64` registers (if applicable).
566    pub x86_regs: Option<Registers>,
567    /// ARM64 registers (if applicable).
568    pub arm64_regs: Option<Arm64Registers>,
569    /// Additional architecture-specific state (opaque bytes).
570    pub extra_state: Vec<u8>,
571}
572
573impl VcpuSnapshot {
574    /// Creates a new `x86_64` vCPU snapshot.
575    #[must_use]
576    pub const fn new_x86(id: u32, regs: Registers) -> Self {
577        Self {
578            id,
579            arch: CpuArch::X86_64,
580            x86_regs: Some(regs),
581            arm64_regs: None,
582            extra_state: Vec::new(),
583        }
584    }
585
586    /// Creates a new ARM64 vCPU snapshot.
587    #[must_use]
588    pub const fn new_arm64(id: u32, regs: Arm64Registers) -> Self {
589        Self {
590            id,
591            arch: CpuArch::Aarch64,
592            x86_regs: None,
593            arm64_regs: Some(regs),
594            extra_state: Vec::new(),
595        }
596    }
597
598    /// Returns `true` if this snapshot contains only default (zeroed) register
599    /// state, i.e. it was created as a placeholder and does not represent real
600    /// captured vCPU registers.
601    #[must_use]
602    pub fn is_placeholder(&self) -> bool {
603        match (&self.x86_regs, &self.arm64_regs) {
604            (Some(regs), _) => regs.rip == 0 && regs.rsp == 0 && regs.rflags == 0,
605            (_, Some(regs)) => regs.pc == 0 && regs.sp == 0 && regs.pstate == 0,
606            (None, None) => true,
607        }
608    }
609}
610
611/// Device snapshot state.
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct DeviceSnapshot {
614    /// Device type.
615    pub device_type: VirtioDeviceType,
616    /// Device name/identifier.
617    pub name: String,
618    /// Device-specific state (serialized).
619    pub state: Vec<u8>,
620}
621
622/// Memory region info for snapshots.
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct MemoryRegionSnapshot {
625    /// Guest physical address start.
626    pub guest_addr: u64,
627    /// Region size in bytes.
628    pub size: u64,
629    /// Whether this region is read-only.
630    pub read_only: bool,
631    /// Offset in the memory dump file.
632    pub file_offset: u64,
633}
634
635/// Dirty page tracking info.
636#[derive(Debug, Clone)]
637pub struct DirtyPageInfo {
638    /// Guest physical address of the page.
639    pub guest_addr: u64,
640    /// Page size (usually 4KB).
641    pub size: u64,
642}
643
644/// Full VM snapshot metadata.
645#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct VmSnapshot {
647    /// Snapshot format version.
648    pub version: u32,
649    /// CPU architecture.
650    pub arch: CpuArch,
651    /// vCPU states.
652    pub vcpus: Vec<VcpuSnapshot>,
653    /// Device states.
654    pub devices: Vec<DeviceSnapshot>,
655    /// Memory region info.
656    pub memory_regions: Vec<MemoryRegionSnapshot>,
657    /// Total memory size.
658    pub total_memory: u64,
659    /// Whether memory is compressed.
660    pub compressed: bool,
661    /// Compression algorithm (if compressed).
662    pub compression: Option<String>,
663    /// Parent snapshot ID (for incremental).
664    pub parent_id: Option<String>,
665}