From 5ba80d247886f6bb0944ef2f719e64447522adca Mon Sep 17 00:00:00 2001 From: Tomaz Zaman Date: Wed, 19 Aug 2026 10:42:24 +0200 Subject: [PATCH 77/77] layerscape: mono_gateway-dk A/B atomic updates with auto-rollback Phase 3: sysupgrade writes the INACTIVE rootfs slot and U-Boot rolls back to the previous slot if the new one fails to boot. No firmware rebuild - driven entirely from OpenWrt via fw_setenv (env in QSPI mtd3). - armv8_64b.mk: extlinux.conf is slot-agnostic (root=...p${rootpart}, expanded by U-Boot sysboot) so one image boots either slot; pull in uboot-envtools and the new mono-ab-env package. - package/mono/ab-env: mono-fw-setenv writes BOTH env media (mtd3 + the eMMC copy) so slot/bootcount hold regardless of the firmware-boot DIP switch; 95-mono-ab-env installs the static A/B env (bootcmd, set_slot_a/b, bootlimit, altbootcmd) on first boot, versioned via mono_ab_env_ver so a later release can refresh it; 96-mono-markgood confirms the slot by clearing the trial - a uci-default, since procd skips a classic init boot() on the first boot after sysupgrade. - platform.sh: write the inactive slot (p3/p4 or p1/p2), back up config to it, flip the slot and arm upgrade_available=1 (the CONFIG_BOOTCOUNT_ ENV gate); RAMFS_COPY ships fw_setenv into the stage2 ramdisk. - stockfixes.cil: label /etc/fw_env.d/emmc envtools.conf.file so the envtools domain reads it (0 AVCs). - test_mono_root_part.sh: cover A/B slot selection. HW-validated on the DUT: A<->B flips, auto arm/disarm, and a corrupted slot auto-rolling back to the good slot; /data shared across slots. Co-Authored-By: Claude Opus 4.8 --- .../uboot-envtools/files/layerscape | 15 +++ package/mono/ab-env/Makefile | 43 ++++++ package/mono/ab-env/files/95-mono-ab-env | 64 +++++++++ package/mono/ab-env/files/96-mono-markgood | 46 +++++++ package/mono/ab-env/files/fw_env.emmc | 6 + package/mono/ab-env/files/mono-fw-setenv | 24 ++++ .../selinux-policy/files/cil/stockfixes.cil | 19 +++ .../base-files/lib/upgrade/platform.sh | 122 +++++++++++++----- target/linux/layerscape/image/armv8_64b.mk | 9 +- .../layerscape/tests/test_mono_root_part.sh | 41 ++++-- 10 files changed, 338 insertions(+), 51 deletions(-) create mode 100644 package/mono/ab-env/Makefile create mode 100644 package/mono/ab-env/files/95-mono-ab-env create mode 100644 package/mono/ab-env/files/96-mono-markgood create mode 100644 package/mono/ab-env/files/fw_env.emmc create mode 100644 package/mono/ab-env/files/mono-fw-setenv diff --git a/package/boot/uboot-tools/uboot-envtools/files/layerscape b/package/boot/uboot-tools/uboot-envtools/files/layerscape index acc5a073ad..9eaeadd9f0 100644 --- a/package/boot/uboot-tools/uboot-envtools/files/layerscape +++ b/package/boot/uboot-tools/uboot-envtools/files/layerscape @@ -15,6 +15,21 @@ case "$board" in traverse,ten64) ubootenv_add_uci_config "/dev/mtd3" "0x0000" "0x80000" "0x80000" ;; + mono,gateway-dk) + # U-Boot's live env is in QSPI: mtd3 "uboot-env" @ flash 0x300000. + # NOT the eMMC 0x300000 copy - that's a stale factory image (reads + # "bootcmd=run emmc"); the board actually boots "run openwrt" from mtd3. + # Verified on hw: fw_printenv against mtd3 returns the live env. Same + # partition as the ten64 stanza above; single copy, CONFIG_ENV_SIZE=0x2000 + # / CONFIG_ENV_SECT_SIZE=0x10000. Lets OpenWrt fw_setenv the A/B slot + + # reset bootcount (mark-good) into the same env U-Boot reads. + # The board has a firmware-boot DIP switch (qspi/nor vs eMMC); this mtd3 + # config is the QSPI primary (what plain fw_printenv reads). A/B writes go + # to BOTH media via mono-fw-setenv (package/mono/ab-env), so the slot + + # bootcount stay correct regardless of DIP position - no boot-source + # detection needed. The eMMC copy config is /etc/fw_env.d/emmc. + ubootenv_add_uci_config "/dev/mtd3" "0x0" "0x2000" "0x10000" + ;; esac config_load ubootenv diff --git a/package/mono/ab-env/Makefile b/package/mono/ab-env/Makefile new file mode 100644 index 0000000000..a53cf60421 --- /dev/null +++ b/package/mono/ab-env/Makefile @@ -0,0 +1,43 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=mono-ab-env +PKG_VERSION:=1 +PKG_RELEASE:=1 +PKG_LICENSE:=GPL-2.0 + +include $(INCLUDE_DIR)/package.mk + +define Package/mono-ab-env + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Mono Gateway A/B U-Boot env helper + DEPENDS:=@TARGET_layerscape +uboot-envtools +endef + +define Package/mono-ab-env/description + Writes U-Boot env changes to BOTH firmware env media - QSPI (mtd3, the primary + /etc/fw_env.config) and the eMMC copy (/dev/mmcblk0 @0x300000) - so the A/B + slot + bootcount stay correct regardless of the board's firmware-boot DIP + switch. Provides mono-fw-setenv (a write-both wrapper around fw_setenv) and + ships /etc/fw_env.d/emmc. +endef + +define Build/Compile +endef + +define Package/mono-ab-env/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/mono-fw-setenv $(1)/usr/sbin/mono-fw-setenv + $(INSTALL_DIR) $(1)/etc/fw_env.d + $(INSTALL_DATA) ./files/fw_env.emmc $(1)/etc/fw_env.d/emmc + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./files/95-mono-ab-env $(1)/etc/uci-defaults/95-mono-ab-env + # mark-good is a uci-default, NOT an init script: procd does not run a classic + # init boot()/start() on the first boot after sysupgrade (verified on hw), but + # uci-defaults do run then - which is exactly when a freshly-flashed slot must + # confirm itself. It self-deletes once the trial is clear, and every image + # ships it so each update's first boot re-runs it. + $(INSTALL_BIN) ./files/96-mono-markgood $(1)/etc/uci-defaults/96-mono-markgood +endef + +$(eval $(call BuildPackage,mono-ab-env)) diff --git a/package/mono/ab-env/files/95-mono-ab-env b/package/mono/ab-env/files/95-mono-ab-env new file mode 100644 index 0000000000..12a5f7e11f --- /dev/null +++ b/package/mono/ab-env/files/95-mono-ab-env @@ -0,0 +1,64 @@ +#!/bin/sh +# 95-mono-ab-env - install/refresh the static A/B U-Boot env. +# +# Sets the boot machinery every fielded unit needs so U-Boot can boot either +# slot and auto-roll-back a bad update. Written to BOTH env media via +# mono-fw-setenv (QSPI mtd3 + eMMC) so it holds regardless of the firmware-boot +# DIP switch. Ships in every image; a uci-default so it reliably runs on the +# first boot after sysupgrade (procd skips a classic init boot() there). +# +# VERSIONED: the env carries mono_ab_env_ver. The installer skips only when the +# stored version is >= the version it ships; otherwise it (re)writes the static +# lines and stamps the new version. So a future release that changes the env +# (e.g. a fixed altbootcmd, a new var) just bumps MONO_AB_ENV_VER and every board +# picks it up on that release - a plain presence check could never refresh it. +# +# It writes only the STATIC lines + stamps the version, and sets `slot` only when +# it is unset (a fresh install), so a refresh never yanks a board off its running +# slot. The dynamic trial vars (upgrade_available, bootcount) are never touched +# here: platform_do_upgrade_mono arms them, mark-good disarms them. +# +# NOTE: factory units should also carry these in the U-Boot firmware env +# (meta-mono environment.txt) so a brand-new unit boots A/B before OpenWrt's +# first boot; this hook is the delivery path for OTA-updated existing units. +MONO_AB_ENV_VER=1 + +# Already at this version (or newer)? Nothing to do. An unset marker (r8 unit, or +# a board migrated by a pre-versioning r9) fails the -n test and falls through to +# install; a non-numeric value makes the -ge test error (silenced) and also falls +# through - both erring toward (re)install, never a wrong skip. +cur=$(fw_printenv -n mono_ab_env_ver 2>/dev/null) +[ -n "$cur" ] && [ "$cur" -ge "$MONO_AB_ENV_VER" ] 2>/dev/null && exit 0 + +# slot is RUNTIME state: preserve it on a refresh, derive it only on a fresh +# install (unset) from the running kernel's root= (p4 => slot B, else A). +slot=$(fw_printenv -n slot 2>/dev/null) +if [ -z "$slot" ]; then + slot=a + for arg in $(cat /proc/cmdline 2>/dev/null); do + case "$arg" in root=/dev/mmcblk*p4) slot=b ;; esac + done +fi + +env_batch="/tmp/mono-ab-install.env" +cat > "$env_batch" </dev/null +[ "$(rootfs_type 2>/dev/null)" = "tmpfs" ] && exit 1 + +ua=$(fw_printenv -n upgrade_available 2>/dev/null) +bc=$(fw_printenv -n bootcount 2>/dev/null) + +# Already clean (unset or 0)? Nothing to do - drop the hook, no flash write. +if { [ -z "$ua" ] || [ "$ua" = 0 ]; } && { [ -z "$bc" ] || [ "$bc" = 0 ]; }; then + exit 0 +fi + +# First healthy boot after an update: disarm the trial on BOTH env media +# (upgrade_available is the CONFIG_BOOTCOUNT_ENV gate; clear bootcount too). +rc=0 +mono-fw-setenv upgrade_available 0 || rc=1 +mono-fw-setenv bootcount 0 || rc=1 +if [ "$rc" = 0 ]; then + logger -t mono-markgood "slot confirmed good (rollback trial cleared)" + exit 0 +fi +logger -t mono-markgood "failed to clear rollback trial; will retry next boot" +exit 1 diff --git a/package/mono/ab-env/files/fw_env.emmc b/package/mono/ab-env/files/fw_env.emmc new file mode 100644 index 0000000000..1413cc03c7 --- /dev/null +++ b/package/mono/ab-env/files/fw_env.emmc @@ -0,0 +1,6 @@ +# Gateway-DK eMMC U-Boot env copy - used when the firmware-boot DIP switch +# selects eMMC (the QSPI/NOR primary lives in /etc/fw_env.config = mtd3). +# Matches U-Boot: CONFIG_ENV_OFFSET=0x300000, CONFIG_ENV_SIZE=0x2000, +# CONFIG_ENV_SECT_SIZE=0x10000. mono-fw-setenv writes both copies. +# MTD device Device offset Env. size Flash sector size +/dev/mmcblk0 0x300000 0x2000 0x10000 diff --git a/package/mono/ab-env/files/mono-fw-setenv b/package/mono/ab-env/files/mono-fw-setenv new file mode 100644 index 0000000000..c656b08a05 --- /dev/null +++ b/package/mono/ab-env/files/mono-fw-setenv @@ -0,0 +1,24 @@ +#!/bin/sh +# mono-fw-setenv - apply a U-Boot env change to BOTH firmware env media. +# +# The Gateway-DK has a firmware-boot DIP switch (QSPI/NOR vs eMMC). Each medium +# carries its own single-copy U-Boot env: +# QSPI = mtd3 -> /etc/fw_env.config (also what plain fw_printenv reads) +# eMMC = mmcblk0 @0x300000 -> /etc/fw_env.d/emmc +# U-Boot reads whichever the DIP switch selects, so to keep the A/B slot + +# bootcount correct either way we write BOTH. Both regions are always writable +# on this board (mtd3 NOR erase+write; the eMMC env slot sits below GPT p1, +# unclaimed by any partition), so both should normally succeed - a failure of +# either is a real signal. Strict: exits non-zero unless BOTH writes succeed. +# +# All args pass straight through to fw_setenv, e.g. +# mono-fw-setenv slot b +# mono-fw-setenv bootcount 0 +# mono-fw-setenv -s /tmp/ab.env (batch: "key value" lines) +QSPI_CFG=/etc/fw_env.config +EMMC_CFG=/etc/fw_env.d/emmc + +rc=0 +fw_setenv -c "$QSPI_CFG" "$@" || { echo "mono-fw-setenv: QSPI (mtd3) env write FAILED" >&2; rc=1; } +fw_setenv -c "$EMMC_CFG" "$@" || { echo "mono-fw-setenv: eMMC env write FAILED" >&2; rc=1; } +exit "$rc" diff --git a/package/system/selinux-policy/files/cil/stockfixes.cil b/package/system/selinux-policy/files/cil/stockfixes.cil index 5e63a9cf34..5563774bb2 100644 --- a/package/system/selinux-policy/files/cil/stockfixes.cil +++ b/package/system/selinux-policy/files/cil/stockfixes.cil @@ -504,3 +504,22 @@ (in wget (call .conf.read_file_files (subj))) + +;; =========================================================================== +;; mono A/B — label the eMMC U-Boot env config so envtools can read it +;; =========================================================================== +;; Denials: file conf.file { read open } for comm="fw_setenv" path= +;; "/etc/fw_env.d/emmc". mono-fw-setenv (package/mono/ab-env) writes the U-Boot +;; env to BOTH media, passing the eMMC copy via `fw_setenv -c /etc/fw_env.d/emmc` +;; (the QSPI/mtd3 primary is /etc/fw_env.config). That primary already carries +;; envtools.conf.file (envtoolssysagent.cil:21), but the new eMMC file defaults to +;; the generic conf.file type, which envtools.printenv.subj cannot read. Give the +;; FILE that same envtools.conf.file type via the base block's file_context (the +;; odhcp.server.tmp filecon idiom above; base module untouched). +;; Label ONLY the file, NOT the dir: envtools.printenv.subj already searches +;; generic conf.file dirs (the original denials were file read/open, never a dir +;; search), but has no search on envtools.conf.file DIRS — relabeling the dir just +;; trades the read denial for a dir-search denial. Leave /etc/fw_env.d as the +;; default conf.file (searchable); only the file needs the private type. +(in envtools.conf + (filecon "/etc/fw_env\.d/emmc" file file_context)) diff --git a/target/linux/layerscape/base-files/lib/upgrade/platform.sh b/target/linux/layerscape/base-files/lib/upgrade/platform.sh index c8a4790f11..24b9012161 100644 --- a/target/linux/layerscape/base-files/lib/upgrade/platform.sh +++ b/target/linux/layerscape/base-files/lib/upgrade/platform.sh @@ -5,12 +5,13 @@ # platform_do_upgrade_mono streams each tar member to its partition through a fifo # (mono_dd_member) so tar's exit status is checked, not just dd's - busybox ash has -# no PIPESTATUS. mkfifo is the ONLY tool that path needs beyond stage2's base ramfs -# list (which already ships busybox, fwtool, gzip, hexdump, tar and dd). Left out, -# mkfifo is "not found" in the upgrade ramdisk and EVERY flash silently fails at the -# boot-partition write, then reverts to the old image on reboot. -RAMFS_COPY_BIN="mkfifo" -RAMFS_COPY_DATA="" +# no PIPESTATUS. Beyond stage2's base ramfs (busybox, fwtool, gzip, hexdump, tar, +# dd), the mono A/B flash needs: mkfifo (the fifo); fw_setenv + mono-fw-setenv to +# flip the boot slot after writing the INACTIVE slot; and the env configs as DATA +# so fw_setenv writes the same env U-Boot reads (mtd3 QSPI + eMMC copy). Left out, +# the flash silently fails at the boot write, or cannot switch slot, and reverts. +RAMFS_COPY_BIN="mkfifo fw_setenv fw_printenv mono-fw-setenv" +RAMFS_COPY_DATA="/etc/fw_env.config /etc/fw_env.d/emmc" REQUIRE_IMAGE_METADATA=1 @@ -25,9 +26,15 @@ REQUIRE_IMAGE_METADATA=1 # 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. +# added, these sectors must track the mono_gpt.py layout. +# A/B slots (mono_gpt.py): slot A = bootA p1 / rootA p2; slot B = bootB p3 / rootB +# p4. Fixed GPT sectors: bootA@65536, rootA@196608, bootB@2293760, rootB@2424832 +# (data p5 fills the rest and is never touched by sysupgrade). HW-confirmed on the +# DUT via /sys/class/block/mmcblk0pN/start. MONO_ROOT_START_SECTOR=196608 MONO_BOOT_START_SECTOR=65536 +MONO_ROOTB_START_SECTOR=2424832 +MONO_BOOTB_START_SECTOR=2293760 mono_cmdline_root() { # Last root= wins, matching the kernel (a later root= overrides an earlier). @@ -46,23 +53,26 @@ mono_part_matches() { # Echo the validated rootfs partition (e.g. /dev/mmcblk0p2), or fail (return 1). mono_gateway_root_part() { - local root part disk removable type + local root part disk idx sector 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; } + # A/B: the running rootfs is slot A (p2 @196608) or slot B (p4 @2424832). + # The boot partitions (p1/p3) and /data (p5) are never a valid root=. case "$root" in - /dev/mmcblk*p2) part="${root##*/}" ;; + /dev/mmcblk*p2) part="${root##*/}"; idx=2; sector="$MONO_ROOT_START_SECTOR" ;; + /dev/mmcblk*p4) part="${root##*/}"; idx=4; sector="$MONO_ROOTB_START_SECTOR" ;; *) echo "Refusing unsupported root device: $root" >&2; return 1 ;; esac - disk="${part%p2}" + disk="${part%p[0-9]}" 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; } + mono_part_matches "$part" "$idx" "$sector" || { + echo "Refusing: $part is not an A/B rootfs slot (want index $idx at sector $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)" @@ -71,6 +81,22 @@ mono_gateway_root_part() { echo "/dev/$part" } +# A/B slot selection for sysupgrade: from the validated RUNNING rootfs, echo the +# INACTIVE slot we must write, as: +# +# (running A -> write B: p3/p4; running B -> write A: p1/p2). Fails if the running +# slot cannot be validated. +mono_gateway_inactive_slot() { + local run disk + run="$(mono_gateway_root_part)" || return 1 + disk="${run%p[0-9]}" + case "$run" in + *p2) echo "b ${disk}p3 ${disk}p4 3 $MONO_BOOTB_START_SECTOR 4 $MONO_ROOTB_START_SECTOR" ;; + *p4) echo "a ${disk}p1 ${disk}p2 1 $MONO_BOOT_START_SECTOR 2 $MONO_ROOT_START_SECTOR" ;; + *) return 1 ;; + esac +} + platform_do_upgrade_sdboot() { local diskdev partdev parttype=ext4 local tar_file="$1" @@ -188,40 +214,66 @@ platform_do_upgrade_mono() { 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. + # A/B: write the INACTIVE slot, leaving the running slot intact as the rollback + # target. mono_gateway_inactive_slot validates the running rootfs and returns + # the slot we must write. + local new_slot bootpart rootpart boot_idx boot_sector root_idx root_sector ab + ab="$(mono_gateway_inactive_slot)" || { + echo "Refusing upgrade: could not determine the inactive A/B slot (running rootfs did not validate)"; return 1; } + set -- $ab + new_slot="$1"; bootpart="$2"; rootpart="$3" + boot_idx="$4"; boot_sector="$5"; root_idx="$6"; root_sector="$7" + echo "A/B: writing INACTIVE slot ${new_slot} (boot ${bootpart}, root ${rootpart})" + + # Validate BOTH targets (index + fixed GPT sector) before any write, so a wrong + # or blank GPT is refused rather than dd'd. Boot is written first and is the more + # catastrophic target. + [ -b "$bootpart" ] && mono_part_matches "${bootpart#/dev/}" "$boot_idx" "$boot_sector" || { + echo "Inactive boot $bootpart failed validation (want index $boot_idx at sector $boot_sector)"; return 1; } + [ -b "$rootpart" ] && mono_part_matches "${rootpart#/dev/}" "$root_idx" "$root_sector" || { + echo "Inactive root $rootpart failed validation (want index $root_idx at sector $root_sector)"; return 1; } + + # The "kernel" member is the complete boot partition image (Image.gz + dtb + + # extlinux.conf), so dtb + boot config always match the kernel. The GPT, the raw + # boot firmware in the first 32 MiB, the ACTIVE slot, and /data (p5) are never + # touched. 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. An old-layout unit (2-partition GPT, - # no /data) is migrated to the A/B + /data layout on first boot by the - # 05-mono-gateway-migrate uci-default, not here - sysupgrade only ever - # rewrites bootA (p1) and rootA (p2), never the GPT or /data (p5). + + # Both slots are on disk. Flip the boot pointer to the freshly written slot and + # ARM the rollback trial: bootcount=0 (fresh budget) + upgrade_available=1 (the + # CONFIG_BOOTCOUNT_ENV gate - U-Boot only counts/rolls-back while this is 1; the + # new slot's mark-good clears it on a healthy boot). mono-fw-setenv writes BOTH + # env media (QSPI mtd3 + eMMC) so it holds regardless of the firmware-boot DIP + # switch. If the SLOT flip fails, abort WITHOUT switching so the unit still boots + # the good (running) slot; if only arming fails, warn - the new slot still boots, + # just without the auto-rollback safety net. + echo "Activating slot ${new_slot} (both env media)..." + mono-fw-setenv slot "$new_slot" || { + echo "Refusing: could not set boot slot to ${new_slot}; keeping current slot"; return 1; } + mono-fw-setenv bootcount 0 || echo "Warning: could not reset bootcount" + mono-fw-setenv upgrade_available 1 || echo "Warning: could not arm rollback (upgrade_available); new slot boots without auto-rollback" + # The new slot's first-boot uci-default re-expands the 384M rootfs to fill its + # partition. sysupgrade now reboots into slot ${new_slot}; if it fails to boot + # bootlimit times, U-Boot's altbootcmd flips back to the good slot. Migration of + # an old 2-partition unit still happens on first boot (05-mono-gateway-migrate). } platform_copy_config_mono() { - local rootpart - rootpart="$(mono_gateway_root_part)" || { - echo "Could not validate rootfs for config backup"; return 1; } + # The config backup must land on the slot we just wrote (the INACTIVE slot), + # so the new slot restores it on first boot - NOT the running slot. + local ab rootpart + ab="$(mono_gateway_inactive_slot)" || { + echo "Could not determine the inactive A/B slot for config backup"; return 1; } + set -- $ab + rootpart="$3" # fields: ... mkdir -p /tmp/new_root if mount -t ext4 -o rw,noatime "$rootpart" /tmp/new_root; then - echo "Saving config backup to new rootfs..." + echo "Saving config backup to the new slot rootfs ($rootpart)..." cp -af "$UPGRADE_BACKUP" /tmp/new_root/sysupgrade.tgz umount /tmp/new_root fi diff --git a/target/linux/layerscape/image/armv8_64b.mk b/target/linux/layerscape/image/armv8_64b.mk index ebca67de9d..4110ec22d4 100644 --- a/target/linux/layerscape/image/armv8_64b.mk +++ b/target/linux/layerscape/image/armv8_64b.mk @@ -415,7 +415,12 @@ define Build/mono-bootfs $(CP) $(DEVICE_DTS_DIR)/$(DEVICE_DTS).dtb $@.bootdir/boot/ # SELinux mode is set by /etc/selinux/config (SELINUX=permissive during bring-up); # a kernel `enforcing=` arg here is overridden at policy load, so it's not used. - printf 'label OpenWrt\n\tkernel /boot/Image.gz\n\tfdt /boot/%s.dtb\n\tappend root=/dev/mmcblk0p2 rootwait console=ttyS0,115200 earlycon=uart8250,mmio,0x21c0500\n' \ + # root= is per-slot: U-Boot's sysboot expands $${rootpart} (set by bootcmd's + # set_slot_a/b) in the append line (pxe_utils cli_simple_process_macros), so + # ONE image boots either A/B slot. $$ keeps make from eating the ${..} - it + # must reach the file literally for U-Boot to expand at boot. default+timeout + # make the single label auto-boot even if a second entry is ever added. + printf 'default OpenWrt\ntimeout 10\nlabel OpenWrt\n\tkernel /boot/Image.gz\n\tfdt /boot/%s.dtb\n\tappend root=/dev/mmcblk0p$${rootpart} rootwait console=ttyS0,115200 earlycon=uart8250,mmio,0x21c0500 panic=10\n' \ "$(DEVICE_DTS)" > $@.bootdir/boot/extlinux/extlinux.conf # GPT blobs travel in the boot partition: the 33-sector backup tail (first # boot dd's it to the device end so the on-disk table is complete) and the @@ -470,7 +475,7 @@ define Device/mono_gateway-dk block-mount kmod-usb-storage-uas kmod-fs-exfat kmod-fs-ntfs3 \ kmod-fs-vfat smartmontools usbutils pciutils i2c-tools \ tmux vim-full curl rsync jq less bind-dig openssh-sftp-server \ - usign ca-bundle file ip-full resize2fs e2fsprogs mono-update luci-app-mono-update \ + usign ca-bundle file ip-full resize2fs e2fsprogs uboot-envtools mono-ab-env mono-update luci-app-mono-update \ cmmqos \ policycoreutils-setfiles policycoreutils-sestatus KERNEL_NAME := Image diff --git a/target/linux/layerscape/tests/test_mono_root_part.sh b/target/linux/layerscape/tests/test_mono_root_part.sh index efd60a4ba4..5e89d2a170 100755 --- a/target/linux/layerscape/tests/test_mono_root_part.sh +++ b/target/linux/layerscape/tests/test_mono_root_part.sh @@ -29,21 +29,24 @@ grep -q "MONO_ROOT_START_SECTOR=196608" "$S/platform.sh" || { echo "FAIL: rootfs 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" +valid() { # $1 = disk name (default mmcblk0); running slot A (root=p2) by default + local d=${1:-mmcblk0} 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" + mkdir -p "$S/dev" "$S/sys/block/$d/device" \ + "$S/sys/class/block/${d}p1" "$S/sys/class/block/${d}p2" \ + "$S/sys/class/block/${d}p3" "$S/sys/class/block/${d}p4" + printf 'root=/dev/%sp2 rootwait console=ttyS0,115200\n' "$d" > "$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" + : > "$S/dev/${d}p2"; : > "$S/dev/${d}p4" + echo 1 > "$S/sys/class/block/${d}p1/partition"; echo 65536 > "$S/sys/class/block/${d}p1/start" + echo 2 > "$S/sys/class/block/${d}p2/partition"; echo 196608 > "$S/sys/class/block/${d}p2/start" + echo 3 > "$S/sys/class/block/${d}p3/partition"; echo 2293760 > "$S/sys/class/block/${d}p3/start" + echo 4 > "$S/sys/class/block/${d}p4/partition"; echo 2424832 > "$S/sys/class/block/${d}p4/start" echo 0 > "$S/sys/block/$d/removable" echo MMC > "$S/sys/block/$d/device/type" } +# Switch the running slot to B (root=p4) after a valid() setup. +use_slot_b() { local d=${1:-mmcblk0}; printf 'root=/dev/%sp4 rootwait console=ttyS0,115200\n' "$d" > "$S/cmdline"; } 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=$? @@ -71,12 +74,22 @@ valid; rm -f "$S/dev/mmcblk0p2"; check 1 "rootfs d 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)" +# A/B: slot B rootfs (p4 @2424832) is a valid running root; boot parts (p1/p3) are not. +valid; use_slot_b; check 0 "valid p4 @2424832 (slot B running)" /dev/mmcblk0p4 +valid; printf 'root=/dev/mmcblk0p3 rootwait\n' > "$S/cmdline"; check 1 "root= is p3 (boot partition, not a slot)" +valid; use_slot_b; echo 65536 > "$S/sys/class/block/mmcblk0p4/start"; check 1 "slot B rootfs at wrong start sector" -echo "== derivation: writer picks the sibling boot partition ==" +echo "== A/B: sysupgrade writes the INACTIVE slot ==" +# running slot A (root=p2) -> write slot B: bootB p3, rootB p4 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 +ab=$(mono_gateway_inactive_slot 2>/dev/null) +if [ "$ab" = "b /dev/mmcblk0p3 /dev/mmcblk0p4 3 2293760 4 2424832" ]; then echo "ok A active -> write B" +else echo "FAIL inactive from A: '$ab'"; fails=$((fails+1)); fi +# running slot B (root=p4) -> write slot A: bootA p1, rootA p2 +valid; use_slot_b +ab=$(mono_gateway_inactive_slot 2>/dev/null) +if [ "$ab" = "a /dev/mmcblk0p1 /dev/mmcblk0p2 1 65536 2 196608" ]; then echo "ok B active -> write A" +else echo "FAIL inactive from B: '$ab'"; 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 -- 2.47.3