From cc9ab795626044dddb296c65db739efefed3b08a Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:13:12 +0300 Subject: [PATCH 1/2] load_novatek: give Linux 48M of a 128 MB board, not 32M 128 MB NT9856x boards have been set up with osmem=32M, which leaves Linux 25 MB. That is one live viewer before majestic's low-memory floor sheds it, and on a clean boot about 3.4 MB free while it streams, while HDAL keeps a 96 MB pool it does not fill (41 MB free). At osmem=48M the same NT98566 + SC501AI runs with 23 MB available, majestic admits seven viewers, and HDAL still keeps 25 MB free. The vendor U-Boot reads osmem from its environment, so the firmware moves it there: load_novatek, on a board whose device tree says 128 MB of DRAM, with osmem unset or still at the old 32M, writes osmem=48M and the matching mem= in bootargs in one fw_setenv --script write, effective at the next boot -- the two can never be written apart, which would hang the kernel or crash HDAL. A bootargs that carries the ${osmem} placeholder keeps it and only osmem moves; a bootargs whose mem= already disagrees is not touched. osmem_default marks the move done, so a split an owner picks afterwards is left alone. Nothing changes on any other board or setting. .github/scripts/test_load_novatek.sh runs the real script under busybox with a faked device tree, command line and environment, and checks every case that must write and every case that must not, check_mem's guard included. --- .github/scripts/ci-matrix.py | 2 +- .github/scripts/test_load_novatek.sh | 137 ++++++++++++++++++ .github/workflows/shell-tests.yml | 1 + CLAUDE.md | 1 + .../files/script/load_novatek | 57 +++++++- 5 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 .github/scripts/test_load_novatek.sh diff --git a/.github/scripts/ci-matrix.py b/.github/scripts/ci-matrix.py index 016ec9c4b..81f11f5b6 100644 --- a/.github/scripts/ci-matrix.py +++ b/.github/scripts/ci-matrix.py @@ -209,7 +209,7 @@ "test_ca_bundle_lite.sh", "test_network_failover.sh", "lint-workflow-shell.py", "push_build.py", "soc_aliases.py", "test_automount.sh", "test_check_mac.sh", "test_excludes_report.sh", - "test_load_hisilicon.sh", "test_load_goke.sh", "test_load_hisilicon_layout.sh", "test_push_build.py", "test_shell_parse.sh", + "test_load_hisilicon.sh", "test_load_goke.sh", "test_load_novatek.sh", "test_load_hisilicon_layout.sh", "test_push_build.py", "test_shell_parse.sh", "test_strip_shell_comments.sh", "test_sysupgrade.sh", "test_crashlog.sh", "test_majestic_supervise.sh", "test_vigil_supervise.sh", "test_init_overlay.sh", diff --git a/.github/scripts/test_load_novatek.sh b/.github/scripts/test_load_novatek.sh new file mode 100644 index 000000000..06c75e491 --- /dev/null +++ b/.github/scripts/test_load_novatek.sh @@ -0,0 +1,137 @@ +#!/bin/sh +# load_novatek (NT98562/NT98566): what it does to the U-Boot environment. +# +# The vendor U-Boot lays memory out from osmem and the kernel takes mem= from +# bootargs; load_novatek keeps the two from disagreeing (check_mem, #2657) and +# moves a 128 MB board from the old osmem=32M to 48M once (set_default_osmem, +# #2107). Both write the environment of a camera nobody is watching, so every +# case that must NOT write is tested as carefully as the ones that must. +# +# Runs the real script the way S70vendor does at boot, under busybox sh as on +# the camera, from a wrapper that defines insmod, devmem, fw_printenv, +# fw_setenv, ipcinfo and logger as shell functions -- busybox sh runs its own +# applets ahead of anything on PATH, but not ahead of functions -- so every +# module load and environment write is recorded. The device tree and the +# kernel command line come from LOAD_NOVATEK_DT and LOAD_NOVATEK_CMDLINE. + +set -eu + +if ! command -v busybox >/dev/null 2>&1; then + echo "FAIL busybox is needed: it is the shell the camera runs" + exit 1 +fi + +script=$(pwd)/general/package/novatek-osdrv-nt9856x/files/script/load_novatek +fail=0 +ok() { echo "ok $*"; } +bad() { echo "FAIL $*"; fail=$((fail + 1)); } + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +cat >"$work/wrapper" <<'EOF' +insmod() { echo "insmod $1" >>"$CALLS"; } +devmem() { :; } +logger() { :; } +ipcinfo() { echo sc501ai; } +fw_printenv() { [ "$1" = -n ] && shift; sed -n "s/^$1=//p" "$ENV_FILE" | grep . || return 1; } +fw_setenv() { + if [ "$1" = --script ]; then + echo "fw_setenv --script" >>"$CALLS" + sed 's/^/batch /' "$2" >>"$CALLS" + else + echo "fw_setenv $*" >>"$CALLS" + fi +} +export SENSOR=sc501ai +set -- -i +. "$SCRIPT" +EOF + +# A one-address, one-size reg property: 8 big-endian bytes, base 0. +reg() { + mkdir -p "$(dirname "$1")" + size=$(( $2 << 20 )) + printf "\\000\\000\\000\\000\\$(printf %03o $(( (size >> 24) & 255 )))\\$(printf %03o $(( (size >> 16) & 255 )))\\$(printf %03o $(( (size >> 8) & 255 )))\\$(printf %03o $(( size & 255 )))" >"$1" +} + +# run DRAM_MB DT_MEM_MB CMDLINE then the environment, one key=value per +# argument; every call is left in $work/calls +run() { + rm -rf "$work/dt" + reg "$work/dt/nvt_memory_cfg/dram/reg" "$1" + reg "$work/dt/memory/reg" "$2" + printf '%s\n' "$3" >"$work/cmdline" + shift 3 + : >"$work/env" + for kv in "$@"; do + printf '%s\n' "$kv" >>"$work/env" + done + : >"$work/calls" + (cd "$work" && SCRIPT="$script" CALLS="$work/calls" ENV_FILE="$work/env" \ + LOAD_NOVATEK_DT="$work/dt" LOAD_NOVATEK_CMDLINE="$work/cmdline" \ + busybox sh "$work/wrapper" >"$work/out" 2>&1) || true +} + +has() { + if grep -qF -- "$1" "$work/calls"; then ok "$2"; else bad "$2 -- no '$1' in: $(tr '\n' ';' <"$work/calls")"; fi +} +lacks() { + if grep -qF -- "$1" "$work/calls"; then bad "$2 -- found '$1' in: $(tr '\n' ';' <"$work/calls")"; else ok "$2"; fi +} +loaded() { + if grep -q '^insmod hdal/kwrap.ko' "$work/calls"; then ok "$1"; else bad "$1 -- HDAL was not loaded"; fi +} + +lit='earlyprintk console=ttyS0,115200 mem=32M panic=20 root=/dev/mtdblock3 rootfstype=squashfs' +ph='earlyprintk console=ttyS0,115200 mem=${osmem} panic=20 root=/dev/mtdblock3 rootfstype=squashfs' + +echo "=== the 48M default on 128 MB boards ===" + +run 128 32 "$lit" "osmem=32M" "bootargs=$lit" +has "fw_setenv --script" "osmem=32M on 128 MB: one batch write" +has "batch osmem 48M" "... osmem goes to 48M" +has "batch bootargs earlyprintk console=ttyS0,115200 mem=48M panic=20" "... and mem= with it, the rest of bootargs untouched" +has "batch osmem_default 48M" "... marked done" +lacks "fw_setenv osmem" "... osmem is never written on its own, apart from mem=" +loaded "... and this boot still loads HDAL at 32M" + +run 128 32 "$lit" "bootargs=$lit" +has "batch osmem 48M" "osmem unset (U-Boot's 32M default): moved too" + +run 128 32 "$lit" "osmem=32M" "bootargs=$ph" +has "batch osmem 48M" "bootargs with the \${osmem} placeholder: osmem moves" +lacks "batch bootargs" "... and bootargs is left alone, the placeholder carries it" + +run 128 48 "earlyprintk mem=48M panic=20" "osmem=48M" "osmem_default=48M" "bootargs=earlyprintk mem=48M panic=20" +lacks "fw_setenv" "the boot after: nothing written" +loaded "... and HDAL loads at 48M" + +run 128 32 "$lit" "osmem=32M" "osmem_default=48M" "bootargs=$lit" +lacks "fw_setenv" "an owner who set 32M again after the move keeps it" + +run 128 54 "earlyprintk mem=54M" "osmem=54M" "bootargs=earlyprintk mem=54M" +lacks "fw_setenv" "an osmem of the owner's own (54M): left alone" + +run 64 32 "$lit" "osmem=32M" "bootargs=$lit" +lacks "fw_setenv" "a 64 MB board: left alone" + +run 128 32 "earlyprintk mem=32M" "osmem=32M" "bootargs=earlyprintk mem=40M panic=20" +lacks "fw_setenv" "saved bootargs already disagreeing (mem=40M): not touched" + +rm -rf "$work/dt" +echo "=== the mem= / osmem guard ===" + +run 128 32 "earlyprintk mem=54M panic=20" "osmem=32M" "bootargs=earlyprintk mem=54M panic=20" +has "fw_setenv bootargs earlyprintk mem=32M panic=20" "mem=54M past a 32M layout: bootargs brought back to the layout" +lacks "insmod hdal/kwrap.ko" "... and HDAL is not loaded over the kernel's memory" +lacks "fw_setenv --script" "... and the 48M move waits for a consistent boot" + +run 128 32 "earlyprintk panic=20 mem=54M" "osmem=32M" "bootargs=earlyprintk panic=20 mem=54M" +has "fw_setenv bootargs earlyprintk panic=20 mem=32M" "... mem= as the last argument too" + +if [ "$fail" -ne 0 ]; then + echo "$fail check(s) failed" + exit 1 +fi +echo "all load_novatek checks passed" diff --git a/.github/workflows/shell-tests.yml b/.github/workflows/shell-tests.yml index 3421a71ae..4bc2e6eae 100644 --- a/.github/workflows/shell-tests.yml +++ b/.github/workflows/shell-tests.yml @@ -30,6 +30,7 @@ jobs: sudo apt-get update -qq sudo apt-get install -y -qq busybox-static - run: bash .github/scripts/test_load_goke.sh + - run: bash .github/scripts/test_load_novatek.sh - run: bash .github/scripts/test_load_hisilicon_layout.sh sysupgrade-verify: diff --git a/CLAUDE.md b/CLAUDE.md index 4bd3bad2d..37fa8fa1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -188,6 +188,7 @@ nor a camera. There is no test framework; these are scripts, run them from the r ```sh bash .github/scripts/test_load_hisilicon.sh # os_mem_size derivation +bash .github/scripts/test_load_novatek.sh # the osmem / mem= writes (needs busybox) bash .github/scripts/test_sysupgrade.sh # sysupgrade rootfs verification bash .github/scripts/test_check_mac.sh # the MAC the camera gives itself bash .github/scripts/test_excludes_report.sh # excludes lists report stale entries diff --git a/general/package/novatek-osdrv-nt9856x/files/script/load_novatek b/general/package/novatek-osdrv-nt9856x/files/script/load_novatek index ce2be0bd6..9b3cb9b15 100755 --- a/general/package/novatek-osdrv-nt9856x/files/script/load_novatek +++ b/general/package/novatek-osdrv-nt9856x/files/script/load_novatek @@ -152,6 +152,15 @@ insert_ko() { #insmod hdal/kdrv_videoout/fbdev/nvt_fb.ko } +proc_cmdline=${LOAD_NOVATEK_CMDLINE:-/proc/cmdline} +device_tree=${LOAD_NOVATEK_DT:-/proc/device-tree} + +# The size cell of a one-address, one-size DT reg property, in MB. +dt_mb() { + [ -r "$1" ] || return 1 + echo $(( 0x$(hexdump -ve '1/1 "%02x"' "$1" | cut -c9-16) >> 20 )) +} + # The vendor U-Boot lays memory out from osmem: the DT memory node it hands # over ends there and the HDAL pool starts there. The kernel's RAM comes from # mem= alone, and the bootcmd these boards were given re-runs the saved @@ -161,11 +170,9 @@ insert_ko() { # The opposite drift leaves the DT above the kernel's RAM and hangs it before # the console, which nothing here can catch. #2107. check_mem() { - reg=/proc/device-tree/memory/reg - [ -r $reg ] || return 0 - dt_mem=$(( 0x$(hexdump -ve '1/1 "%02x"' $reg | cut -c9-16) >> 20 )) + dt_mem=$(dt_mb "$device_tree/memory/reg") || return 0 booted_mem= - for arg in $(cat /proc/cmdline); do + for arg in $(cat "$proc_cmdline"); do case "$arg" in mem=*M) booted_mem=${arg#mem=} @@ -191,7 +198,49 @@ check_mem() { exit 1 } +# 128 MB boards were set up with osmem=32M, which leaves Linux 25 MB -- one +# live viewer before majestic's low-memory floor sheds it -- and HDAL a 96 MB +# pool it does not fill. At 48M Linux has 16 MB more (six viewers measured on +# an NT98566 + SC501AI) and HDAL still keeps 25 MB free, 14.5 MB with a JPEG +# path running beside the stream. Moved once, for the next boot: osmem and +# mem= in one environment write, so they cannot disagree, with osmem_default +# marking it done, so a split an owner picks afterwards is left as it is. +# #2107. +set_default_osmem() { + dram=$(dt_mb "$device_tree/nvt_memory_cfg/dram/reg") || return 0 + [ "$dram" -eq 128 ] || return 0 + [ -z "$(fw_printenv -n osmem_default 2>/dev/null)" ] || return 0 + case "$(fw_printenv -n osmem 2>/dev/null)" in + "" | 32M) ;; + *) return 0 ;; + esac + + bootargs=$(fw_printenv -n bootargs 2>/dev/null) + case " $bootargs " in + *' mem=${osmem'*) + bootargs= + ;; + *" mem=32M "*) + bootargs=$(echo " $bootargs" | sed "s/ mem=32M/ mem=48M/; s/^ //") + ;; + *) + return 0 + ;; + esac + + batch=$(mktemp) || return 0 + { + echo "osmem 48M" + [ -n "$bootargs" ] && echo "bootargs $bootargs" + echo "osmem_default 48M" + } >"$batch" + fw_setenv --script "$batch" && + logger -s -p daemon.info -t novatek "Linux memory set to 48M of 128M; applies at the next boot" + rm -f "$batch" +} + check_mem +set_default_osmem cd /lib/modules/4.19.91/novatek insert_ko From 4fc6d8958baba769523752fc8cf5b6d15f751e43 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:18:30 +0300 Subject: [PATCH 2/2] load_novatek: move osmem only over a bootargs with one mem=, and test that the script ran A saved bootargs with a second mem= beside mem=32M was half-moved and marked done. The move now needs exactly one mem=, of the old split or the ${osmem} placeholder. The test records each run's exit status, so a case that must not write cannot pass because the script died first, and gains the two-mem= case. --- .github/scripts/test_load_novatek.sh | 20 +++++++++++++++++-- .../files/script/load_novatek | 18 ++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/scripts/test_load_novatek.sh b/.github/scripts/test_load_novatek.sh index 06c75e491..6b126d72c 100644 --- a/.github/scripts/test_load_novatek.sh +++ b/.github/scripts/test_load_novatek.sh @@ -68,9 +68,16 @@ run() { printf '%s\n' "$kv" >>"$work/env" done : >"$work/calls" + rc=0 (cd "$work" && SCRIPT="$script" CALLS="$work/calls" ENV_FILE="$work/env" \ LOAD_NOVATEK_DT="$work/dt" LOAD_NOVATEK_CMDLINE="$work/cmdline" \ - busybox sh "$work/wrapper" >"$work/out" 2>&1) || true + busybox sh "$work/wrapper" >"$work/out" 2>&1) || rc=$? +} + +# The script ran to its end (0), or stopped where it means to (check_mem's +# exit 1) -- so a no-write case cannot pass because the script died first. +exited() { + if [ "$rc" = "$1" ]; then ok "$2"; else bad "$2 -- exit $rc, want $1: $(tail -3 "$work/out" | tr '\n' ';')"; fi } has() { @@ -80,7 +87,7 @@ lacks() { if grep -qF -- "$1" "$work/calls"; then bad "$2 -- found '$1' in: $(tr '\n' ';' <"$work/calls")"; else ok "$2"; fi } loaded() { - if grep -q '^insmod hdal/kwrap.ko' "$work/calls"; then ok "$1"; else bad "$1 -- HDAL was not loaded"; fi + if grep -q '^insmod nvt_sen_sc501ai.ko' "$work/calls"; then ok "$1"; else bad "$1 -- HDAL and the sensor were not loaded"; fi } lit='earlyprintk console=ttyS0,115200 mem=32M panic=20 root=/dev/mtdblock3 rootfstype=squashfs' @@ -93,6 +100,7 @@ has "fw_setenv --script" "osmem=32M on 128 MB: one batch write" has "batch osmem 48M" "... osmem goes to 48M" has "batch bootargs earlyprintk console=ttyS0,115200 mem=48M panic=20" "... and mem= with it, the rest of bootargs untouched" has "batch osmem_default 48M" "... marked done" +exited 0 "... and the script runs to its end" lacks "fw_setenv osmem" "... osmem is never written on its own, apart from mem=" loaded "... and this boot still loads HDAL at 32M" @@ -109,15 +117,22 @@ loaded "... and HDAL loads at 48M" run 128 32 "$lit" "osmem=32M" "osmem_default=48M" "bootargs=$lit" lacks "fw_setenv" "an owner who set 32M again after the move keeps it" +exited 0 "... and the script runs to its end" run 128 54 "earlyprintk mem=54M" "osmem=54M" "bootargs=earlyprintk mem=54M" lacks "fw_setenv" "an osmem of the owner's own (54M): left alone" run 64 32 "$lit" "osmem=32M" "bootargs=$lit" lacks "fw_setenv" "a 64 MB board: left alone" +exited 0 "... and the script runs to its end" +loaded "... and loads HDAL" run 128 32 "earlyprintk mem=32M" "osmem=32M" "bootargs=earlyprintk mem=40M panic=20" lacks "fw_setenv" "saved bootargs already disagreeing (mem=40M): not touched" +exited 0 "... and the script runs to its end" + +run 128 32 "$lit" "osmem=32M" "bootargs=earlyprintk mem=32M panic=20 mem=40M" +lacks "fw_setenv" "two mem= in saved bootargs: not touched, not half-moved" rm -rf "$work/dt" echo "=== the mem= / osmem guard ===" @@ -126,6 +141,7 @@ run 128 32 "earlyprintk mem=54M panic=20" "osmem=32M" "bootargs=earlyprintk mem= has "fw_setenv bootargs earlyprintk mem=32M panic=20" "mem=54M past a 32M layout: bootargs brought back to the layout" lacks "insmod hdal/kwrap.ko" "... and HDAL is not loaded over the kernel's memory" lacks "fw_setenv --script" "... and the 48M move waits for a consistent boot" +exited 1 "... and the script stops there" run 128 32 "earlyprintk panic=20 mem=54M" "osmem=32M" "bootargs=earlyprintk panic=20 mem=54M" has "fw_setenv bootargs earlyprintk panic=20 mem=32M" "... mem= as the last argument too" diff --git a/general/package/novatek-osdrv-nt9856x/files/script/load_novatek b/general/package/novatek-osdrv-nt9856x/files/script/load_novatek index 9b3cb9b15..4bfc20cfd 100755 --- a/general/package/novatek-osdrv-nt9856x/files/script/load_novatek +++ b/general/package/novatek-osdrv-nt9856x/files/script/load_novatek @@ -215,12 +215,24 @@ set_default_osmem() { *) return 0 ;; esac + # Exactly one mem=, and the one that goes with the old split: a second + # one, or any other value, is a bootargs somebody wrote by hand, and + # moving part of it would leave it disagreeing with itself. bootargs=$(fw_printenv -n bootargs 2>/dev/null) - case " $bootargs " in - *' mem=${osmem'*) + mem= + for arg in $bootargs; do + case "$arg" in + mem=*) + [ -z "$mem" ] || return 0 + mem=$arg + ;; + esac + done + case "$mem" in + 'mem=${osmem}' | 'mem=${osmem:-32M}') bootargs= ;; - *" mem=32M "*) + mem=32M) bootargs=$(echo " $bootargs" | sed "s/ mem=32M/ mem=48M/; s/^ //") ;; *)