Skip to content

fix(squash-lib): mirror host merged-usr symlinks in the squash loader - #2603

Open
prabhakarpujeri wants to merge 1 commit into
dracut-ng:mainfrom
prabhakarpujeri:fix/squash-lib-initrd-merged-usr
Open

fix(squash-lib): mirror host merged-usr symlinks in the squash loader#2603
prabhakarpujeri wants to merge 1 commit into
dracut-ng:mainfrom
prabhakarpujeri:fix/squash-lib-initrd-merged-usr

Conversation

@prabhakarpujeri

Copy link
Copy Markdown

Problem

The squash loader (squash-lib) assembles its own root directory and creates usr/bin, usr/sbin and usr/lib as plain directories, only symlinking the top-level bin/sbin/lib to their usr counterparts:

for _dir in squash usr/bin usr/sbin usr/lib; do
    mkdir -p "$squashdir/$_dir"
    [[ $_dir == usr/* ]] && ln_r "/$_dir" "${_dir#usr}"
done

On merged-usr hosts where /usr/sbin itself is a symlink (/usr/sbin -> bin, e.g. Fedora 42+), binaries resolved on the host's unified tree are installed into only one of the two split trees in the loader. In particular the busybox module creates its applet links with a shell-side ln_r /usr/bin/busybox "$_path" (no symlink resolution by dracut-install in that path), so sh physically lands in the loader's real usr/sbin/ directory while /bin/sh == /usr/bin/sh stays missing. The kernel then cannot execute the shebang of init-squash.sh:

Run /init as init process
Failed to execute /init (error -2)

This is what breaks the kdump squashfs image on Fedora 42+ in #2454. The main initramfs path does not hit this because create_directories() in dracut.sh mirrors the host layout (if [ -L "/$d" ] → install as symlink).

Fix

Mirror the host layout in squash_install(): if the host directory is a symlink, replicate the same symlink in the loader directory; otherwise create a real directory as before. Binaries installed to either merged tree (usr/sbin/foo or usr/bin/foo) then always land in the single shared tree, matching host semantics.

Root-cause alternative to #2455: instead of special-casing the busybox sh symlink, this fixes the directory structure itself, so it covers every binary — not just sh — for both the busybox and the non-busybox loader paths.

Testing

Reproduced the exact failure from #2454 and verified the fix on two Fedora 44 systems (qemu/KVM guest boot on real hardware, busybox package installed, squashfs handler available, PATH with /usr/sbin first as in the kdump build environment):

check before after
loader layout usr/sbin real dir, sh -> ../bin/busybox stranded there, usr/bin/sh absent usr/sbin -> bin, usr/bin/sh -> busybox
exec test (chroot into extracted loader tree — the kernel's ENOENT path) failed to run command '/init': No such file or directory (error -2) /bin/sh executes (exit 0)
qemu boot of the image Run /init as init processFailed to execute /init (error -2) /init executes, squashfs mounts, switch_root into the inner systemd initramfs, boot proceeds to initrd-switch-root.target (ends there only because the synthetic test image has no real root= device)

Boot logs were captured from the VM serial console for both runs.

shellcheck modules.d/88squash-lib/module-setup.sh clean.

Fixes: #2454

@prabhakarpujeri
prabhakarpujeri requested a review from a team as a code owner August 17, 2026 13:10
@github-actions github-actions Bot added modules Issue tracker for all modules squash-lib labels Aug 17, 2026
@devkontrol

Copy link
Copy Markdown
Collaborator

The main initramfs path does not hit this because create_directories() in dracut.sh mirrors the host layout
Mirror the host layout in squash_install()

Why not use the exact same code/steps in squash_install that is used by create_directories ?

Can we use inst_symlink instead of ln -sfn ?

I do not mind duplicating some code if we need to.

@devkontrol

Copy link
Copy Markdown
Collaborator

CC @challvy

@prabhakarpujeri

Copy link
Copy Markdown
Author

Thanks for the review! I tried both suggestions; unfortunately they don't work in this stage of the build:

  1. inst_symlink() always installs with -D $initdir and does not honor $dstdir; for a symlink source it first installs the resolved target into the main initdir and creates the link there — a plain call cannot stage the link inside $squashdir (which mvs to the final image root).
  2. create_directories "/squash_root" ... does route through inst_symlink, but the links it produces via dracut-install's convert_abs_rel() are relative between the staging paths (e.g. usr/sbin -> ../../usr/bin). That text only resolves at boot time thanks to root clamping; during the build it points outside $squashdir, so the subsequent busybox applet loop (ln_r /usr/bin/busybox "$_path") fails for every link: ln: failed to create symbolic link: No such file or directory — empirically 0 of the 247 applet links end up in the image.
  3. Overriding initdir locally for the call is not possible either: initdir is readonly (dracut.sh:1469).

What this change does instead is the same intent expressed stage-locally: replicate the host's exact symlink (copy the readlink text) into the loader tree, which is valid both while the loader directory is being populated and at boot, and which makes the loader layout match the main initramfs bit-for-bit. Verified by image listings (247 busybox applet links, usr/sbin -> bin) and by qemu boots of the loader before (Failed to execute /init (error -2)) and after (init runs, image mounts, inner systemd initramfs reaches initrd-switch-root.target).

Happy to converge on a different shape if you prefer — as you said, duplicating a few lines here seemed acceptable, and the prefix/indirection layers of create_directories()/inst_symlink() turned out not to match this use.

@prabhakarpujeri
prabhakarpujeri force-pushed the fix/squash-lib-initrd-merged-usr branch from 5450038 to 7548337 Compare August 17, 2026 15:22
@devkontrol

Copy link
Copy Markdown
Collaborator

Thanks @prabhakarpujeri . LGTM. Plan to approve the PR once GitHub is up for the task.

@pvalena pvalena left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

The squash loader assembles its own root directory and creates
usr/bin, usr/sbin and usr/lib as plain directories, only symlinking
the top-level bin/sbin/lib to their usr counterparts.

On merged-usr hosts where /usr/sbin itself is a symlink (/usr/sbin ->
bin, e.g. Fedora 42+), binaries resolved on the host's unified tree
are installed into only one of the two split trees in the loader. In
particular the busybox module places its sh applet symlink at
usr/sbin/sh while /bin/sh == /usr/bin/sh stays missing, so the kernel
cannot execute the shebang of init-squash.sh:

    Run /init as init process
    Failed to execute /init (error -2)

This breaks the kdump squashfs image on Fedora 42+ (issue dracut-ng#2454).

Mirror the host layout instead: if the host directory is a symlink,
replicate the same symlink in the loader directory; otherwise create
a real directory as before. This is the root-cause fix for the
directory-layout problem identified in PR dracut-ng#2455, which proposed to
special-case only the busybox sh symlink.

Reproduced the failure on Fedora 44 (busybox, squashfs, qemu):
before the change the loader contains usr/sbin as a real directory
holding 'sh -> ../bin/busybox' with /usr/bin/sh absent, and booting
the image fails to execute /init with error -2. After the change the
loader has 'usr/sbin -> bin' and 'usr/bin/sh -> busybox', /init
executes, the squash image is mounted and the inner systemd initramfs
reaches initrd-switch-root.target.

Fixes: dracut-ng#2454
@bdrung

bdrung commented Aug 19, 2026

Copy link
Copy Markdown
Member

create_directories could be changed to not use inst_symlink and then create_directories can be used here as well to avoid duplication.

The difference between ln -snf and inst_symlink is that it will also create the target directory.

While looking at the code I found an issue in create_directories: #2619

# (/usr/bin/sh). The target text is copied from the host so
# that the link is valid both at boot time and while the
# loader directory is still being populated.
ln -sfn "$(readlink "${dracutsysrootdir-}/$_dir")" "$squashdir/$_dir"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the symlinks points to a directory that this loop does not create?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. The loop covers exactly the four paths merged-usr symlinks can point into — usr/bin, usr/sbin, usr/lib, squash — so a host symlink's target is always one the loop creates. E.g. on Fedora the host has /usr/sbin -> bin; the loop sees usr/sbin is a link, recreates it as usr/sbin -> bin in the loader, and usr/bin (created by the same loop, either as a real dir or as a symlink as the host demands) is the target bin resolves to. State after the loop on this host: usr/bin dir, usr/sbin -> bin, and busybox + sh reachable from both — which is also verified by the empirical before/after tests in the description (init boots into the inner systemd initramfs with the fix; Failed to execute /init (error -2) without).

Exotic host symlinks pointing outside the loop's set (e.g. /usr/sbin -> ../opt/sbin) would become a dangling symlink in the loader — same as today's behavior, and not harmful (nothing installs through that path, so the link is inert).

@prabhakarpujeri

Copy link
Copy Markdown
Author

Agreed, that's a good follow-up direction. With #2619 (thanks for fixing that) the remaining obstacle for reusing create_directories here is its internal use of inst_symlink — which always stages source resolution with -D $initdir and produces link text between staging paths (usr/sbin -> ../../usr/bin after convert_abs_rel). That text only resolves correctly at boot time, so the busybox applet loop, which installs into $squashdir/usr/sbin/... during the build, hits No such file or directory for every link.

Untangling create_directories from inst_symlink (your idea, not for this PR) would let the loader call it with the squash prefix — happy to fold the loader over to that helper once the coupling is untangled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

modules Issue tracker for all modules squash-lib

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(busybox): ensure /usr/bin/sh symlink is always created

4 participants