vmm/cpu_config/x86_64/cpuid/
normalize.rs

1// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::cpu_config::x86_64::cpuid::{
5    CpuidEntry, CpuidKey, CpuidRegisters, CpuidTrait, KvmCpuidFlags, cpuid,
6};
7use crate::logger::warn;
8use crate::vmm_config::machine_config::MAX_SUPPORTED_VCPUS;
9
10/// Error type for [`super::Cpuid::normalize`].
11#[allow(clippy::module_name_repetitions)]
12#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
13pub enum NormalizeCpuidError {
14    /// Provided `cpu_bits` is >=8: {0}.
15    CpuBits(u8),
16    /// Failed to apply modifications to Intel CPUID: {0}
17    Intel(#[from] crate::cpu_config::x86_64::cpuid::intel::NormalizeCpuidError),
18    /// Failed to apply modifications to AMD CPUID: {0}
19    Amd(#[from] crate::cpu_config::x86_64::cpuid::amd::NormalizeCpuidError),
20    /// Failed to set feature information leaf: {0}
21    FeatureInformation(#[from] FeatureInformationError),
22    /// Failed to set extended topology leaf: {0}
23    ExtendedTopology(#[from] ExtendedTopologyError),
24    /// Failed to set extended cache features leaf: {0}
25    ExtendedCacheFeatures(#[from] ExtendedCacheFeaturesError),
26    /// Failed to set vendor ID in leaf 0x0: {0}
27    VendorId(#[from] VendorIdError),
28}
29
30/// Error type for setting leaf 0 section.
31#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
32pub enum VendorIdError {
33    /// Leaf 0x0 is missing from CPUID.
34    MissingLeaf0,
35}
36
37/// Error type for setting leaf 1 section of `IntelCpuid::normalize`.
38#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
39pub enum FeatureInformationError {
40    /// Leaf 0x1 is missing from CPUID.
41    MissingLeaf1,
42    /// Failed to set `Initial APIC ID`: {0}
43    InitialApicId(CheckedAssignError),
44    /// Failed to set `CLFLUSH line size`: {0}
45    Clflush(CheckedAssignError),
46    /// Failed to get max CPUs per package: {0}
47    GetMaxCpusPerPackage(GetMaxCpusPerPackageError),
48    /// Failed to set max CPUs per package: {0}
49    SetMaxCpusPerPackage(CheckedAssignError),
50}
51
52/// Error type for `get_max_cpus_per_package`.
53#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
54pub enum GetMaxCpusPerPackageError {
55    /// Failed to get max CPUs per package as `cpu_count == 0`
56    Underflow,
57    /// Failed to get max CPUs per package as `cpu_count > 128`
58    Overflow,
59}
60
61/// Error type for setting leaf b section of `IntelCpuid::normalize`.
62#[rustfmt::skip]
63#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
64pub enum ExtendedTopologyError {
65    /// Failed to set domain type (CPUID.(EAX=0xB,ECX={0}):ECX[15:8]): {1}
66    DomainType(u32, CheckedAssignError),
67    /// Failed to set input ECX (CPUID.(EAX=0xB,ECX={0}):ECX[7:0]): {1}
68    InputEcx(u32, CheckedAssignError),
69    /// Failed to set number of logical processors (CPUID.(EAX=0xB,ECX={0}):EBX[15:0]): {1}
70    NumLogicalProcs(u32, CheckedAssignError),
71    /// Failed to set right-shift bits (CPUID.(EAX=0xB,ECX={0}):EAX[4:0]): {1}
72    RightShiftBits(u32, CheckedAssignError),
73}
74
75/// Error type for setting leaf 0x80000006 of Cpuid::normalize().
76#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
77pub enum ExtendedCacheFeaturesError {
78    /// Leaf 0x80000005 is missing from CPUID.
79    MissingLeaf0x80000005,
80    /// Leaf 0x80000006 is missing from CPUID.
81    MissingLeaf0x80000006,
82}
83
84/// Error type for setting a bit range.
85#[derive(Debug, PartialEq, Eq, thiserror::Error)]
86#[error("Given value is greater than maximum storable value in bit range.")]
87pub struct CheckedAssignError;
88
89/// Sets a given bit to a true or false (1 or 0).
90#[allow(clippy::arithmetic_side_effects)]
91pub fn set_bit(x: &mut u32, bit: u8, y: bool) {
92    debug_assert!(bit < 32);
93    *x = (*x & !(1 << bit)) | ((u32::from(u8::from(y))) << bit);
94}
95
96/// Sets a given range to a given value.
97pub fn set_range(
98    x: &mut u32,
99    range: std::ops::RangeInclusive<u8>,
100    y: u32,
101) -> Result<(), CheckedAssignError> {
102    let start = *range.start();
103    let end = *range.end();
104
105    debug_assert!(end >= start);
106    debug_assert!(end < 32);
107
108    // Ensure `y` fits within the number of bits in the specified range.
109    // Note that
110    // - 1 <= `num_bits` <= 32 from the above assertion
111    // - if `num_bits` equals to 32, `y` always fits within it since `y` is `u32`.
112    let num_bits = end - start + 1;
113    if num_bits < 32 && y >= (1u32 << num_bits) {
114        return Err(CheckedAssignError);
115    }
116
117    let mask = get_mask(range);
118    *x = (*x & !mask) | (y << start);
119
120    Ok(())
121}
122
123/// Gets a given range within a given value.
124pub fn get_range(x: u32, range: std::ops::RangeInclusive<u8>) -> u32 {
125    let start = *range.start();
126    let end = *range.end();
127
128    debug_assert!(end >= start);
129    debug_assert!(end < 32);
130
131    let mask = get_mask(range);
132    (x & mask) >> start
133}
134
135/// Returns a mask where the given range is ones.
136const fn get_mask(range: std::ops::RangeInclusive<u8>) -> u32 {
137    let num_bits = *range.end() - *range.start() + 1;
138    let shift = *range.start();
139
140    if num_bits == 32 {
141        u32::MAX
142    } else {
143        ((1u32 << num_bits) - 1) << shift
144    }
145}
146
147// We use this 2nd implementation so we can conveniently define functions only used within
148// `normalize`.
149#[allow(clippy::multiple_inherent_impl)]
150impl super::Cpuid {
151    /// Applies required modifications to CPUID respective of a vCPU.
152    ///
153    /// # Errors
154    ///
155    /// When:
156    /// - [`super::IntelCpuid::normalize`] errors.
157    /// - [`super::AmdCpuid::normalize`] errors.
158    // As we pass through host frequency, we require CPUID and thus `cfg(cpuid)`.
159    #[inline]
160    pub fn normalize(
161        &mut self,
162        // The index of the current logical CPU in the range [0..cpu_count].
163        cpu_index: u8,
164        // The total number of logical CPUs.
165        cpu_count: u8,
166        // The number of bits needed to enumerate logical CPUs per core.
167        cpu_bits: u8,
168    ) -> Result<(), NormalizeCpuidError> {
169        let cpus_per_core = 1u8
170            .checked_shl(u32::from(cpu_bits))
171            .ok_or(NormalizeCpuidError::CpuBits(cpu_bits))?;
172        self.update_vendor_id()?;
173        self.update_feature_info_entry(cpu_index, cpu_count)?;
174        self.update_extended_topology_entry(cpu_index, cpu_count, cpu_bits, cpus_per_core)?;
175        self.update_extended_cache_features()?;
176
177        // Apply manufacturer specific modifications.
178        match self {
179            // Apply Intel specific modifications.
180            Self::Intel(intel_cpuid) => {
181                intel_cpuid.normalize(cpu_index, cpu_count, cpus_per_core)?;
182            }
183            // Apply AMD specific modifications.
184            Self::Amd(amd_cpuid) => amd_cpuid.normalize(cpu_index, cpu_count, cpus_per_core)?,
185        }
186
187        Ok(())
188    }
189
190    /// Pass-through the vendor ID from the host. This is used to prevent modification of the vendor
191    /// ID via custom CPU templates.
192    fn update_vendor_id(&mut self) -> Result<(), VendorIdError> {
193        let leaf_0 = self
194            .get_mut(&CpuidKey::leaf(0x0))
195            .ok_or(VendorIdError::MissingLeaf0)?;
196
197        let host_leaf_0 = cpuid(0x0);
198
199        leaf_0.result.ebx = host_leaf_0.ebx;
200        leaf_0.result.ecx = host_leaf_0.ecx;
201        leaf_0.result.edx = host_leaf_0.edx;
202
203        Ok(())
204    }
205
206    // Update feature information entry
207    fn update_feature_info_entry(
208        &mut self,
209        cpu_index: u8,
210        cpu_count: u8,
211    ) -> Result<(), FeatureInformationError> {
212        let leaf_1 = self
213            .get_mut(&CpuidKey::leaf(0x1))
214            .ok_or(FeatureInformationError::MissingLeaf1)?;
215
216        // CPUID.01H:EBX[15:08]
217        // CLFLUSH line size (Value * 8 = cache line size in bytes; used also by CLFLUSHOPT).
218        set_range(&mut leaf_1.result.ebx, 8..=15, 8).map_err(FeatureInformationError::Clflush)?;
219
220        // CPUID.01H:EBX[23:16]
221        // Maximum number of addressable IDs for logical processors in this physical package.
222        //
223        // The nearest power-of-2 integer that is not smaller than EBX[23:16] is the number of
224        // unique initial APIC IDs reserved for addressing different logical processors in a
225        // physical package. This field is only valid if CPUID.1.EDX.HTT[bit 28]= 1.
226        let max_cpus_per_package = u32::from(
227            get_max_cpus_per_package(cpu_count)
228                .map_err(FeatureInformationError::GetMaxCpusPerPackage)?,
229        );
230        set_range(&mut leaf_1.result.ebx, 16..=23, max_cpus_per_package)
231            .map_err(FeatureInformationError::SetMaxCpusPerPackage)?;
232
233        // CPUID.01H:EBX[31:24]
234        // Initial APIC ID.
235        //
236        // The 8-bit initial APIC ID in EBX[31:24] is replaced by the 32-bit x2APIC ID, available
237        // in Leaf 0BH and Leaf 1FH.
238        set_range(&mut leaf_1.result.ebx, 24..=31, u32::from(cpu_index))
239            .map_err(FeatureInformationError::InitialApicId)?;
240
241        // CPUID.01H:ECX[15] (Mnemonic: PDCM)
242        // Performance and Debug Capability: A value of 1 indicates the processor supports the
243        // performance and debug feature indication MSR IA32_PERF_CAPABILITIES.
244        set_bit(&mut leaf_1.result.ecx, 15, false);
245
246        // CPUID.01H:ECX[24] (Mnemonic: TSC-Deadline)
247        // A value of 1 indicates that the processor’s local APIC timer supports one-shot operation
248        // using a TSC deadline value.
249        set_bit(&mut leaf_1.result.ecx, 24, true);
250
251        // CPUID.01H:ECX[31] (Mnemonic: Hypervisor)
252        set_bit(&mut leaf_1.result.ecx, 31, true);
253
254        // CPUID.01H:EDX[28] (Mnemonic: HTT)
255        // Max APIC IDs reserved field is Valid. A value of 0 for HTT indicates there is only a
256        // single logical processor in the package and software should assume only a single APIC ID
257        // is reserved. A value of 1 for HTT indicates the value in CPUID.1.EBX[23:16] (the Maximum
258        // number of addressable IDs for logical processors in this package) is valid for the
259        // package.
260        set_bit(&mut leaf_1.result.edx, 28, cpu_count > 1);
261
262        Ok(())
263    }
264
265    /// Update extended topology entry
266    fn update_extended_topology_entry(
267        &mut self,
268        cpu_index: u8,
269        cpu_count: u8,
270        cpu_bits: u8,
271        cpus_per_core: u8,
272    ) -> Result<(), ExtendedTopologyError> {
273        // The following commit changed the behavior of KVM_GET_SUPPORTED_CPUID to no longer
274        // include CPUID.(EAX=0BH,ECX=1).
275        // https://lore.kernel.org/all/20221027092036.2698180-1-pbonzini@redhat.com/
276        self.inner_mut()
277            .entry(CpuidKey::subleaf(0xB, 0x1))
278            .or_insert(CpuidEntry {
279                flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
280                result: CpuidRegisters {
281                    eax: 0x0,
282                    ebx: 0x0,
283                    ecx: 0x0,
284                    edx: 0x0,
285                },
286            });
287
288        for index in 0.. {
289            if let Some(subleaf) = self.get_mut(&CpuidKey::subleaf(0xB, index)) {
290                // Reset eax, ebx, ecx
291                subleaf.result.eax = 0;
292                subleaf.result.ebx = 0;
293                subleaf.result.ecx = 0;
294                // CPUID.(EAX=0BH,ECX=N).EDX[31:0]
295                // x2APIC ID of the current logical processor.
296                subleaf.result.edx = u32::from(cpu_index);
297                subleaf.flags = KvmCpuidFlags::SIGNIFICANT_INDEX;
298
299                match index {
300                    // CPUID.(EAX=0BH,ECX=N):EAX[4:0]
301                    // The number of bits that the x2APIC ID must be shifted to the right to address
302                    // instances of the next higher-scoped domain. When logical processor is not
303                    // supported by the processor, the value of this field at the Logical Processor
304                    // domain sub-leaf may be returned as either 0 (no allocated bits in the x2APIC
305                    // ID) or 1 (one allocated bit in the x2APIC ID); software should plan
306                    // accordingly.
307
308                    // CPUID.(EAX=0BH,ECX=N):EBX[15:0]
309                    // The number of logical processors across all instances of this domain within
310                    // the next-higher scoped domain. (For example, in a processor socket/package
311                    // comprising "M" dies of "N" cores each, where each core has "L" logical
312                    // processors, the "die" domain sub-leaf value of this field would be M*N*L.)
313                    // This number reflects configuration as shipped by Intel. Note, software must
314                    // not use this field to enumerate processor topology.
315
316                    // CPUID.(EAX=0BH,ECX=N):ECX[7:0]
317                    // The input ECX sub-leaf index.
318
319                    // CPUID.(EAX=0BH,ECX=N):ECX[15:8]
320                    // Domain Type. This field provides an identification value which indicates the
321                    // domain as shown below. Although domains are ordered, their assigned
322                    // identification values are not and software should not depend on it.
323                    //
324                    // Hierarchy    Domain              Domain Type Identification Value
325                    // -----------------------------------------------------------------
326                    // Lowest       Logical Processor   1
327                    // Highest      Core                2
328                    //
329                    // (Note that enumeration values of 0 and 3-255 are reserved.)
330
331                    // Logical processor domain
332                    0 => {
333                        // To get the next level APIC ID, shift right with at most 1 because we have
334                        // maximum 2 logical procerssors per core that can be represented by 1 bit.
335                        set_range(&mut subleaf.result.eax, 0..=4, u32::from(cpu_bits))
336                            .map_err(|err| ExtendedTopologyError::RightShiftBits(index, err))?;
337
338                        // When cpu_count == 1 or HT is disabled, there is 1 logical core at this
339                        // domain; otherwise there are 2
340                        set_range(&mut subleaf.result.ebx, 0..=15, u32::from(cpus_per_core))
341                            .map_err(|err| ExtendedTopologyError::NumLogicalProcs(index, err))?;
342
343                        // Skip setting 0 to ECX[7:0] since it's already reset to 0.
344
345                        // Set the domain type identification value for logical processor,
346                        set_range(&mut subleaf.result.ecx, 8..=15, 1)
347                            .map_err(|err| ExtendedTopologyError::DomainType(index, err))?;
348                    }
349                    // Core domain
350                    1 => {
351                        // Configure such that the next higher-scoped domain (i.e. socket) include
352                        // all logical processors.
353                        //
354                        // The CPUID.(EAX=0BH,ECX=1).EAX[4:0] value must be an integer N such that
355                        // 2^N is greater than or equal to the maximum number of vCPUs.
356                        set_range(
357                            &mut subleaf.result.eax,
358                            0..=4,
359                            MAX_SUPPORTED_VCPUS.next_power_of_two().ilog2(),
360                        )
361                        .map_err(|err| ExtendedTopologyError::RightShiftBits(index, err))?;
362                        set_range(&mut subleaf.result.ebx, 0..=15, u32::from(cpu_count))
363                            .map_err(|err| ExtendedTopologyError::NumLogicalProcs(index, err))?;
364
365                        // Setting the input ECX value (i.e. `index`)
366                        set_range(&mut subleaf.result.ecx, 0..=7, index)
367                            .map_err(|err| ExtendedTopologyError::InputEcx(index, err))?;
368
369                        // Set the domain type identification value for core.
370                        set_range(&mut subleaf.result.ecx, 8..=15, 2)
371                            .map_err(|err| ExtendedTopologyError::DomainType(index, err))?;
372                    }
373                    _ => {
374                        // KVM no longer returns any subleaves greater than 0. The patch was merged
375                        // in v6.2 and backported to v5.10. So for all our supported kernels,
376                        // subleaves >= 2 should not be included.
377                        // https://github.com/torvalds/linux/commit/45e966fcca03ecdcccac7cb236e16eea38cc18af
378                        //
379                        // However, we intentionally leave Firecracker not fail for unsupported
380                        // kernels to keep working. Note that we can detect KVM regression thanks
381                        // to the test that compares a fingerprint with its baseline.
382                        warn!("Subleaf {index} not expected for CPUID leaf 0xB.");
383                        subleaf.result.ecx = index;
384                    }
385                }
386            } else {
387                break;
388            }
389        }
390
391        Ok(())
392    }
393
394    // Update extended cache features entry
395    fn update_extended_cache_features(&mut self) -> Result<(), ExtendedCacheFeaturesError> {
396        // Leaf 0x800000005 indicates L1 Cache and TLB Information.
397        let guest_leaf_0x80000005 = self
398            .get_mut(&CpuidKey::leaf(0x80000005))
399            .ok_or(ExtendedCacheFeaturesError::MissingLeaf0x80000005)?;
400        guest_leaf_0x80000005.result = cpuid(0x80000005).into();
401
402        // Leaf 0x80000006 indicates L2 Cache and TLB and L3 Cache Information.
403        let guest_leaf_0x80000006 = self
404            .get_mut(&CpuidKey::leaf(0x80000006))
405            .ok_or(ExtendedCacheFeaturesError::MissingLeaf0x80000006)?;
406        guest_leaf_0x80000006.result = cpuid(0x80000006).into();
407        guest_leaf_0x80000006.result.edx &= !0x00030000; // bits [17:16] are reserved
408        Ok(())
409    }
410}
411
412/// The maximum number of logical processors per package is computed as the closest
413/// power of 2 higher or equal to the CPU count configured by the user.
414const fn get_max_cpus_per_package(cpu_count: u8) -> Result<u8, GetMaxCpusPerPackageError> {
415    // This match is better than but approximately equivalent to
416    // `2.pow((cpu_count as f32).log2().ceil() as u8)` (`2^ceil(log_2(c))`).
417    match cpu_count {
418        0 => Err(GetMaxCpusPerPackageError::Underflow),
419        // `0u8.checked_next_power_of_two()` returns `Some(1)`, this is not the desired behaviour so
420        // we use `next_power_of_two()` instead.
421        1..=128 => Ok(cpu_count.next_power_of_two()),
422        129..=u8::MAX => Err(GetMaxCpusPerPackageError::Overflow),
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use std::collections::BTreeMap;
429
430    use super::*;
431    use crate::cpu_config::x86_64::cpuid::{AmdCpuid, Cpuid, IntelCpuid};
432
433    #[test]
434    fn get_max_cpus_per_package_test() {
435        assert_eq!(
436            get_max_cpus_per_package(0),
437            Err(GetMaxCpusPerPackageError::Underflow)
438        );
439        assert_eq!(get_max_cpus_per_package(1), Ok(1));
440        assert_eq!(get_max_cpus_per_package(2), Ok(2));
441        assert_eq!(get_max_cpus_per_package(3), Ok(4));
442        assert_eq!(get_max_cpus_per_package(4), Ok(4));
443        assert_eq!(get_max_cpus_per_package(5), Ok(8));
444        assert_eq!(get_max_cpus_per_package(8), Ok(8));
445        assert_eq!(get_max_cpus_per_package(9), Ok(16));
446        assert_eq!(get_max_cpus_per_package(16), Ok(16));
447        assert_eq!(get_max_cpus_per_package(17), Ok(32));
448        assert_eq!(get_max_cpus_per_package(32), Ok(32));
449        assert_eq!(get_max_cpus_per_package(33), Ok(64));
450        assert_eq!(get_max_cpus_per_package(64), Ok(64));
451        assert_eq!(get_max_cpus_per_package(65), Ok(128));
452        assert_eq!(get_max_cpus_per_package(128), Ok(128));
453        assert_eq!(
454            get_max_cpus_per_package(129),
455            Err(GetMaxCpusPerPackageError::Overflow)
456        );
457        assert_eq!(
458            get_max_cpus_per_package(u8::MAX),
459            Err(GetMaxCpusPerPackageError::Overflow)
460        );
461    }
462
463    #[test]
464    fn test_update_vendor_id() {
465        // Check `update_vendor_id()` passes through the vendor ID from the host correctly.
466
467        // Pseudo CPUID with invalid vendor ID.
468        let mut guest_cpuid = Cpuid::Intel(IntelCpuid(BTreeMap::from([(
469            CpuidKey {
470                leaf: 0x0,
471                subleaf: 0x0,
472            },
473            CpuidEntry {
474                flags: KvmCpuidFlags::EMPTY,
475                result: CpuidRegisters {
476                    eax: 0,
477                    ebx: 0x0123_4567,
478                    ecx: 0x89ab_cdef,
479                    edx: 0x55aa_55aa,
480                },
481            },
482        )])));
483
484        // Pass through vendor ID from host.
485        guest_cpuid.update_vendor_id().unwrap();
486
487        // Check if the guest vendor ID matches the host one.
488        let guest_leaf_0 = guest_cpuid
489            .get(&CpuidKey {
490                leaf: 0x0,
491                subleaf: 0x0,
492            })
493            .unwrap();
494        let host_leaf_0 = cpuid(0x0);
495        assert_eq!(guest_leaf_0.result.ebx, host_leaf_0.ebx);
496        assert_eq!(guest_leaf_0.result.ecx, host_leaf_0.ecx);
497        assert_eq!(guest_leaf_0.result.edx, host_leaf_0.edx);
498    }
499
500    #[test]
501    fn check_leaf_0xb_subleaf_0x1_added() {
502        // Check leaf 0xb / subleaf 0x1 is added in `update_extended_topology_entry()` even when it
503        // isn't included.
504
505        // Pseudo CPU setting
506        let smt = false;
507        let cpu_index = 0;
508        let cpu_count = 2;
509        let cpu_bits = u8::from(cpu_count > 1 && smt);
510        let cpus_per_core = 1u8
511            .checked_shl(u32::from(cpu_bits))
512            .ok_or(NormalizeCpuidError::CpuBits(cpu_bits))
513            .unwrap();
514
515        // Case 1: Intel CPUID
516        let mut intel_cpuid = Cpuid::Intel(IntelCpuid(BTreeMap::from([(
517            CpuidKey {
518                leaf: 0xb,
519                subleaf: 0,
520            },
521            CpuidEntry {
522                flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
523                result: CpuidRegisters {
524                    eax: 0,
525                    ebx: 0,
526                    ecx: 0,
527                    edx: 0,
528                },
529            },
530        )])));
531        let result = intel_cpuid.update_extended_topology_entry(
532            cpu_index,
533            cpu_count,
534            cpu_bits,
535            cpus_per_core,
536        );
537        result.unwrap();
538        assert!(intel_cpuid.inner().contains_key(&CpuidKey {
539            leaf: 0xb,
540            subleaf: 0x1
541        }));
542
543        // Case 2: AMD CPUID
544        let mut amd_cpuid = Cpuid::Amd(AmdCpuid(BTreeMap::from([(
545            CpuidKey {
546                leaf: 0xb,
547                subleaf: 0,
548            },
549            CpuidEntry {
550                flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
551                result: CpuidRegisters {
552                    eax: 0,
553                    ebx: 0,
554                    ecx: 0,
555                    edx: 0,
556                },
557            },
558        )])));
559        let result =
560            amd_cpuid.update_extended_topology_entry(cpu_index, cpu_count, cpu_bits, cpus_per_core);
561        result.unwrap();
562        assert!(amd_cpuid.inner().contains_key(&CpuidKey {
563            leaf: 0xb,
564            subleaf: 0x1
565        }));
566    }
567}