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
5.3 KiB
mtk_root — build notes, surprises, and mistakes
Written 2026-08-04, while preparing for a Ulefone Armor X12 Pro that had not yet arrived.
Installed versions (this host, 2026-08-04)
| Tool | Version |
|---|---|
| adb | 1.0.41 (android-tools) |
| fastboot | 37.0.0-android-tools |
| python3 | 3.14.6 |
| shellcheck | present |
| shfmt | 3.13.1 |
| systemd / udevadm | 261 (udevadm verify available) |
| mtkclient | rev 0542a87, 2026-08-02 |
| Magisk | v30.7 |
Magisk provenance
Kept here as well as in ~/.cache/mtk-root/stock/README.md, because the cache
is not version-controlled and vanishes with a cleanup.
| field | value |
|---|---|
| version | v30.7 (released 2026-02-23) |
| asset | Magisk-v30.7.apk |
| sha256 | e0d32d2123532860f97123d927b1bb86c4e08e6fd8a48bfc6b5bee0afae9ebd5 |
| size | 11613864 bytes |
Trap: the same GitHub release also publishes app-debug.apk (~25 MB),
and a naive "first .apk asset" match picks it. That is the debug build, not the
one to install. I hit this during the fetch. Match the filename exactly.
Re-check before use:
curl -fsSL https://api.github.com/repos/topjohnwu/Magisk/releases/latest \
| grep -E '"(tag_name|browser_download_url)"'
Surprises
1. The Ulefone is probably the init_boot branch, not boot.
The Armor X12 Pro launched on Android 13 (Aug 2023). Per AOSP, devices
launching on 13+ carry the ramdisk in init_boot, so the Ulefone is likely
the opposite branch from the Pixel used to validate this toolkit. The Pixel run
therefore does not exercise the code path the Ulefone will need. Fixtures
cover it; hardware has not.
2. The mtkclient venv on this host was broken, not missing.
~/.cache/bl9000-root/mtkclient/venv existed and looked complete, but every
import usb failed under Python 3.14 — a Python upgrade had orphaned it. This
is why 00-preflight.sh tests importability rather than directory existence:
the failure mode that costs an evening is the one that looks fine on disk.
3. /etc/udev/rules.d/51-android.rules had a world-writable catch-all.
ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666" grants every local
process read/write access to every USB device — storage, security keys, not
just phones. Commented out on 2026-08-04 via install.sh --fix-udev; backup at
51-android.rules.bak-20260804T183038Z. adb connectivity was re-verified
afterwards. Note 51-pinephone.rules still sets MODE="0666", but scoped to
specific devices rather than a wildcard.
4. SIGPIPE killed the preflight script.
adb version | head -1 under set -o pipefail exits 141 when head closes the
pipe. The script aborted mid-run on its very first execution. Version strings
are now captured whole and sliced with parameter expansion.
5. A test fix silently punched a hole in the read-only guard.
To stop the static scan tripping on the word "fastboot" inside printf text, I
first stripped all quoted strings — which also hid
mtk_adb shell "reboot bootloader" from every check. The guard protecting a
daily-driver phone was weaker after the "fix" than before it. Now only the
arguments of printf/echo/cat are stripped, and
test_read_only_scan_catches_injection asserts the scan actually fails on
injected commands.
6. 20-dump-stock.sh exited silently when no recon had run.
find on a nonexistent cache dir fails under set -e, so the script printed
its title and stopped with no explanation. Guarded; it now says which command
to run first.
Assumptions that need checking against the real device
- Ulefone property values are guessed.
ro.product.manufacturer=Ulefone,ro.product.model=Armor X12 Pro,ro.board.platform=mt6765. If the unit reports otherwise,classify_devicereturnsUNKNOWNand everything refuses — safe, but it will look like a bug. Run./10-recon.sh --explain-classification, then widen the patterns at the top of../lib/mtk_common.sh. - Path semantics A/B/C/D are inferred. The companion document defining them
was never provided, so
10-recon.shprints an assumption banner every run. - The Ulefone USB vendor ID is unknown.
udev/60-mtk-root.rulescovers MediaTek0e8d; if the device enumerates under a Ulefone-specific VID, add it (lsusbwith the phone attached). - mtkclient's install steps may drift.
install.shfalls back topip install <clone>whenrequirements.txtis absent, but read the current README if the venv build ever fails.
Verified good news
mtkclient rev 0542a87 has an explicit MT6765 entry in
mtkclient/config/brom_config.py (name="MT6765/MT8768t", loader
mt6765_payload.bin) — the Ulefone's exact chipset is supported. This does
not establish that this particular unit's BROM is accessible: SLA/DAA
authentication state is per-device and undocumented.