Skip to content

reboot: Refuse to reboot while a shutdown inhibitor blocks it - #9

Draft
cgwalters-bot wants to merge 2 commits into
mainfrom
bot/reboot-check-inhibitors
Draft

cgwalters-bot wants to merge 2 commits into
mainfrom
bot/reboot-check-inhibitors

Conversation

@cgwalters-bot

@cgwalters-bot cgwalters-bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

When bootc reboots (upgrade --apply, switch --apply, rollback --apply, and so bootc-fetch-apply-updates.service), it runs systemctl reboot as root and not on a tty. In that case systemctl doesn't check inhibitors itself. logind in systemd 257 and later refuses such a reboot while a block shutdown inhibitor is held, but older systems such as CentOS Stream 9 (systemd 252) just reboot. Anything holding such a lock is explicitly asking not to be rebooted underneath it.

This PR does what rpm-ostree does (coreos/rpm-ostree#2862). Before rebooting, bootc asks logind for its inhibitors (ListInhibitors via busctl --json=short). It refuses when a lock has mode block and its what includes shutdown, and the error names who holds the lock. delay, block-weak and sleep/idle-only locks don't count. The error suggests systemctl reboot --check-inhibitors=no, and says pending changes take effect on the next boot. That wording also covers rollback and the composefs paths, not just a staged update. The man pages and --apply help text describe this.

If logind can't be queried (e.g. it isn't running), bootc prints a warning and reboots anyway. That deliberately differs from rpm-ostree, which fails closed. Failing closed would break every --apply, and the update timer, on such systems. Where logind does run, systemd 257 and newer already enforce block locks themselves.

An earlier version of this PR passed systemctl reboot --check-inhibitors=yes instead. That also refuses while any non-root user is logged in, including the administrator's own SSH session or a stray tmux. That would break sudo bootc upgrade --apply and automatic updates on many systems, so this version doesn't use it.

This builds on #13 (reboot: Propagate a refused reboot as an error). With that change, a reboot refused by logind shows up as an error instead of a hang. The composefs systemctl soft-reboot path is left alone, because logind 257+ already enforces shutdown inhibitors for it.

Behavior change: on any systemd version, bootc-fetch-apply-updates.service now fails instead of rebooting while such a lock is held. The update is applied at the next reboot, or retried on the next timer run.

Related: bootc-dev#1047. The issue asks for --check-inhibitors=yes, which this deliberately doesn't use (see above).

Testing

Rebased onto main b06e0b0. The conflicts were context only: main now documents --soft-reboot in the --apply help and moved the soft reboot text out of bootc-upgrade(8), so the inhibitor sentences sit next to the new text.

I tested at 6fb9c7c on a 16-core devspace (cgwalters-devspace-36601734885, CentOS Stream 10 base). The branch was built in its own checkout, with its own cargo target, and the built image's bootc RPM is g6fb9c7c017.

  • just validate passed.
  • just unit-tests passed: 467 tests, including two new table-driven tests. reboot::tests::test_blocking_inhibitors checks which locks count: block/shutdown in a combined what counts, while delay, block-weak and sleep-only locks don't. Its input is in the JSON format that busctl --json=short prints on systemd 257. test_blocking_inhibitors_malformed covers empty input, non-JSON, a missing or wrong-shape data, too few fields, and wrongly typed fields.
  • just test-tmt plan-24-image-upgrade-reboot (centos-bootc:stream10) passed. With systemd-inhibit --what=shutdown --mode=block --who=bootc-test held, bootc switch --apply failed with Reboot blocked by shutdown inhibitor: "bootc-test" (PID 1659, UID 0): testing. Pending changes take effect on the next boot; use systemctl reboot --check-inhibitors=no to reboot anyway. Once the lock was released, --apply rebooted into the new image. That run logged in over SSH as root, so this doesn't show the logged-in-user case directly. The bootc side doesn't look at sessions at all.
  • Not tested end to end: the fail-open path when logind is unavailable. It's a small match on the query result. The query and parse failures it handles are covered by the malformed-input test.
  • I didn't rerun on CentOS Stream 9. The bcvk probe in the prep PR shows that, on systemd 252, logind itself lets root reboot through a block lock, which is the case this check covers.

Generated-by: https://github.com/cgwalters/#llms


Review draft in cgwalters-forge, not upstream yet. This section is removed when the PR is opened upstream.

  • Upstream: bootc-dev/bootc, base main
  • Board item: PVTI_lADOE9oHIs4BlJLczg9kENU
  • Fork CI: off; the devspace testing described above is this PR's CI, and upstream CI runs once it is opened there

To review:

  • Approve, or comment /promote on a line of its own, to open it upstream, ready for review. Either covers only the commits pushed so far.
  • If upstream requires DCO, approving also signs off: promote adds Signed-off-by: Colin Walters <walters@verbum.org> to the commits lacking it (the bot's and yours; anyone else's only if you ask), with you as committer.
  • Add a /draft line (in the same comment or before) to open it upstream as a draft (/ready undoes that).
  • Close to drop it.
  • Edit the title and description freely: they become the upstream PR's. Review comments are squashed into the commits they concern, with a reply here.

@cgwalters-bot
cgwalters-bot force-pushed the bot/reboot-check-inhibitors branch from 9b54ac7 to 379e389 Compare September 24, 2026 07:28
@cgwalters-bot cgwalters-bot changed the title reboot: Check for inhibitors by default reboot: Refuse to reboot while a shutdown inhibitor blocks it Sep 24, 2026
@cgwalters-bot
cgwalters-bot force-pushed the bot/reboot-check-inhibitors branch from 379e389 to 3c679b3 Compare September 24, 2026 10:02
We run `systemctl reboot` via `systemd-run` so it's outside our mount
namespace, but without --wait we only learn whether the transient
unit was started, not whether the reboot was accepted. If logind
refuses it, bootc then parks forever waiting for a SIGTERM that never
comes, and e.g. `bootc upgrade --apply` from
bootc-fetch-apply-updates.service would just hang instead of failing.

One way to get there is a block mode shutdown inhibitor: since systemd
257, logind rejects reboot requests from root too while one is held,
unless the caller explicitly asks to skip inhibitors. On CentOS Stream
10 with such a lock held, `systemd-run -- systemctl reboot` exits 0
while the system stays up, so bootc would wait forever.

With --wait and --pipe, the exit status and systemctl's error message
come back to us. --collect avoids leaving a failed transient unit
behind in that case.

There is a small race with --wait: once the reboot is accepted,
shutdown can stop the transient unit or kill systemd-run before it
reports success, and bootc would then print an error and exit while
the system goes down instead of parking until SIGTERM. The window is
short since `systemctl reboot` returns as soon as the job is queued,
and the reboot happens either way; only the exit status of the bootc
process being shut down is affected.

Generated-by: AI
Something that takes a block mode shutdown inhibitor lock (a
long-running job, a package manager, etc.) is explicitly asking not to
be rebooted underneath it. But systemctl only checks inhibitors itself
for interactive invocations, and bootc runs it as root from a
systemd-run transient unit. Since systemd 257 logind refuses such
reboots from root anyway, but older systems (e.g. CentOS Stream 9 with
252) don't.

Do what rpm-ostree does (coreos/rpm-ostree#2862): before rebooting, ask
logind for its inhibitors and refuse if one blocks shutdown, naming
who holds it. We deliberately don't use `systemctl reboot
--check-inhibitors=yes`: that also refuses while any other user is
logged in, which includes the administrator's own SSH session or a
stray tmux, and would break both `sudo bootc upgrade --apply` and
automatic updates on many systems.

We also deliberately differ from rpm-ostree, which fails closed, when
logind can't be queried (e.g. it isn't running): bootc warns and
reboots anyway. Otherwise every `--apply` and the update timer would
fail on such systems, while where logind does run, systemd 257 and
newer already enforce block locks themselves.

This is a behavior change for bootc-fetch-apply-updates.service: while
such a lock is held it now fails instead of rebooting, on any systemd
version. The update stays staged and is applied at the next reboot or
retried on the next timer run; `systemctl reboot --check-inhibitors=no`
reboots anyway.

The composefs `systemctl soft-reboot` path is left alone, since logind
257+ already enforces shutdown inhibitors for it.

Related: bootc-dev#1047
Generated-by: AI
@cgwalters-bot
cgwalters-bot force-pushed the bot/reboot-check-inhibitors branch from 3c679b3 to 6fb9c7c Compare September 29, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant