qualcommax: ipq60xx: enable dual-boot for 360v6
authorZhenyu Qi <qzydustin@hotmail.com>
Sat, 13 Dec 2025 21:04:51 +0000 (14:04 -0700)
committerRobert Marko <robimarko@gmail.com>
Sun, 28 Dec 2025 11:45:40 +0000 (12:45 +0100)
Add dual-partition upgrade support for Qihoo 360v6 using the generic
bootconfig.sh library. This enables safe system upgrades with automatic
failover capability.

The device uses Qualcomm's bootconfig structure to control A/B partition
switching. The bootloader dynamically maps physical NAND partitions to
logical MTD devices based on the bootconfig, ensuring firmware always
writes to the inactive partition.

Implementation details:
- Use bootconfig.sh library (copied from ipq50xx) for bootconfig operations
- Operate on 'rootfs' partition by name instead of hardcoded offset
- Add magic header validation for safety
- Remove OEM UBI volumes (wifi_fw and ubi_rootfs) before sysupgrade
- Toggle bootconfig before removing OEM volumes

Hardware details:
- SoC: Qualcomm IPQ6000
- Flash: NAND with dual rootfs partitions (mtd16/mtd17)
- Bootconfig: controls slot selection via partition name lookup

Installation:
Standard sysupgrade process. After upgrade, the system will boot
from the new partition while preserving the old system as backup.

The OEM volume cleanup is necessary because these volumes are created
by the stock firmware and are not automatically cleaned by the standard
nand_upgrade_prepare_ubi() function, which only removes volumes named
'kernel', 'rootfs', and 'rootfs_data'. Without this cleanup, the remaining
OEM volumes consume available space, causing the creation of rootfs_data
to fail during sysupgrade.

Tested on Qihoo 360v6 running stock firmware and OpenWrt.

Signed-off-by: Zhenyu Qi <qzydustin@hotmail.com>
Link: https://github.com/openwrt/openwrt/pull/21154
Signed-off-by: Robert Marko <robimarko@gmail.com>
target/linux/qualcommax/ipq60xx/base-files/lib/functions/bootconfig.sh [new file with mode: 0644]
target/linux/qualcommax/ipq60xx/base-files/lib/upgrade/platform.sh

diff --git a/target/linux/qualcommax/ipq60xx/base-files/lib/functions/bootconfig.sh b/target/linux/qualcommax/ipq60xx/base-files/lib/functions/bootconfig.sh
new file mode 100644 (file)
index 0000000..3632c6a
--- /dev/null
@@ -0,0 +1,177 @@
+. /lib/functions.sh
+
+PART_SIZE=20
+NAME_SIZE=16
+MAX_NUM_PARTS=16
+
+MAGIC_START_HEX="a0 a1 a2 a3"
+MAGIC_START_TRY_HEX="a1 a1 a2 a3"
+MAGIC_END_HEX="b0 b1 b2 b3"
+
+validate_bootconfig_magic() {
+       local file=$1
+       magic_start=$(hexdump -v -n 4 -e '4/1 "%02x "' "$file")
+       magic_end=$(hexdump -v -s 332 -n 4 -e '4/1 "%02x "' "$file")
+       
+       if [ "$magic_start" != "$MAGIC_START_HEX" ] && \
+          [ "$magic_start" != "$MAGIC_START_TRY_HEX" ]; then
+               echo "Not a valid bootconfig file, start magic does not match" >&2
+               return 1
+       fi
+
+       if [ "$magic_end" != "$MAGIC_END_HEX" ]; then
+               echo "Not a valid bootconfig file, end magic does not match" >&2
+               return 1
+       fi
+       return 0
+}
+
+get_bootconfig_numparts() {
+       local file=$1
+       numpartshex=$(hexdump -v -s 8 -n 4 -e '4/1 "%02x "' "$file")
+       numparts=$(( 0x$(echo $numpartshex | awk '{print $4$3$2$1}') ))
+       echo ${numparts}
+}
+
+get_bootconfig_partidx() {
+       local file=$1
+       local partname=$2
+       local numparts=$(get_bootconfig_numparts "$file")
+
+       if [ -z "$numparts" ]; then
+               echo "Could not get number of partitions" >&2
+               return
+       fi
+
+       if [ $numparts -gt $MAX_NUM_PARTS ]; then
+               numparts=$MAX_NUM_PARTS
+       fi
+
+       for i in $(seq 0 $((numparts -1))); do
+               nameoffset=$((12 + i * $PART_SIZE))
+               nameraw=$(dd if="$file" bs=1 skip="$nameoffset" count=12 2>/dev/null)
+               name=${nameraw//S'\x00'/}
+               if [ "$partname" = "$name" ]; then
+                       echo $i
+               fi
+       done
+}
+
+get_bootconfig_primaryboot() {
+       local file=$1
+       local partname=$2
+
+       local partidx=$(get_bootconfig_partidx "$file" "$partname")
+       if ! echo "$partidx" | grep -Eq '^[0-9]+$'; then
+               echo "Could not get partition index for $partname in $file" >&2
+               return
+       fi
+
+       if [ "$partidx" -ge 0 ] && [ "$partidx" -lt $MAX_NUM_PARTS ]; then
+               offset=$((12 + $partidx * $PART_SIZE + $NAME_SIZE))
+               primaryboothex=$(hexdump -v -s "$offset" -n 4 -e '4/1 "%02x "' $file)
+               primaryboot=$(( 0x$(echo $primaryboothex | awk '{print $4$3$2$1}') ))
+               echo $primaryboot
+       fi
+}
+
+_set_bootconfig_primaryboot() {
+       local file=$1
+       local partname=$2
+       local primaryboot=$3
+       local primaryboothex
+       local partidx
+       local primarybootoffset
+
+       partidx=$(get_bootconfig_partidx "$file" "$partname")
+       if ! echo "$partidx" | grep -Eq '^[0-9]+$'; then
+               echo "Could not get partition index for $2" >&2
+               return 1
+       fi
+       primarybootoffset=$((12 + $partidx * $PART_SIZE + $NAME_SIZE))
+
+       case "$primaryboot" in
+               0)
+                       printf "\x00\x00\x00\x00" | dd of="$file" seek="$primarybootoffset" bs=1 count=4 conv=notrunc 2>/dev/null
+                       ;;
+               1)
+                       printf "\x01\x00\x00\x00" | dd of="$file" seek="$primarybootoffset" bs=1 count=4 conv=notrunc 2>/dev/null
+                       ;;
+               *)
+                       echo "invalid argument: primaryboot must be 0 or 1" >&2
+                       return 1
+                       ;;
+       esac
+}
+
+set_bootconfig_primaryboot() {
+       local file=$1
+       local partname=$2
+       local primaryboot=$3
+
+       [ -z "$file" ] || [ -z "$partname" ] || [ -z "$primaryboot" ] && {
+               echo "usage: $0 <file> <partition name> <0|1>"
+               return 1
+       }
+
+       [ ! -e "$file" ] && {
+               echo "file $file not found" >&2
+               return 1
+       }
+
+       [ ! -w $file ] && {
+               echo "file $file not writable" >&2
+               return 1
+       }
+
+       validate_bootconfig_magic "$file"
+       [ $? -ne 0 ] && return 1
+
+       _set_bootconfig_primaryboot $file $partname $primaryboot
+       [ $? -ne 0 ] && return 1
+
+       return 0
+}
+
+toggle_bootconfig_primaryboot() {
+       local file=$1
+       local partname=$2
+       local primaryboot
+       
+       [ -z "$file" ] || [ -z "$partname" ] && {
+               echo "usage: $0 <file> <partition name>"
+               return 1
+       }
+
+       [ ! -e "$file" ] && {
+               echo "file $file not found" >&2
+               return 1
+       }
+
+       [ ! -w $file ] && {
+               echo "file $file not writable" >&2
+               return 1
+       }
+
+       validate_bootconfig_magic "$file"
+       [ $? -ne 0 ] && return 1
+       
+       primaryboot=$(get_bootconfig_primaryboot "$1" "$2")
+
+       case "$primaryboot" in
+               0)
+                       _set_bootconfig_primaryboot "$1" "$2" 1
+                       ;;
+               1)
+                       _set_bootconfig_primaryboot "$1" "$2" 0
+                       ;;
+               *)
+                       echo "invalid value: primaryboot must be 0 or 1" >&2
+                       return 1
+                       ;;
+       esac
+
+       [ $? -ne 0 ] && return 1
+
+       return 0
+}
index d31363521fe810d86b63c882fdf4d548a67853b7..edd0127143954ba7f366a16c345b5f8e22672003 100644 (file)
@@ -1,3 +1,5 @@
+. /lib/functions/bootconfig.sh
+
 PART_NAME=firmware
 REQUIRE_IMAGE_METADATA=1
 
@@ -27,6 +29,43 @@ remove_oem_ubi_volume() {
        fi
 }
 
+qihoo_bootconfig_toggle_rootfs() {
+       local partname=$1
+       local tempfile
+       local mtdidx
+
+       mtdidx=$(find_mtd_index "$partname")
+       [ ! "$mtdidx" ] && {
+               echo "cannot find mtd index for $partname"
+               return 1
+       }
+
+       tempfile=/tmp/mtd"$mtdidx".bin
+       dd if=/dev/mtd"$mtdidx" of="$tempfile" bs=1 count=336 2>/dev/null
+       [ $? -ne 0 ] || [ ! -f "$tempfile" ] && {
+               echo "failed to create a temp copy of /dev/mtd$mtdidx"
+               return 1
+       }
+
+       toggle_bootconfig_primaryboot "$tempfile" "rootfs"
+       [ $? -ne 0 ] && {
+               echo "failed to toggle primaryboot for rootfs partition"
+               return 1
+       }
+
+       mtd write "$tempfile" /dev/mtd"$mtdidx" 2>/dev/null
+       [ $? -ne 0 ] && {
+               echo "failed to write temp copy back to /dev/mtd$mtdidx"
+               return 1
+       }
+
+       # Update bootconfig1 if exists
+       local mtdidx1=$(find_mtd_index "${partname}1")
+       [ -n "$mtdidx1" ] && mtd write "$tempfile" /dev/mtd"$mtdidx1" 2>/dev/null
+
+       return 0
+}
+
 tplink_get_boot_part() {
        local cur_boot_part
        local args
@@ -117,8 +156,14 @@ platform_do_upgrade() {
                ;;
        glinet,gl-ax1800|\
        glinet,gl-axt1800|\
-       netgear,wax214|\
+       netgear,wax214)
+               nand_do_upgrade "$1"
+               ;;
        qihoo,360v6)
+               CI_UBIPART="rootfs_1"
+               qihoo_bootconfig_toggle_rootfs "0:bootconfig"
+               remove_oem_ubi_volume wifi_fw
+               remove_oem_ubi_volume ubi_rootfs
                nand_do_upgrade "$1"
                ;;
        netgear,wax610|\