request_firmware() from a file's ->open() shares that open()'s symlink budget, and a split firmware runs out
scope: generic · severity: finding · confidence: proven · subsystem: kernel
Do not load firmware from a file’s ->open(). ->open() runs inside
the path walk that opened the node, and every lookup the firmware loader
makes from there is nested in that walk. fs/namei.c makes nested lookups
share one symlink count, to bound recursion: __set_nameidata() starts the
inner walk at the outer one’s total_link_count, restore_nameidata()
copies it back. The limit, MAXSYMLINKS, is 40, and it covers everything
done during the one open().
The loader tries each search path in turn: firmware_class.path, the two
/lib/firmware/updates paths, /lib/firmware/<release>, /lib/firmware.
On a merged-/usr rootfs /lib is a symlink, so each attempt costs one
symlink, and a file found only in the last path costs five. pmOS sets
firmware_class.path to /lib/firmware/postmarketos, which usually does
not exist, so that is the usual cost. Eight files use up the budget, and
the ninth fails with -ELOOP on every path, including one that doesn’t
exist and should have said -ENOENT. That is how to recognise it: the
first files of a split .mdt + .bNN image load fine and a later one
fails with -40 on every path. Drivers that load at probe time or from a
worker never see this, which is why “the same firmware directory works for
ath10k” proves nothing here.
Wrong fixes that appear to work: pointing firmware_class.path at
/usr/lib/firmware (the first path now succeeds with no symlink, but the
driver still fails for a user whose files live in /lib/firmware/updates),
or shipping fewer, larger files.
Fix: load from somewhere without an outer walk, such as the first ioctl,
probe, or a workqueue. linux-ws fpc1020.c loads its trustlet on the first
FPC_TEE_IOC_XFER.
