File: check-missing-firmware.sh

package info (click to toggle)
hw-detect 1.159
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,628 kB
  • sloc: sh: 1,515; makefile: 98; ansic: 39
file content (431 lines) | stat: -rwxr-xr-x 12,466 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#!/bin/sh
set -e
. /usr/share/debconf/confmodule

DENIED=/tmp/missing-firmware-denied

if [ "x$1" = "x-n" ]; then
	NONINTERACTIVE=1
	shift
else
	NONINTERACTIVE=""
fi

IFACES="$@"

log () {
	logger -t check-missing-firmware "$@"
}

log_output() {
	log-output -t check-missing-firmware "$@"
}

# USB is special, and we don't want to take it all done:
get_usb_module() {
	address="$1"
	device="/sys/bus/usb/devices/$address"

	# Make sure there's a single subdirectory (e.g. 4-1.5:1.0 below 4-1.5):
	subdirs=$(find -L "$device" -maxdepth 1 -type d -name "$address:*")
	subdirs_n=$(echo "$subdirs" | wc -w)
	if [ $(echo "$subdirs" | wc -w) != 1 ]; then
		log "failed to perform usb $address lookup (got: $subdirs_n entries, expected: 1)"
		log "=> sticking with the usb module"
		echo 'usb'
		return
	fi

	# Make sure driver resolution returns something:
	driver=$(basename $(readlink "$subdirs/driver") 2>/dev/null)
	if [ "$driver" = "" ]; then
		log "failed to perform usb $address lookup (no driver found)"
		log "=> sticking with the usb module"
		echo 'usb'
		return
	fi
	echo $driver
}

# MHI is special, but different from USB; at least with ath11k_pci,
# /sys/bus/mhi/devices/mhi0* doesn't list anything ath11k-related.
# The mhi module's holders directory lists ath11k_pci and qrtr_mhi
# though!
get_mhi_holders() {
	holders=$(find -L /sys/module/mhi/holders/ -mindepth 1 -maxdepth 1 -exec basename {} ';')
	if [ "$holders" = "" ]; then
		log "failed to perform mhi lookup (no holders found)"
		log "=> sticking with the mhi module"
		echo 'mhi'
	else
		echo $holders
	fi
}

# Some modules only try to load firmware once brought up. So bring up and
# then down any interfaces specified by ethdetect. Don't touch interfaces
# that users might have configured (manually or via preseeding) though!
upnics() {
	for iface in $IFACES; do
		# Don't rely on ip's output, it lacks at least state UP/DOWN:
		sys_iface="/sys/class/net/$iface"
		if grep -qs ^up$ "$sys_iface/operstate"; then
			log "leaving network interface $iface alone (state=up)"
			continue
		elif [ -e "$sys_iface/master" ]; then
			# Most likely bonding:
			master=$(basename $(readlink "$sys_iface/master") 2>/dev/null)
			log "leaving network interface $iface alone (master=${master:-<unknown>})"
			continue
		fi
		log "taking network interface $iface up/down"
		ip link set "$iface" up || true
		ip link set "$iface" down || true
	done
}

# Checks if a given module is a nic module and has an interface that
# is up and has an IP address. Such modules should not be reloaded,
# to avoid taking down the network after it's been configured.
nic_is_configured() {
	module="$1"

	for iface in $(ip -o link show up | cut -d : -f 2); do
		dir="/sys/class/net/$iface/device/driver"
		if [ -e "$dir" ] && [ "$(basename "$(readlink "$dir")")" = "$module" ]; then
			if ip address show scope global dev "$iface" | grep -q 'scope global'; then
				return 0
			fi
		fi
	done

	return 1
}

get_fresh_dmesg() {
	dmesg_file=/tmp/dmesg.txt
	dmesg_ts=/tmp/dmesg-ts.txt

	# Get current dmesg:
	dmesg > $dmesg_file

	# Truncate if needed:
	if [ -f $dmesg_ts ]; then
		# Transform [foo] into \[foo\] to make it possible to search for
		# "^$tspattern" (-F for fixed string doesn't play well with ^ to
		# anchor the pattern on the left):
		tspattern=$(cat $dmesg_ts | sed 's,\[,\\[,;s,\],\\],')
		log "looking at dmesg again, restarting from timestamp: $(cat $dmesg_ts)"

		# Find the line number for the first match, empty if not found:
		ln=$(grep -n "^$tspattern" $dmesg_file |sed 's/:.*//'|head -n 1)
		if [ ! -z "$ln" ]; then
			log "timestamp found, truncating dmesg accordingly"
			sed -i "1,$ln d" $dmesg_file
		else
			log "timestamp not found, using whole dmesg"
		fi
	else
		log "looking at dmesg for the first time"
	fi

	if [ -s $dmesg_file ]; then
		# Save the last timestamp:
		grep -o '^\[ *[0-9.]\+\]' $dmesg_file | tail -n 1 > $dmesg_ts
		log "saving timestamp for a later use: $(cat $dmesg_ts)"
	else
		log "keeping timestamp (no new lines): $(cat $dmesg_ts)"
	fi

	# Write and clean-up:
	cat $dmesg_file
	rm $dmesg_file
}

check_missing () {
	upnics

	# Give modules some time to request firmware.
	sleep 1

	modules=""
	files=""

	# Parse dmesg using a started parttern to detect firmware
	# files the kernel drivers look for (#725714):
	fwlist=/tmp/check-missing-firmware-dmesg.list
	get_fresh_dmesg | sed -rn 's/^(\[[^]]*\] )?([^ ]+) ([^ ]+): firmware: failed to load ([^ ]+) .*/\2 \3 \4/p' > $fwlist
	while read module address fwfile ; do
	    # rewrite module is necessary
	    case "$module" in
		usb)
		    module=$(get_usb_module "$address")
		    log "using module $module instead of usb $address"
		;;
		mhi)
		    module=$(get_mhi_holders)
		    log "using $module instead of mhi"
		;;
	    esac

	    # ignore specific files:
	    #  - iwlwifi, debug-only (#969264, #966218)
	    if [ "$fwfile" = "iwl-debug-yoyo.bin" ]; then
		log "ignoring firmware file $fwfile requested by $module"
		continue
	    fi

	    log "looking for firmware file $fwfile requested by $module"
	    if [ ! -e /lib/firmware/$fwfile ] ; then
		if grep -q "^$fwfile$" $DENIED 2>/dev/null; then
		    log "listed in $DENIED"
		    continue
		fi
		files="${files:+$files }$fwfile"
		modules="$module${modules:+ $modules}"
	    fi
	done < $fwlist

	if [ -n "$modules" ]; then
		# Uniquify since a single module may request *many* firmware files,
		# and a file might be requested several times:
		modules=$(echo $modules | tr " " "\n" | sort -u)
		files=$(echo $files | tr " " "\n" | sort -u)
		log "missing firmware files ($files) for $modules"
		return 0
	else
		log "no missing firmware in loaded kernel modules"
		return 1
	fi
}

# If found, copy firmware file; preserve subdirs.
try_copy () {
	local fwfile=$1
	local sdir file f target

	sdir=$(dirname $fwfile | sed "s/^\.$//")
	file=$(basename $fwfile)
	for f in "/media/$fwfile" "/media/firmware/$fwfile" \
		 ${sdir:+"/media/$file" "/media/firmware/$file"}; do
		if [ -e "$f" ]; then
			target="/lib/firmware${sdir:+/$sdir}"
			log "copying loose file $file from '$(dirname $f)' to '$target'"
			mkdir -p "$target"
			rm -f "$target/$file"
			cp -aL "$f" "$target" || true
			break
		fi
	done
}

first_try=1
first_ask=1
ask_load_firmware () {
	if [ "$first_try" ]; then
		first_try=""
		return 0
	fi

	if [ "$NONINTERACTIVE" ]; then
		if [ ! "$first_ask" ]; then
			return 1
		else
			first_ask=""
			return 0
		fi
	fi

	db_subst hw-detect/load_firmware FILES "$files"
	if ! db_input high hw-detect/load_firmware; then
		if [ ! "$first_ask" ]; then
			exit 1;
		else
			first_ask=""
		fi
	fi
	if ! db_go; then
		exit 10 # back up
	fi
	db_get hw-detect/load_firmware
	if [ "$RET" = true ]; then
		return 0
	else
		echo "$files" | tr ' ' '\n' >> $DENIED
		return 1
	fi
}

list_deb_firmware () {
	udpkg -c "$1" \
		| grep '^\./lib/firmware/' \
		| sed -e 's!^\./lib/firmware/!!' \
		| grep -v '^$'
}

check_deb_arch () {
	arch=$(udpkg -f "$1" | grep '^Architecture:' | sed -e 's/Architecture: *//')
	[ "$arch" = all ] || [ "$arch" = "$(udpkg --print-architecture)" ]
}

get_deb_component () {
	# This trusts the contents of the .deb, but packages in the archive could
	# have overrides (controlled by ftpmaster):
	section=$(udpkg -f "$1" | grep '^Section:' | sed -e 's/Section: *//')
	if ! echo "$section" | grep -qs '/'; then
		echo "main"
	else
		echo "$section" | sed 's,/.*,,'
	fi
}

# Remove non-accepted firmware package
remove_pkg() {
	pkgname="$1"
	# Remove all files listed in /var/lib/dpkg/info/$pkgname.md5sums
	for file in $(cut -d" " -f 2- /var/lib/dpkg/info/$pkgname.md5sums) ; do
		rm /$file
	done
}

install_firmware_pkg () {
	# cache deb for installation into /target later
	mkdir -p /var/cache/firmware/
	cp -aL "$1" /var/cache/firmware/ || true
	filename="$(basename "$1")"
	pkgname="$(udpkg -f "$1" | grep '^Package:' | sed -e 's/^Package: *//')"
	udpkg --unpack "/var/cache/firmware/$filename"
	if [ -f /var/lib/dpkg/info/$pkgname.preinst ] ; then
		# Run preinst script to see if the firmware
		# license is accepted Exit code of preinst
		# decide if the package should be installed or
		# not.
		if /var/lib/dpkg/info/$pkgname.preinst ; then
			:
		else
			remove_pkg "$pkgname"
			rm "/var/cache/firmware/$filename"
			removed=1
		fi
	fi
	if [ "$removed" != 1 ]; then
		echo "$2" >> /var/cache/firmware/components
		echo "$pkgname $2 dmesg" >> /var/log/firmware-summary
	fi
}

# Try to load debs that contain the missing firmware.
# This does not use anna because debs can have arbitrary
# dependencies, which anna might try to install.
check_for_firmware() {
	echo "$files" | sed -e 's/ /\n/g' >/tmp/grepfor
	for dir in $@; do
		# An index file might exist, mapping firmware files to firmware
		# packages, saving us from iterating over each firmware *.deb:
		if [ -f $dir/Contents-firmware ]; then
			log "lookup with $dir/Contents-firmware"
			# Duplicating stdin makes license prompts work again (#1033921). The
			# workaround is meant for Bookworm, but this should be reconsidered
			# (#1035356, #1029843).
			{
			grep -f /tmp/grepfor $dir/Contents-firmware | while read fw_file fw_pkg_file component; do
				# Don't install a package for each file it ships!
				if grep -qs "^$fw_pkg_file$" /tmp/pkginstalled 2>/dev/null; then
					continue
				fi
				if check_deb_arch "$dir/$fw_pkg_file"; then
					log "installing firmware package $dir/$fw_pkg_file ($component)"
					install_firmware_pkg "$dir/$fw_pkg_file" "$component" <&9 || true
					echo "$fw_pkg_file" >> /tmp/pkginstalled
				fi
			done
			} 9<&0
			continue
		fi

		# If no such index exists, fall back to iterating over everyone:
		log "lookup without $dir/Contents-firmware"
		for filename in $dir/*.deb; do
			if [ -f "$filename" ]; then
				if check_deb_arch "$filename" && list_deb_firmware "$filename" | grep -qf /tmp/grepfor; then
					log "installing firmware package $filename"
					install_firmware_pkg "$filename" $(get_deb_component "$filename") || true
				fi
			fi
		done
	done
	rm -f /tmp/grepfor
	rm -f /tmp/pkginstalled
}

# For those who don't want to load any firmware, even if available on
# installation images (#1029848). The loop is still entered so that
# logs are generated.
db_get hw-detect/firmware-lookup
firmware_lookup="$RET"

# NOTE: The ask_load_firmware function returns true the first time around,
# without asking any questions. For consistency, skip mountmedia calls during
# the first iteration of the loop. For systems which have all firmware material
# found in {,/cdrom}/firmware, this also means a noticeable speed-up.
loop=0
while check_missing && ask_load_firmware; do
	loop=$((loop+1))
	log "mainloop iteration #$loop"

	if [ "$firmware_lookup" = "never" ]; then
		log "firmware lookup disabled (=$firmware_lookup), exiting"
		exit 0
	fi

	# first, check if needed firmware debs are available on the
	# PXE initrd or the installation CD.
	if [ -d /firmware ]; then
		check_for_firmware /firmware
	fi
	if [ -d /cdrom/firmware ]; then
		check_for_firmware /cdrom/firmware
	fi

	# Whether we should keep both mountmedia calls, and whether mountmedia
	# is doing a good job is discussed in #1029543:
	if [ "$loop" -gt 1 ]; then
		# second, look for loose firmware files on the media device.
		if mountmedia; then
			for file in $files; do
				try_copy "$file"
			done
			umount /media || true
		fi

		# last, look for firmware debs on the media device
		if mountmedia driver; then
			check_for_firmware /media /media/firmware
			umount /media || true
		fi
	fi

	# remove and reload modules so they see the new firmware
	for module in $modules; do
		if ! nic_is_configured $module; then
			log "removing and loading kernel module $module"
			log_output modprobe -r $module || true
			log_output modprobe $module || true

			# iterate to avoid dealing with multiplicity explicitly:
			for driver in $(find /sys/bus/*/drivers -name "$module"); do
				# module name mentioned in dmesg might differ from the actual module
				# (rtw_8821ce vs. rtw88_8821ce, see #973733); also beware of the
				# module symlink, it doesn't always exist:
				if [ -e "$driver/module" ]; then
					actual_module=$(basename $(readlink -f "$driver/module"))
					if [ "$actual_module" != "$module" ]; then
						log "removing and loading kernel module $actual_module as well (actual module for $module)"
						log_output modprobe -r $actual_module || true
						log_output modprobe $actual_module || true
					fi
				fi
			done
		fi
	done
done