#!/bin/sh # lib/firmware-initramfs.sh — el firmware que el kernel pide ANTES de montar la raíz. # # ══ EL DEFECTO, MEDIDO EN METAL EL 2026-10-14/15 ═══════════════════════════════════════════════ # «que ni ethernet funciona». La imagen ya trae el firmware completo (`firmware-metal`, 1047 MiB en # `/lib/firmware`) y **para el arranque no alcanza**, por una razón de tiempos: # # iwlwifi 0000:00:14.3: Detected Intel(R) Wi-Fi 6 AX201 160MHz # iwlwifi 0000:00:14.3: Direct firmware load for iwlwifi-QuZ-a0-hr-b0-77.ucode failed: -2 # iwlwifi 0000:00:14.3: no suitable firmware found! # # Esos mensajes salen a los **0,49 s de arranque**: el driver va COMPILADO EN EL KERNEL # (`linux-metal` no habilita `CONFIG_MODULES`), así que hace su `probe` cuando el único filesystem # que existe es el initramfs. `/lib/firmware` de la raíz todavía no está montada, y `request_firmware` # no reintenta nunca: el dispositivo queda sin inicializar para toda la sesión. # # ══ POR QUÉ ESTO VA EN EL INSTALADOR Y NO EN LA IMAGEN ═════════════════════════════════════════ # Meter `/lib/firmware` entero en el initramfs es 1 GB cargado en RAM en cada arranque. Meter «el # wifi» tampoco sirve: la familia `iwlwifi` sola son 287 MB, y recortarla por generación es adivinar # —el nombre que el driver pide sale del chip Y de la versión de API que ese kernel soporta—. # # La salida es que lo decida **quien sabe en qué máquina está**: el instalador, que corre EN el metal # de destino. Y no tiene que adivinar nada, porque **el medio arranca con el MISMO problema**: su # initramfs tampoco trae firmware, así que su propio `dmesg` ya tiene la lista EXACTA de ficheros # que ese hardware pidió, con el nombre que el driver usó. Se leen de ahí y se copian; en una máquina # donde todo cargó bien, la lista sale vacía y esto no toca nada. # # Vive aparte para poder certificarla sin root ni hardware, igual que `lib/usuarios-del-home.sh` y # `lib/cuenta.sh`: `scripts/probar-firmware-initramfs.sh` le da un `dmesg` de mentira y un cpio de # juguete. Y viaja sola al initramfs del instalador porque los generadores derivan las librerías del # propio script que las sourcea. # El techo. La ESP del instalador son 64 MiB y ahí viven el kernel (~12 MiB) y este initramfs; un # injerto sin tope podría no entrar, y «no entra en la ESP» se descubre al final de una instalación. FW_IRD_TOPE_KIB="${FW_IRD_TOPE_KIB:-24576}" # 24 MiB # firmware_pedido_del_dmesg [fichero] — las rutas de firmware que el kernel pidió y NO encontró, una # por línea, sin repetir. Sin argumento lee el `dmesg` de esta máquina. # # Se cubren los tres formatos que emite el kernel, porque cambian entre versiones y entre el camino # directo y el de respaldo — y mirar sólo uno da «no falta nada» sobre un wifi muerto: # · `Direct firmware load for failed with error -2` # · `firmware: failed to load (-2)` # · `Falling back to sysfs fallback for: ` firmware_pedido_del_dmesg() { { if [ -n "${1:-}" ]; then cat "$1"; else dmesg 2>/dev/null; fi; } | sed -n \ -e 's/.*Direct firmware load for \([^ ]*\) failed.*/\1/p' \ -e 's/.*firmware: failed to load \([^ ]*\) .*/\1/p' \ -e 's/.*Falling back to sysfs fallback for: *\([^ ]*\).*/\1/p' \ | sed 's/[,:]$//' | sort -u } # firmware_al_initramfs [fichero-dmesg] — injerta en el # initramfs los blobs que el dmesg pidió. Devuelve 0 siempre que el initramfs quede USABLE: si no # hay nada que poner, lo deja intacto y lo dice. # # ⚠ Lo que NO hace: no falla la instalación porque falte un blob. Una máquina que se instala sin wifi # es recuperable; una que no arranca porque alguien reempaquetó mal su initramfs, no. firmware_al_initramfs() { _fi_ird="$1"; _fi_src="$2"; _fi_dmesg="${3:-}" [ -r "$_fi_ird" ] || { echo " firmware: no encuentro el initramfs $_fi_ird — lo salteo" >&2; return 0; } _fi_lista=$(firmware_pedido_del_dmesg "$_fi_dmesg") if [ -z "$_fi_lista" ]; then echo " firmware: el kernel de este arranque no pidió ningún blob que le faltara — nada que injertar" return 0 fi _fi_tmp="${TMPDIR:-/tmp}/fw-ird.$$" rm -rf "$_fi_tmp"; mkdir -p "$_fi_tmp/arbol" || return 0 # Desempaquetar. Si esto falla, el initramfs queda tal cual: no se toca el original hasta el final. if ! ( cd "$_fi_tmp/arbol" && gzip -dc "$_fi_ird" | cpio -idm --quiet ) 2>/dev/null; then echo " firmware: no pude desempaquetar el initramfs; lo dejo como está" >&2 rm -rf "$_fi_tmp"; return 0 fi # El `/init` es la razón de existir de este fichero. Si no está, algo entendí mal y me aparto. [ -f "$_fi_tmp/arbol/init" ] || { echo " firmware: el initramfs no trae /init — no lo toco" >&2 rm -rf "$_fi_tmp"; return 0; } _fi_n=0; _fi_kib=0; _fi_falta=0 for _fi_f in $_fi_lista; do # Una ruta con `..` o absoluta saldría del árbol: el dmesg es texto del kernel, pero esto # escribe ficheros y no se confía en una cadena parseada. case "$_fi_f" in /*|*..*) echo " firmware: ⚠ descarto una ruta sospechosa: $_fi_f" >&2; continue ;; esac _fi_org="" for _fi_c in "$_fi_src/lib/firmware/$_fi_f" "$_fi_src/$_fi_f"; do [ -e "$_fi_c" ] && { _fi_org="$_fi_c"; break; } done if [ -z "$_fi_org" ]; then # Se NOMBRA. Un blob que el hardware pide y la imagen no trae es una capacidad que esa # máquina no va a tener, y el dueño merece saber cuál. echo " firmware: ⚠ el hardware pide «$_fi_f» y la imagen no lo trae" _fi_falta=$((_fi_falta + 1)) continue fi _fi_peso=$(du -k "$(readlink -f "$_fi_org")" 2>/dev/null | cut -f1) _fi_peso=${_fi_peso:-0} if [ $((_fi_kib + _fi_peso)) -gt "$FW_IRD_TOPE_KIB" ]; then echo " firmware: ⚠ «$_fi_f» no entra en el tope de $((FW_IRD_TOPE_KIB / 1024)) MiB del initramfs: lo salteo" continue fi mkdir -p "$_fi_tmp/arbol/lib/firmware/$(dirname "$_fi_f")" # `-L`: si en la imagen el blob es un enlace (los 2415 que `linux-firmware` reconstruye desde # WHENCE), al initramfs va el CONTENIDO — adentro no hay a dónde apuntar. cp -L "$_fi_org" "$_fi_tmp/arbol/lib/firmware/$_fi_f" 2>/dev/null || continue _fi_n=$((_fi_n + 1)); _fi_kib=$((_fi_kib + _fi_peso)) done if [ "$_fi_n" = 0 ]; then echo " firmware: no pude poner ninguno de los $(echo "$_fi_lista" | wc -w) pedidos; el initramfs queda igual" rm -rf "$_fi_tmp"; return 0 fi # Reempaquetar y comprobar ANTES de pisar el original. Un initramfs a medio escribir es una # máquina que no arranca, y eso no se diagnostica desde la máquina que no arranca. ( cd "$_fi_tmp/arbol" && find . | cpio -o -H newc --quiet --owner=root:root 2>/dev/null | gzip -9 > "$_fi_tmp/nuevo.cpio.gz" ) || { echo " firmware: falló el reempaquetado; dejo el initramfs original" >&2 rm -rf "$_fi_tmp"; return 0; } # ⚠ `cpio -t` imprime las rutas SIN el `./` con que `find .` las guardó: el patrón acepta las dos # formas. Costó tres «NO» sobre un initramfs correcto el 2026-10-14. _fi_dentro=$(gzip -dc "$_fi_tmp/nuevo.cpio.gz" | cpio -t --quiet 2>/dev/null | grep -c "^\(\./\)\?init$" || true) if [ "${_fi_dentro:-0}" -lt 1 ]; then echo " firmware: el initramfs nuevo no tiene /init — NO lo uso" >&2 rm -rf "$_fi_tmp"; return 0 fi cp "$_fi_ird" "$_fi_ird.antes-del-firmware" 2>/dev/null || true cp "$_fi_tmp/nuevo.cpio.gz" "$_fi_ird" echo " firmware: $_fi_n blob(s) injertados en el initramfs ($((_fi_kib)) KiB)$([ "$_fi_falta" -gt 0 ] && echo ", $_fi_falta sin encontrar")" echo " ahora pesa $(wc -c < "$_fi_ird") bytes" rm -rf "$_fi_tmp" return 0 }