From 7f9a863a066e59a274e1b07ed60637fc8a9a06b6 Mon Sep 17 00:00:00 2001 From: Tomaz Zaman Date: Wed, 12 Aug 2026 00:28:06 +0200 Subject: [PATCH 37/77] layerscape: validate the mono_gateway-dk flash target before writing The mono sysupgrade path dd'd the image to hardcoded /dev/mmcblk0p1/p2 with errors suppressed and always reported success, so a wrong target or a truncated write could reboot into a brick silently. Add mono_gateway_root_part(): require the DT compatible, the kernel's own root=, and partition 2 of a non-removable MMC disk at the expected start sector. platform_check_image() pre-flights it (the stage-1 gate that actually refuses a bad flash); the writer and config-copy use the validated partition, and the boot partition is validated too. mono_dd_member() checks both tar and dd, so a short image can't be masked by dd exiting 0. The start sector is the load-bearing adaptation: the original guard used 65536 (its rootfs); on our 2-partition GPT 65536 is /boot and the rootfs is 196608. Tests in target/linux/layerscape/tests/. Inspired by Christopher van de Sande's device-validation guards (github.com/cvandesande). Co-Authored-By: Claude Opus 4.8 --- .../10-mono-gateway-expand-rootfs | 11 +- .../base-files/lib/upgrade/platform.sh | 102 ++++++++++++++++-- .../layerscape/tests/test_mono_root_part.sh | 96 +++++++++++++++++ 3 files changed, 201 insertions(+), 8 deletions(-) create mode 100755 target/linux/layerscape/tests/test_mono_root_part.sh diff --git a/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs b/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs index fa66a82414..7d7bdf0785 100644 --- a/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs +++ b/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs @@ -10,6 +10,9 @@ # uci-defaults self-delete on exit 0; this script exits NON-zero (retries # next boot) until the backup GPT is confirmed present, and is idempotent. . /lib/functions.sh +# Reuse the validated rootfs selector from the sysupgrade platform script +# (single source of truth for the eMMC checks; see platform.sh). +[ -f /lib/upgrade/platform.sh ] && . /lib/upgrade/platform.sh case "$(board_name)" in mono,gateway-dk|mono,gateway-dk-sdboot) ;; @@ -20,8 +23,12 @@ disk=/dev/mmcblk0 sysfs=/sys/block/mmcblk0/size # size in 512-byte sectors (no tools) rc=0 -# 1. grow rootfs (no-op once full) -root_dev=$(readlink -f /dev/root 2>/dev/null) +# 1. grow rootfs (no-op once full). Prefer the validated eMMC rootfs; fall back +# to the booted /dev/root, then the hardcoded node, so first boot still +# expands if validation is unavailable. resize2fs on the mounted root is safe. +root_dev="" +command -v mono_gateway_root_part >/dev/null 2>&1 && root_dev="$(mono_gateway_root_part 2>/dev/null)" +[ -b "$root_dev" ] || root_dev=$(readlink -f /dev/root 2>/dev/null) [ -b "$root_dev" ] || root_dev=/dev/mmcblk0p2 resize2fs "$root_dev" 2>&1 | logger -t mono-firstboot diff --git a/target/linux/layerscape/base-files/lib/upgrade/platform.sh b/target/linux/layerscape/base-files/lib/upgrade/platform.sh index 991e973c06..ade407fbae 100644 --- a/target/linux/layerscape/base-files/lib/upgrade/platform.sh +++ b/target/linux/layerscape/base-files/lib/upgrade/platform.sh @@ -8,6 +8,63 @@ RAMFS_COPY_DATA="" REQUIRE_IMAGE_METADATA=1 +# --- Mono Gateway: validated flash target ----------------------------------- +# Confirm the disk we are about to write really is THIS board's soldered eMMC +# and return its rootfs partition, so a wrong or absent device is refused +# instead of blindly dd'd. Inspired by Christopher van de Sande's +# device-validation guards (github.com/cvandesande). That fork's single- +# partition layout put the rootfs at sector 65536; our 2-partition GPT puts the +# BOOT partition there and the rootfs (p2) at 196608 = (32 + MONO_BOOTFS_SIZE[64]) +# * 2048 - so copying that 65536 check verbatim would select our boot partition +# and overwrite it. +# This helper is the single source of truth; the first-boot expand uci-default +# sources this file to reuse it. Per-device: when non-DK Gateway boards are +# added, MONO_ROOT_START_SECTOR must track their (32 + boot size) * 2048. +MONO_ROOT_START_SECTOR=196608 +MONO_BOOT_START_SECTOR=65536 + +mono_cmdline_root() { + # Last root= wins, matching the kernel (a later root= overrides an earlier). + local arg val= + for arg in $(cat /proc/cmdline); do + case "$arg" in root=*) val="${arg#root=}" ;; esac + done + [ -n "$val" ] && echo "$val" +} + +# True (0) if partition $1 (e.g. mmcblk0p2) has index $2 and starts at sector $3. +mono_part_matches() { + [ "$(cat "/sys/class/block/$1/partition" 2>/dev/null)" = "$2" ] && + [ "$(cat "/sys/class/block/$1/start" 2>/dev/null)" = "$3" ] +} + +# Echo the validated rootfs partition (e.g. /dev/mmcblk0p2), or fail (return 1). +mono_gateway_root_part() { + local root part disk removable type + + grep -q "mono,gateway-dk" /sys/firmware/devicetree/base/compatible 2>/dev/null || { + echo "Not a Mono Gateway - refusing" >&2; return 1; } + root="$(mono_cmdline_root)" || { echo "Cannot determine root= from /proc/cmdline" >&2; return 1; } + case "$root" in + /dev/mmcblk*p2) part="${root##*/}" ;; + *) echo "Refusing unsupported root device: $root" >&2; return 1 ;; + esac + disk="${part%p2}" + case "$disk" in + mmcblk | mmcblk*[!0-9]*) echo "Refusing malformed eMMC name: $disk" >&2; return 1 ;; + esac + [ -b "/dev/$part" ] || { echo "$part is not a block device" >&2; return 1; } + [ -d "/sys/block/$disk" ] || { echo "eMMC $disk not present in sysfs" >&2; return 1; } + mono_part_matches "$part" 2 "$MONO_ROOT_START_SECTOR" || { + echo "Refusing: $part is not partition 2 at sector $MONO_ROOT_START_SECTOR" >&2; return 1; } + removable="$(cat "/sys/block/$disk/removable" 2>/dev/null)" + [ "$removable" = "0" ] || { echo "Refusing removable disk $disk (not soldered eMMC)" >&2; return 1; } + type="$(cat "/sys/block/$disk/device/type" 2>/dev/null)" + [ "$type" = "MMC" ] || { echo "Refusing non-eMMC disk $disk (type '$type')" >&2; return 1; } + + echo "/dev/$part" +} + platform_do_upgrade_sdboot() { local diskdev partdev parttype=ext4 local tar_file="$1" @@ -67,26 +124,56 @@ platform_copy_config_sdboot() { umount /mnt fi } +# Stream a sysupgrade tar member to a block device, failing if EITHER tar or dd +# fails. A plain "tar | dd || ..." only observes dd's exit status, so a +# truncated/erroring tar with a dd that exits 0 would silently write a short +# image; the fifo + wait captures tar's exit too. +mono_dd_member() { # tar_file board_dir member device + local rc_t rc_d fifo=/tmp/mono-upgrade.fifo + rm -f "$fifo"; mkfifo "$fifo" || return 1 + tar xf "$1" "$2/$3" -O > "$fifo" & + local tp=$! + dd of="$4" bs=1M conv=fsync < "$fifo"; rc_d=$? + wait "$tp"; rc_t=$? + rm -f "$fifo" + [ "$rc_d" = 0 ] && [ "$rc_t" = 0 ] +} + platform_do_upgrade_mono() { local tar_file="$1" local board_dir=$(tar tf $tar_file | grep -m 1 '^sysupgrade-.*/$') board_dir=${board_dir%/} + local rootpart bootpart disk + rootpart="$(mono_gateway_root_part)" || { + echo "Refusing upgrade: could not validate the target eMMC"; return 1; } + disk="${rootpart%p2}" + bootpart="${disk}p1" # boot is the sibling of the validated rootfs + # Boot is written FIRST and is the more catastrophic target, so validate it + # to the same standard as the rootfs (index 1 at the expected start sector). + [ -b "$bootpart" ] && mono_part_matches "${bootpart#/dev/}" 1 "$MONO_BOOT_START_SECTOR" || { + echo "Boot partition $bootpart failed validation"; return 1; } + # The "kernel" member is the complete boot partition image # (Image.gz + dtb + extlinux.conf), so the device tree and boot # config always match the kernel they were built with. The GPT # and the raw boot firmware in the first 32 MiB are never touched. - echo "Writing boot partition..." - tar xf $tar_file ${board_dir}/kernel -O | dd of=/dev/mmcblk0p1 bs=1M conv=fsync 2>/dev/null - echo "Writing rootfs..." - tar xf $tar_file ${board_dir}/root -O | dd of=/dev/mmcblk0p2 bs=1M conv=fsync 2>/dev/null + echo "Writing boot partition to $bootpart..." + mono_dd_member "$tar_file" "$board_dir" kernel "$bootpart" || { + echo "Boot partition write to $bootpart failed"; return 1; } + echo "Writing rootfs to $rootpart..." + mono_dd_member "$tar_file" "$board_dir" root "$rootpart" || { + echo "Rootfs write to $rootpart failed"; return 1; } # rootfs ships at 384M; the uci-defaults script in it re-expands # to the full partition on first boot } platform_copy_config_mono() { + local rootpart + rootpart="$(mono_gateway_root_part)" || { + echo "Could not validate rootfs for config backup"; return 1; } mkdir -p /tmp/new_root - if mount -t ext4 -o rw,noatime /dev/mmcblk0p2 /tmp/new_root; then + if mount -t ext4 -o rw,noatime "$rootpart" /tmp/new_root; then echo "Saving config backup to new rootfs..." cp -af "$UPGRADE_BACKUP" /tmp/new_root/sysupgrade.tgz umount /tmp/new_root @@ -125,6 +212,9 @@ platform_check_image() { ;; mono,gateway-dk | \ mono,gateway-dk-sdboot) + # Pre-flight: refuse before writing anything if the target eMMC + # does not validate, rather than failing part-way through the flash. + mono_gateway_root_part >/dev/null || return 1 return 0 ;; fsl,ls1012a-frdm | \ @@ -166,7 +256,7 @@ platform_do_upgrade() { mono,gateway-dk | \ mono,gateway-dk-sdboot) platform_do_upgrade_mono "$1" - return 0 + return $? ;; traverse,ten64) platform_do_upgrade_traverse_slotubi "${1}" diff --git a/target/linux/layerscape/tests/test_mono_root_part.sh b/target/linux/layerscape/tests/test_mono_root_part.sh new file mode 100755 index 0000000000..efd60a4ba4 --- /dev/null +++ b/target/linux/layerscape/tests/test_mono_root_part.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Unit tests for the mono flash-target guards in the layerscape sysupgrade +# platform.sh: mono_gateway_root_part() (device validation) and mono_dd_member() +# (fail-if-either-tar-or-dd-fails streaming). Sandboxes the absolute paths the +# validator reads and relaxes the block-special check to plain existence (a unit +# test cannot mknod). Every DECISION is exercised, including the 196608-vs-65536 +# boot-partition trap that makes this a real adaptation of cvandesande's guard. +# +# The DT compatible node is written as TEXT here (not the on-device +# NUL-separated form) so the result is independent of which grep the host +# provides; busybox grep matching the real NUL node is verified separately. +# Run from anywhere; skips if platform.sh is absent. +set -u +HERE=$(cd "$(dirname "$0")" && pwd) +PLATFORM="$HERE/../base-files/lib/upgrade/platform.sh" +[ -f "$PLATFORM" ] || { echo "SKIP: platform.sh not found ($PLATFORM)"; exit 0; } +S=$(mktemp -d); trap 'rm -rf "$S"' EXIT +fails=0 + +sed -e "s#/proc/cmdline#$S/cmdline#g" \ + -e "s#/sys/firmware/devicetree/base/compatible#$S/compatible#g" \ + -e "s#/sys/block/#$S/sys/block/#g" \ + -e "s#/sys/class/block/#$S/sys/class/block/#g" \ + -e 's#-b "/dev/\$part"#-e "'"$S"'/dev/\$part"#g' \ + "$PLATFORM" > "$S/platform.sh" +. "$S/platform.sh" + +grep -q "MONO_ROOT_START_SECTOR=196608" "$S/platform.sh" || { echo "FAIL: rootfs sector not 196608"; exit 1; } +grep -q "MONO_BOOT_START_SECTOR=65536" "$S/platform.sh" || { echo "FAIL: boot sector not 65536"; exit 1; } +grep -q '\-b "/dev/' "$S/platform.sh" && { echo "FAIL: block check not relaxed by sed"; exit 1; } + +valid() { # $1 = disk name (default mmcblk0) + local d=${1:-mmcblk0} p; p="${d}p2" + rm -rf "$S/sys" "$S/dev" + mkdir -p "$S/dev" "$S/sys/block/$d/device" "$S/sys/class/block/$p" \ + "$S/sys/class/block/${d}p1" + printf 'root=/dev/%s rootwait console=ttyS0,115200\n' "$p" > "$S/cmdline" + printf 'mono,gateway-dk fsl,ls1046a\n' > "$S/compatible" + : > "$S/dev/$p" + echo 2 > "$S/sys/class/block/$p/partition" + echo 196608 > "$S/sys/class/block/$p/start" + echo 1 > "$S/sys/class/block/${d}p1/partition" + echo 65536 > "$S/sys/class/block/${d}p1/start" + echo 0 > "$S/sys/block/$d/removable" + echo MMC > "$S/sys/block/$d/device/type" +} +check() { # $1: 0=accept 1=reject ; $2: desc ; $3: expected output (accept only) + local want="${3:-/dev/mmcblk0p2}" out rc + out=$(mono_gateway_root_part 2>/dev/null); rc=$? + if [ "$1" = 0 ]; then + if [ "$rc" = 0 ] && [ "$out" = "$want" ]; then echo "ok accept: $2" + else echo "FAIL accept: $2 (rc=$rc out='$out' want='$want')"; fails=$((fails+1)); fi + else + if [ "$rc" != 0 ]; then echo "ok reject: $2" + else echo "FAIL reject: $2 (wrongly accepted '$out')"; fails=$((fails+1)); fi + fi +} + +echo "== mono_gateway_root_part ==" +valid; check 0 "valid p2 @196608 on non-removable eMMC" +valid mmcblk10; check 0 "high disk index mmcblk10" /dev/mmcblk10p2 +valid; echo 65536 > "$S/sys/class/block/mmcblk0p2/start"; check 1 "rootfs start 65536 (boot-partition trap)" +valid; echo 1 > "$S/sys/class/block/mmcblk0p2/partition"; check 1 "partition index 1, not 2" +valid; echo 1 > "$S/sys/block/mmcblk0/removable"; check 1 "removable disk" +valid; echo SD > "$S/sys/block/mmcblk0/device/type"; check 1 "non-MMC bus type" +valid; printf 'fsl,ls1046a\n' > "$S/compatible"; check 1 "not a Mono Gateway (DT compatible)" +valid; printf 'root=/dev/mmcblk0p1 rootwait\n' > "$S/cmdline"; check 1 "root= is p1 (wrong partition)" +valid; printf 'root=/dev/sda2 rootwait\n' > "$S/cmdline"; check 1 "root= is non-eMMC (sda2)" +valid; printf 'root=/dev/mmcblk0boot0p2 rootwait\n' > "$S/cmdline"; check 1 "eMMC hardware boot partition name" +valid; rm -f "$S/dev/mmcblk0p2"; check 1 "rootfs device node missing" +valid; printf 'console=ttyS0 rootwait\n' > "$S/cmdline"; check 1 "no root= on cmdline" +# last root= wins (kernel semantics): an earlier p1 is overridden by a later p2 +valid; printf 'root=/dev/mmcblk0p1 root=/dev/mmcblk0p2 rootwait\n' > "$S/cmdline"; check 0 "last root= wins (p1 then p2)" + +echo "== derivation: writer picks the sibling boot partition ==" +valid +rp=$(mono_gateway_root_part 2>/dev/null); dk=${rp%p2}; bp=${dk}p1 +if [ "$rp" = /dev/mmcblk0p2 ] && [ "$bp" = /dev/mmcblk0p1 ]; then echo "ok rootpart=$rp bootpart=$bp" +else echo "FAIL derivation: rootpart='$rp' bootpart='$bp'"; fails=$((fails+1)); fi + +echo "== mono_dd_member: fails if EITHER tar or dd fails ==" +# success: stubbed tar emits data, real dd writes it to a sandbox file +tar() { printf 'IMAGE-BYTES'; } +mono_dd_member x y root "$S/out"; rc=$? +if [ "$rc" = 0 ] && [ "$(cat "$S/out")" = "IMAGE-BYTES" ]; then echo "ok success: streams + rc 0" +else echo "FAIL success: rc=$rc out='$(cat "$S/out" 2>/dev/null)'"; fails=$((fails+1)); fi +# tar failure: stubbed tar emits a short stream then exits non-zero; dd reads it +# and exits 0 - the fix must still report failure (the whole point of finding #1). +tar() { printf 'SHORT'; return 2; } +mono_dd_member x y root "$S/out2"; rc=$? +if [ "$rc" != 0 ]; then echo "ok tar-failure detected despite dd exit 0" +else echo "FAIL tar-failure masked (rc=$rc) - the bug is back"; fails=$((fails+1)); fi +unset -f tar + +echo +if [ "$fails" -eq 0 ]; then echo "ALL PASS"; else echo "$fails FAILED"; exit 1; fi -- 2.47.3