Skip to content

feat(aarch64): support external graceful shutdown via SendCtrlAltDel#5984

Open
14sea wants to merge 5 commits into
firecracker-microvm:mainfrom
14sea:fix/aarch64-pl061-shutdown
Open

feat(aarch64): support external graceful shutdown via SendCtrlAltDel#5984
14sea wants to merge 5 commits into
firecracker-microvm:mainfrom
14sea:fix/aarch64-pl061-shutdown

Conversation

@14sea

@14sea 14sea commented Jun 19, 2026

Copy link
Copy Markdown

Changes

Add external graceful shutdown support on aarch64 by reusing the existing
SendCtrlAltDel action.

  • Expose a minimal PL061 GPIO controller as an aarch64 MMIO device and
    describe a gpio-keys power button (KEY_POWER) in the FDT.
  • Reuse SendCtrlAltDel: on x86_64 it still injects CTRL+ALT+DEL through the
    i8042 device (unchanged); on aarch64 it drives a short, synchronous
    press/release pulse on the virtual power button. The API no longer rejects the
    action with a 400 on aarch64.
  • 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.
  • Save/restore the PL061 register state across snapshots so the button keeps
    working on a restored microVM. The snapshot version is bumped 10.0.0
    11.0.0.
  • Add the guest-kernel options the gpio-keys path needs
    (CONFIG_GPIOLIB, CONFIG_GPIO_PL061, CONFIG_INPUT_KEYBOARD,
    CONFIG_KEYBOARD_GPIO) to resources/guest_configs/ci.config, and update
    docs/api_requests/actions.md, docs/device-api.md and CHANGELOG.md.

This follows the direction confirmed by @ShadowCurse in
#2046:
no feature gating (always enabled on aarch64), reuse SendCtrlAltDel, and rely
on the systemd-logind already present in the CI rootfs.

A couple of points worth a maintainer's eye:

  • Kernel config placement. I put the gpio-keys options in ci.config rather
    than a separate debug.config-style fragment, because the integration tests
    boot the non-debug CI kernels (which only concatenate ci.config), and the
    file already carries the x86 "CTRL+ALT+DEL support" section. Happy to move it
    if you'd prefer a dedicated fragment.
  • test_send_ctrl_alt_del on aarch64. It is kept skipif-skipped on aarch64
    for now: the kernel config is included here, but a fresh PR runs against the
    currently-published guest-kernel artifacts, which do not yet have gpio-keys.
    Once the CI guest kernels are rebuilt with this ci.config change the skip can
    be removed so the existing test exercises the aarch64 path. Would you like me
    to drop the skip in this PR, or as a follow-up after the artifacts are
    rebuilt?

Hardware validation

Validated on real aarch64 + KVM hardware (RK3568, Ubuntu guest), on this exact
rebased commit:

  • No interrupt storm: /proc/interrupts shows the line as Edge; the count
    increments by exactly +2 per action (press + release) and host CPU stays
    effectively idle (~0.7% over the window). A level storm would peg a CPU.
  • Graceful poweroff: with systemd-logind active, SendCtrlAltDel
    systemd-poweroffreboot: Power downKVM_SYSTEM_EVENT
    Firecracker exiting successfully. exit_code=0.
  • Snapshot/restore: after a full snapshot → restore cycle, the restored guest
    still takes the interrupt (+2) and emits a byte-identical KEY_POWER
    press/release event stream on /dev/input/event0, confirming the PL061
    interrupt-enable state survives restore.

Reason

Closes #2046. aarch64 previously rejected SendCtrlAltDel with a 400 and had no
mechanism to request a graceful shutdown of a microVM from the host, unlike
x86_64. This brings aarch64 to parity using the standard guest gpio-keys path.

License Acceptance

By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache 2.0 license. For more information on following Developer
Certificate of Origin and signing off your commits, please check
CONTRIBUTING.md.

PR Checklist

  • I have read and understand CONTRIBUTING.md.
  • I have verified the PR builds and passes clippy -D warnings on both
    x86_64 and aarch64 (tools/devtool build on aarch64 hardware); CI runs the
    full checkbuild --all.
  • I have run tools/devtool checkstyle to verify that the PR passes the
    automated style checks.
  • I have described what is done in these changes, why they are needed, and
    how they are solving the problem in a clear and encompassing way.
  • I have updated any relevant documentation (both in code and in the docs)
    in the PR.
  • I have mentioned all user-facing changes in CHANGELOG.md.
  • If a specific issue led to this PR, this PR closes the issue.
  • When making API changes, I have followed the
    Runbook for Firecracker API changes.
  • I have tested all new and changed functionalities in unit tests and/or
    integration tests (PL061 device unit tests added; the test_send_ctrl_alt_del
    integration test remains aarch64-skipped pending a gpio-keys CI kernel — see
    above).
  • I have linked an issue to every new TODO.

  • This functionality cannot be added in rust-vmm.

@JackThomson2 JackThomson2 requested a review from ShadowCurse June 24, 2026 14:07

@ShadowCurse ShadowCurse left a comment

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.

Overall this seems pretty good. I'll let you know when guest kernels are rebuilt for testing.

raw_interrupt_status: u8,
/// Alternate function select (GPIOAFSEL).
alternate_function_select: u8,
metrics: &'static PL061DeviceMetrics,

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.

There will be only one gpio device, so no need to store a ref to it. Just access METRICS directly.

Comment on lines +111 to +125
data: u8,
/// Pin directions (GPIODIR): 0 = input, 1 = output.
direction: u8,
/// Interrupt sense (GPIOIS): 0 = edge, 1 = level.
interrupt_sense: u8,
/// Interrupt both-edges select (GPIOIBE).
interrupt_both_edges: u8,
/// Interrupt event/polarity (GPIOIEV).
interrupt_event: u8,
/// Interrupt mask/enable (GPIOIE).
interrupt_mask: u8,
/// Raw (pre-mask) interrupt status (GPIORIS).
raw_interrupt_status: u8,
/// Alternate function select (GPIOAFSEL).
alternate_function_select: u8,

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.

Since this is the exact state as in PL061State, just use it here. Would be easier to store and set it later.

Comment on lines +202 to +205
pub fn set_input_level(&mut self, line: u8, high: bool) -> Result<(), PL061Error> {
if line >= GPIO_PIN_COUNT {
return Err(PL061Error::InvalidGpioLine(line));
}

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.

As I can see, this is never accessed outside this file, so it can be made private. This also allows you to remove the line check (and the InvalidGpioLine error variant with it). You can also replace the check with assert!(line < GPIO_PIN_COUNT).

Comment on lines +227 to +228
0..PL061_DATA_REG_END => Some(u32::from(self.data & data_mask(offset))),
PL061_DIR => Some(u32::from(self.direction)),

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.

I think:

let result = match offset {
  0..PL061_DATA_REG_END => self.data & data_mask(offset),
  PL061_DIR => self.direction,
  ...
  _ => return None,
};
Some(u32::from(result))

would be more readable with less syntax noise.

PL061_MIS => Some(u32::from(self.masked_interrupt_status())),
PL061_AFSEL => Some(u32::from(self.alternate_function_select)),
PL061_ID_REG_START..PL061_ID_REG_END => {
let index = usize::try_from((offset - PL061_ID_REG_START) >> 2).unwrap();

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.

this is a general comment about usage of try_from: here and in many other place you already guaranteed that these conversions cannot fail (in this example you already did a check offset >= PL061_REGISTER_SPACE_SIZE in the bus_read, in others you mask off the last byte of the value before converting it to u8). Because of this, there is no reason to use try_from. For u64 -> usize you can use vmm::utils::u64_to_usize. For u8 just use as u8 with #[allow(clippy::cast_possible_truncation)].

Comment on lines +311 to +315
let value = match data.len() {
1 => u32::from(data[0]),
4 => u32::from_le_bytes(data.try_into().unwrap()),
_ => unreachable!(),
};

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.

the write_reg converts the value: u32 into u8 all the time, so there is no reason to even try to construct u32. Just pass u8 directly. This will also remove needed conversion to u8 in the write_reg.

write_u32(&mut device, 0x004, 0b1111_1111);

assert_eq!(read_u32(&mut device, PL061_DIR), 0b0000_0011);
assert_eq!(read_u32(&mut device, 0x004), 0b0000_0001);

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.

what are these specific 0x004 and 0x010 registers?

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.

These aren't separate registers — they're masked accesses into the GPIODATA aperture (0x000..0x3FF). The PL061 encodes a per-line bitmask in address bits [9:2] of the access, so an access at offset line_mask << 2 reads/writes only the selected lines (the PrimeCell "masked GPIO data" mechanism, which is also how the Linux gpio-pl061 driver does single-line I/O).

  • 0x004 = 0b0000_0001 << 2 → mask for line 0
  • 0x010 = 0b0000_0100 << 2 → mask for line 2

I've reworked the test to be self-documenting via a data_aperture(line_mask) helper instead of bare offsets, and the aperture is documented at the PL061_DATA_REG_END / data_mask() definitions.

Comment thread CHANGELOG.md

### 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.

Comment thread src/vmm/src/arch/mod.rs Outdated
Comment on lines +58 to +60
// New variants must be appended at the end: `DeviceType` is serialized into snapshots with
// `bitcode`, which encodes enum variants positionally, so reordering would break older
// snapshots that encoded later variants.

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.

this is not needed

@pytest.mark.skipif(
platform.machine() != "x86_64", reason="not yet implemented on aarch64"
platform.machine() != "x86_64",
reason="On x86 CTRL+ALT+DEL triggers a kernel-level reboot via the i8042 device. 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.

This can be in a separate commit to simply drop/update it when guest kernels will be rebuilt with new configs

14sea and others added 5 commits June 28, 2026 17:03
Add a minimal PL061 GPIO controller modelled as an aarch64 MMIO device. It
implements the core register bank (data with the address-masked aperture,
direction, the interrupt sense/both-edges/event/mask/status/clear registers
and the PrimeCell identification registers) plus host-side input injection so
higher layers can drive a virtual GPIO line.

The interrupt line is modelled as edge-triggered to match Firecracker's plain
KVM 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 register state is serializable so it can be carried across snapshots.

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>
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 firecracker-microvm#2046.

Attach the 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 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.
- The gpio-keys kernel symbols are added to the shared CI kernel config so the
  guest can advertise the power key once the CI kernels are rebuilt.

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>
Describe the aarch64 behaviour of the SendCtrlAltDel action (virtual power
button via the PL061 gpio-keys device) in the actions and device-api docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: 14sea <wanhuaning@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: 14sea <wanhuaning@gmail.com>
Update the API test now that SendCtrlAltDel is accepted on aarch64. Kept as a
separate commit so it can be easily dropped or updated once the CI guest kernels
are rebuilt with the gpio-keys config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: 14sea <wanhuaning@gmail.com>
@14sea 14sea force-pushed the fix/aarch64-pl061-shutdown branch from e41324e to 5ab0bc7 Compare June 28, 2026 16:20
@14sea

14sea commented Jun 28, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Force-pushed addressing all comments:

  • Commits split into device impl / VMM wiring / docs / CHANGELOG, with the test_api.py change kept separate.
  • gpio_pl061: access METRICS directly; PL061Device now embeds PL061State; set_input_level is private with an assert! (dropped the InvalidGpioLine variant); read_reg rewritten as a single match; removed the infallible try_from usages (u64_to_usize / as u8); write_reg takes a u8 directly.
  • Removed the unneeded comment in arch/mod.rs.
  • Answered the GPIODATA masked-access question inline.

Each commit builds standalone and the gpio_pl061 unit tests pass on aarch64.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AArch64: graceful power-off from external

2 participants