squib_arch/layout.rs
1//! aarch64 memory layout — pinned constants for squib's microvm.
2//!
3//! The fixed layout is sized for the 32-vCPU worst case to defeat the GICR-vs-virtio-MMIO
4//! overlap that an earlier draft introduced (D22). The const-evaluated overlap-check at
5//! the bottom of this file ensures any future regression breaks the build, not the boot.
6//!
7//! See [13-arch-and-boot.md § 2](../../../specs/13-arch-and-boot.md#2-memory-layout-concrete)
8//! and [99-key-decisions.md § D22](../../../specs/99-key-decisions.md#d22).
9
10use squib_core::MAX_SUPPORTED_VCPUS;
11
12/// MSI region base. HVF's GIC interception only kicks in once
13/// distributor + redistributor + MSI are all configured (skipping MSI
14/// causes HVF to silently treat GIC accesses as unmapped, which
15/// surfaces as data aborts to the host). We park MSI well below the
16/// GICD at a 16 MiB-aligned address. The MSI region itself is sized
17/// by `hv_gic_get_msi_region_size` (typically 64 KiB on macOS 15+);
18/// reserve 16 MiB of headroom in case Apple grows it.
19pub const MSI_REGION_BASE: u64 = 0x0500_0000;
20
21/// GIC distributor base address.
22pub const GICD_BASE: u64 = 0x0800_0000;
23
24/// Maximum GICD region we reserve. The live size is queried from
25/// `hv_gic_get_distributor_size`; this is the placement-only cap.
26pub const GICD_RESERVED_SIZE: u64 = 0x0001_0000;
27
28/// GIC redistributor window base.
29///
30/// The live region is `[GICR_BASE, GICR_BASE + vcpu_count *
31/// hv_gic_get_redistributor_size)`. The reserved window is sized for the worst case
32/// (`MAX_SUPPORTED_VCPUS` × 128 KiB = 4 MiB) plus headroom up to `PL011_BASE`.
33pub const GICR_BASE: u64 = 0x080A_0000;
34
35/// End of the GICR reservation window. The live GICR live region must not exceed this.
36pub const GICR_RESERVED_END: u64 = 0x0E0A_0000;
37
38/// PL011 UART base. FDT SPI cell 1 → INTID 33, level-high.
39pub const PL011_BASE: u64 = 0x0E0A_0000;
40
41/// PL011 UART region size.
42pub const PL011_SIZE: u64 = 0x1000;
43
44/// virtio-MMIO region base.
45///
46/// 32 × 4 KiB slots. FDT SPI cells 16..47 → raw INTIDs 48..79 (edge-rising).
47pub const VIRTIO_MMIO_BASE: u64 = 0x0F00_0000;
48
49/// Total virtio-MMIO region size: 32 slots × 4 KiB.
50pub const VIRTIO_MMIO_SIZE: u64 = 32 * 0x1000;
51
52/// First DRAM byte. Matches Firecracker's `DRAM_MEM_START`.
53pub const DRAM_BASE: u64 = 0x8000_0000;
54
55/// Maximum DRAM extent — `0x00FF_8000_0000` matches upstream `DRAM_MEM_MAX_SIZE` of 1022 GiB
56/// above `DRAM_BASE`.
57pub const DRAM_MAX_END: u64 = 0x00FF_8000_0000 + DRAM_BASE;
58
59/// Kernel image is loaded at `DRAM_BASE + KERNEL_LOAD_OFFSET`. Matches Firecracker's reserved
60/// 2 MiB for system metadata at the head of DRAM.
61pub const KERNEL_LOAD_OFFSET: u64 = 0x0020_0000;
62
63/// Initrd start offset: `DRAM_BASE + 256 MiB`, or `kernel_end + 16 MiB` rounded up to the
64/// 2 MiB grain — whichever is larger. The constant here is the lower bound; the builder
65/// computes the actual offset against the kernel size.
66pub const INITRD_FALLBACK_OFFSET: u64 = 0x1000_0000;
67
68/// FDT region size — at most 2 MiB per `arm64/booting.rst`.
69pub const FDT_MAX_SIZE: u64 = 0x0020_0000;
70
71/// Worst-case GICR live size (`MAX_SUPPORTED_VCPUS × 128 KiB`).
72///
73/// The actual live size on the host is `vcpu_count × hv_gic_get_redistributor_size`,
74/// which Apple may grow in a future macOS release. This constant is the floor that the
75/// reserved layout window must accommodate.
76pub const GICR_REDISTRIBUTOR_SIZE_PER_VCPU: u64 = 0x0002_0000;
77const _GICR_LIVE_MAX: u64 = (MAX_SUPPORTED_VCPUS as u64) * GICR_REDISTRIBUTOR_SIZE_PER_VCPU;
78
79// === Compile-time overlap check (I-AB-6 in 13 § 10) ===
80//
81// Any future revert that re-introduces the GICR/virtio overlap from the original
82// draft breaks the build, not the boot. Three independent assertions:
83// 1. The live GICR for 32 vCPUs fits inside the reserved [GICR_BASE, GICR_RESERVED_END).
84// 2. PL011 sits exactly at GICR_RESERVED_END (no slack we'd silently lose).
85// 3. virtio-MMIO sits above PL011 + PL011_SIZE.
86const _: () = {
87 assert!(
88 GICR_BASE + _GICR_LIVE_MAX <= GICR_RESERVED_END,
89 "GICR live region for 32 vCPUs overflows the reserved GICR window (D22)"
90 );
91 assert!(
92 PL011_BASE == GICR_RESERVED_END,
93 "PL011 must abut the GICR reservation (D22)"
94 );
95 assert!(
96 VIRTIO_MMIO_BASE >= PL011_BASE + PL011_SIZE,
97 "virtio-MMIO base must sit above PL011 (D22 — overlap regression)"
98 );
99 assert!(
100 VIRTIO_MMIO_BASE + VIRTIO_MMIO_SIZE <= DRAM_BASE,
101 "virtio-MMIO region overlaps DRAM (D22 — overlap regression)"
102 );
103};
104
105/// Page geometry constants — three sizes that must never be conflated.
106///
107/// On Apple Silicon the host page is 16 KiB, the HVF stage-2 granule is 16 KiB, and the
108/// dirty-tracking page defaults to 2 MiB with adaptive step-down to 16 KiB for hot regions.
109/// See [99-key-decisions.md § D21](../../../specs/99-key-decisions.md#d21).
110#[derive(Debug, Clone, Copy)]
111pub struct PageGeometry {
112 /// Apple Silicon host page (16 KiB) — `vm_page_size` returns this.
113 pub host_page: u64,
114 /// HVF stage-2 translation granule (16 KiB on Apple Silicon).
115 pub hvf_stage2_granule: u64,
116 /// Default tracking-page granularity for dirty-page bitmaps (2 MiB).
117 pub tracking_page_default: u64,
118 /// Step-down tracking page used by the adaptive heuristic for hot regions (16 KiB).
119 pub tracking_page_hot: u64,
120}
121
122impl PageGeometry {
123 /// The Apple-Silicon page geometry. macOS 15+ on M-series.
124 pub const APPLE_SILICON: Self = Self {
125 host_page: 16 * 1024,
126 hvf_stage2_granule: 16 * 1024,
127 tracking_page_default: 2 * 1024 * 1024,
128 tracking_page_hot: 16 * 1024,
129 };
130}
131
132/// Frozen memory layout for a microvm.
133#[derive(Debug, Clone, Copy)]
134pub struct MemoryLayout {
135 /// GIC distributor base.
136 pub gicd_base: u64,
137 /// GIC redistributor window base.
138 pub gicr_base: u64,
139 /// PL011 UART base.
140 pub pl011_base: u64,
141 /// virtio-MMIO region base (slot 0).
142 pub virtio_mmio_base: u64,
143 /// virtio-MMIO slot stride (4 KiB).
144 pub virtio_mmio_stride: u64,
145 /// Number of virtio-MMIO slots reserved.
146 pub virtio_mmio_slots: u32,
147 /// DRAM start.
148 pub dram_base: u64,
149 /// Page geometry.
150 pub page_geometry: PageGeometry,
151}
152
153/// Canonical squib memory layout. There is exactly one — D22 is "fixed".
154pub const MEMORY_LAYOUT: MemoryLayout = MemoryLayout {
155 gicd_base: GICD_BASE,
156 gicr_base: GICR_BASE,
157 pl011_base: PL011_BASE,
158 virtio_mmio_base: VIRTIO_MMIO_BASE,
159 virtio_mmio_stride: 0x1000,
160 virtio_mmio_slots: 32,
161 dram_base: DRAM_BASE,
162 page_geometry: PageGeometry::APPLE_SILICON,
163};
164
165/// Result of [`overlap_check`].
166#[derive(Debug, Clone, Copy, Eq, PartialEq)]
167pub enum LayoutOverlap {
168 /// The live GICR region fits within the reserved window for the given vCPU count.
169 Ok,
170 /// The live GICR region for `vcpu_count` would overlap the PL011/virtio-MMIO band.
171 /// `live_gicr_end` is `GICR_BASE + vcpu_count × redistributor_size_per_vcpu`.
172 GicrOverlapsMmio {
173 /// vCPU count that triggered the overlap.
174 vcpu_count: u32,
175 /// Computed live GICR end address.
176 live_gicr_end: u64,
177 /// PL011 base (the boundary the live GICR must not cross).
178 boundary: u64,
179 },
180}
181
182/// Runtime layout overlap check used by `squib-vmm::builder` before `hv_gic_create`.
183///
184/// Pairs with the const overlap-check above. The const-check is an absolute floor
185/// (32 vCPUs × the constant 128 KiB redistributor stride); the runtime check uses the
186/// host-reported `redistributor_size_per_vcpu`, since Apple may bump the size in a future
187/// macOS release.
188///
189/// # Errors
190/// Surfaces a structured [`LayoutOverlap::GicrOverlapsMmio`] result if the layout cannot
191/// fit `vcpu_count` redistributors in the reserved GICR window. This must be surfaced as
192/// `Error::Config` to the API layer; the VMM must not call `hv_gic_create` after a
193/// non-OK result.
194#[must_use]
195pub fn overlap_check(vcpu_count: u32, redistributor_size_per_vcpu: u64) -> LayoutOverlap {
196 let live_end =
197 GICR_BASE.saturating_add(u64::from(vcpu_count).saturating_mul(redistributor_size_per_vcpu));
198 if live_end > PL011_BASE {
199 LayoutOverlap::GicrOverlapsMmio {
200 vcpu_count,
201 live_gicr_end: live_end,
202 boundary: PL011_BASE,
203 }
204 } else {
205 LayoutOverlap::Ok
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn d22_reserved_window_has_correct_size() {
215 // 96 MiB — the headroom the spec calls out.
216 assert_eq!(GICR_RESERVED_END - GICR_BASE, 96 * 1024 * 1024);
217 }
218
219 #[test]
220 fn worst_case_gicr_fits_under_pl011() {
221 // 32 vCPUs × 128 KiB = 4 MiB, well under the 96 MiB reservation.
222 const { assert!(_GICR_LIVE_MAX < GICR_RESERVED_END - GICR_BASE) };
223 }
224
225 #[test]
226 fn pl011_abuts_gicr_window() {
227 assert_eq!(PL011_BASE, GICR_RESERVED_END);
228 }
229
230 #[test]
231 fn virtio_mmio_does_not_overlap_pl011() {
232 const { assert!(VIRTIO_MMIO_BASE >= PL011_BASE + PL011_SIZE) };
233 }
234
235 #[test]
236 fn virtio_mmio_does_not_overlap_dram() {
237 const { assert!(VIRTIO_MMIO_BASE + VIRTIO_MMIO_SIZE <= DRAM_BASE) };
238 }
239
240 #[test]
241 fn page_geometry_apple_silicon_matches_d21() {
242 let p = PageGeometry::APPLE_SILICON;
243 assert_eq!(p.host_page, 16 * 1024);
244 assert_eq!(p.hvf_stage2_granule, 16 * 1024);
245 assert_eq!(p.tracking_page_default, 2 * 1024 * 1024);
246 assert_eq!(p.tracking_page_hot, 16 * 1024);
247 }
248
249 #[test]
250 fn overlap_check_passes_at_max_vcpus() {
251 assert_eq!(
252 overlap_check(MAX_SUPPORTED_VCPUS, 0x0002_0000),
253 LayoutOverlap::Ok
254 );
255 }
256
257 #[test]
258 fn overlap_check_fails_if_apple_grows_redistributor_excessively() {
259 // Hypothetical: Apple bumps redistributor stride to 4 MiB. 32 × 4 MiB = 128 MiB
260 // > 96 MiB reservation, so the runtime check must reject before hv_gic_create.
261 let result = overlap_check(MAX_SUPPORTED_VCPUS, 4 * 1024 * 1024);
262 assert!(matches!(result, LayoutOverlap::GicrOverlapsMmio { .. }));
263 }
264
265 #[test]
266 fn overlap_check_zero_vcpus_is_trivially_ok() {
267 assert_eq!(overlap_check(0, 0x0002_0000), LayoutOverlap::Ok);
268 }
269
270 #[test]
271 fn overlap_check_does_not_overflow_on_huge_redistributor_size() {
272 // Saturating arithmetic must not panic on adversarial inputs.
273 let result = overlap_check(MAX_SUPPORTED_VCPUS, u64::MAX);
274 assert!(matches!(result, LayoutOverlap::GicrOverlapsMmio { .. }));
275 }
276}