testsAndMisc/linux_configuration/scripts/lib/mtk_common.sh
Krzysztof kuhy Rudnicki 1b43ba093c
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
mtk_root: build the rooting toolkit before the phone lands
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
2026-08-04 20:38:44 +02:00

504 lines
17 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# mtk_common.sh - Shared helpers for the mtk_root toolkit.
#
# Source this file; do not execute it directly.
#
# Every read from a device goes through dev_getprop() or dev_list_by_name().
# Those two functions are the ONLY seam between the toolkit and real hardware:
# when MTK_ROOT_FIXTURE points at a directory they read text files instead of
# talking to adb, which is what makes the whole test suite offline and
# hermetic. If you add a new device read, route it through these or the tests
# stop meaning anything.
# ============================================================================
# ----------------------------------------------------------------------------
# Device classification patterns.
#
# The Ulefone values are GUESSED - the device has not arrived. If it reports
# something outside these patterns, classify_device returns UNKNOWN and every
# caller refuses to act. That is the intended failure mode, but on day one it
# will look like a bug: run `10-recon.sh --explain-classification` to see which
# predicate missed, then fix the pattern here. This is the one block that
# should need editing when the hardware lands.
# ----------------------------------------------------------------------------
readonly MTK_PLATFORM_PATTERN='^mt[0-9]{4}'
readonly ULEFONE_VENDOR_PATTERN='[Uu]lefone'
readonly ULEFONE_MODEL_PATTERN='[Aa]rmor'
readonly PIXEL_VENDOR_PATTERN='^[Gg]oogle$'
readonly PIXEL_MODEL_PATTERN='[Pp]ixel'
# Plausible size band for a ramdisk-carrying partition, in bytes. Used by
# mtk_sane_ramdisk_size to flag a dump that is obviously wrong - a truncated
# read, or a whole-disk grab where a partition was meant.
MTK_RAMDISK_MIN_BYTES=$((1024 * 1024))
MTK_RAMDISK_MAX_BYTES=$((128 * 1024 * 1024))
readonly MTK_RAMDISK_MIN_BYTES MTK_RAMDISK_MAX_BYTES
# ----------------------------------------------------------------------------
# Logging. Everything goes to stderr so stdout stays clean for captured data.
# ----------------------------------------------------------------------------
mtk_info() {
printf '\033[0;34m[INFO]\033[0m %s\n' "$*" >&2
}
mtk_warn() {
printf '\033[0;33m[WARN]\033[0m %s\n' "$*" >&2
}
mtk_error() {
printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2
}
mtk_fatal() {
printf '\033[0;31m[FATAL]\033[0m %s\n' "$*" >&2
exit 1
}
mtk_heading() {
printf '\n\033[1;36m== %s ==\033[0m\n' "$*" >&2
}
# ----------------------------------------------------------------------------
# Paths
# ----------------------------------------------------------------------------
# Artifacts live outside the repo: the pre-commit hook blocks every binary
# file, and a factory image is several GB. Only text manifests get committed.
mtk_cache_dir() {
printf '%s' "${MTK_ROOT_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/mtk-root}"
}
mtk_stock_dir() {
printf '%s/stock' "$(mtk_cache_dir)"
}
mtk_mtkclient_dir() {
printf '%s/mtkclient' "$(mtk_cache_dir)"
}
mtk_venv_python() {
printf '%s/venv/bin/python' "$(mtk_mtkclient_dir)"
}
# ----------------------------------------------------------------------------
# Device read seam
# ----------------------------------------------------------------------------
# Strip anything a property value has no business containing. adb shell also
# returns CRLF line endings, which silently break string comparisons.
mtk_sanitize() {
printf '%s' "$1" | tr -d '\r' | tr -cd 'A-Za-z0-9 ._:/=+,-'
}
# dev_getprop <property-name>
# Prints the value, or nothing if unset. Never fails - an absent property and
# an empty one are the same thing to every caller here.
dev_getprop() {
local prop="$1"
local raw=""
if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then
# Fixture format is one `key=value` per line, matching `getprop` output
# once the [brackets] are stripped.
raw="$(grep -m1 -- "^${prop}=" "${MTK_ROOT_FIXTURE}/props.txt" 2>/dev/null || true)"
raw="${raw#*=}"
else
raw="$(mtk_adb shell getprop "$prop" 2>/dev/null || true)"
fi
mtk_sanitize "$raw"
}
# dev_list_by_name
# Prints one partition name per line, unsorted, as the device reports them.
dev_list_by_name() {
if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then
tr -d '\r' <"${MTK_ROOT_FIXTURE}/by-name.txt" 2>/dev/null || true
return 0
fi
mtk_adb shell 'ls -1 /dev/block/by-name/ 2>/dev/null' 2>/dev/null | tr -d '\r' || true
}
# mtk_adb <args...> - serial-pinned adb, so a second phone plugged in midway
# cannot silently become the target.
mtk_adb() {
adb -s "${MTK_SERIAL:?mtk_select_device must run first}" "$@"
}
# ----------------------------------------------------------------------------
# Device presence and authorization
# ----------------------------------------------------------------------------
# mtk_list_devices - prints "<serial> <state>" per attached device.
mtk_list_devices() {
adb devices 2>/dev/null |
awk 'NR > 1 && $2 ~ /^(device|offline|unauthorized|recovery|sideload)$/ { print $1, $2 }'
}
# mtk_device_state <serial> - device|unauthorized|offline|absent
mtk_device_state() {
local serial="$1"
local state=""
state="$(mtk_list_devices | awk -v s="$serial" '$1 == s { print $2; exit }')"
printf '%s' "${state:-absent}"
}
# mtk_select_device [requested-serial]
# Sets MTK_SERIAL. Refuses when the choice is ambiguous rather than picking
# one - a wrong guess here targets the wrong phone.
mtk_select_device() {
local requested="${1:-${MTK_SERIAL:-}}"
local -a device_rows=()
local -a serials=()
local row=""
if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then
MTK_SERIAL="${MTK_SERIAL:-fixture}"
export MTK_SERIAL
return 0
fi
mapfile -t device_rows < <(mtk_list_devices)
if [[ ${#device_rows[@]} -eq 0 ]]; then
mtk_error "No device visible to adb."
mtk_error "Check: cable seated, USB debugging enabled, and try 'adb devices'."
return 1
fi
for row in "${device_rows[@]}"; do
serials+=("${row%% *}")
done
if [[ -n $requested ]]; then
for row in "${serials[@]}"; do
if [[ $row == "$requested" ]]; then
MTK_SERIAL="$requested"
export MTK_SERIAL
return 0
fi
done
mtk_error "Requested serial '${requested}' is not attached. Attached: ${serials[*]}"
return 1
fi
if [[ ${#serials[@]} -gt 1 ]]; then
mtk_error "Multiple devices attached (${serials[*]}); refusing to guess."
mtk_error "Re-run with --serial <serial>."
return 1
fi
MTK_SERIAL="${serials[0]}"
export MTK_SERIAL
return 0
}
# mtk_require_authorized
# An unauthorized device answers every getprop with an empty string, which
# reads exactly like "this device has no properties". Catch it up front so the
# report never blames the device for a prompt sitting unanswered on its screen.
mtk_require_authorized() {
local state=""
[[ -n ${MTK_ROOT_FIXTURE:-} ]] && return 0
state="$(mtk_device_state "${MTK_SERIAL}")"
case "$state" in
device)
return 0
;;
unauthorized)
mtk_error "Device ${MTK_SERIAL} is UNAUTHORIZED."
mtk_error "The RSA authorization prompt is on the phone's screen - unlock it and tap Allow."
mtk_error "If no prompt appears: revoke USB debugging authorizations on the phone, then replug."
return 1
;;
offline)
mtk_error "Device ${MTK_SERIAL} is OFFLINE. Replug the cable, or run 'adb kill-server'."
return 1
;;
*)
mtk_error "Device ${MTK_SERIAL} is in state '${state}', which this toolkit does not read."
return 1
;;
esac
}
# ----------------------------------------------------------------------------
# Classification
# ----------------------------------------------------------------------------
# classify_device
# Sets MTK_DEVICE_CLASS to ULEFONE, PIXEL or UNKNOWN, and MTK_CLASS_REASON to
# a human-readable explanation of how it got there.
#
# ULEFONE requires a positive MediaTek signal AND a vendor/model match - both,
# never either, and never "not a Pixel, so presumably the Ulefone". A third
# phone attached by accident must land in UNKNOWN.
classify_device() {
local manufacturer="" model="" platform="" hardware="" device=""
local mtk_match=0 ulefone_match=0 pixel_match=0
manufacturer="$(dev_getprop ro.product.manufacturer)"
model="$(dev_getprop ro.product.model)"
platform="$(dev_getprop ro.board.platform)"
hardware="$(dev_getprop ro.hardware)"
device="$(dev_getprop ro.product.device)"
MTK_PROP_MANUFACTURER="$manufacturer"
MTK_PROP_MODEL="$model"
MTK_PROP_PLATFORM="$platform"
MTK_PROP_HARDWARE="$hardware"
MTK_PROP_DEVICE="$device"
if [[ $platform =~ $MTK_PLATFORM_PATTERN ]] || [[ $hardware =~ $MTK_PLATFORM_PATTERN ]]; then
mtk_match=1
fi
if [[ $manufacturer =~ $ULEFONE_VENDOR_PATTERN ]] || [[ $model =~ $ULEFONE_MODEL_PATTERN ]]; then
ulefone_match=1
fi
if [[ $manufacturer =~ $PIXEL_VENDOR_PATTERN ]] && [[ $model =~ $PIXEL_MODEL_PATTERN ]]; then
pixel_match=1
fi
if [[ $mtk_match -eq 1 && $ulefone_match -eq 1 ]]; then
MTK_DEVICE_CLASS="ULEFONE"
MTK_CLASS_REASON="MediaTek platform [${platform:-unset}/${hardware:-unset}] and Ulefone identity [${manufacturer} ${model}]"
elif [[ $pixel_match -eq 1 ]]; then
MTK_DEVICE_CLASS="PIXEL"
MTK_CLASS_REASON="Google Pixel identity [${manufacturer} ${model}]"
else
MTK_DEVICE_CLASS="UNKNOWN"
MTK_CLASS_REASON="no rule matched: manufacturer=[${manufacturer}] model=[${model}] platform=[${platform}] hardware=[${hardware}]"
fi
export MTK_DEVICE_CLASS MTK_CLASS_REASON
export MTK_PROP_MANUFACTURER MTK_PROP_MODEL MTK_PROP_PLATFORM MTK_PROP_HARDWARE MTK_PROP_DEVICE
}
# mtk_explain_classification - why classify_device decided what it did.
mtk_explain_classification() {
printf 'ro.product.manufacturer = %s\n' "${MTK_PROP_MANUFACTURER:-}"
printf 'ro.product.model = %s\n' "${MTK_PROP_MODEL:-}"
printf 'ro.product.device = %s\n' "${MTK_PROP_DEVICE:-}"
printf 'ro.board.platform = %s\n' "${MTK_PROP_PLATFORM:-}"
printf 'ro.hardware = %s\n' "${MTK_PROP_HARDWARE:-}"
printf '\n'
printf 'MediaTek pattern : %s\n' "$MTK_PLATFORM_PATTERN"
printf 'Ulefone vendor : %s\n' "$ULEFONE_VENDOR_PATTERN"
printf 'Ulefone model : %s\n' "$ULEFONE_MODEL_PATTERN"
printf 'Pixel vendor : %s\n' "$PIXEL_VENDOR_PATTERN"
printf 'Pixel model : %s\n' "$PIXEL_MODEL_PATTERN"
printf '\n'
printf 'Verdict: %s\n' "${MTK_DEVICE_CLASS:-unset}"
printf 'Reason : %s\n' "${MTK_CLASS_REASON:-unset}"
}
# mtk_assert_mediatek
# A positive assertion, deliberately not "is not a Pixel". Used to gate the
# only script that touches partitions.
mtk_assert_mediatek() {
if [[ ${MTK_PROP_PLATFORM:-} =~ $MTK_PLATFORM_PATTERN ]]; then
return 0
fi
if [[ ${MTK_PROP_HARDWARE:-} =~ $MTK_PLATFORM_PATTERN ]]; then
return 0
fi
return 1
}
# ----------------------------------------------------------------------------
# Partition discovery
# ----------------------------------------------------------------------------
# discover_partitions
# Sets MTK_AB_SCHEME, MTK_SLOT_SUFFIX, MTK_RAMDISK_PART, MTK_VBMETA_PART,
# MTK_BY_NAME (newline-separated listing).
#
# The governing rule: never name a partition in a conditional that was not
# first observed in the listing. The AOSP rule (devices launching on Android
# 13+ carry the ramdisk in init_boot; earlier ones keep it in boot) explains
# WHY the preference order below is correct, but the code never consults the
# API level to decide - it reads the filesystem. That is what lets one code
# path serve both a Pixel (no init_boot -> falls through to boot) and a
# 13-launch MediaTek device (init_boot present -> taken) without branching on
# device identity.
discover_partitions() {
local entries="" suffix="" candidate="" name=""
entries="$(dev_list_by_name)"
MTK_BY_NAME="$entries"
export MTK_BY_NAME
if [[ -z $entries ]]; then
MTK_AB_SCHEME="unknown"
MTK_SLOT_SUFFIX=""
MTK_RAMDISK_PART=""
MTK_VBMETA_PART=""
export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART
return 1
fi
# Slot scheme comes from observed suffixes, not from a product table.
if grep -qE '_a$' <<<"$entries"; then
MTK_AB_SCHEME="A/B"
else
MTK_AB_SCHEME="A-only"
fi
# Prefer the device's own view of the active slot; fall back to _a only
# because a dump has to start somewhere, and say so in the report.
suffix="$(dev_getprop ro.boot.slot_suffix)"
if [[ -z $suffix && $MTK_AB_SCHEME == "A/B" ]]; then
suffix="_a"
MTK_SLOT_ASSUMED=1
else
MTK_SLOT_ASSUMED=0
fi
MTK_SLOT_SUFFIX="$suffix"
# init_boot first: its presence is decisive. A device that has it keeps the
# ramdisk there even though boot also exists, so checking boot first would
# pick the wrong partition on exactly the hardware this toolkit is for.
MTK_RAMDISK_PART=""
for candidate in init_boot boot; do
for name in "${candidate}${suffix}" "${candidate}"; do
if grep -qxF -- "$name" <<<"$entries"; then
MTK_RAMDISK_PART="$name"
break 2
fi
done
done
MTK_VBMETA_PART=""
for name in "vbmeta${suffix}" "vbmeta"; do
if grep -qxF -- "$name" <<<"$entries"; then
MTK_VBMETA_PART="$name"
break
fi
done
export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART MTK_SLOT_ASSUMED
[[ -n $MTK_RAMDISK_PART ]]
}
# mtk_ramdisk_carrier - "init_boot" or "boot", suffix stripped.
mtk_ramdisk_carrier() {
local part="${MTK_RAMDISK_PART:-}"
printf '%s' "${part%_[ab]}"
}
# mtk_check_layout_expectation
# Cross-check only. first_api_level >= 33 predicts init_boot; below predicts
# boot. A mismatch means either a vendor deviation or a bad read - the human
# should look, but the filesystem still wins.
mtk_check_layout_expectation() {
local api_level="" carrier="" expected=""
api_level="$(dev_getprop ro.product.first_api_level)"
carrier="$(mtk_ramdisk_carrier)"
[[ -z $api_level || -z $carrier ]] && return 0
[[ $api_level =~ ^[0-9]+$ ]] || return 0
if [[ $api_level -ge 33 ]]; then
expected="init_boot"
else
expected="boot"
fi
if [[ $carrier != "$expected" ]]; then
mtk_warn "Layout disagrees with expectation: first_api_level=${api_level} predicts '${expected}', device shows '${carrier}'."
mtk_warn "Trusting the device. Verify before flashing anything."
return 1
fi
return 0
}
# ----------------------------------------------------------------------------
# Host environment checks (read-only; install.sh owns the mutating half)
# ----------------------------------------------------------------------------
# mtk_udev_catchall_files
# Prints any rules file containing an ACTIVE wildcard-vendor rule granting
# world read/write. Such a rule applies to every USB device on the system -
# storage, security keys, everything - not just phones.
#
# Commented-out lines are ignored: --fix-udev disables a rule by commenting it
# rather than deleting it, so matching those would leave the check permanently
# red after it had already been dealt with. Only *.rules files are considered,
# since udev itself skips any other suffix (including our .bak- backups).
mtk_udev_catchall_files() {
local file=""
for file in /etc/udev/rules.d/*.rules; do
[[ -f $file ]] || continue
if grep -qE '^[^#]*idVendor\}=="\*"[^#]*MODE="0666"|^[^#]*MODE="0666"[^#]*idVendor\}=="\*"' \
"$file" 2>/dev/null; then
printf '%s\n' "$file"
fi
done
}
# mtk_check_venv - is the mtkclient venv actually usable?
# Checks importability, not directory existence: the venv found on this host
# in Aug 2026 existed, looked complete, and failed at `import usb` because a
# Python upgrade had orphaned it.
mtk_check_venv() {
local python_bin=""
python_bin="$(mtk_venv_python)"
if [[ ! -x $python_bin ]]; then
printf 'missing: no interpreter at %s' "$python_bin"
return 1
fi
if ! "$python_bin" -c 'import usb, usb.core, serial' 2>/dev/null; then
local err=""
err="$("$python_bin" -c 'import usb, usb.core, serial' 2>&1 | tail -1)"
printf 'broken: %s' "$err"
return 1
fi
printf 'ok: %s' "$("$python_bin" --version 2>&1)"
return 0
}
# mtk_sha256 <file>
mtk_sha256() {
sha256sum "$1" | awk '{ print $1 }'
}
# mtk_file_size <file>
mtk_file_size() {
stat -c '%s' "$1"
}
# mtk_sane_ramdisk_size <bytes>
# A dump outside this band is not necessarily wrong, but it is worth stopping
# for: an empty or truncated read and an accidental whole-disk grab both look
# like success to mtkclient's exit code.
mtk_sane_ramdisk_size() {
local size="$1"
if [[ ! $size =~ ^[0-9]+$ ]]; then
printf 'size is not a number: %s' "$size"
return 1
fi
if [[ $size -lt $MTK_RAMDISK_MIN_BYTES ]]; then
printf 'implausibly small: %s bytes (< %s)' "$size" "$MTK_RAMDISK_MIN_BYTES"
return 1
fi
if [[ $size -gt $MTK_RAMDISK_MAX_BYTES ]]; then
printf 'implausibly large: %s bytes (> %s)' "$size" "$MTK_RAMDISK_MAX_BYTES"
return 1
fi
printf '%s bytes' "$size"
return 0
}