Repository: systemd/systemd
Pull request #35144: boot: add reboot-on-error config option
Author: login: nkraetzschmar
Labels: name: documentation
name: sd-boot/sd-stub/bootctl
Created: 2024-11-12T23:12:35Z | Merged: 2025-03-12T14:47:38Z

**Description:**

This PR introduces a new configuration option `reboot-on-error` to systemd-boot. When enabled, this option causes the system to automatically reboot if the selected boot entry fails to start. This enhancement is particularly useful for scenarios involving automatic boot assessment and boot counting, especially in unattended systems like servers or cloud environments.

**Rationale:**

Currently, when using boot counting, if the boot process fails before the kernel takes over (e.g. if the kernel is invalid or the UKI secureboot signature is invalid), systemd-boot returns an error code and control passes back to the EFI firmware. In environments like QEMU with EDK2 firmware, this results in user being dropped into an EFI shell. This behavior is undesirable for unattended systems where manual intervention is impractical.

By enabling `reboot-on-error`, the system will automatically reboot upon a boot failure. Since the boot counting mechanism decrements the tries left counter before each attempt, the system will eventually select an alternative boot entry. This ensures that systems can recover from failed boots without requiring user interaction.


Diff:
diff --git a/man/loader.conf.xml b/man/loader.conf.xml
index 8e67c2bc1ae38..22deb8568b718 100644
--- a/man/loader.conf.xml
+++ b/man/loader.conf.xml
@@ -399,6 +399,36 @@ sbvarsign --attr "${attr}" --key KEK.key --cert KEK.pem --output db.auth db db.e
 
         <xi:include href="version-info.xml" xpointer="v251"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term>reboot-on-error</term>
+
+        <listitem><para>Controls auto reboot in case the selected entry fails to start.
+
+        <variablelist>
+          <varlistentry>
+            <term><option>yes</option></term>
+            <listitem><para>Reboot the system if the selected boot entry failed to start.</para></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>no</option></term>
+            <listitem><para>Don't reboot - pass control back to EFI firmware.</para></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>auto</option></term>
+            <listitem>
+            <para>Perform the reboot if and only if boot counting is enabled for this entry and the tries left counter wasn't already at 0.</para>
+            <para>This is the default, as it is typically a safe option, that ensures a clean measurement log on each boot attempt, but also does not risk an unbounded reboot loop.</para>
+            </listitem>
+          </varlistentry>
+
+        </variablelist>
+        </para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
diff --git a/src/boot/boot.c b/src/boot/boot.c
index cf36c105f669d..af7ab4e79371f 100644
--- a/src/boot/boot.c
+++ b/src/boot/boot.c
@@ -79,6 +79,12 @@ typedef enum LoaderType {
 /* Whether to persistently save the selected entry in an EFI variable, if that's requested. */
 #define LOADER_TYPE_SAVE_ENTRY(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_UKI_URL, LOADER_TYPE2_UKI)
 
+typedef enum {
+        REBOOT_NO,
+        REBOOT_YES,
+        REBOOT_AUTO,
+} RebootOnError;
+
 typedef struct BootEntry {
         char16_t *id;         /* The unique identifier for this entry (typically the filename of the file defining the entry, possibly suffixed with a profile id) */
         char16_t *id_without_profile; /* same, but without any profile id suffixed */
@@ -123,6 +129,7 @@ typedef struct {
         bool auto_poweroff;
         bool auto_reboot;
         bool reboot_for_bitlocker;
+        RebootOnError reboot_on_error;
         secure_boot_enroll secure_boot_enroll;
         bool force_menu;
         bool use_saved_entry;
@@ -317,6 +324,20 @@ static void print_status(Config *config, char16_t *loaded_image_path) {
         printf("                  beep: %ls\n", yes_no(config->beep));
         printf("  reboot-for-bitlocker: %ls\n", yes_no(config->reboot_for_bitlocker));
 
+        switch (config->reboot_on_error) {
+        case REBOOT_NO:
+                printf("       reboot-on-error: no\n");
+                break;
+        case REBOOT_YES:
+                printf("       reboot-on-error: yes\n");
+                break;
+        case REBOOT_AUTO:
+                printf("       reboot-on-error: auto\n");
+                break;
+        default:
+                assert_not_reached();
+        }
+
         switch (config->secure_boot_enroll) {
         case ENROLL_OFF:
                 printf("    secure-boot-enroll: off\n");
@@ -1055,6 +1076,17 @@ static void config_defaults_load_from_file(Config *config, char *content) {
                                 log_error("Error parsing 'reboot-for-bitlocker' config option, ignoring: %s",
                                           value);
 
+                } else if (streq8(key, "reboot-on-error")) {
+                        if (streq8(value, "auto"))
+                                config->reboot_on_error = REBOOT_AUTO;
+                        else {
+                                bool reboot_yes_no;
+                                if (!parse_boolean(value, &reboot_yes_no))
+                                        log_error("Error parsing 'reboot-on-error' config option, ignoring: %s", value);
+
+                                config->reboot_on_error = reboot_yes_no ? REBOOT_YES : REBOOT_NO;
+                        }
+
                 } else if (streq8(key, "secure-boot-enroll")) {
                         if (streq8(value, "manual"))
                                 config->secure_boot_enroll = ENROLL_MANUAL;
@@ -1421,6 +1453,7 @@ static void config_load_defaults(Config *config, EFI_FILE *root_dir) {
                 .editor = true,
                 .auto_entries = true,
                 .auto_firmware = true,
+                .reboot_on_error = REBOOT_AUTO,
                 .secure_boot_enroll = ENROLL_IF_SAFE,
                 .idx_default_efivar = IDX_INVALID,
                 .console_mode = CONSOLE_MODE_KEEP,
@@ -2998,8 +3031,14 @@ static EFI_STATUS run(EFI_HANDLE image) {
                         (void) process_random_seed(root_dir);
 
                 err = ASSERT_PTR(entry->call)(entry, root_dir, image);
-                if (err != EFI_SUCCESS)
+                if (err != EFI_SUCCESS) {
+                        if (config.reboot_on_error == REBOOT_YES || (config.reboot_on_error == REBOOT_AUTO && entry->tries_left > 0)) {
+                                printf("Failed to start boot entry. Rebooting in 5s.\n");
+                                BS->Stall(5 * 1000 * 1000);
+                                (void) call_reboot_system(/* entry= */ NULL, /* root_dir= */ NULL, /* parent_image= */ NULL);
+                        }
                         return err;
+                }
 
                 menu = true;
                 config.timeout_sec = 0;
diff --git a/tools/command_ignorelist b/tools/command_ignorelist
index 5e746a46659a3..5ebdd2b7b48db 100644
--- a/tools/command_ignorelist
+++ b/tools/command_ignorelist
@@ -578,3 +578,6 @@ file-hierarchy.xml /refsect1[title="Home Directory"]/variablelist/varlistentry[t
 systemd-measure.xml /refsect1[title="Options"]/variablelist/varlistentry[term="--linux=PATH"]
 systemd-nspawn.xml /refsect1[title="Hotkeys"]/variablelist/varlistentry[term="Ctrl-] Ctrl-] Ctrl-]"]
 run0.xml /refsect1[title="Options"]/variablelist/varlistentry[term="--pty"]
+loader.conf.xml /refsect1[title="Options"]/variablelist/varlistentry[term="reboot-on-error"]/listitem/para/variablelist/varlistentry[term="yes"]
+loader.conf.xml /refsect1[title="Options"]/variablelist/varlistentry[term="reboot-on-error"]/listitem/para/variablelist/varlistentry[term="no"]
+loader.conf.xml /refsect1[title="Options"]/variablelist/varlistentry[term="reboot-on-error"]/listitem/para/variablelist/varlistentry[term="auto"]


Reviews:
user: login: yuwata
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-11-14T03:56:02Z
user: login: nkraetzschmar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T12:08:08Z
user: login: nkraetzschmar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T12:08:46Z
user: login: nkraetzschmar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T12:50:04Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T17:41:45Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T17:42:47Z
user: login: poettering
body: I think the concept makes a lot of sense, reviewed now.

Sorry for taking so long
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-03T17:43:04Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:28:05Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:29:27Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:31:17Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:32:18Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:32:42Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:35:36Z
user: login: poettering
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:35:50Z

Review comments:
diff_hunk: @@ -1305,6 +1311,7 @@ static void config_defaults_load_from_file(Config *config, char *content) {
                                 }
                                 config->console_mode = u;
                         }
+
path: src/boot/boot.c
user: login: yuwata
body: Please do not insert an unnecessary empty line.
created_at: 2024-11-14T03:50:38Z
author_association: MEMBER
diff_hunk: @@ -2950,8 +2957,13 @@ static EFI_STATUS run(EFI_HANDLE image) {
                 (void) process_random_seed(root_dir);
 
                 err = image_start(image, entry);
-                if (err != EFI_SUCCESS)
-                        return err;
+                if (err != EFI_SUCCESS) {
+                        log_error_status(err, "Error starting image: %m");
path: src/boot/boot.c
user: login: yuwata
body: image_start() already logs when failed. Not necessary to log again here.
created_at: 2024-11-14T03:54:28Z
author_association: MEMBER
diff_hunk: @@ -2950,8 +2957,13 @@ static EFI_STATUS run(EFI_HANDLE image) {
                 (void) process_random_seed(root_dir);
 
                 err = image_start(image, entry);
-                if (err != EFI_SUCCESS)
-                        return err;
+                if (err != EFI_SUCCESS) {
+                        log_error_status(err, "Error starting image: %m");
+                        if (config.reboot_on_err)
+                                reboot_system();
path: src/boot/boot.c
user: login: yuwata
body: Rather than introducing the new knob and reboot system, maybe better to continue the loop on failure unconditionally.
created_at: 2024-11-14T03:55:56Z
author_association: MEMBER
diff_hunk: @@ -1305,6 +1311,7 @@ static void config_defaults_load_from_file(Config *config, char *content) {
                                 }
                                 config->console_mode = u;
                         }
+
path: src/boot/boot.c
user: login: nkraetzschmar
body: updated PR to remove this
created_at: 2024-11-14T12:08:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2950,8 +2957,13 @@ static EFI_STATUS run(EFI_HANDLE image) {
                 (void) process_random_seed(root_dir);
 
                 err = image_start(image, entry);
-                if (err != EFI_SUCCESS)
-                        return err;
+                if (err != EFI_SUCCESS) {
+                        log_error_status(err, "Error starting image: %m");
path: src/boot/boot.c
user: login: nkraetzschmar
body: updated PR to remove this
created_at: 2024-11-14T12:08:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2950,8 +2957,13 @@ static EFI_STATUS run(EFI_HANDLE image) {
                 (void) process_random_seed(root_dir);
 
                 err = image_start(image, entry);
-                if (err != EFI_SUCCESS)
-                        return err;
+                if (err != EFI_SUCCESS) {
+                        log_error_status(err, "Error starting image: %m");
+                        if (config.reboot_on_err)
+                                reboot_system();
path: src/boot/boot.c
user: login: nkraetzschmar
body: Thanks for your feedback. Continuing with the next entry on failure might not be ideal in all scenarios. If the EFI stub of a UKI progresses far enough before returning an error, it may have already extended measurements into PCR 11 and 12.

In systems using TPM-based disk encryption bound to PCR 11[^1], this could prevent the fallback entry from decrypting the disk successfully. Rebooting will reset the PCRs to prevent such issues.

[^1]: as described in https://0pointer.net/blog/brave-new-trusted-boot-world.html
created_at: 2024-11-14T12:50:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2950,8 +2956,12 @@ static EFI_STATUS run(EFI_HANDLE image) {
                 (void) process_random_seed(root_dir);
 
                 err = image_start(image, entry);
-                if (err != EFI_SUCCESS)
-                        return err;
+                if (err != EFI_SUCCESS) {
+                        if (config.reboot_on_err)
+                                reboot_system();
+                        else
path: src/boot/boot.c
user: login: poettering
body: you can drop the `else`
created_at: 2025-03-03T17:41:45Z
author_association: MEMBER
diff_hunk: @@ -2950,8 +2956,12 @@ static EFI_STATUS run(EFI_HANDLE image) {
                 (void) process_random_seed(root_dir);
 
                 err = image_start(image, entry);
-                if (err != EFI_SUCCESS)
-                        return err;
+                if (err != EFI_SUCCESS) {
+                        if (config.reboot_on_err)
+                                reboot_system();
path: src/boot/boot.c
user: login: poettering
body: before rebooting we should print a message explaining the situation, and then sleep for 5s or so (and saying so in the message), so that users can see what's going on. otherwise their screen would be flushed immediately quite likely with no chance to debug this.
created_at: 2025-03-03T17:42:47Z
author_association: MEMBER
diff_hunk: @@ -399,6 +399,14 @@ sbvarsign --attr "${attr}" --key KEK.key --cert KEK.pem --output db.auth db db.e
 
         <xi:include href="version-info.xml" xpointer="v251"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term>reboot-on-error</term>
+
+        <listitem><para>Takes a boolean argument. Enable or disable (the default) auto reboot in case the selected entry fails to start.</para>
+
+        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
path: man/loader.conf.xml
user: login: poettering
body: 257 is already released, cannot change the past. bump to 258
created_at: 2025-03-06T16:28:05Z
author_association: MEMBER
diff_hunk: @@ -399,6 +399,14 @@ sbvarsign --attr "${attr}" --key KEK.key --cert KEK.pem --output db.auth db db.e
 
         <xi:include href="version-info.xml" xpointer="v251"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term>reboot-on-error</term>
+
+        <listitem><para>Takes a boolean argument. Enable or disable (the default) auto reboot in case the selected entry fails to start.</para>
path: man/loader.conf.xml
user: login: poettering
body: this is a bit confusing. i.e. i guess `(the default)` is supposed to apply to the `disable` left of it. But it could be misunderstood as if it applies to `auto reboot` to the right of it. Which would have opposing meaning.

Maybe just add another sentence: Defaults to off.
created_at: 2025-03-06T16:29:27Z
author_association: MEMBER
diff_hunk: @@ -54,6 +54,12 @@ typedef enum LoaderType {
         _LOADER_TYPE_MAX,
 } LoaderType;
 
+typedef enum {
+        REBOOT_NO = false,
+        REBOOT_YES = true,
+        REBOOT_AUTO,
+} reboot_on_err;
path: src/boot/boot.c
user: login: poettering
body: we usually use CamelCase for type names. and we name the enum and the typedef the same usually. also, we don't like needless abbreviations. Hence change this to RebootOnError please
created_at: 2025-03-06T16:31:17Z
author_association: MEMBER
diff_hunk: @@ -1279,7 +1299,9 @@ static void config_defaults_load_from_file(Config *config, char *content) {
                                           value);
 
                 } else if (streq8(key, "reboot-on-error")) {
-                        if (!parse_boolean(value, &config->reboot_on_err))
+                        if (streq8(value, "auto"))
+                                config->reboot_on_err = REBOOT_AUTO;
+                        else if (!parse_boolean(value, (bool *) &config->reboot_on_err))
path: src/boot/boot.c
user: login: poettering
body: pleas don't do that cast. yes, in practice bool and the enum will both be implemented as int. but that's not universally the case. just write it to a temporarily bool variable please, and assign it to reboot_on_error then
created_at: 2025-03-06T16:32:18Z
author_association: MEMBER
diff_hunk: @@ -97,7 +103,7 @@ typedef struct {
         bool auto_poweroff;
         bool auto_reboot;
         bool reboot_for_bitlocker;
-        bool reboot_on_err;
+        reboot_on_err reboot_on_err;
path: src/boot/boot.c
user: login: poettering
body: please name the field like the configuration setting, i.e. reboot_on_error.
created_at: 2025-03-06T16:32:42Z
author_association: MEMBER
diff_hunk: @@ -403,9 +403,27 @@ sbvarsign --attr "${attr}" --key KEK.key --cert KEK.pem --output db.auth db db.e
       <varlistentry>
         <term>reboot-on-error</term>
 
-        <listitem><para>Takes a boolean argument. Enable or disable (the default) auto reboot in case the selected entry fails to start.</para>
+        <listitem><para>Controls auto reboot in case the selected entry fails to start.
 
-        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+        <variablelist>
+          <varlistentry>
+            <term><option>yes</option></term>
+            <listitem><para>Reboot the system if the selected boot entry failed to start.</para></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>no</option></term>
+            <listitem><para>Don't reboot - pass control back to EFI firmware.</para></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>auto</option></term>
+            <listitem><para>Perform the reboot if and only if boot counting is enabled for this entry. (This is the default)</para></listitem>
path: man/loader.conf.xml
user: login: poettering
body: maybe be more explicit here, also say that the boot counter cannot have hit zero yet. and also maybe give a very brief explanation why. i.e. add "This is typically a safe option, as it ensures a clean measurement log on each boot attempt, but also does not risk an unbounded reboot loop.
created_at: 2025-03-06T16:35:36Z
author_association: MEMBER
diff_hunk: @@ -403,9 +403,27 @@ sbvarsign --attr "${attr}" --key KEK.key --cert KEK.pem --output db.auth db db.e
       <varlistentry>
         <term>reboot-on-error</term>
 
-        <listitem><para>Takes a boolean argument. Enable or disable (the default) auto reboot in case the selected entry fails to start.</para>
+        <listitem><para>Controls auto reboot in case the selected entry fails to start.
 
-        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+        <variablelist>
+          <varlistentry>
+            <term><option>yes</option></term>
+            <listitem><para>Reboot the system if the selected boot entry failed to start.</para></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>no</option></term>
+            <listitem><para>Don't reboot - pass control back to EFI firmware.</para></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><option>auto</option></term>
+            <listitem><para>Perform the reboot if and only if boot counting is enabled for this entry. (This is the default)</para></listitem>
path: man/loader.conf.xml
user: login: poettering
body: please end the 2nd sentence in a full stop
created_at: 2025-03-06T16:35:50Z
author_association: MEMBER

PR comments:
user: login: bluca
created_at: 2024-11-12T23:40:57Z
body: > In environments like QEMU with EDK2 firmware, this results in user being dropped into an EFI shell. This behavior is undesirable for unattended systems where manual intervention is impractical.

Why is the EFI shell enabled in such unattended systems in the first place?
author_association: MEMBER
user: login: nkraetzschmar
created_at: 2024-11-13T00:06:22Z
body: Being dropped into the EFI shell is the default behaviour for OVMF/edk2 (at least the version shipped by debian). However, even explicitly disabling this in the firmware settings just results in

```
../src/boot/linux.c:120@linux_exec: Bad kernel image: Load error
BdsDxe: No bootable option or device was found.
BdsDxe: Press any key to enter the Boot Manager Menu.
```

so user interaction is still required, now just being dropped into the firmware settings rather than a shell.
This behaviour might be different across various firmwares, some might automatically reboot as desired, but adding an option to ensure this happens would make it work regardless of firmware implementation.
author_association: CONTRIBUTOR
user: login: poettering
created_at: 2025-03-03T17:41:02Z
body: So i think the concept makes sense, and probably should even be the default in some cases. Yes, the PCR thing matters, if we fail to boot some image X then this quite likely will show up in the measurement logs which might affect image Y's success later if we bind to those measurements in FDE or so.

however, blinldy rebooting would also suck in many cases, because on next boot we'd just boot up the same entry again and then reboot again and so on, with no chance to recover.

Hence, I'd prefer if this logic would be tweaked like this:

1. yes, there should be a config file setting, to enable or disable reboot on entry failure. but it should have a third value "auto", and it should be the default actually.
2. In "auto" mode we should reboot if entries fail where we can guarantee we make progress. Specifically: if an entry with boot counting enabled fails, then we should reboot after decreasing the counter and syncing it. However, if boot counting is off or the counter is already zero, then we should continue with the next boot, because otherwise we would not make any progress.

Does that make sense?
author_association: MEMBER
user: login: nkraetzschmar
created_at: 2025-03-06T09:48:39Z
body: Thanks for looking into it.

I've added the print message and sleep functionality as requested. Although I'm not sure if the way I've done it simply via `BS->Stall` is ideal, or if this would be better done by something along the lines of [secure-boot.c:94](https://github.com/systemd/systemd/blob/c0af4859d006fd45fa2d4dbba2827975a7514e00/src/boot/secure-boot.c#L94) => allowing the user to abort the reboot and continue with the default EFI firmware behaviour. I'd love some input on this.

The idea of the "auto" mode sounds great, I'll try to implement that later.
author_association: CONTRIBUTOR
user: login: nkraetzschmar
created_at: 2025-03-06T16:20:32Z
body: Added logic for the `auto` mode now
author_association: CONTRIBUTOR
user: login: poettering
created_at: 2025-03-06T16:38:38Z
body: > I've added the print message and sleep functionality as requested. Although I'm not sure if the way I've done it simply via `BS->Stall` is ideal, or if this would be better done by something along the lines of [secure-boot.c:94](https://github.com/systemd/systemd/blob/c0af4859d006fd45fa2d4dbba2827975a7514e00/src/boot/secure-boot.c#L94) => allowing the user to abort the reboot and continue with the default EFI firmware behaviour. I'd love some input on this.

if you want to add such a more interactive thing, by all means, that would be welcome, but also: i am fine merging things without.

would require some minor refactoring, i.e. there should be a common helper call that encapsulates the query
author_association: MEMBER
user: login: poettering
created_at: 2025-03-06T16:39:55Z
body: also, please squash these three commits. in particular as some of them just fix issues in the earlier ones. 

we prefer "perfect" PRs, i.e. where each commit encapsulates a logical step, not necessarily a historical one. Where things are perfectly bisectable.
author_association: MEMBER
user: login: nkraetzschmar
created_at: 2025-03-07T08:40:06Z
body: applied the requested changes and rebased (+ squashed) on latest commit from main
author_association: CONTRIBUTOR
user: login: poettering
created_at: 2025-03-11T06:09:22Z
body: (please hit "resolve conversation" on  all items you have addressed)
author_association: MEMBER
user: login: poettering
created_at: 2025-03-11T06:10:11Z
body: `check-version-history` test failed. your man page addition is missing the version info annotation
author_association: MEMBER
user: login: poettering
created_at: 2025-03-11T06:11:41Z
body: > `check-version-history` test failed. your man page addition is missing the version info annotation

or actually it doesn't, the checker is just a bit dumb, it doesn#t understand that your inner enumeration doesn't need it because your outer entry has it. hence maybe add this to the exception list. the checker output actually gives you a hint how to do that.
author_association: MEMBER
user: login: poettering
created_at: 2025-03-11T06:13:27Z
body: lgtm now, but the man page version thing needs to be addressed still
author_association: MEMBER