Skip to content

[bug]: create_boot_options reads /etc/kernel/cmdline.d/ fragments twice for the running snapshot, duplicating kernel parameters in the boot entry #411

Description

@Xarianne

Pre‑submission checklist

  • I searched existing issues and discussions to avoid duplicates
  • I tested with the latest released packages or the current git (if applicable)
  • I can reproduce this on a clean boot or after reboot

Short summary

A cmdline.d fragment is appended twice to a boot entry's options line when the entry belongs to the currently-booted snapshot, because the etc/kernel/cmdline.d loop globs both the snapshot path and the live path (which resolve to the same directory for the running root) with no dedup guard.

Observed behavior

After placing a kernel parameter in /etc/kernel/cmdline.d/<file>.conf and running sdbootutil update-entry (or update-all-entries), the generated boot entry for the currently running snapshot contains that parameter twice in its options line.

Example with /etc/kernel/cmdline.d/amdgpu.conf containing amdgpu.ppfeaturemask=0xffffffff:

$ grep '^options' /boot/loader/entries/opensuse-tumbleweed-7.1.5-1-default-89.conf
options    root=/dev/mapper/cr_root splash=silent mitigations=auto quiet security=selinux amdgpu.ppfeaturemask=0xffffffff amdgpu.ppfeaturemask=0xffffffff rootflags=subvol=@/.snapshots/89/snapshot

Only the running snapshot's entry is affected. Entries for other snapshots (e.g. 93, 94, 95) contain the parameter 0 times (or once if regenerated while not the running root) — they are not duplicated.

Expected behavior

Each cmdline.d fragment should be read exactly once, regardless of whether the processed subvol is the currently running snapshot. The options line should contain amdgpu.ppfeaturemask=0xffffffff a single time.

Steps to reproduce

  1. Boot openSUSE Tumbleweed with systemd-boot on a btrfs+snapper root (i.e. booted from a snapper snapshot, rootflags=subvol=@/.snapshots/N/snapshot).
  2. Create a cmdline.d fragment:
    sudo mkdir -p /etc/kernel/cmdline.d
    echo 'amdgpu.ppfeaturemask=0xffffffff' | sudo tee /etc/kernel/cmdline.d/amdgpu.conf
    
  3. Regenerate the running snapshot's entry:
    sudo sdbootutil update-entry "$(uname -r)"
    
  4. Inspect the entry for the running snapshot:
    sudo grep -c amdgpu.ppfeaturemask /boot/loader/entries/*-"$(uname -r)"-*.conf
    
    • The running snapshot's entry reports 2.
    • Other snapshots' entries report 0.

Root cause (analysis)

In create_boot_options(), the etc cmdline.d loop globs both the snapshot path and the live path in the same for, with no dedup guard:

for i in "${subvol:1}"/etc/kernel/cmdline.d/* /etc/kernel/cmdline.d/*; do
    dbg_cat "$i"
    [ -s "$i" ] || continue
    while read -r line; do
        [[ "$line" == '#'* ]] && continue
        cmdline="${cmdline:+$cmdline }${line}"
    done < "$i"
done

When subvol is the currently running snapshot, ${subvol:1}/etc/kernel/cmdline.d/* and /etc/kernel/cmdline.d/* resolve to the same physical directory (the running root's /etc/kernel/cmdline.d/), so every fragment is read and appended twice.

The usr/kernel/cmdline.d loop immediately above already carries a guard for the analogous collision:

for i in "${subvol:1}"/usr/kernel/cmdline.d/* /usr/kernel/cmdline.d/*; do
    [ -e "${subvol:1}/etc/kernel/cmdline.d/$(basename "$i")" ] && continue
    ...
done

The etc loop is missing an equivalent guard.

Impact

  • Functionally harmless for identical-value parameters (the kernel applies the last occurrence, which is the same value), but it clutters the options line and is misleading in bootctl list output and entry-diffing tools.
  • A duplicated parameter would silently resolve to "last wins" if a fragment and the flat /etc/kernel/cmdline (or two fragments) ever disagree, masking conflicts instead of surfacing them.

Product / distro

Tumbleweed

Architecture

x86_64

Platform

Bare metal

Bootloader

systemd-boot

sdbootutil version

sdbootutil-1+git20260714.d9bb736-1.2.x86_64

System specs (paste output)

## os-release
NAME="openSUSE Tumbleweed"
# VERSION="20260731"
ID="opensuse-tumbleweed"
ID_LIKE="opensuse suse"
VERSION_ID="20260731"
PRETTY_NAME="openSUSE Tumbleweed"
ANSI_COLOR="0;32"
CPE_NAME="cpe:2.3:o:opensuse:tumbleweed:20260731:*:*:*:*:*:*:*"
BUG_REPORT_URL="https://bugzilla.opensuse.org"
SUPPORT_URL="https://bugs.opensuse.org"
HOME_URL="https://www.opensuse.org"
DOCUMENTATION_URL="https://en.opensuse.org/Portal:Tumbleweed"
LOGO="distributor-logo-Tumbleweed"

## uname
Linux gecko 7.1.5-1-default #1 SMP PREEMPT_DYNAMIC Tue Jul 28 09:31:57 UTC 2026 (862e13e) x86_64 x86_64 x86_64 GNU/Linux

## bootctl status
System:
      Firmware: UEFI 2.110 (American Megatrends 5.41)
 Firmware Arch: x64
   Secure Boot: enabled (user)
  TPM2 Support: yes
  Measured UKI: no
   Measured OS: no
   Boot into FW: supported
 Platform Lang: en_US.UTF-8

Current Boot Loader:
        Product: systemd-boot 261.2
       Features: ✓ Boot counting
                 ✓ Menu timeout control
                 ✓ One-shot menu timeout control
                 ✓ Default entry control
                 ✓ One-shot menu entry control
                 ✓ Support for XBOOTLDR partition
                 ✓ Support for passing random seed to OS
                 ✓ Load drop-in drivers
                 ✓ Support Type #1 sort-key field
                 ✓ Support @saved pseudo-entry
                 ✓ Support Type #1 devicetree field
                 ✓ Enroll SecureBoot keys
                 ✓ Retain SHIM protocols

## lsblk
NAME          SIZE TYPE  FSTYPE      MOUNTPOINTS
nvme1n1       1.8T disk
├─nvme1n1p1   1.3T part  xfs
├─nvme1n1p2     1G part  vfat        /boot
└─nvme1n1p3 485.3G part  crypto_LUKS
  └─cr_root 485.3G crypt btrfs       /var
                                    /usr/local
                                    /root
                                    /srv
                                    /home
                                    /opt
                                    /.snapshots
                                    /
└─nvme1n1p4     2G part  crypto_LUKS
  └─cr_swap     2G crypt swap        [SWAP]

## systemd-detect-virt
none

## sdbootutil version
sdbootutil-1+git20260714.d9bb736-1.2.x86_64

Debug trace excerpts (sanitized)

Obtained with `sudo sdbootutil --start-trace-code update-entry 7.1.5-1-default`, then trimmed to the relevant `create_boot_options` lines. 


sdbootutil:1270:create_boot_options: for i in "${subvol:1}"/etc/kernel/cmdline.d/* /etc/kernel/cmdline.d/*
sdbootutil:1271:create_boot_options: dbg_cat /.snapshots/89/snapshot/etc/kernel/cmdline.d/amdgpu.conf
sdbootutil:301:dbg_cat: cat /.snapshots/89/snapshot/etc/kernel/cmdline.d/amdgpu.conf
sdbootutil:1275:create_boot_options: cmdline='root=/dev/nvme0n1p3 splash=silent mitigations=auto quiet security=selinux amdgpu.ppfeaturemask=0xffffffff'
sdbootutil:1270:create_boot_options: for i in "${subvol:1}"/etc/kernel/cmdline.d/* /etc/kernel/cmdline.d/*
sdbootutil:1271:create_boot_options: dbg_cat /etc/kernel/cmdline.d/amdgpu.conf
sdbootutil:301:dbg_cat: cat /etc/kernel/cmdline.d/amdgpu.conf
sdbootutil:1275:create_boot_options: cmdline='root=/dev/nvme0n1p3 splash=silent mitigations=auto quiet security=selinux amdgpu.ppfeaturemask=0xffffffff amdgpu.ppfeaturemask=0xffffffff'


`/.snapshots/89/snapshot/etc/kernel/cmdline.d/amdgpu.conf` and `/etc/kernel/cmdline.d/amdgpu.conf` are the same inode (the running root is `@/.snapshots/89/snapshot`), confirming the same physical file is read twice within one `create_boot_options` call.

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions