Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ and this project adheres to

### Added

- [#2046](https://github.com/firecracker-microvm/firecracker/issues/2046): The

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split this commit into several smaller ones so it is easier to review. Something like:

  • add PL061 device (just the impl)
  • wire the device inside vmm
  • add documentation
  • add changelog entry

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — split into 5 commits as suggested: (1) add the PL061 device impl, (2) wire it into the VMM, (3) docs, (4) CHANGELOG entry. The test_api.py change is its own commit (5) so it can be dropped/updated independently once the CI kernels are rebuilt with the gpio-keys config.

`SendCtrlAltDel` action is now supported on aarch64. It injects a virtual
power-button press through a new PL061 GPIO controller exposed to the guest as
a `gpio-keys` power button, enabling external graceful shutdown (aarch64
previously rejected the action with a 400).

### Changed

- Bumped the snapshot version to 11.0.0 because the aarch64 PL061 GPIO device
adds new state to the snapshot format.

### Deprecated

### Removed
Expand Down
24 changes: 17 additions & 7 deletions docs/api_requests/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,29 @@ curl --unix-socket /tmp/firecracker.socket -i \
-d '{ "action_type": "FlushMetrics" }'
```

## [Intel and AMD only] SendCtrlAltDel
## SendCtrlAltDel

This action will send the CTRL+ALT+DEL key sequence to the microVM. By
This action requests an orderly shutdown of the microVM from the host. Since
Firecracker exits when the guest powers off (CPU reset), `SendCtrlAltDel` can be
used to trigger a clean shutdown of the microVM. The mechanism differs per
architecture, but the API request is the same on both.

On **x86_64**, this action sends the CTRL+ALT+DEL key sequence to the microVM. By
convention, this sequence has been used to trigger a soft reboot and, as such,
most Linux distributions perform an orderly shutdown and reset upon receiving
this keyboard input. Since Firecracker exits on CPU reset, `SendCtrlAltDel` can
be used to trigger a clean shutdown of the microVM.

For this action, Firecracker emulates a standard AT keyboard, connected via an
i8042 controller. Driver support for both these devices needs to be present in
this keyboard input. Firecracker emulates a standard AT keyboard, connected via
an i8042 controller. Driver support for both these devices needs to be present in
the guest OS. For Linux, that means the guest kernel needs `CONFIG_SERIO_I8042`
and `CONFIG_KEYBOARD_ATKBD`.

On **aarch64**, this action injects a virtual power-button press. Firecracker
exposes a PL061 GPIO controller and describes a `gpio-keys` power button (mapped
to `KEY_POWER`) in the device tree. Driver support needs to be present in the
guest OS; for Linux that means `CONFIG_GPIOLIB`, `CONFIG_GPIO_PL061`,
`CONFIG_INPUT_KEYBOARD` and `CONFIG_KEYBOARD_GPIO`, plus a userspace consumer of
the power-key event (for example `systemd-logind` with the default
`HandlePowerKey=poweroff`).

> [!NOTE]
>
> At boot time, the Linux driver for i8042 spends a few tens of milliseconds
Expand Down
5 changes: 5 additions & 0 deletions docs/device-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,8 @@ specification:
| `FlushMetrics` | O | O | O | O | O | O |
| `InstanceStart` | O | O | O | O | O | O |
| `SendCtrlAltDel` | **R** | O | O | O | O | O |

The `keyboard` requirement for `SendCtrlAltDel` applies to x86_64, which emulates
an i8042 keyboard controller. On aarch64 the action instead drives a PL061 GPIO
power button (exposed to the guest as `gpio-keys`); see
[actions](api_requests/actions.md#sendctrlaltdel).
7 changes: 7 additions & 0 deletions resources/guest_configs/ci.config
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ CONFIG_SERIO_LIBPS2=y
CONFIG_SERIO_GSCPS2=y
CONFIG_KEYBOARD_ATKBD=y
CONFIG_INPUT_KEYBOARD=y
# On aarch64 SendCtrlAltDel presses a gpio-keys power button wired to a PL061
# GPIO controller; these enable the guest-side driver chain so systemd-logind
# can act on KEY_POWER. The PL061 driver is ARM AMBA only, so olddefconfig
# drops these on x86_64.
CONFIG_GPIOLIB=y
CONFIG_GPIO_PL061=y
CONFIG_KEYBOARD_GPIO=y
25 changes: 1 addition & 24 deletions src/firecracker/src/api_server/request/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ use vmm::rpc_interface::VmmAction;

use super::super::parsed_request::{ParsedRequest, RequestError};
use super::Body;
#[cfg(target_arch = "aarch64")]
use super::StatusCode;

// The names of the members from this enum must precisely correspond (as a string) to the possible
// values of "action_type" from the json request body. This is useful to get a strongly typed
Expand Down Expand Up @@ -37,17 +35,7 @@ pub(crate) fn parse_put_actions(body: &Body) -> Result<ParsedRequest, RequestErr
match action_body.action_type {
ActionType::FlushMetrics => Ok(ParsedRequest::new_sync(VmmAction::FlushMetrics)),
ActionType::InstanceStart => Ok(ParsedRequest::new_sync(VmmAction::StartMicroVm)),
ActionType::SendCtrlAltDel => {
// SendCtrlAltDel not supported on aarch64.
#[cfg(target_arch = "aarch64")]
return Err(RequestError::Generic(
StatusCode::BadRequest,
"SendCtrlAltDel does not supported on aarch64.".to_string(),
));

#[cfg(target_arch = "x86_64")]
Ok(ParsedRequest::new_sync(VmmAction::SendCtrlAltDel))
}
ActionType::SendCtrlAltDel => Ok(ParsedRequest::new_sync(VmmAction::SendCtrlAltDel)),
}
}

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

#[cfg(target_arch = "x86_64")]
{
let json = r#"{
"action_type": "SendCtrlAltDel"
Expand All @@ -80,16 +67,6 @@ mod tests {
assert_eq!(result.unwrap(), req);
}

#[cfg(target_arch = "aarch64")]
{
let json = r#"{
"action_type": "SendCtrlAltDel"
}"#;

let result = parse_put_actions(&Body::new(json));
result.unwrap_err();
}

{
let json = r#"{
"action_type": "FlushMetrics"
Expand Down
63 changes: 62 additions & 1 deletion src/vmm/src/arch/aarch64/fdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const GIC_PHANDLE: u32 = 1;
const CLOCK_PHANDLE: u32 = 2;
// This is a value for uniquely identifying the FDT node declaring the MSI controller.
const MSI_PHANDLE: u32 = 3;
// This is a value for uniquely identifying the PL061 GPIO controller node.
const GPIO_PL061_PHANDLE: u32 = 4;
// You may be wondering why this big value?
// This phandle is used to uniquely identify the FDT nodes containing cache information. Each cpu
// can have a variable number of caches, some of these caches may be shared with other cpus.
Expand All @@ -51,6 +53,9 @@ const GIC_FDT_IRQ_TYPE_PPI: u32 = 1;
// From https://elixir.bootlin.com/linux/v4.9.62/source/include/dt-bindings/interrupt-controller/irq.h#L17
const IRQ_TYPE_EDGE_RISING: u32 = 1;
const IRQ_TYPE_LEVEL_HI: u32 = 4;
const KEY_POWER: u32 = 116;
const POWER_BUTTON_GPIO_PIN: u32 = 0;
const GPIO_ACTIVE_HIGH: u32 = 0;

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

fn create_gpio_pl061_node(fdt: &mut FdtWriter, dev_info: &MMIODeviceInfo) -> Result<(), FdtError> {
let compatible = b"arm,pl061\0arm,primecell\0";

let gpio = fdt.begin_node(&format!("pl061@{:x}", dev_info.addr))?;
fdt.property("compatible", compatible)?;
fdt.property_array_u64("reg", &[dev_info.addr, dev_info.len])?;
fdt.property_u32("clocks", CLOCK_PHANDLE)?;
fdt.property_string("clock-names", "apb_pclk")?;
fdt.property_u32("#gpio-cells", 2)?;
fdt.property_null("gpio-controller")?;
fdt.property_u32("phandle", GPIO_PL061_PHANDLE)?;
// The PL061 interrupt is injected through a plain KVM irqfd (no resample fd), which
// models an edge-triggered line: each `trigger()` delivers a single pulse and there is
// no path to de-assert a level. Declaring it level-high would make the GIC re-fire the
// interrupt forever after the guest EOIs it (interrupt storm). Match the edge-triggered
// model used by every other Firecracker SPI (serial, vmgenid, vmclock).
fdt.property_array_u32(
"interrupts",
&[
GIC_FDT_IRQ_TYPE_SPI,
dev_info.gsi.unwrap(),
IRQ_TYPE_EDGE_RISING,
],
)?;
fdt.end_node(gpio)?;

Ok(())
}

fn create_gpio_keys_node(fdt: &mut FdtWriter) -> Result<(), FdtError> {
let gpio_keys = fdt.begin_node("gpio-keys")?;
fdt.property_string("compatible", "gpio-keys")?;

// A single KEY_POWER button bound to line 0 of the PL061 above (via its phandle), so the
// guest's gpio-keys driver reports a power-key event when the host asserts that line.
let power_button = fdt.begin_node("poweroff")?;
fdt.property_string("label", "GPIO Key Poweroff")?;
fdt.property_u32("linux,code", KEY_POWER)?;
fdt.property_array_u32(
"gpios",
&[GPIO_PL061_PHANDLE, POWER_BUTTON_GPIO_PIN, GPIO_ACTIVE_HIGH],
)?;
fdt.end_node(power_button)?;

fdt.end_node(gpio_keys)?;

Ok(())
}

fn create_devices_node(
fdt: &mut FdtWriter,
device_manager: &DeviceManager,
Expand All @@ -469,6 +523,11 @@ fn create_devices_node(
create_serial_node(fdt, serial_info)?;
}

if let Some(gpio_pl061_info) = device_manager.mmio_devices.gpio_pl061_device_info() {
create_gpio_pl061_node(fdt, gpio_pl061_info)?;
create_gpio_keys_node(fdt)?;
}

let mut virtio_mmio = device_manager.mmio_devices.virtio_device_info();

// Sort out virtio devices by address from low to high and insert them into fdt table.
Expand Down Expand Up @@ -623,7 +682,9 @@ mod tests {
"psci",
"rtc@40001000",
"uart@40002000",
"virtio_mmio@40003000",
"pl061@40003000",
"gpio-keys",
"virtio_mmio@40004000",
"vmgenid",
"ptp@2149572608",
];
Expand Down
4 changes: 3 additions & 1 deletion src/vmm/src/arch/aarch64/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,11 @@ pub const BOOT_DEVICE_MEM_START: u64 = MMIO32_MEM_START;
pub const RTC_MEM_START: u64 = BOOT_DEVICE_MEM_START + MMIO_LEN;
/// Memory region start for Serial device.
pub const SERIAL_MEM_START: u64 = RTC_MEM_START + MMIO_LEN;
/// Memory region start for PL061 GPIO device.
pub const GPIO_PL061_MEM_START: u64 = SERIAL_MEM_START + MMIO_LEN;

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

Expand Down
3 changes: 3 additions & 0 deletions src/vmm/src/arch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub enum DeviceType {
Rtc,
/// Device Type: BootTimer.
BootTimer,
/// Device Type: PL061 GPIO.
#[cfg(target_arch = "aarch64")]
GpioPl061,
}

/// Default page size for the guest OS.
Expand Down
53 changes: 51 additions & 2 deletions src/vmm/src/device_manager/mmio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use vm_allocator::AllocPolicy;
use crate::EventManager;
use crate::arch::BOOT_DEVICE_MEM_START;
#[cfg(target_arch = "aarch64")]
use crate::arch::{RTC_MEM_START, SERIAL_MEM_START};
use crate::arch::{GPIO_PL061_MEM_START, RTC_MEM_START, SERIAL_MEM_START};
#[cfg(target_arch = "aarch64")]
use crate::devices::legacy::{RTCDevice, SerialDevice};
use crate::devices::legacy::{PL061Device, RTCDevice, SerialDevice};
use crate::devices::pseudo::BootTimer;
use crate::devices::virtio::device::{VirtioDevice, VirtioDeviceId, VirtioDeviceType};
use crate::devices::virtio::transport::mmio::MmioTransport;
Expand Down Expand Up @@ -132,6 +132,9 @@ pub struct MMIODeviceManager {
#[cfg(target_arch = "aarch64")]
/// Serial device on Aarch64 platforms
pub(crate) serial: Option<MMIODevice<SerialDevice>>,
#[cfg(target_arch = "aarch64")]
/// PL061 GPIO controller on Aarch64 platforms
pub(crate) gpio_pl061: Option<MMIODevice<PL061Device>>,
#[cfg(target_arch = "x86_64")]
// We create the AML byte code for every VirtIO device in the order we build
// it, so that we ensure the root block device is appears first in the DSDT.
Expand Down Expand Up @@ -367,6 +370,47 @@ impl MMIODeviceManager {
Ok(())
}

#[cfg(target_arch = "aarch64")]
/// Create and register a MMIO PL061 GPIO device at the specified MMIO configuration if
/// given as parameter, otherwise allocate a new MMIO resources for it.
pub fn register_mmio_gpio_pl061(
&mut self,
vm: &KvmVm,
gpio_pl061: Arc<Mutex<PL061Device>>,
device_info_opt: Option<MMIODeviceInfo>,
) -> Result<(), MmioError> {
let device_info = if let Some(device_info) = device_info_opt {
device_info
} else {
let gsi = vm.resource_allocator().allocate_gsi_legacy(1)?;
MMIODeviceInfo {
addr: GPIO_PL061_MEM_START,
len: MMIO_LEN,
gsi: Some(gsi[0]),
}
};

vm.register_irq(
&gpio_pl061.lock().expect("Poisoned lock").interrupt_evt,
device_info.gsi.unwrap(),
)
.map_err(MmioError::RegisterIrqFd)?;

let device = MMIODevice {
resources: device_info,
inner: gpio_pl061,
sub_id: None,
};

vm.common.mmio_bus.insert(
device.inner.clone(),
device.resources.addr,
device.resources.len,
)?;
self.gpio_pl061 = Some(device);
Ok(())
}

/// Register a boot timer device.
pub fn register_mmio_boot_timer(
&mut self,
Expand Down Expand Up @@ -443,6 +487,11 @@ impl MMIODeviceManager {
pub fn serial_device_info(&self) -> Option<&MMIODeviceInfo> {
self.serial.as_ref().map(|device| &device.resources)
}

#[cfg(target_arch = "aarch64")]
pub fn gpio_pl061_device_info(&self) -> Option<&MMIODeviceInfo> {
self.gpio_pl061.as_ref().map(|device| &device.resources)
}
}

#[cfg(test)]
Expand Down
16 changes: 14 additions & 2 deletions src/vmm/src/device_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ use crate::EventManager;
use crate::device_manager::acpi::ACPIDeviceError;
#[cfg(target_arch = "x86_64")]
use crate::devices::legacy::I8042Device;
#[cfg(target_arch = "aarch64")]
use crate::devices::legacy::RTCDevice;
use crate::devices::legacy::SerialDevice;
use crate::devices::legacy::serial::{SerialOut, SerialOutInner};
#[cfg(target_arch = "aarch64")]
use crate::devices::legacy::{PL061Device, PL061Error, RTCDevice};
use crate::devices::pseudo::BootTimer;
use crate::devices::virtio::ActivateError;
use crate::devices::virtio::balloon::BalloonError;
Expand Down Expand Up @@ -99,6 +99,9 @@ pub enum AttachDeviceError {
#[cfg(target_arch = "aarch64")]
/// Error creating serial device: {0}
CreateSerial(#[from] std::io::Error),
#[cfg(target_arch = "aarch64")]
/// Error creating PL061 GPIO device: {0}
CreateGpioPl061(#[from] PL061Error),
/// Error attach PCI device: {0}
PciTransport(#[from] PciManagerError),
/// Operation not supported on this VM type
Expand Down Expand Up @@ -362,6 +365,9 @@ impl DeviceManager {

let rtc = Arc::new(Mutex::new(RTCDevice::new()));
self.mmio_devices.register_mmio_rtc(vm, rtc, None)?;
let gpio_pl061 = Arc::new(Mutex::new(PL061Device::new()?));
self.mmio_devices
.register_mmio_gpio_pl061(vm, gpio_pl061, None)?;
Ok(())
}

Expand Down Expand Up @@ -662,6 +668,9 @@ pub enum DevicePersistError {
#[cfg(target_arch = "aarch64")]
/// Legacy: {0}
Legacy(#[from] std::io::Error),
#[cfg(target_arch = "aarch64")]
/// PL061 GPIO: {0}
GpioPl061(#[from] PL061Error),
/// Net: {0}
Net(#[from] NetPersistError),
/// Vsock: {0}
Expand Down Expand Up @@ -835,6 +844,7 @@ pub(crate) mod tests {
let mut vmm = default_vmm();
assert!(vmm.device_manager.mmio_devices.rtc.is_none());
assert!(vmm.device_manager.mmio_devices.serial.is_none());
assert!(vmm.device_manager.mmio_devices.gpio_pl061.is_none());

let mut cmdline = Cmdline::new(4096).unwrap();
let mut event_manager = EventManager::new().unwrap();
Expand All @@ -849,6 +859,7 @@ pub(crate) mod tests {
.unwrap();
assert!(vmm.device_manager.mmio_devices.rtc.is_some());
assert!(vmm.device_manager.mmio_devices.serial.is_none());
assert!(vmm.device_manager.mmio_devices.gpio_pl061.is_some());

let mut vmm = default_vmm();
cmdline.insert("console", "/dev/blah").unwrap();
Expand All @@ -863,6 +874,7 @@ pub(crate) mod tests {
.unwrap();
assert!(vmm.device_manager.mmio_devices.rtc.is_some());
assert!(vmm.device_manager.mmio_devices.serial.is_some());
assert!(vmm.device_manager.mmio_devices.gpio_pl061.is_some());

assert!(
cmdline
Expand Down
Loading