mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 22:54:01 +01:00
Prep for a Ulefone Armor X12 Pro (MediaTek MT6765) that has not arrived, validated read-only against the Pixel 6a that has. Partition discovery never hardcodes boot vs init_boot. It reads /dev/block/by-name/, derives the slot scheme from observed _a suffixes, and picks the ramdisk carrier by preference order init_boot then boot over what is actually present. The AOSP launch-version rule explains why that ordering is right; the code consults the filesystem, not the API level. One path therefore serves both a Pixel (no init_boot, falls to boot_a) and a 13-launch MediaTek device (init_boot_a taken). Patching the wrong one yields a phone that will not boot. Classification refuses rather than guesses: ULEFONE needs a positive MediaTek signal AND a vendor match, with no "not a Pixel therefore Ulefone" fallback. Carrier lock is always reported UNDETERMINED, because sys.oem_unlock_allowed, ro.oem_unlock_supported and settings global oem_unlock_allowed read identically whether a device is carrier-locked or merely has the toggle off; asserting a negative would wrongly rule out a path. No flashing script, deliberately. Automating irreversible partition writes defeats the confirmation steps that catch a wrong partition or missing slot suffix. Three bugs found by running the code rather than only linting it: - SIGPIPE: `adb version | head -1` under pipefail exited 141 and killed 00-preflight.sh mid-run - 20-dump-stock.sh exited silently when no recon had run, because find on a nonexistent cache dir fails under set -e - the read-only static scan had a hole: stripping all quoted strings to avoid false positives on printf text also hid `adb shell "reboot bootloader"`. Now only printf/echo/cat arguments are stripped, and a negative self-test asserts the scan fails on injected commands Also narrowed a world-writable udev catch-all (idVendor=="*", MODE="0666") that granted every local process read/write access to every USB device; commented with a backup, adb connectivity re-verified afterwards. Validated: 58/58 offline fixture tests; 00-preflight.sh HOST READY (12/12); 10-recon.sh read-only against the Pixel reports PIXEL, boot_a, A/B, no init_boot, path C-or-D indeterminate, with the device unchanged afterwards. Untested and labelled as such in READY.md: the init_boot branch on real hardware, mtkclient over BROM, and the whole flash/verify sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
422 lines
14 KiB
Bash
Executable File
422 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# ============================================================================
|
|
# Fixture tests for the mtk_root toolkit.
|
|
#
|
|
# Every test runs against a fixture directory (props.txt + by-name.txt) via
|
|
# MTK_ROOT_FIXTURE, so no phone is ever touched and the suite is hermetic.
|
|
#
|
|
# What these tests DO prove: the partition-selection logic, the classification
|
|
# refusals, and the read-only guarantee of 10-recon.sh.
|
|
# What they do NOT prove: that mtkclient can read any partition on real
|
|
# hardware. Fixtures cannot establish that.
|
|
# ============================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
|
readonly SCRIPT_DIR
|
|
readonly TOOLKIT_DIR="$SCRIPT_DIR/.."
|
|
readonly FIXTURES="$SCRIPT_DIR/fixtures"
|
|
readonly RECON="$TOOLKIT_DIR/10-recon.sh"
|
|
readonly DUMP="$TOOLKIT_DIR/20-dump-stock.sh"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
WORKROOT=""
|
|
|
|
cleanup() {
|
|
[[ -n $WORKROOT && -d $WORKROOT ]] && rm -rf "$WORKROOT"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
pass() {
|
|
PASS=$((PASS + 1))
|
|
printf ' [PASS] %s\n' "$1"
|
|
}
|
|
|
|
fail() {
|
|
FAIL=$((FAIL + 1))
|
|
printf ' [FAIL] %s\n' "$1"
|
|
[[ -n ${2:-} ]] && printf ' %s\n' "$2"
|
|
return 0
|
|
}
|
|
|
|
assert_contains() {
|
|
if [[ $2 == *"$3"* ]]; then
|
|
pass "$1"
|
|
else
|
|
fail "$1" "expected to find: $3"
|
|
fi
|
|
}
|
|
|
|
assert_not_contains() {
|
|
if [[ $2 != *"$3"* ]]; then
|
|
pass "$1"
|
|
else
|
|
fail "$1" "did not expect: $3"
|
|
fi
|
|
}
|
|
|
|
assert_eq() {
|
|
if [[ $2 == "$3" ]]; then
|
|
pass "$1"
|
|
else
|
|
fail "$1" "expected '$3', got '$2'"
|
|
fi
|
|
}
|
|
|
|
# strip_noncode <file>
|
|
# Reduce a script to the lines that could actually execute something, so the
|
|
# read-only scan below can look for dangerous verbs in command position.
|
|
#
|
|
# Drops comments, and drops the ARGUMENTS of output builtins only - 10-recon.sh
|
|
# exists to talk about flashing and unlocking, so those words appear
|
|
# legitimately inside printf/echo text. Everything else keeps its quotes: an
|
|
# earlier version stripped ALL quoted strings, which let
|
|
# `mtk_adb shell "reboot bootloader"` slip through the scan entirely.
|
|
strip_noncode() {
|
|
sed -E \
|
|
-e 's/(^|[[:space:]])#.*$/\1/' \
|
|
-e 's/^[[:space:]]*(printf|echo|cat)[[:space:]].*$//' \
|
|
-e 's/\|[[:space:]]*(printf|echo|cat)[[:space:]].*$//' \
|
|
"$1"
|
|
}
|
|
|
|
# run_recon <fixture-name> [extra args...] - prints combined output.
|
|
run_recon() {
|
|
local fixture="$1"
|
|
shift
|
|
MTK_ROOT_FIXTURE="$FIXTURES/$fixture" \
|
|
MTK_ROOT_CACHE="$WORKROOT/cache" \
|
|
MTK_SERIAL="TEST$RANDOM" \
|
|
"$RECON" "$@" 2>&1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
test_initboot_preferred_over_boot() {
|
|
printf '\ninit_boot is chosen over a co-present boot\n'
|
|
local out=""
|
|
out="$(run_recon ulefone-initboot-ab)"
|
|
|
|
# The decisive case: this fixture has BOTH boot_a and init_boot_a. Choosing
|
|
# boot_a here would produce a device that does not boot after flashing.
|
|
assert_contains "selects init_boot_a" "$out" "partition init_boot_a"
|
|
assert_contains "instructs patching INIT_BOOT" "$out" "must patch INIT_BOOT"
|
|
assert_not_contains "does not select boot_a" "$out" "partition boot_a"
|
|
assert_contains "detects A/B" "$out" "A/B scheme : A/B"
|
|
assert_contains "vbmeta suffixed" "$out" "vbmeta_a"
|
|
}
|
|
|
|
test_a_only_scheme() {
|
|
printf '\nA-only devices get bare partition names\n'
|
|
local out=""
|
|
out="$(run_recon ulefone-initboot-aonly)"
|
|
|
|
assert_contains "detects A-only" "$out" "A-only"
|
|
assert_contains "bare init_boot" "$out" "partition init_boot"
|
|
assert_contains "bare vbmeta" "$out" "vbmeta : vbmeta"
|
|
}
|
|
|
|
test_boot_carrier_when_no_initboot() {
|
|
printf '\nfalls back to boot when init_boot is absent\n'
|
|
local out=""
|
|
out="$(run_recon pixel-boot-ab)"
|
|
|
|
assert_contains "selects boot_a" "$out" "partition boot_a"
|
|
assert_contains "instructs patching BOOT" "$out" "must patch BOOT"
|
|
# The word "init_boot" legitimately appears in the explanatory text ("no
|
|
# init_boot present"), so assert on the selection line specifically rather
|
|
# than on the whole report.
|
|
local carrier_line=""
|
|
carrier_line="$(grep 'ramdisk lives in' <<<"$out" || true)"
|
|
assert_contains "carrier reported as boot" "$carrier_line" "ramdisk lives in: boot"
|
|
assert_not_contains "carrier is not init_boot" "$carrier_line" "init_boot"
|
|
}
|
|
|
|
test_unknown_device_refused() {
|
|
printf '\nunknown device is refused, never assumed\n'
|
|
local out="" rc=0
|
|
out="$(run_recon unknown-device)" || rc=$?
|
|
|
|
assert_eq "exits non-zero" "$rc" "1"
|
|
assert_contains "says NO PATH" "$out" "NO PATH"
|
|
assert_contains "refuses to guess" "$out" "Refusing to guess"
|
|
# The specific danger: treating "not a Pixel" as "therefore the Ulefone".
|
|
assert_not_contains "does not claim PATH A" "$out" "PATH A"
|
|
assert_not_contains "does not claim PATH B" "$out" "PATH B"
|
|
}
|
|
|
|
test_pixel_is_protected() {
|
|
printf '\nPixel is recognised and refused as a target\n'
|
|
local out=""
|
|
out="$(run_recon pixel-boot-ab)"
|
|
|
|
assert_contains "classified PIXEL" "$out" "classified as : PIXEL"
|
|
assert_contains "indeterminate C/D" "$out" "PATH C or D"
|
|
assert_contains "will not act on it" "$out" "will not act on a Pixel"
|
|
}
|
|
|
|
test_carrier_lock_never_asserted() {
|
|
printf '\ncarrier lock is reported as undetermined, never negative\n'
|
|
local out=""
|
|
out="$(run_recon pixel-boot-ab)"
|
|
|
|
assert_contains "reports UNDETERMINED" "$out" "Carrier lock : UNDETERMINED"
|
|
assert_contains "explains why" "$out" "NOT the same"
|
|
# A false "not carrier locked" would wrongly rule a path back in.
|
|
assert_not_contains "no false negative" "$out" "not carrier locked"
|
|
}
|
|
|
|
test_assumption_banner_always_shown() {
|
|
printf '\npath semantics are flagged as assumed on every run\n'
|
|
local out=""
|
|
for fixture in ulefone-initboot-ab pixel-boot-ab unknown-device; do
|
|
out="$(run_recon "$fixture")" || true
|
|
assert_contains "banner present ($fixture)" "$out" "PATH SEMANTICS ARE ASSUMED"
|
|
done
|
|
}
|
|
|
|
test_recon_is_read_only() {
|
|
printf '\n10-recon.sh contains no mutating command\n'
|
|
|
|
local code="" hits=""
|
|
code="$(strip_noncode "$RECON")"
|
|
|
|
hits="$(grep -nE '(^|[^[:alnum:]_.-])(fastboot|magiskboot|mtk\.py)([^[:alnum:]_-]|$)' <<<"$code" || true)"
|
|
assert_eq "no fastboot/magiskboot invocation" "${hits:-none}" "none"
|
|
|
|
hits="$(grep -nE 'adb[^|]*(reboot|push|install|remount)' <<<"$code" || true)"
|
|
assert_eq "no adb reboot/push/install/remount" "${hits:-none}" "none"
|
|
|
|
hits="$(grep -nE 'settings[[:space:]]+put|svc[[:space:]]+(power|data)' <<<"$code" || true)"
|
|
assert_eq "no settings put / svc" "${hits:-none}" "none"
|
|
|
|
hits="$(grep -nE '\bdd[[:space:]]+if=|\bmkfs|\berase\b|flashing[[:space:]]+unlock' <<<"$code" || true)"
|
|
assert_eq "no dd/mkfs/erase/unlock" "${hits:-none}" "none"
|
|
|
|
# Belt and braces: the only adb subcommands this script may ever run.
|
|
local subcommands=""
|
|
subcommands="$(grep -oE 'mtk_adb[[:space:]]+[a-z-]+' "$RECON" | awk '{ print $2 }' | sort -u | tr '\n' ' ')"
|
|
assert_eq "mtk_adb used only for shell" "${subcommands% }" "shell"
|
|
}
|
|
|
|
test_read_only_scan_catches_injection() {
|
|
printf '\nthe read-only scan actually fails on a mutating command\n'
|
|
|
|
# A guard that has never been observed failing is not known to work. Inject
|
|
# real dangerous commands into a copy and assert each is caught. The quoted
|
|
# form is the specific case a previous version of this scan let through.
|
|
local evil="$WORKROOT/evil.sh" code="" hits=""
|
|
|
|
sed 's|^ classify_device$| mtk_adb shell "reboot bootloader"\n classify_device|' \
|
|
"$RECON" >"$evil"
|
|
code="$(strip_noncode "$evil")"
|
|
hits="$(grep -cE 'adb[^|]*(reboot|push|install|remount)' <<<"$code" || true)"
|
|
if [[ ${hits:-0} -gt 0 ]]; then
|
|
pass "catches quoted 'adb shell \"reboot ...\"'"
|
|
else
|
|
fail "catches quoted 'adb shell \"reboot ...\"'" "injection went undetected"
|
|
fi
|
|
|
|
sed 's|^ classify_device$| fastboot flashing unlock\n classify_device|' \
|
|
"$RECON" >"$evil"
|
|
code="$(strip_noncode "$evil")"
|
|
hits="$(grep -cE '(^|[^[:alnum:]_.-])(fastboot|magiskboot|mtk\.py)([^[:alnum:]_-]|$)' <<<"$code" || true)"
|
|
if [[ ${hits:-0} -gt 0 ]]; then
|
|
pass "catches bare fastboot invocation"
|
|
else
|
|
fail "catches bare fastboot invocation" "injection went undetected"
|
|
fi
|
|
|
|
sed 's|^ classify_device$| mtk_adb shell settings put global foo 1\n classify_device|' \
|
|
"$RECON" >"$evil"
|
|
code="$(strip_noncode "$evil")"
|
|
hits="$(grep -cE 'settings[[:space:]]+put|svc[[:space:]]+(power|data)' <<<"$code" || true)"
|
|
if [[ ${hits:-0} -gt 0 ]]; then
|
|
pass "catches settings put"
|
|
else
|
|
fail "catches settings put" "injection went undetected"
|
|
fi
|
|
|
|
# And the scan must still pass on the real, unmodified script.
|
|
code="$(strip_noncode "$RECON")"
|
|
hits="$(grep -nE 'adb[^|]*(reboot|push|install|remount)' <<<"$code" || true)"
|
|
assert_eq "no false positive on the real script" "${hits:-none}" "none"
|
|
}
|
|
|
|
test_dump_refuses_non_mediatek() {
|
|
printf '\n20-dump-stock.sh refuses a non-MediaTek device\n'
|
|
[[ -x $DUMP ]] || {
|
|
fail "20-dump-stock.sh present" "not found or not executable"
|
|
return 0
|
|
}
|
|
|
|
local out="" rc=0
|
|
out="$(MTK_ROOT_FIXTURE="$FIXTURES/pixel-boot-ab" \
|
|
MTK_ROOT_CACHE="$WORKROOT/cache" \
|
|
MTK_SERIAL="TESTPIXEL" \
|
|
"$DUMP" --yes 2>&1)" || rc=$?
|
|
|
|
assert_eq "exits non-zero on Pixel" "$rc" "1"
|
|
assert_contains "refuses explicitly" "$out" "refus"
|
|
assert_not_contains "no dump attempted" "$out" "Dumping"
|
|
}
|
|
|
|
test_dump_happy_path() {
|
|
printf '\n20-dump-stock.sh produces a manifest from a stubbed read\n'
|
|
[[ -x $DUMP ]] || {
|
|
fail "20-dump-stock.sh present" "not found or not executable"
|
|
return 0
|
|
}
|
|
|
|
# Stub stands in for mtkclient: writes a plausibly-sized file. This exercises
|
|
# find_latest_facts, read_facts_value, verify_dump, the size band, the sha256
|
|
# column and the manifest - none of which the refusal test reaches.
|
|
local stub="$WORKROOT/stub-dump.sh"
|
|
cat >"$stub" <<'STUB'
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
truncate -s 8M "$2"
|
|
printf 'ANDROID!' | dd of="$2" conv=notrunc status=none
|
|
STUB
|
|
chmod +x "$stub"
|
|
|
|
local cache="$WORKROOT/dumpcache"
|
|
mkdir -p "$cache"
|
|
local serial="DUMPTEST1"
|
|
|
|
# Chain the real recon output in, rather than hand-writing a facts file:
|
|
# this also proves the two scripts agree on the format.
|
|
MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
|
|
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" "$RECON" >/dev/null 2>&1
|
|
|
|
local out="" rc=0
|
|
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
|
|
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" \
|
|
MTK_DUMP_CMD="$stub" "$DUMP" --yes 2>&1)" || rc=$?
|
|
|
|
assert_eq "exits zero on success" "$rc" "0"
|
|
assert_contains "dumped the init_boot partition" "$out" "init_boot_a"
|
|
|
|
local manifest="$cache/stock/$serial/manifest.txt"
|
|
if [[ ! -f $manifest ]]; then
|
|
fail "manifest written" "not found at $manifest"
|
|
return 0
|
|
fi
|
|
pass "manifest written"
|
|
|
|
local content=""
|
|
content="$(cat "$manifest")"
|
|
assert_contains "manifest names the image" "$content" "init_boot_a.img"
|
|
assert_contains "manifest records vbmeta" "$content" "vbmeta_a.img"
|
|
assert_contains "manifest records carrier" "$content" "ramdisk_carrier=init_boot"
|
|
assert_contains "manifest records size" "$content" "8388608 bytes"
|
|
# A sha256 is 64 hex chars; assert one is actually present.
|
|
if grep -qE '^[0-9a-f]{64} ' <<<"$content"; then
|
|
pass "manifest records sha256"
|
|
else
|
|
fail "manifest records sha256" "no 64-char hex digest found"
|
|
fi
|
|
|
|
# Idempotence: a second run must not silently re-dump over a good backup.
|
|
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
|
|
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" \
|
|
MTK_DUMP_CMD="$stub" "$DUMP" --yes 2>&1)" || true
|
|
assert_contains "second run is a no-op" "$out" "already exists"
|
|
}
|
|
|
|
test_dump_rejects_truncated_image() {
|
|
printf '\n20-dump-stock.sh flags an implausibly small dump\n'
|
|
[[ -x $DUMP ]] || return 0
|
|
|
|
# mtkclient exits 0 on a truncated read, so size is the only signal that the
|
|
# "backup" is worthless. A 512-byte init_boot must not pass silently.
|
|
local stub="$WORKROOT/stub-truncated.sh"
|
|
cat >"$stub" <<'STUB'
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
truncate -s 512 "$2"
|
|
STUB
|
|
chmod +x "$stub"
|
|
|
|
local cache="$WORKROOT/truncache"
|
|
mkdir -p "$cache"
|
|
local serial="DUMPTEST2"
|
|
|
|
MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
|
|
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" "$RECON" >/dev/null 2>&1
|
|
|
|
local out="" rc=0
|
|
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
|
|
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" \
|
|
MTK_DUMP_CMD="$stub" "$DUMP" --yes 2>&1)" || rc=$?
|
|
|
|
assert_eq "exits non-zero" "$rc" "1"
|
|
assert_contains "calls it implausibly small" "$out" "implausibly small"
|
|
assert_contains "warns against trusting it" "$out" "Do NOT treat this as a usable backup"
|
|
}
|
|
|
|
test_dump_requires_recon_first() {
|
|
printf '\n20-dump-stock.sh refuses without a recon facts file\n'
|
|
[[ -x $DUMP ]] || return 0
|
|
|
|
local out="" rc=0
|
|
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
|
|
MTK_ROOT_CACHE="$WORKROOT/emptycache" MTK_SERIAL="NOFACTS1" \
|
|
MTK_DUMP_CMD="/bin/true" "$DUMP" --yes 2>&1)" || rc=$?
|
|
|
|
assert_eq "exits non-zero" "$rc" "1"
|
|
assert_contains "points at 10-recon.sh" "$out" "10-recon.sh"
|
|
assert_contains "explains it will not guess" "$out" "rather than guessing"
|
|
}
|
|
|
|
test_facts_file_written() {
|
|
printf '\nfacts file is written and machine-readable\n'
|
|
local out="" facts=""
|
|
out="$(run_recon ulefone-initboot-ab)"
|
|
|
|
facts="$(find "$WORKROOT/cache" -name 'device-facts-ULEFONE-*.txt' -print -quit 2>/dev/null)"
|
|
if [[ -z $facts ]]; then
|
|
fail "facts file created" "none found under $WORKROOT/cache"
|
|
return 0
|
|
fi
|
|
pass "facts file created"
|
|
|
|
local content=""
|
|
content="$(cat "$facts")"
|
|
assert_contains "records ramdisk partition" "$content" "ramdisk_partition=init_boot_a"
|
|
assert_contains "records carrier" "$content" "ramdisk_carrier=init_boot"
|
|
assert_contains "records scheme" "$content" "ab_scheme=A/B"
|
|
assert_contains "includes by-name listing" "$content" "[by-name]"
|
|
}
|
|
|
|
main() {
|
|
WORKROOT="$(mktemp -d)"
|
|
readonly WORKROOT
|
|
mkdir -p "$WORKROOT/cache"
|
|
|
|
printf '=== mtk_root fixture tests ===\n'
|
|
|
|
test_initboot_preferred_over_boot
|
|
test_a_only_scheme
|
|
test_boot_carrier_when_no_initboot
|
|
test_unknown_device_refused
|
|
test_pixel_is_protected
|
|
test_carrier_lock_never_asserted
|
|
test_assumption_banner_always_shown
|
|
test_recon_is_read_only
|
|
test_read_only_scan_catches_injection
|
|
test_dump_refuses_non_mediatek
|
|
test_dump_happy_path
|
|
test_dump_rejects_truncated_image
|
|
test_dump_requires_recon_first
|
|
test_facts_file_written
|
|
|
|
printf '\n=== %d passed, %d failed ===\n' "$PASS" "$FAIL"
|
|
[[ $FAIL -eq 0 ]]
|
|
}
|
|
|
|
main "$@"
|