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}