Skip to content

Commit e41324e

Browse files
14seaCopilotclaude
committed
feat(aarch64): support external graceful shutdown via SendCtrlAltDel
Until now `SendCtrlAltDel` was rejected with a 400 on aarch64 and there was no way to request a graceful shutdown of a microVM from the host. Fixes #2046. Add a minimal PL061 GPIO controller as an aarch64 MMIO device and describe a `gpio-keys` power button (KEY_POWER) in the FDT. `SendCtrlAltDel` is reused: on x86_64 it still injects CTRL+ALT+DEL through the i8042 device; on aarch64 it drives a short press/release pulse on the virtual power button. A guest with the standard gpio-keys driver and a power-key consumer (e.g. systemd-logind, which defaults to HandlePowerKey=poweroff) then shuts down cleanly and Firecracker exits on the resulting KVM_SYSTEM_EVENT_SHUTDOWN. Details: - The PL061 SPI is declared edge-triggered to match Firecracker's plain irqfd injection (no resample fd). A level-high line would never be de-asserted and the GIC would re-fire it after the guest EOIs, storming the host. - The PL061 register state is saved and restored across snapshots, so the power button keeps working on a restored microVM. The snapshot version is bumped to 11.0.0 accordingly. - The press/release pulse runs synchronously so the two edges stay atomic with respect to other API actions; a concurrent snapshot can never capture the button half-pressed. Validated on aarch64 + KVM hardware: KEY_POWER is delivered to the guest with no interrupt storm, systemd-logind powers the VM off automatically, and the path also works after a snapshot/restore cycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: 14sea <wanhuaning@gmail.com>
1 parent 6a8f200 commit e41324e

17 files changed

Lines changed: 983 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,17 @@ and this project adheres to
1010

1111
### Added
1212

13+
- [#2046](https://github.com/firecracker-microvm/firecracker/issues/2046): The
14+
`SendCtrlAltDel` action is now supported on aarch64. It injects a virtual
15+
power-button press through a new PL061 GPIO controller exposed to the guest as
16+
a `gpio-keys` power button, enabling external graceful shutdown (aarch64
17+
previously rejected the action with a 400).
18+
1319
### Changed
1420

21+
- Bumped the snapshot version to 11.0.0 because the aarch64 PL061 GPIO device
22+
adds new state to the snapshot format.
23+
1524
### Deprecated
1625

1726
### Removed

docs/api_requests/actions.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,29 @@ curl --unix-socket /tmp/firecracker.socket -i \
3131
-d '{ "action_type": "FlushMetrics" }'
3232
```
3333

34-
## [Intel and AMD only] SendCtrlAltDel
34+
## SendCtrlAltDel
3535

36-
This action will send the CTRL+ALT+DEL key sequence to the microVM. By
36+
This action requests an orderly shutdown of the microVM from the host. Since
37+
Firecracker exits when the guest powers off (CPU reset), `SendCtrlAltDel` can be
38+
used to trigger a clean shutdown of the microVM. The mechanism differs per
39+
architecture, but the API request is the same on both.
40+
41+
On **x86_64**, this action sends the CTRL+ALT+DEL key sequence to the microVM. By
3742
convention, this sequence has been used to trigger a soft reboot and, as such,
3843
most Linux distributions perform an orderly shutdown and reset upon receiving
39-
this keyboard input. Since Firecracker exits on CPU reset, `SendCtrlAltDel` can
40-
be used to trigger a clean shutdown of the microVM.
41-
42-
For this action, Firecracker emulates a standard AT keyboard, connected via an
43-
i8042 controller. Driver support for both these devices needs to be present in
44+
this keyboard input. Firecracker emulates a standard AT keyboard, connected via
45+
an i8042 controller. Driver support for both these devices needs to be present in
4446
the guest OS. For Linux, that means the guest kernel needs `CONFIG_SERIO_I8042`
4547
and `CONFIG_KEYBOARD_ATKBD`.
4648

49+
On **aarch64**, this action injects a virtual power-button press. Firecracker
50+
exposes a PL061 GPIO controller and describes a `gpio-keys` power button (mapped
51+
to `KEY_POWER`) in the device tree. Driver support needs to be present in the
52+
guest OS; for Linux that means `CONFIG_GPIOLIB`, `CONFIG_GPIO_PL061`,
53+
`CONFIG_INPUT_KEYBOARD` and `CONFIG_KEYBOARD_GPIO`, plus a userspace consumer of
54+
the power-key event (for example `systemd-logind` with the default
55+
`HandlePowerKey=poweroff`).
56+
4757
> [!NOTE]
4858
>
4959
> At boot time, the Linux driver for i8042 spends a few tens of milliseconds

docs/device-api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,8 @@ specification:
160160
| `FlushMetrics` | O | O | O | O | O | O |
161161
| `InstanceStart` | O | O | O | O | O | O |
162162
| `SendCtrlAltDel` | **R** | O | O | O | O | O |
163+
164+
The `keyboard` requirement for `SendCtrlAltDel` applies to x86_64, which emulates
165+
an i8042 keyboard controller. On aarch64 the action instead drives a PL061 GPIO
166+
power button (exposed to the guest as `gpio-keys`); see
167+
[actions](api_requests/actions.md#sendctrlaltdel).

resources/guest_configs/ci.config

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ CONFIG_SERIO_LIBPS2=y
1313
CONFIG_SERIO_GSCPS2=y
1414
CONFIG_KEYBOARD_ATKBD=y
1515
CONFIG_INPUT_KEYBOARD=y
16+
# On aarch64 SendCtrlAltDel presses a gpio-keys power button wired to a PL061
17+
# GPIO controller; these enable the guest-side driver chain so systemd-logind
18+
# can act on KEY_POWER. The PL061 driver is ARM AMBA only, so olddefconfig
19+
# drops these on x86_64.
20+
CONFIG_GPIOLIB=y
21+
CONFIG_GPIO_PL061=y
22+
CONFIG_KEYBOARD_GPIO=y

src/firecracker/src/api_server/request/actions.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ use vmm::rpc_interface::VmmAction;
77

88
use super::super::parsed_request::{ParsedRequest, RequestError};
99
use super::Body;
10-
#[cfg(target_arch = "aarch64")]
11-
use super::StatusCode;
1210

1311
// The names of the members from this enum must precisely correspond (as a string) to the possible
1412
// values of "action_type" from the json request body. This is useful to get a strongly typed
@@ -37,17 +35,7 @@ pub(crate) fn parse_put_actions(body: &Body) -> Result<ParsedRequest, RequestErr
3735
match action_body.action_type {
3836
ActionType::FlushMetrics => Ok(ParsedRequest::new_sync(VmmAction::FlushMetrics)),
3937
ActionType::InstanceStart => Ok(ParsedRequest::new_sync(VmmAction::StartMicroVm)),
40-
ActionType::SendCtrlAltDel => {
41-
// SendCtrlAltDel not supported on aarch64.
42-
#[cfg(target_arch = "aarch64")]
43-
return Err(RequestError::Generic(
44-
StatusCode::BadRequest,
45-
"SendCtrlAltDel does not supported on aarch64.".to_string(),
46-
));
47-
48-
#[cfg(target_arch = "x86_64")]
49-
Ok(ParsedRequest::new_sync(VmmAction::SendCtrlAltDel))
50-
}
38+
ActionType::SendCtrlAltDel => Ok(ParsedRequest::new_sync(VmmAction::SendCtrlAltDel)),
5139
}
5240
}
5341

@@ -69,7 +57,6 @@ mod tests {
6957
assert_eq!(result.unwrap(), req);
7058
}
7159

72-
#[cfg(target_arch = "x86_64")]
7360
{
7461
let json = r#"{
7562
"action_type": "SendCtrlAltDel"
@@ -80,16 +67,6 @@ mod tests {
8067
assert_eq!(result.unwrap(), req);
8168
}
8269

83-
#[cfg(target_arch = "aarch64")]
84-
{
85-
let json = r#"{
86-
"action_type": "SendCtrlAltDel"
87-
}"#;
88-
89-
let result = parse_put_actions(&Body::new(json));
90-
result.unwrap_err();
91-
}
92-
9370
{
9471
let json = r#"{
9572
"action_type": "FlushMetrics"

src/vmm/src/arch/aarch64/fdt.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const GIC_PHANDLE: u32 = 1;
3232
const CLOCK_PHANDLE: u32 = 2;
3333
// This is a value for uniquely identifying the FDT node declaring the MSI controller.
3434
const MSI_PHANDLE: u32 = 3;
35+
// This is a value for uniquely identifying the PL061 GPIO controller node.
36+
const GPIO_PL061_PHANDLE: u32 = 4;
3537
// You may be wondering why this big value?
3638
// This phandle is used to uniquely identify the FDT nodes containing cache information. Each cpu
3739
// can have a variable number of caches, some of these caches may be shared with other cpus.
@@ -51,6 +53,9 @@ const GIC_FDT_IRQ_TYPE_PPI: u32 = 1;
5153
// From https://elixir.bootlin.com/linux/v4.9.62/source/include/dt-bindings/interrupt-controller/irq.h#L17
5254
const IRQ_TYPE_EDGE_RISING: u32 = 1;
5355
const IRQ_TYPE_LEVEL_HI: u32 = 4;
56+
const KEY_POWER: u32 = 116;
57+
const POWER_BUTTON_GPIO_PIN: u32 = 0;
58+
const GPIO_ACTIVE_HIGH: u32 = 0;
5459

5560
/// Errors thrown while configuring the Flattened Device Tree for aarch64.
5661
#[derive(Debug, thiserror::Error, displaydoc::Display)]
@@ -457,6 +462,55 @@ fn create_rtc_node(fdt: &mut FdtWriter, dev_info: &MMIODeviceInfo) -> Result<(),
457462
Ok(())
458463
}
459464

465+
fn create_gpio_pl061_node(fdt: &mut FdtWriter, dev_info: &MMIODeviceInfo) -> Result<(), FdtError> {
466+
let compatible = b"arm,pl061\0arm,primecell\0";
467+
468+
let gpio = fdt.begin_node(&format!("pl061@{:x}", dev_info.addr))?;
469+
fdt.property("compatible", compatible)?;
470+
fdt.property_array_u64("reg", &[dev_info.addr, dev_info.len])?;
471+
fdt.property_u32("clocks", CLOCK_PHANDLE)?;
472+
fdt.property_string("clock-names", "apb_pclk")?;
473+
fdt.property_u32("#gpio-cells", 2)?;
474+
fdt.property_null("gpio-controller")?;
475+
fdt.property_u32("phandle", GPIO_PL061_PHANDLE)?;
476+
// The PL061 interrupt is injected through a plain KVM irqfd (no resample fd), which
477+
// models an edge-triggered line: each `trigger()` delivers a single pulse and there is
478+
// no path to de-assert a level. Declaring it level-high would make the GIC re-fire the
479+
// interrupt forever after the guest EOIs it (interrupt storm). Match the edge-triggered
480+
// model used by every other Firecracker SPI (serial, vmgenid, vmclock).
481+
fdt.property_array_u32(
482+
"interrupts",
483+
&[
484+
GIC_FDT_IRQ_TYPE_SPI,
485+
dev_info.gsi.unwrap(),
486+
IRQ_TYPE_EDGE_RISING,
487+
],
488+
)?;
489+
fdt.end_node(gpio)?;
490+
491+
Ok(())
492+
}
493+
494+
fn create_gpio_keys_node(fdt: &mut FdtWriter) -> Result<(), FdtError> {
495+
let gpio_keys = fdt.begin_node("gpio-keys")?;
496+
fdt.property_string("compatible", "gpio-keys")?;
497+
498+
// A single KEY_POWER button bound to line 0 of the PL061 above (via its phandle), so the
499+
// guest's gpio-keys driver reports a power-key event when the host asserts that line.
500+
let power_button = fdt.begin_node("poweroff")?;
501+
fdt.property_string("label", "GPIO Key Poweroff")?;
502+
fdt.property_u32("linux,code", KEY_POWER)?;
503+
fdt.property_array_u32(
504+
"gpios",
505+
&[GPIO_PL061_PHANDLE, POWER_BUTTON_GPIO_PIN, GPIO_ACTIVE_HIGH],
506+
)?;
507+
fdt.end_node(power_button)?;
508+
509+
fdt.end_node(gpio_keys)?;
510+
511+
Ok(())
512+
}
513+
460514
fn create_devices_node(
461515
fdt: &mut FdtWriter,
462516
device_manager: &DeviceManager,
@@ -469,6 +523,11 @@ fn create_devices_node(
469523
create_serial_node(fdt, serial_info)?;
470524
}
471525

526+
if let Some(gpio_pl061_info) = device_manager.mmio_devices.gpio_pl061_device_info() {
527+
create_gpio_pl061_node(fdt, gpio_pl061_info)?;
528+
create_gpio_keys_node(fdt)?;
529+
}
530+
472531
let mut virtio_mmio = device_manager.mmio_devices.virtio_device_info();
473532

474533
// Sort out virtio devices by address from low to high and insert them into fdt table.
@@ -623,7 +682,9 @@ mod tests {
623682
"psci",
624683
"rtc@40001000",
625684
"uart@40002000",
626-
"virtio_mmio@40003000",
685+
"pl061@40003000",
686+
"gpio-keys",
687+
"virtio_mmio@40004000",
627688
"vmgenid",
628689
"ptp@2149572608",
629690
];

src/vmm/src/arch/aarch64/layout.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ pub const BOOT_DEVICE_MEM_START: u64 = MMIO32_MEM_START;
120120
pub const RTC_MEM_START: u64 = BOOT_DEVICE_MEM_START + MMIO_LEN;
121121
/// Memory region start for Serial device.
122122
pub const SERIAL_MEM_START: u64 = RTC_MEM_START + MMIO_LEN;
123+
/// Memory region start for PL061 GPIO device.
124+
pub const GPIO_PL061_MEM_START: u64 = SERIAL_MEM_START + MMIO_LEN;
123125

124126
/// Beginning of memory region for device MMIO 32-bit accesses
125-
pub const MEM_32BIT_DEVICES_START: u64 = SERIAL_MEM_START + MMIO_LEN;
127+
pub const MEM_32BIT_DEVICES_START: u64 = GPIO_PL061_MEM_START + MMIO_LEN;
126128
/// Size of memory region for device MMIO 32-bit accesses
127129
pub const MEM_32BIT_DEVICES_SIZE: u64 = PCI_MMCONFIG_START - MEM_32BIT_DEVICES_START;
128130

src/vmm/src/arch/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ pub enum DeviceType {
5555
Rtc,
5656
/// Device Type: BootTimer.
5757
BootTimer,
58+
// New variants must be appended at the end: `DeviceType` is serialized into snapshots with
59+
// `bitcode`, which encodes enum variants positionally, so reordering would break older
60+
// snapshots that encoded later variants.
61+
/// Device Type: PL061 GPIO.
62+
#[cfg(target_arch = "aarch64")]
63+
GpioPl061,
5864
}
5965

6066
/// Default page size for the guest OS.

src/vmm/src/device_manager/mmio.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ use vm_allocator::AllocPolicy;
2020
use crate::EventManager;
2121
use crate::arch::BOOT_DEVICE_MEM_START;
2222
#[cfg(target_arch = "aarch64")]
23-
use crate::arch::{RTC_MEM_START, SERIAL_MEM_START};
23+
use crate::arch::{GPIO_PL061_MEM_START, RTC_MEM_START, SERIAL_MEM_START};
2424
#[cfg(target_arch = "aarch64")]
25-
use crate::devices::legacy::{RTCDevice, SerialDevice};
25+
use crate::devices::legacy::{PL061Device, RTCDevice, SerialDevice};
2626
use crate::devices::pseudo::BootTimer;
2727
use crate::devices::virtio::device::{VirtioDevice, VirtioDeviceId, VirtioDeviceType};
2828
use crate::devices::virtio::transport::mmio::MmioTransport;
@@ -132,6 +132,9 @@ pub struct MMIODeviceManager {
132132
#[cfg(target_arch = "aarch64")]
133133
/// Serial device on Aarch64 platforms
134134
pub(crate) serial: Option<MMIODevice<SerialDevice>>,
135+
#[cfg(target_arch = "aarch64")]
136+
/// PL061 GPIO controller on Aarch64 platforms
137+
pub(crate) gpio_pl061: Option<MMIODevice<PL061Device>>,
135138
#[cfg(target_arch = "x86_64")]
136139
// We create the AML byte code for every VirtIO device in the order we build
137140
// it, so that we ensure the root block device is appears first in the DSDT.
@@ -367,6 +370,47 @@ impl MMIODeviceManager {
367370
Ok(())
368371
}
369372

373+
#[cfg(target_arch = "aarch64")]
374+
/// Create and register a MMIO PL061 GPIO device at the specified MMIO configuration if
375+
/// given as parameter, otherwise allocate a new MMIO resources for it.
376+
pub fn register_mmio_gpio_pl061(
377+
&mut self,
378+
vm: &KvmVm,
379+
gpio_pl061: Arc<Mutex<PL061Device>>,
380+
device_info_opt: Option<MMIODeviceInfo>,
381+
) -> Result<(), MmioError> {
382+
let device_info = if let Some(device_info) = device_info_opt {
383+
device_info
384+
} else {
385+
let gsi = vm.resource_allocator().allocate_gsi_legacy(1)?;
386+
MMIODeviceInfo {
387+
addr: GPIO_PL061_MEM_START,
388+
len: MMIO_LEN,
389+
gsi: Some(gsi[0]),
390+
}
391+
};
392+
393+
vm.register_irq(
394+
&gpio_pl061.lock().expect("Poisoned lock").interrupt_evt,
395+
device_info.gsi.unwrap(),
396+
)
397+
.map_err(MmioError::RegisterIrqFd)?;
398+
399+
let device = MMIODevice {
400+
resources: device_info,
401+
inner: gpio_pl061,
402+
sub_id: None,
403+
};
404+
405+
vm.common.mmio_bus.insert(
406+
device.inner.clone(),
407+
device.resources.addr,
408+
device.resources.len,
409+
)?;
410+
self.gpio_pl061 = Some(device);
411+
Ok(())
412+
}
413+
370414
/// Register a boot timer device.
371415
pub fn register_mmio_boot_timer(
372416
&mut self,
@@ -443,6 +487,11 @@ impl MMIODeviceManager {
443487
pub fn serial_device_info(&self) -> Option<&MMIODeviceInfo> {
444488
self.serial.as_ref().map(|device| &device.resources)
445489
}
490+
491+
#[cfg(target_arch = "aarch64")]
492+
pub fn gpio_pl061_device_info(&self) -> Option<&MMIODeviceInfo> {
493+
self.gpio_pl061.as_ref().map(|device| &device.resources)
494+
}
446495
}
447496

448497
#[cfg(test)]

0 commit comments

Comments
 (0)