Skip to main content

squib_arch/
psci.rs

1//! PSCI 1.1 dispatch table.
2//!
3//! Squib's HVC handler reads X0 (function ID) and dispatches against this table. Unknown
4//! function IDs return `PSCI_NOT_SUPPORTED` — never panic, never abort. SMC traps follow
5//! the same path: HVF surfaces `EC_SMC` (`0x17`), the VMM places `PSCI_NOT_SUPPORTED` in
6//! X0 and advances PC by 4. This matches what KVM does when no PSCI conduit is registered
7//! for SMC and what mainline Linux expects when the FDT declares `psci.method = "hvc"`.
8//!
9//! See [13-arch-and-boot.md § 5](../../../specs/13-arch-and-boot.md#5-psci-dispatch).
10
11/// `PSCI_VERSION` function ID (PSCI 0.2, smc32).
12pub const PSCI_VERSION: u32 = 0x8400_0000;
13/// `CPU_OFF` function ID (smc32).
14pub const CPU_OFF: u32 = 0x8400_0002;
15/// `CPU_ON` function ID (smc64).
16pub const CPU_ON: u32 = 0xC400_0003;
17/// `AFFINITY_INFO` function ID (smc64).
18pub const AFFINITY_INFO: u32 = 0xC400_0004;
19/// `MIGRATE_INFO_TYPE` function ID (smc32).
20pub const MIGRATE_INFO_TYPE: u32 = 0x8400_0006;
21/// `SYSTEM_OFF` function ID (smc32).
22pub const SYSTEM_OFF: u32 = 0x8400_0008;
23/// `SYSTEM_RESET` function ID (smc32).
24pub const SYSTEM_RESET: u32 = 0x8400_0009;
25/// `PSCI_FEATURES` function ID (smc32).
26pub const PSCI_FEATURES: u32 = 0x8400_000A;
27/// `CPU_SUSPEND` function ID (smc64) — declared in FDT but currently routed to NOT_SUPPORTED.
28pub const CPU_SUSPEND: u32 = 0xC400_0001;
29/// `MIGRATE` function ID (smc64) — declared in FDT but routed to NOT_SUPPORTED.
30pub const MIGRATE: u32 = 0xC400_0005;
31
32/// PSCI return code: success.
33pub const PSCI_RET_SUCCESS: i32 = 0;
34/// PSCI return code: NOT_SUPPORTED.
35pub const PSCI_RET_NOT_SUPPORTED: i32 = -1;
36/// PSCI return code: INVALID_PARAMETERS.
37pub const PSCI_RET_INVALID_PARAMETERS: i32 = -2;
38/// PSCI return code: ALREADY_ON.
39pub const PSCI_RET_ALREADY_ON: i32 = -4;
40/// PSCI return code: ON_PENDING.
41pub const PSCI_RET_ON_PENDING: i32 = -5;
42/// PSCI return code: INTERNAL_FAILURE.
43pub const PSCI_RET_INTERNAL_FAILURE: i32 = -6;
44
45/// PSCI 1.1 version word — `(major << 16) | minor`.
46pub const PSCI_VERSION_VALUE: u32 = 0x0001_0001;
47
48/// Affinity-info return values.
49pub mod affinity {
50    /// Target CPU is on.
51    pub const ON: i32 = 0;
52    /// Target CPU is off.
53    pub const OFF: i32 = 1;
54    /// Target CPU is on but a wake-up is pending (race window between `CPU_ON` and the
55    /// secondary actually starting).
56    pub const ON_PENDING: i32 = 2;
57}
58
59/// Recognized PSCI function names — the dispatcher emits one of these for every known ID.
60#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
61pub enum PsciFunction {
62    /// `PSCI_VERSION` — return the PSCI version (1.1).
63    Version,
64    /// `CPU_OFF` — park the calling vCPU.
65    CpuOff,
66    /// `CPU_ON` — bring up a secondary vCPU.
67    CpuOn {
68        /// Target CPU MPIDR.
69        target_cpu: u64,
70        /// Entry point address for the secondary.
71        entry_point: u64,
72        /// Context ID passed back in X0 to the secondary.
73        context_id: u64,
74    },
75    /// `AFFINITY_INFO` — query a vCPU's PSCI state.
76    AffinityInfo {
77        /// Target CPU MPIDR.
78        target_cpu: u64,
79        /// Affinity level (0..=3).
80        lowest_affinity_level: u32,
81    },
82    /// `MIGRATE_INFO_TYPE` — squib always returns `2` (TOS not present).
83    MigrateInfoType,
84    /// `SYSTEM_OFF` — guest powered down.
85    SystemOff,
86    /// `SYSTEM_RESET` — guest requested reset.
87    SystemReset,
88    /// `PSCI_FEATURES` — query whether a PSCI function is implemented.
89    Features {
90        /// The function ID being queried.
91        target_fn: u32,
92    },
93}
94
95/// What the dispatcher tells the caller to do after decoding the HVC.
96#[derive(Debug, Clone, Copy, Eq, PartialEq)]
97pub enum PsciOutcome {
98    /// Direct return: place [`PsciReturn`] in X0 and advance PC by 4.
99    Return(PsciReturn),
100    /// `CPU_OFF`: park the calling vCPU; never returns to it.
101    ParkCallerCpuOff,
102    /// `CPU_ON`: signal a secondary vCPU actor with the given (target_cpu, entry, context).
103    /// X0 of the calling vCPU receives [`PsciReturn::Success`] (or `AlreadyOn`/`OnPending`
104    /// after the actor responds — the caller hands the actor the request and parks until
105    /// it acks).
106    BringUpSecondary {
107        /// MPIDR of the target.
108        target_cpu: u64,
109        /// Entry point.
110        entry_point: u64,
111        /// Context ID passed in X0 of the secondary on first run.
112        context_id: u64,
113    },
114    /// `AFFINITY_INFO`: caller computes the actor's state and returns it via
115    /// [`PsciReturn::Word`]. The dispatcher emits this outcome with the parsed args; the
116    /// VMM does the lookup.
117    QueryAffinityInfo {
118        /// Target MPIDR.
119        target_cpu: u64,
120        /// Lowest-affinity-level argument from PSCI.
121        lowest_affinity_level: u32,
122    },
123    /// `SYSTEM_OFF`: tear down the VM cleanly; the call does not return.
124    SystemOff,
125    /// `SYSTEM_RESET`: tear down + recreate.
126    SystemReset,
127}
128
129/// Return value placed in X0 after a PSCI call.
130#[derive(Debug, Clone, Copy, Eq, PartialEq)]
131pub enum PsciReturn {
132    /// `PSCI_RET_SUCCESS`.
133    Success,
134    /// `PSCI_RET_NOT_SUPPORTED`.
135    NotSupported,
136    /// `PSCI_RET_INVALID_PARAMETERS`.
137    InvalidParameters,
138    /// Numeric word — used by `AFFINITY_INFO` (0/1/2), `PSCI_VERSION`, and `MIGRATE_INFO_TYPE`.
139    Word(u32),
140}
141
142impl PsciReturn {
143    /// The 64-bit value placed in X0.
144    #[must_use]
145    #[allow(clippy::cast_sign_loss)] // PSCI return codes are signed; X0 carries the bit pattern
146    pub const fn as_x0(self) -> u64 {
147        match self {
148            Self::Success => PSCI_RET_SUCCESS as u64,
149            Self::NotSupported => PSCI_RET_NOT_SUPPORTED as u64,
150            Self::InvalidParameters => PSCI_RET_INVALID_PARAMETERS as u64,
151            Self::Word(w) => w as u64,
152        }
153    }
154}
155
156/// Decode a PSCI call into a structured outcome.
157///
158/// `func_id` is X0 truncated to 32 bits (PSCI is a 32-bit function-ID protocol carried in
159/// X0, even on smc64). `args` is `[X1, X2, X3]` — the additional argument registers.
160///
161/// Unknown function IDs always return `PsciOutcome::Return(PsciReturn::NotSupported)`.
162/// This matches Arm's PSCI 1.1 specification and is the contract every Linux kernel
163/// expects.
164#[must_use]
165pub fn dispatch(func_id: u32, args: [u64; 3]) -> PsciOutcome {
166    match func_id {
167        PSCI_VERSION => PsciOutcome::Return(PsciReturn::Word(PSCI_VERSION_VALUE)),
168        CPU_OFF => PsciOutcome::ParkCallerCpuOff,
169        CPU_ON => PsciOutcome::BringUpSecondary {
170            target_cpu: args[0],
171            entry_point: args[1],
172            context_id: args[2],
173        },
174        AFFINITY_INFO => PsciOutcome::QueryAffinityInfo {
175            target_cpu: args[0],
176            #[allow(clippy::cast_possible_truncation)] // PSCI defines this as a 32-bit field
177            lowest_affinity_level: args[1] as u32,
178        },
179        MIGRATE_INFO_TYPE => PsciOutcome::Return(PsciReturn::Word(2)),
180        SYSTEM_OFF => PsciOutcome::SystemOff,
181        SYSTEM_RESET => PsciOutcome::SystemReset,
182        PSCI_FEATURES => {
183            // Bit 0 of args[0] is the function ID being queried.
184            #[allow(clippy::cast_possible_truncation)] // PSCI function IDs are u32
185            let queried = args[0] as u32;
186            let supported = matches!(
187                queried,
188                PSCI_VERSION
189                    | CPU_OFF
190                    | CPU_ON
191                    | AFFINITY_INFO
192                    | MIGRATE_INFO_TYPE
193                    | SYSTEM_OFF
194                    | SYSTEM_RESET
195                    | PSCI_FEATURES
196            );
197            if supported {
198                PsciOutcome::Return(PsciReturn::Success)
199            } else {
200                PsciOutcome::Return(PsciReturn::NotSupported)
201            }
202        }
203        _ => PsciOutcome::Return(PsciReturn::NotSupported),
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn psci_version_returns_one_one() {
213        let outcome = dispatch(PSCI_VERSION, [0; 3]);
214        assert_eq!(
215            outcome,
216            PsciOutcome::Return(PsciReturn::Word(PSCI_VERSION_VALUE))
217        );
218        assert_eq!(PsciReturn::Word(PSCI_VERSION_VALUE).as_x0(), 0x0001_0001);
219    }
220
221    #[test]
222    fn cpu_off_parks_caller() {
223        assert_eq!(dispatch(CPU_OFF, [0; 3]), PsciOutcome::ParkCallerCpuOff);
224    }
225
226    #[test]
227    fn cpu_on_carries_target_entry_context() {
228        let outcome = dispatch(CPU_ON, [0xAABB_CCDD, 0x8000_0000, 0xDEAD_BEEF]);
229        assert_eq!(
230            outcome,
231            PsciOutcome::BringUpSecondary {
232                target_cpu: 0xAABB_CCDD,
233                entry_point: 0x8000_0000,
234                context_id: 0xDEAD_BEEF,
235            }
236        );
237    }
238
239    #[test]
240    fn affinity_info_passes_through_args() {
241        let outcome = dispatch(AFFINITY_INFO, [0x100, 1, 0]);
242        assert_eq!(
243            outcome,
244            PsciOutcome::QueryAffinityInfo {
245                target_cpu: 0x100,
246                lowest_affinity_level: 1,
247            }
248        );
249    }
250
251    #[test]
252    fn migrate_info_type_returns_2() {
253        assert_eq!(
254            dispatch(MIGRATE_INFO_TYPE, [0; 3]),
255            PsciOutcome::Return(PsciReturn::Word(2))
256        );
257    }
258
259    #[test]
260    fn system_off_and_reset_route_correctly() {
261        assert_eq!(dispatch(SYSTEM_OFF, [0; 3]), PsciOutcome::SystemOff);
262        assert_eq!(dispatch(SYSTEM_RESET, [0; 3]), PsciOutcome::SystemReset);
263    }
264
265    #[test]
266    fn psci_features_says_yes_for_implemented_calls() {
267        assert_eq!(
268            dispatch(PSCI_FEATURES, [u64::from(PSCI_VERSION), 0, 0]),
269            PsciOutcome::Return(PsciReturn::Success)
270        );
271        assert_eq!(
272            dispatch(PSCI_FEATURES, [u64::from(SYSTEM_OFF), 0, 0]),
273            PsciOutcome::Return(PsciReturn::Success)
274        );
275    }
276
277    #[test]
278    fn psci_features_says_no_for_cpu_suspend() {
279        // CPU_SUSPEND is in the FDT but currently routed to NOT_SUPPORTED.
280        assert_eq!(
281            dispatch(PSCI_FEATURES, [u64::from(CPU_SUSPEND), 0, 0]),
282            PsciOutcome::Return(PsciReturn::NotSupported)
283        );
284    }
285
286    #[test]
287    fn unknown_function_id_returns_not_supported() {
288        for func_id in [0x0000_0000, 0x8400_00FF, 0xC400_00FF, 0xDEAD_BEEF, u32::MAX] {
289            let outcome = dispatch(func_id, [0; 3]);
290            assert_eq!(
291                outcome,
292                PsciOutcome::Return(PsciReturn::NotSupported),
293                "func_id {func_id:#010x}"
294            );
295        }
296    }
297
298    #[test]
299    fn cpu_suspend_and_migrate_route_to_not_supported_in_smc_view() {
300        // Phase 1 routes both to NOT_SUPPORTED. CPU_SUSPEND lights up later when we
301        // implement PSCI suspend semantics.
302        assert_eq!(
303            dispatch(CPU_SUSPEND, [0; 3]),
304            PsciOutcome::Return(PsciReturn::NotSupported)
305        );
306        assert_eq!(
307            dispatch(MIGRATE, [0; 3]),
308            PsciOutcome::Return(PsciReturn::NotSupported)
309        );
310    }
311
312    #[test]
313    fn psci_return_x0_bit_pattern_matches_signed_constants() {
314        // PSCI_RET_NOT_SUPPORTED = -1 in i32 → 0xFFFF_FFFF when stored in u32, then
315        // sign-extended to 0xFFFF_FFFF_FFFF_FFFF when promoted to u64. Linux reads X0
316        // as a signed i32, so the upper 32 bits don't matter — but the bit pattern of
317        // the low 32 must match. (From the spec: SMC dispatch sets X0 to
318        // `0xFFFF_FFFF` as a signed i32 = `-1`.)
319        let not_supported_low = (PsciReturn::NotSupported.as_x0() & 0xFFFF_FFFF) as u32;
320        let invalid_low = (PsciReturn::InvalidParameters.as_x0() & 0xFFFF_FFFF) as u32;
321        assert_eq!(not_supported_low, 0xFFFF_FFFF);
322        assert_eq!(PsciReturn::Success.as_x0(), 0);
323        assert_eq!(invalid_low, 0xFFFF_FFFE);
324    }
325}