Skip to main content

openhcl_boot/
sidecar.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use crate::cmdline::SidecarOptions;
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 num_cpus: usize,
30    pub per_cpu_state: &'a sidecar_defs::PerCpuState,
31    pub node_params: &'a [SidecarNodeParams],
32    pub nodes: &'a [SidecarNodeOutput],
33    pub start_reftime: u64,
34    pub end_reftime: u64,
35}
36
37impl SidecarConfig<'_> {
38    /// Returns an object to be appended to the Linux kernel command line to
39    /// configure it properly for sidecar.
40    pub fn kernel_command_line(&self) -> SidecarKernelCommandLine<'_> {
41        SidecarKernelCommandLine(self)
42    }
43}
44
45pub struct SidecarKernelCommandLine<'a>(&'a SidecarConfig<'a>);
46
47impl core::fmt::Display for SidecarKernelCommandLine<'_> {
48    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
49        // Generate boot_cpus= parameter listing CPUs that Linux should start
50        // directly (all others will be managed by sidecar).
51        // When per-CPU overrides are active (servicing restore with outstanding IO),
52        // list every CPU that sidecar should NOT start.
53        // Otherwise, list just the base VP of each sidecar node (default behavior).
54        f.write_str("boot_cpus=")?;
55        let mut comma = "";
56        if self.0.per_cpu_state.per_cpu_state_specified {
57            for (i, &starts) in self.0.per_cpu_state.sidecar_starts_cpu[..self.0.num_cpus]
58                .iter()
59                .enumerate()
60            {
61                if !starts {
62                    write!(f, "{comma}{i}")?;
63                    comma = ",";
64                }
65            }
66        } else {
67            for node in self.0.node_params {
68                write!(f, "{comma}{}", node.base_vp)?;
69                comma = ",";
70            }
71        }
72        Ok(())
73    }
74}
75
76/// Returns true if, with per-CPU sidecar overrides active, the sidecar node
77/// whose VPs are `base_vp..base_vp + size` has no application processors left
78/// for sidecar to start (every AP is kernel-started).
79///
80/// The first VP of a node is its base VP and is always kernel-started, so
81/// sidecar only ever starts the remaining VPs.
82fn sidecar_node_is_empty(
83    overrides: &sidecar_defs::PerCpuState,
84    base_vp: usize,
85    size: usize,
86) -> bool {
87    // Without per-CPU overrides sidecar starts every non-base VP.
88    if !overrides.per_cpu_state_specified {
89        return size == 1;
90    }
91    // Empty iff no non-base VP is left for sidecar to start.
92    !(1..size).any(|i| overrides.sidecar_starts_cpu[base_vp + i])
93}
94
95pub fn start_sidecar<'a>(
96    p: &ShimParams,
97    partition_info: &PartitionInfo,
98    address_space: &mut AddressSpaceManager,
99    sidecar_params: &'a mut SidecarParams,
100    sidecar_output: &'a mut SidecarOutput,
101) -> Option<SidecarConfig<'a>> {
102    if !cfg!(target_arch = "x86_64") || p.isolation_type != IsolationType::None {
103        return None;
104    }
105
106    if p.sidecar_size == 0 {
107        log::info!("sidecar: not present in image");
108        return None;
109    }
110
111    match partition_info.boot_options.sidecar {
112        SidecarOptions::DisabledCommandLine => {
113            log::info!("sidecar: disabled via command line");
114            return None;
115        }
116        SidecarOptions::DisabledPerCpuStateCapacity => {
117            log::info!(
118                "sidecar: disabled because servicing per-CPU overrides exceed state capacity"
119            );
120            return None;
121        }
122        SidecarOptions::Enabled { enable_logging, .. } => {
123            sidecar_params.enable_logging = enable_logging;
124        }
125    }
126
127    // Ensure the host didn't provide an out-of-bounds NUMA node.
128    let max_vnode = partition_info
129        .cpus
130        .iter()
131        .map(|cpu| cpu.vnode)
132        .chain(partition_info.vtl2_ram.iter().map(|e| e.vnode))
133        .max()
134        .unwrap();
135
136    if max_vnode >= MAX_NUMA_NODES as u32 {
137        log::warn!("sidecar: NUMA node {max_vnode} too large");
138        return None;
139    }
140
141    #[cfg(target_arch = "x86_64")]
142    if !x86defs::cpuid::VersionAndFeaturesEcx::from(
143        safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VersionAndFeatures.0, 0).ecx,
144    )
145    .x2_apic()
146    {
147        // Currently, sidecar needs x2apic to communicate with the kernel
148        log::warn!("sidecar: x2apic not available; not using sidecar");
149        return None;
150    }
151
152    // Split the CPUs by NUMA node, and then into chunks of no more than
153    // MAX_SIDECAR_NODE_SIZE processors.
154    let cpus_by_node = || {
155        partition_info
156            .cpus
157            .chunk_by(|a, b| a.vnode == b.vnode)
158            .flat_map(|cpus| {
159                let chunks = cpus.len().div_ceil(MAX_SIDECAR_NODE_SIZE);
160                cpus.chunks(cpus.len().div_ceil(chunks))
161            })
162    };
163    if cpus_by_node().all(|cpus_by_node| cpus_by_node.len() == 1) {
164        log::info!("sidecar: all NUMA nodes have one CPU");
165        return None;
166    }
167    let mut total_ram;
168    {
169        let SidecarParams {
170            hypercall_page,
171            enable_logging: _,
172            node_count,
173            nodes,
174            initial_state,
175        } = sidecar_params;
176
177        *hypercall_page = 0;
178        #[cfg(target_arch = "x86_64")]
179        {
180            *hypercall_page = crate::hypercall::hvcall().hypercall_page();
181        }
182
183        let mut base_vp = 0;
184        total_ram = 0;
185        *node_count = 0;
186        *initial_state = partition_info.sidecar_cpu_overrides.clone();
187        for cpus in cpus_by_node() {
188            // Skip creating a sidecar node when none of its application
189            // processors are left for sidecar to start (all are kernel-started).
190            if sidecar_node_is_empty(
191                &partition_info.sidecar_cpu_overrides,
192                base_vp as usize,
193                cpus.len(),
194            ) {
195                if initial_state.per_cpu_state_specified {
196                    // Kernel-start the base VP too; its APs are already
197                    // excluded, so every VP in the node ends up in `boot_cpus=`.
198                    initial_state.sidecar_starts_cpu[base_vp as usize] = false;
199                }
200                log::info!(
201                    "sidecar: node at base VP {base_vp} ({} VPs) has no sidecar-started APs; kernel-starting all of them",
202                    cpus.len(),
203                );
204                base_vp += cpus.len() as u32;
205                continue;
206            }
207
208            let required_ram = sidecar_defs::required_memory(cpus.len() as u32) as u64;
209            // Take some VTL2 RAM for sidecar use. Try to use the same NUMA node
210            // as the first CPU.
211            let local_vnode = cpus[0].vnode as usize;
212
213            let mem = match address_space.allocate(
214                Some(local_vnode as u32),
215                required_ram,
216                AllocationType::SidecarNode,
217                AllocationPolicy::LowMemory,
218            ) {
219                Some(mem) => mem,
220                None => {
221                    // Fallback to no numa requirement.
222                    match address_space.allocate(
223                        None,
224                        required_ram,
225                        AllocationType::SidecarNode,
226                        AllocationPolicy::LowMemory,
227                    ) {
228                        Some(mem) => {
229                            log::warn!(
230                                "sidecar: unable to allocate memory for sidecar node on node {local_vnode}, falling back to node {}",
231                                mem.vnode
232                            );
233                            mem
234                        }
235                        None => {
236                            log::warn!("sidecar: not enough memory for sidecar");
237                            return None;
238                        }
239                    }
240                }
241            };
242
243            nodes[*node_count as usize] = SidecarNodeParams {
244                memory_base: mem.range.start(),
245                memory_size: mem.range.len(),
246                base_vp,
247                vp_count: cpus.len() as u32,
248            };
249            log::info!(
250                "sidecar: created node index={} base_vp={base_vp} vp_count={} vnode={local_vnode}",
251                *node_count,
252                cpus.len(),
253            );
254            if initial_state.per_cpu_state_specified {
255                // If per-CPU state is specified, make sure to explicitly state that
256                // sidecar should not start the base vp of this node.
257                // The code that set per_cpu_state_specified should have already ensured that
258                // the array is large enough for any `base_vp` we might have here.
259                initial_state.sidecar_starts_cpu[base_vp as usize] = false;
260                log::info!(
261                    "sidecar: per_cpu_state_specified=true, marking base_vp={} as kernel-started",
262                    base_vp
263                );
264            }
265            base_vp += cpus.len() as u32;
266            *node_count += 1;
267            total_ram += required_ram;
268        }
269    }
270
271    // If per-CPU overrides left every node empty, there is nothing for
272    // sidecar to do; behave as if it were disabled entirely.
273    let node_count = sidecar_params.node_count as usize;
274    if node_count == 0 {
275        log::info!("sidecar: no nodes have sidecar-started APs; disabling sidecar");
276        return None;
277    }
278
279    // SAFETY: the parameter blob is trusted.
280    let sidecar_entry: extern "C" fn(&SidecarParams, &mut SidecarOutput) -> bool =
281        unsafe { core::mem::transmute(p.sidecar_entry_address) };
282
283    let boot_start_reftime = minimal_rt::reftime::reference_time();
284    log::info!(
285        "sidecar starting, {} nodes, {} cpus, {:#x} total bytes",
286        node_count,
287        partition_info.cpus.len(),
288        total_ram
289    );
290    if !sidecar_entry(sidecar_params, sidecar_output) {
291        panic!(
292            "failed to start sidecar: {}",
293            core::str::from_utf8(&sidecar_output.error.buf[..sidecar_output.error.len as usize])
294                .unwrap()
295        );
296    }
297    let boot_end_reftime = minimal_rt::reftime::reference_time();
298
299    let SidecarOutput { nodes, error: _ } = sidecar_output;
300    let config = SidecarConfig {
301        num_cpus: partition_info.cpus.len(),
302        start_reftime: boot_start_reftime,
303        end_reftime: boot_end_reftime,
304        node_params: &sidecar_params.nodes[..node_count],
305        nodes: &nodes[..node_count],
306        per_cpu_state: &sidecar_params.initial_state,
307    };
308    log::info!(
309        "sidecar: boot_cpus parameter: {}",
310        config.kernel_command_line()
311    );
312    Some(config)
313}