Skip to main content

windows_topology_sys/
cpu_set.rs

1// Copyright (c) 2026 Mike Grier
2//! The safe `GetSystemCpuSetInformation` walk.
3//!
4//! A second, independent view of the same processors [`crate::walk`] describes,
5//! read from a different kernel path. Windows exposes two processor-topology
6//! APIs and they are not the same API twice: this one reports **availability**
7//! (parked, and whether the processor is allocated to this process at all),
8//! Windows's **own** last-level-cache grouping, and a scheduling class and
9//! allocation tag that the relationship walk has no equivalent for.
10//!
11//! The buffer discipline matches `GetLogicalProcessorInformationEx`'s, and for
12//! the same reasons:
13//!
14//! - Size first with a null buffer, which fails with `ERROR_INSUFFICIENT_BUFFER`
15//!   and reports the byte count. **The pair is attempted more than once**: the
16//!   machine can grow between the sizing call and the fetch, and a hot-add there
17//!   makes the fetch fail the same way. See
18//!   [`SIZING_ATTEMPTS`](crate::records::SIZING_ATTEMPTS).
19//! - Records are **variable length**: advance by each record's own `Size` field,
20//!   never by `size_of::<SYSTEM_CPU_SET_INFORMATION>()`. The struct's declared
21//!   size describes today's `CpuSetInformation` record, and a future type may be
22//!   longer.
23//! - Read every field with `read_unaligned`, since a record's start is only as
24//!   aligned as the running `Size` sum makes it.
25//!
26//! # What this deliberately does not do
27//!
28//! It does not reconcile anything. `CoreIndex`, `NumaNodeIndex` and
29//! `EfficiencyClass` duplicate facts the relationship walk already reports, and
30//! the two paths can disagree -- under a hypervisor, or where one is stale.
31//! Merging them here would silently pick a winner and destroy the disagreement,
32//! which is the one thing a second observer is *for*. The records come back as
33//! what they are; deciding what to do when they differ is tracked separately.
34//!
35//! ## How the records are walked
36//!
37//! Through [`crate::records`], which both of this crate's enumerations share.
38//! Per [D-24](../DESIGN-NOTES.md#d-24) the operating system is **trusted** for
39//! the structural validity of a buffer it just wrote -- this is not a trust
40//! boundary and there is no validation pass. The walk bounds its reads because
41//! bounds are how it knows where a record ends, which is a decoding
42//! requirement rather than a defence.
43//!
44//! Two consequences a reader should not have to infer: nothing here **panics**
45//! over the shape of the buffer, because a malformed record is not evidence
46//! that this crate reached an inconsistent state; and a record that cannot be
47//! decoded is **recorded** in
48//! [`MachineMemoryTopology::enumeration_anomalies`](crate::MachineMemoryTopology::enumeration_anomalies)
49//! rather than silently dropped, so a short list is distinguishable from a
50//! small machine.
51
52use crate::EnumerationAnomaly;
53use crate::observation::Source;
54use crate::records::RecordWalk;
55use std::io;
56
57use crate::records::SIZING_ATTEMPTS;
58use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
59use windows_sys::Win32::System::SystemInformation::{
60    CpuSetInformation, GetSystemCpuSetInformation, SYSTEM_CPU_SET_INFORMATION,
61};
62use windows_sys::Win32::System::Threading::GetCurrentProcess;
63
64/// One processor as the CPU-set API describes it.
65///
66/// Field-for-field what the record carries, with the two bitfield unions
67/// decoded. Nothing is interpreted and nothing is cross-checked against the
68/// relationship walk -- see this module's note on why reconciling here would
69/// destroy the only thing a second observer is for.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct CpuSet {
73    /// The CPU set's id, which is what `SetThreadSelectedCpuSets` takes. Not a
74    /// processor number, and not interchangeable with one.
75    pub id: u32,
76    /// The processor group.
77    pub group: u16,
78    /// The processor's number within its group.
79    pub logical_processor_index: u8,
80    /// Windows's index for the owning core.
81    pub core_index: u8,
82    /// **Windows's own** last-level-cache grouping: processors sharing a value
83    /// share an LLC in the scheduler's view. Deliberately kept even though the
84    /// relationship walk also reports caches, because this is the OS's opinion
85    /// rather than a partition derived from firmware records, and the two are
86    /// answers to different questions.
87    pub last_level_cache_index: u8,
88    /// Windows's index for the owning NUMA node.
89    pub numa_node_index: u8,
90    /// The scheduler's efficiency class. Carried as reported, with no sentinel:
91    /// a processor absent from this enumeration has no record at all rather
92    /// than a record holding a stand-in value. Contrast
93    /// [`Processor::capacity`](crate::Processor::capacity), which uses `0` for
94    /// both "class zero" and "not known".
95    pub efficiency_class: u8,
96    /// The processor is parked, so the scheduler is currently avoiding it.
97    ///
98    /// **Measured to carry no information on Windows 11 25H2** -- see
99    /// [`Self::allocated_to_target_process`] and D-23 in `DESIGN-NOTES.md`.
100    pub parked: bool,
101    /// The processor is allocated to some process through the CPU-set API.
102    ///
103    /// **Measured to carry no information on Windows 11 25H2** -- see
104    /// [`Self::allocated_to_target_process`].
105    pub allocated: bool,
106    /// The processor is allocated **to this process** through the CPU-set API.
107    ///
108    /// # This is not "may we run here", and it is not populated
109    ///
110    /// Two corrections, both established by experiment rather than reasoning
111    /// (D-23 in `DESIGN-NOTES.md`).
112    ///
113    /// It does not mean the process may use the processor. It means the CPU set
114    /// was explicitly allocated through `SetProcessDefaultCpuSets` or
115    /// `SetThreadSelectedCpuSets`, which a process that never called them has
116    /// not done -- so `false` is the ordinary answer for an ordinary process on
117    /// every processor it is perfectly free to run on.
118    ///
119    /// And on Windows 11 25H2 (10.0.26200.9168, AMD64) it is not populated at
120    /// all. Calling `SetProcessDefaultCpuSets` successfully, and confirming
121    /// with `GetProcessDefaultCpuSets` that the allocation stuck, still leaves
122    /// the whole `AllFlags` byte reading `0x00` for every processor -- under a
123    /// null handle, the current-process pseudo-handle, and a real `OpenProcess`
124    /// handle alike.
125    ///
126    /// **So do not branch on this.** A reader of `false` is reading a byte the
127    /// kernel did not write, not a fact about the machine.
128    pub allocated_to_target_process: bool,
129    /// The processor is marked real-time.
130    ///
131    /// **Measured to carry no information on Windows 11 25H2** -- see
132    /// [`Self::allocated_to_target_process`].
133    pub real_time: bool,
134    /// The scheduling class, which shares its union with a reserved `u32`, so
135    /// confirm its meaning against current SDK documentation before relying on
136    /// it rather than treating this field as self-describing.
137    pub scheduling_class: u8,
138    /// The allocation tag.
139    pub allocation_tag: u64,
140}
141
142/// Bit positions within `AllFlags`, named rather than written inline.
143///
144/// Changing any value is a breaking change: these mirror the SDK's bitfield
145/// order, which is part of the ABI rather than this crate's choice.
146///
147/// # These are unverified, and cannot be verified from this machine
148///
149/// `AllFlags` reads `0x00` for every processor on the build measured (D-23),
150/// even after allocating CPU sets to this process -- so no bit has ever been
151/// observed set, and nothing here distinguishes a correct transcription from a
152/// wrong one. They stand on the SDK's declared order alone.
153///
154/// That is survivable only because nothing branches on them. If a consumer ever
155/// does, these need a machine that populates the byte first.
156mod flags {
157    pub(super) const PARKED: u8 = 1 << 0;
158    pub(super) const ALLOCATED: u8 = 1 << 1;
159    pub(super) const ALLOCATED_TO_TARGET_PROCESS: u8 = 1 << 2;
160    pub(super) const REAL_TIME: u8 = 1 << 3;
161}
162
163/// Enumerate the CPU sets the current process can see.
164///
165/// The current process is named explicitly, because `Process` is **not**
166/// optional in the way a null handle suggests: Microsoft documents it as the
167/// process used to compute `AllocatedToTargetProcess`, so passing null means no
168/// allocation check is made rather than "ask about me". Raised in PR #56
169/// review, where an earlier comment here claimed the opposite.
170///
171/// It makes no difference to the flags on the build this was measured against,
172/// which read zero under a null handle, the pseudo-handle, and a real
173/// `OpenProcess` handle alike -- see [`CpuSet::allocated_to_target_process`] and
174/// D-23 in `DESIGN-NOTES.md`. Asking the documented question anyway is what
175/// makes the zero a fact about the build rather than about the call.
176///
177/// # Errors
178///
179/// Returns any error from `GetSystemCpuSetInformation` other than the expected
180/// sizing failure.
181pub(crate) fn enumerate() -> io::Result<(Vec<CpuSet>, Option<EnumerationAnomaly>)> {
182    // **Sized and fetched in a bounded loop**, because the machine can change
183    // between the two calls -- see `SIZING_ATTEMPTS` for why a second
184    // `ERROR_INSUFFICIENT_BUFFER` is retried rather than returned.
185    let mut grew = None;
186    for _ in 0..SIZING_ATTEMPTS {
187        let mut length: u32 = 0;
188        // SAFETY: a null buffer with a zero length and a valid out-pointer, which
189        // is the documented sizing call. `GetCurrentProcess` is a pseudo-handle
190        // needing no close, and is the documented way to ask about this process.
191        let probe = unsafe {
192            GetSystemCpuSetInformation(
193                std::ptr::null_mut(),
194                0,
195                &raw mut length,
196                GetCurrentProcess(),
197                0,
198            )
199        };
200        if probe != 0 {
201            // Succeeding on the sizing call would mean zero bytes were needed, so
202            // there is nothing to report.
203            return Ok((Vec::new(), None));
204        }
205        let error = io::Error::last_os_error();
206        if error.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as i32) {
207            return Err(error);
208        }
209        if length == 0 {
210            return Ok((Vec::new(), None));
211        }
212
213        // `u64`-backed storage for the same reason the relationship walk uses it:
214        // it guarantees 8-byte alignment for every record header regardless of what
215        // a `Vec<u8>` allocation would have happened to provide. `AllocationTag` is
216        // 8-byte-sized, so this is not merely tidiness.
217        let mut storage = vec![0_u64; (length as usize).div_ceil(8)];
218        let buffer = storage.as_mut_ptr().cast::<u8>();
219        let mut actual_length = length;
220        // SAFETY: `buffer` points to `storage`, whose byte length is at least
221        // `length` (the size the probe just reported) and is 8-byte aligned;
222        // `actual_length` is a valid in/out length pointer.
223        let ok = unsafe {
224            GetSystemCpuSetInformation(
225                buffer.cast(),
226                length,
227                &raw mut actual_length,
228                GetCurrentProcess(),
229                0,
230            )
231        };
232        if ok != 0 {
233            // SAFETY: `buffer` holds `actual_length` bytes written by the call
234            // above: consecutive `SYSTEM_CPU_SET_INFORMATION` records whose `Size`
235            // fields sum to `actual_length`, per the API's contract.
236            return Ok(unsafe { decode(buffer.cast_const(), actual_length) });
237        }
238
239        let error = io::Error::last_os_error();
240        if error.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as i32) {
241            return Err(error);
242        }
243        // The machine grew between the sizing call and this one. Size again.
244        grew = Some(error);
245    }
246
247    Err(grew.expect("SIZING_ATTEMPTS is a non-zero constant, so the loop ran at least once"))
248}
249const SIZE_OFFSET: usize = core::mem::offset_of!(SYSTEM_CPU_SET_INFORMATION, Size);
250const TYPE_OFFSET: usize = core::mem::offset_of!(SYSTEM_CPU_SET_INFORMATION, Type);
251const UNION_OFFSET: usize = core::mem::offset_of!(SYSTEM_CPU_SET_INFORMATION, Anonymous);
252
253/// Walk `length` bytes of consecutive records.
254///
255/// # Safety
256///
257/// `base` must address `length` initialized bytes laid out as consecutive
258/// `SYSTEM_CPU_SET_INFORMATION` records.
259unsafe fn decode(base: *const u8, length: u32) -> (Vec<CpuSet>, Option<EnumerationAnomaly>) {
260    // Offsets within the `CpuSet` arm of the record's union, computed from the
261    // generated types so a binding change moves them rather than silently
262    // shifting what is read.
263    use windows_sys::Win32::System::SystemInformation::{
264        SYSTEM_CPU_SET_INFORMATION_0, SYSTEM_CPU_SET_INFORMATION_0_0,
265    };
266    const CPUSET_OFFSET: usize = core::mem::offset_of!(SYSTEM_CPU_SET_INFORMATION_0, CpuSet);
267    macro_rules! field {
268        ($name:ident) => {
269            UNION_OFFSET
270                + CPUSET_OFFSET
271                + core::mem::offset_of!(SYSTEM_CPU_SET_INFORMATION_0_0, $name)
272        };
273    }
274
275    // SAFETY: forwarded from this function's own contract.
276    let mut walk = unsafe {
277        RecordWalk::new(
278            base,
279            length,
280            SIZE_OFFSET,
281            size_of::<SYSTEM_CPU_SET_INFORMATION>(),
282            Source::CpuSets,
283        )
284    };
285
286    let mut records = Vec::new();
287    for record in &mut walk {
288        // Every read below is bounded by the record's own `Size`, and the walk
289        // has already established that `Size` covers a full
290        // `SYSTEM_CPU_SET_INFORMATION` -- so none of these can fail. They are
291        // written as options rather than asserted because the alternative to a
292        // `None` that skips a record is a panic, and per D-24 this crate does
293        // not panic over the shape of someone else's buffer.
294        // SAFETY: the walk yielded a record addressing `size` initialized bytes.
295        let decoded = unsafe {
296            (|| {
297                if record.read::<i32>(TYPE_OFFSET)? != CpuSetInformation {
298                    return None;
299                }
300                let all_flags = record.read::<u8>(field!(Anonymous1))?;
301                Some(CpuSet {
302                    id: record.read(field!(Id))?,
303                    group: record.read(field!(Group))?,
304                    logical_processor_index: record.read(field!(LogicalProcessorIndex))?,
305                    core_index: record.read(field!(CoreIndex))?,
306                    last_level_cache_index: record.read(field!(LastLevelCacheIndex))?,
307                    numa_node_index: record.read(field!(NumaNodeIndex))?,
308                    efficiency_class: record.read(field!(EfficiencyClass))?,
309                    parked: all_flags & flags::PARKED != 0,
310                    allocated: all_flags & flags::ALLOCATED != 0,
311                    allocated_to_target_process: all_flags & flags::ALLOCATED_TO_TARGET_PROCESS
312                        != 0,
313                    real_time: all_flags & flags::REAL_TIME != 0,
314                    scheduling_class: record.read(field!(Anonymous2))?,
315                    allocation_tag: record.read(field!(AllocationTag))?,
316                })
317            })()
318        };
319        if let Some(cpu_set) = decoded {
320            records.push(cpu_set);
321        }
322    }
323
324    (records, walk.anomaly())
325}
326
327#[cfg(test)]
328mod tests;