Skip to main content

max / alloy

Read the whole unit listing in the installer's greeter check A server-profile install could never finish. The acceptance check asked `systemctl --root=<deployment> list-unit-files greetd.service`, and a pattern that matches nothing exits 1: systemd prints `0 unit files listed` and returns non-zero. The check is a Stage::Resolve, so the non-zero exit failed the install before greeter_ready ever read the listing, and the recovery then wrecked the disk it had just written. The server profile prunes greetd deliberately, so the pattern form failed every server install and passed every client one, which is why nothing had caught it. Measured 2026-08-25 by driving a server-profile ISO through build/vmtest: the install ran to its acceptance checks and died there with "0 unit files listed. command exited with exit status: 1". systemd 255 on the dev host answers the same way for any pattern that matches nothing. Unfiltered, the command exits 0 whether the unit is there or not, absence reaches greeter_ready, and that function already tells absence from disabled-ness. Which is what its doc comment claimed the filtered call did. GO alloy 4606dde0, found while verifying the firewall on a booted image.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-25 23:42 UTC
Signed with PGP, not checked
Commit: 395b8c53f1c022f8bdef8f77085226dac2ca99d3
Parent: dd0d819
1 file changed, +41 insertions, -5 deletions
@@ -1545,11 +1545,20 @@
1545 1545 verdict: Box::new(update_reference_parses),
1546 1546 },
1547 1547 // A way in on first boot.
1548 + //
1549 + // The whole listing rather than `list-unit-files greetd.service`, and
1550 + // the difference is the server profile being installable at all.
1551 + // Measured 2026-08-25 by installing a server-profile ISO in
1552 + // build/vmtest: a pattern that matches nothing exits 1, systemd prints
1553 + // `0 unit files listed`, and a check that cannot run its own verdict
1554 + // fails the install. That profile prunes greetd on purpose, so the
1555 + // pattern form failed every server install and passed every client one.
1556 + // Unfiltered, the command exits 0 either way and absence reaches
1557 + // [`greeter_ready`], which is where the two are told apart.
1548 1558 Check {
1549 1559 invocation: Invocation::new("systemctl")
1550 1560 .arg(format!("--root={root}"))
1551 - .arg("list-unit-files")
1552 - .arg(GREETER_UNIT),
1561 + .arg("list-unit-files"),
1553 1562 verdict: Box::new(|listing| greeter_ready(listing, GREETER_UNIT)),
1554 1563 },
1555 1564 ];
@@ -1696,9 +1705,11 @@
1696 1705
1697 1706 /// Whether the target will present a way to log in.
1698 1707 ///
1699 - /// Reads a `systemctl list-unit-files` listing, which exits 0 whether the unit
1700 - /// exists or not, so absence and disabled-ness are told apart here rather than
1701 - /// by an exit status.
1708 + /// Reads a whole `systemctl list-unit-files` listing, which exits 0 whether the
1709 + /// unit is there or not, so absence and disabled-ness are told apart here rather
1710 + /// than by an exit status. Asking for the unit by name does not have that
1711 + /// property: a pattern matching nothing exits 1, which is why the caller does
1712 + /// not narrow the listing.
1702 1713 ///
1703 1714 /// Absent is fine and not an oversight: the `server` profile prunes greetd
1704 1715 /// deliberately and logs in on a getty. Present but disabled is the failure,
@@ -8429,6 +8440,31 @@
8429 8440 );
8430 8441 }
8431 8442
8443 + // The listing the check actually reads is the whole one, and on a server
8444 + // machine greetd is simply not in it. The shape that failed every server
8445 + // install was asking systemd for the unit by name instead.
8446 + #[test]
8447 + fn a_server_listing_with_no_greeter_line_passes() {
8448 + let listing = "UNIT FILE STATE PRESET\n\
8449 + getty@.service enabled enabled\n\
8450 + sshd.service enabled enabled\n\
8451 + firewalld.service enabled enabled\n\n\
8452 + 3 unit files listed.\n";
8453 + assert!(greeter_ready(listing, GREETER_UNIT).is_ok());
8454 + }
8455 +
8456 + // And it still finds a disabled greeter in a listing full of other units,
8457 + // which is the case the unfiltered read must not lose.
8458 + #[test]
8459 + fn a_disabled_greeter_is_still_found_in_a_full_listing() {
8460 + let listing = "UNIT FILE STATE PRESET\n\
8461 + getty@.service enabled enabled\n\
8462 + greetd.service disabled enabled\n\
8463 + sshd.service enabled enabled\n\n\
8464 + 3 unit files listed.\n";
8465 + assert!(greeter_ready(listing, GREETER_UNIT).is_err());
8466 + }
8467 +
8432 8468 fn choices(encryption: Option<&EncryptionChoice>) -> Choices<'_> {
8433 8469 Choices {
8434 8470 hostname: "alloy",