openhcl_boot/
sidecar.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use crate::boot_logger::log;
5use crate::host_params::MAX_CPU_COUNT;
6use crate::host_params::MAX_NUMA_NODES;
7use crate::host_params::PartitionInfo;
8use crate::host_params::shim_params::IsolationType;
9use crate::host_params::shim_params::ShimParams;
10use crate::memory::AddressSpaceManager;
11use crate::memory::AllocationPolicy;
12use crate::memory::AllocationType;
13use sidecar_defs::SidecarNodeOutput;
14use sidecar_defs::SidecarNodeParams;
15use sidecar_defs::SidecarOutput;
16use sidecar_defs::SidecarParams;
17
18/// The maximum side of a sidecar node. This is tuned to ensure that there are
19/// enough Linux CPUs to manage all the sidecar VPs.
20const MAX_SIDECAR_NODE_SIZE: usize = 32;
21
22// Assert that there are enough sidecar nodes for the maximum number of CPUs, if
23// all NUMA nodes but one have one processor.
24const _: () = assert!(
25    sidecar_defs::MAX_NODES >= (MAX_NUMA_NODES - 1) + MAX_CPU_COUNT.div_ceil(MAX_SIDECAR_NODE_SIZE)
26);
27
28pub struct SidecarConfig<'a> {
29    pub node_params: &'a [SidecarNodeParams],
30    pub nodes: &'a [SidecarNodeOutput],
31    pub start_reftime: u64,
32    pub end_reftime: u64,
33}
34
35impl SidecarConfig<'_> {
36    /// Returns an object to be appended to the Linux kernel command line to
37    /// configure it properly for sidecar.
38    pub fn kernel_command_line(&self) -> SidecarKernelCommandLine<'_> {
39        SidecarKernelCommandLine(self)
40    }
41}
42
43pub struct SidecarKernelCommandLine<'a>(&'a SidecarConfig<'a>);
44
45impl core::fmt::Display for SidecarKernelCommandLine<'_> {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        // Add something like boot_cpus=0,4,8,12 to the command line so that
48        // Linux boots with the base VP of each sidecar node. Other CPUs will
49        // be brought up by the sidecar kernel.
50        f.write_str("boot_cpus=")?;
51        let mut comma = "";
52        for node in self.0.node_params {
53            write!(f, "{}{}", comma, node.base_vp)?;
54            comma = ",";
55        }
56        Ok(())
57    }
58}
59
60pub fn start_sidecar<'a>(
61    p: &ShimParams,
62    partition_info: &PartitionInfo,
63    address_space: &mut AddressSpaceManager,
64    sidecar_params: &'a mut SidecarParams,
65    sidecar_output: &'a mut SidecarOutput,
66) -> Option<SidecarConfig<'a>> {
67    if !cfg!(target_arch = "x86_64") || p.isolation_type != IsolationType::None {
68        return None;
69    }
70
71    if p.sidecar_size == 0 {
72        log!("sidecar: not present in image");
73        return None;
74    }
75
76    if !partition_info.boot_options.sidecar {
77        log!("sidecar: disabled via command line");
78        return None;
79    }
80
81    // Ensure the host didn't provide an out-of-bounds NUMA node.
82    let max_vnode = partition_info
83        .cpus
84        .iter()
85        .map(|cpu| cpu.vnode)
86        .chain(partition_info.vtl2_ram.iter().map(|e| e.vnode))
87        .max()
88        .unwrap();
89
90    if max_vnode >= MAX_NUMA_NODES as u32 {
91        log!("sidecar: NUMA node {max_vnode} too large");
92        return None;
93    }
94
95    #[cfg(target_arch = "x86_64")]
96    if !x86defs::cpuid::VersionAndFeaturesEcx::from(
97        safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VersionAndFeatures.0, 0).ecx,
98    )
99    .x2_apic()
100    {
101        // Currently, sidecar needs x2apic to communicate with the kernel
102        log!("sidecar: x2apic not available; not using sidecar");
103        return None;
104    }
105
106    // Split the CPUs by NUMA node, and then into chunks of no more than
107    // MAX_SIDECAR_NODE_SIZE processors.
108    let cpus_by_node = || {
109        partition_info
110            .cpus
111            .chunk_by(|a, b| a.vnode == b.vnode)
112            .flat_map(|cpus| {
113                let chunks = cpus.len().div_ceil(MAX_SIDECAR_NODE_SIZE);
114                cpus.chunks(cpus.len().div_ceil(chunks))
115            })
116    };
117    if cpus_by_node().all(|cpus_by_node| cpus_by_node.len() == 1) {
118        log!("sidecar: all NUMA nodes have one CPU");
119        return None;
120    }
121    let node_count = cpus_by_node().count();
122
123    let mut total_ram;
124    {
125        let SidecarParams {
126            hypercall_page,
127            enable_logging,
128            node_count,
129            nodes,
130        } = sidecar_params;
131
132        *hypercall_page = 0;
133        #[cfg(target_arch = "x86_64")]
134        {
135            *hypercall_page = crate::hypercall::hvcall().hypercall_page();
136        }
137        *enable_logging = partition_info.boot_options.sidecar_logging;
138
139        let mut base_vp = 0;
140        total_ram = 0;
141        for (cpus, node) in cpus_by_node().zip(nodes) {
142            let required_ram = sidecar_defs::required_memory(cpus.len() as u32) as u64;
143            // Take some VTL2 RAM for sidecar use. Try to use the same NUMA node
144            // as the first CPU.
145            let local_vnode = cpus[0].vnode as usize;
146
147            let mem = match address_space.allocate(
148                Some(local_vnode as u32),
149                required_ram,
150                AllocationType::SidecarNode,
151                AllocationPolicy::LowMemory,
152            ) {
153                Some(mem) => mem,
154                None => {
155                    // Fallback to no numa requirement.
156                    match address_space.allocate(
157                        None,
158                        required_ram,
159                        AllocationType::SidecarNode,
160                        AllocationPolicy::LowMemory,
161                    ) {
162                        Some(mem) => {
163                            log!(
164                                "sidecar: unable to allocate memory for sidecar node on node {local_vnode}, falling back to node {}",
165                                mem.vnode
166                            );
167                            mem
168                        }
169                        None => {
170                            log!("sidecar: not enough memory for sidecar");
171                            return None;
172                        }
173                    }
174                }
175            };
176
177            *node = SidecarNodeParams {
178                memory_base: mem.range.start(),
179                memory_size: mem.range.len(),
180                base_vp,
181                vp_count: cpus.len() as u32,
182            };
183            base_vp += cpus.len() as u32;
184            *node_count += 1;
185            total_ram += required_ram;
186        }
187    }
188
189    // SAFETY: the parameter blob is trusted.
190    let sidecar_entry: extern "C" fn(&SidecarParams, &mut SidecarOutput) -> bool =
191        unsafe { core::mem::transmute(p.sidecar_entry_address) };
192
193    let boot_start_reftime = minimal_rt::reftime::reference_time();
194    log!(
195        "sidecar starting, {} nodes, {} cpus, {:#x} total bytes",
196        node_count,
197        partition_info.cpus.len(),
198        total_ram
199    );
200    if !sidecar_entry(sidecar_params, sidecar_output) {
201        panic!(
202            "failed to start sidecar: {}",
203            core::str::from_utf8(&sidecar_output.error.buf[..sidecar_output.error.len as usize])
204                .unwrap()
205        );
206    }
207    let boot_end_reftime = minimal_rt::reftime::reference_time();
208
209    let SidecarOutput { nodes, error: _ } = sidecar_output;
210    Some(SidecarConfig {
211        start_reftime: boot_start_reftime,
212        end_reftime: boot_end_reftime,
213        node_params: &sidecar_params.nodes[..node_count],
214        nodes: &nodes[..node_count],
215    })
216}