Pre‑submission checklist
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
- Boot openSUSE Tumbleweed with systemd-boot on a btrfs+snapper root (i.e. booted from a snapper snapshot,
rootflags=subvol=@/.snapshots/N/snapshot).
- Create a cmdline.d fragment:
sudo mkdir -p /etc/kernel/cmdline.d
echo 'amdgpu.ppfeaturemask=0xffffffff' | sudo tee /etc/kernel/cmdline.d/amdgpu.conf
- Regenerate the running snapshot's entry:
sudo sdbootutil update-entry "$(uname -r)"
- 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
Pre‑submission checklist
Short summary
A
cmdline.dfragment is appended twice to a boot entry'soptionsline when the entry belongs to the currently-booted snapshot, because theetc/kernel/cmdline.dloop 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>.confand runningsdbootutil update-entry(orupdate-all-entries), the generated boot entry for the currently running snapshot contains that parameter twice in itsoptionsline.Example with
/etc/kernel/cmdline.d/amdgpu.confcontainingamdgpu.ppfeaturemask=0xffffffff: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.dfragment should be read exactly once, regardless of whether the processedsubvolis the currently running snapshot. Theoptionsline should containamdgpu.ppfeaturemask=0xffffffffa single time.Steps to reproduce
rootflags=subvol=@/.snapshots/N/snapshot).2.0.Root cause (analysis)
In
create_boot_options(), theetccmdline.dloop globs both the snapshot path and the live path in the samefor, with no dedup guard:When
subvolis 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.dloop immediately above already carries a guard for the analogous collision:The
etcloop is missing an equivalent guard.Impact
optionsline and is misleading inbootctl listoutput and entry-diffing tools./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)
Debug trace excerpts (sanitized)
Additional context
cmdline.dsupport, closing [feat]: support for cmdline parameters in configuration directory #362).usr/kernel/cmdline.dloop already has the analogous dedup guard ([ -e "${subvol:1}/etc/kernel/cmdline.d/$(basename "$i")" ] && continue); theetc/kernel/cmdline.dloop does not.