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::DisabledServicing => {
117            log::info!("sidecar: disabled because this is a servicing restore");
118            return None;
119        }
120        SidecarOptions::Enabled { enable_logging, .. } => {
121            sidecar_params.enable_logging = enable_logging;
122        }
123    }
124
125    // Ensure the host didn't provide an out-of-bounds NUMA node.
126    let max_vnode = partition_info
127        .cpus
128        .iter()
129        .map(|cpu| cpu.vnode)
130        .chain(partition_info.vtl2_ram.iter().map(|e| e.vnode))
131        .max()
132        .unwrap();
133
134    if max_vnode >= MAX_NUMA_NODES as u32 {
135        log::warn!("sidecar: NUMA node {max_vnode} too large");
136        return None;
137    }
138
139    #[cfg(target_arch = "x86_64")]
140    if !x86defs::cpuid::VersionAndFeaturesEcx::from(
141        safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VersionAndFeatures.0, 0).ecx,
142    )
143    .x2_apic()
144    {
145        // Currently, sidecar needs x2apic to communicate with the kernel
146        log::warn!("sidecar: x2apic not available; not using sidecar");
147        return None;
148    }
149
150    // Split the CPUs by NUMA node, and then into chunks of no more than
151    // MAX_SIDECAR_NODE_SIZE processors.
152    let cpus_by_node = || {
153        partition_info
154            .cpus
155            .chunk_by(|a, b| a.vnode == b.vnode)
156            .flat_map(|cpus| {
157                let chunks = cpus.len().div_ceil(MAX_SIDECAR_NODE_SIZE);
158                cpus.chunks(cpus.len().div_ceil(chunks))
159            })
160    };
161    if cpus_by_node().all(|cpus_by_node| cpus_by_node.len() == 1) {
162        log::info!("sidecar: all NUMA nodes have one CPU");
163        return None;
164    }
165    let mut total_ram;
166    {
167        let SidecarParams {
168            hypercall_page,
169            enable_logging: _,
170            node_count,
171            nodes,
172            initial_state,
173        } = sidecar_params;
174
175        *hypercall_page = 0;
176        #[cfg(target_arch = "x86_64")]
177        {
178            *hypercall_page = crate::hypercall::hvcall().hypercall_page();
179        }
180
181        let mut base_vp = 0;
182        total_ram = 0;
183        *node_count = 0;
184        *initial_state = partition_info.sidecar_cpu_overrides.clone();
185        for cpus in cpus_by_node() {
186            // Skip creating a sidecar node when none of its application
187            // processors are left for sidecar to start (all are kernel-started).
188            if sidecar_node_is_empty(
189                &partition_info.sidecar_cpu_overrides,
190                base_vp as usize,
191                cpus.len(),
192            ) {
193                if initial_state.per_cpu_state_specified {
194                    // Kernel-start the base VP too; its APs are already
195                    // excluded, so every VP in the node ends up in `boot_cpus=`.
196                    initial_state.sidecar_starts_cpu[base_vp as usize] = false;
197                }
198                log::info!(
199                    "sidecar: node at base VP {base_vp} ({} VPs) has no sidecar-started APs; kernel-starting all of them",
200                    cpus.len(),
201                );
202                base_vp += cpus.len() as u32;
203                continue;
204            }
205
206            let required_ram = sidecar_defs::required_memory(cpus.len() as u32) as u64;
207            // Take some VTL2 RAM for sidecar use. Try to use the same NUMA node
208            // as the first CPU.
209            let local_vnode = cpus[0].vnode as usize;
210
211            let mem = match address_space.allocate(
212                Some(local_vnode as u32),
213                required_ram,
214                AllocationType::SidecarNode,
215                AllocationPolicy::LowMemory,
216            ) {
217                Some(mem) => mem,
218                None => {
219                    // Fallback to no numa requirement.
220                    match address_space.allocate(
221                        None,
222                        required_ram,
223                        AllocationType::SidecarNode,
224                        AllocationPolicy::LowMemory,
225                    ) {
226                        Some(mem) => {
227                            log::warn!(
228                                "sidecar: unable to allocate memory for sidecar node on node {local_vnode}, falling back to node {}",
229                                mem.vnode
230                            );
231                            mem
232                        }
233                        None => {
234                            log::warn!("sidecar: not enough memory for sidecar");
235                            return None;
236                        }
237                    }
238                }
239            };
240
241            nodes[*node_count as usize] = SidecarNodeParams {
242                memory_base: mem.range.start(),
243                memory_size: mem.range.len(),
244                base_vp,
245                vp_count: cpus.len() as u32,
246            };
247            if initial_state.per_cpu_state_specified {
248                // If per-CPU state is specified, make sure to explicitly state that
249                // sidecar should not start the base vp of this node.
250                // The code that set per_cpu_state_specified should have already ensured that
251                // the array is large enough for any `base_vp` we might have here.
252                initial_state.sidecar_starts_cpu[base_vp as usize] = false;
253                log::info!(
254                    "sidecar: per_cpu_state_specified=true, marking base_vp={} as kernel-started",
255                    base_vp
256                );
257            }
258            base_vp += cpus.len() as u32;
259            *node_count += 1;
260            total_ram += required_ram;
261        }
262    }
263
264    // If per-CPU overrides left every node empty, there is nothing for
265    // sidecar to do; behave as if it were disabled entirely.
266    let node_count = sidecar_params.node_count as usize;
267    if node_count == 0 {
268        log::info!("sidecar: no nodes have sidecar-started APs; disabling sidecar");
269        return None;
270    }
271
272    // SAFETY: the parameter blob is trusted.
273    let sidecar_entry: extern "C" fn(&SidecarParams, &mut SidecarOutput) -> bool =
274        unsafe { core::mem::transmute(p.sidecar_entry_address) };
275
276    let boot_start_reftime = minimal_rt::reftime::reference_time();
277    log::info!(
278        "sidecar starting, {} nodes, {} cpus, {:#x} total bytes",
279        node_count,
280        partition_info.cpus.len(),
281        total_ram
282    );
283    if !sidecar_entry(sidecar_params, sidecar_output) {
284        panic!(
285            "failed to start sidecar: {}",
286            core::str::from_utf8(&sidecar_output.error.buf[..sidecar_output.error.len as usize])
287                .unwrap()
288        );
289    }
290    let boot_end_reftime = minimal_rt::reftime::reference_time();
291
292    let SidecarOutput { nodes, error: _ } = sidecar_output;
293    Some(SidecarConfig {
294        num_cpus: partition_info.cpus.len(),
295        start_reftime: boot_start_reftime,
296        end_reftime: boot_end_reftime,
297        node_params: &sidecar_params.nodes[..node_count],
298        nodes: &nodes[..node_count],
299        per_cpu_state: &sidecar_params.initial_state,
300    })
301}