# Linux Phone Porting

> Use for every hardware bring-up or debug session that ports mainline Linux to a phone or tablet. Retail-unlock targets require an already-unlocked bootloader; locked retail bootloaders are out of scope. Exploit-booted and firmware-booted targets require a demonstrated boot path; the skill never provides unlock or exploit steps.

- Skill: `angelwzr/linux-phone-porting` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add angelwzr/linux-phone-porting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/angelwzr/linux-phone-porting/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: angelwzr (https://skillmd.com/u/angelwzr)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/angelwzr/linux-phone-porting

---


# Linux Phone Porting

A debug iteration costs a build + flash + boot (5-10 min); blind fixes get refuted more often than they land. Follow this order strictly.

**The chain drives itself; the operator owns the decisions.** When a phase's exit condition is met, start the next phase's entry yourself: setup complete → evidence on the first symptom; evidence captured → the phase 2 sweep opens its ledger; ledger full → hypotheses ranked with falsifiers into phase 3's gate; gate passed → the cheapest decisive experiment, then verification, acceptance and cleanup; three refutations → back to phase 2, wider. Do not wait to be told the next step, and do not stop at a phase boundary to summarize — report compactly and continue. Ask, never guess, at the decision points that belong to the operator, and those are the only stops: unresolved target identity or an unspecified interface/session (phase 0 asks), a boot path the operator must complete (locked bootloader, missing payload demonstration — resume when done), the loop's refutation budget, anything that can destroy excluded userdata or otherwise unrecoverable state, physical hands, and pause/resume prerequisites. Everything else advances.

**Every phase entry and command invocation refreshes the session's task list first.** Before any work, replace the running todo/task list with the incoming unit's steps — one item per step of the phase or command about to run, first step marked in progress. A stale list from the previous unit masquerades as pending work and hides real progress; superseding it is part of starting the unit, not bookkeeping to defer.

Scope: mainline Linux bring-up on phones and tablets plus integration of the selected OS userspace — a method, not certification of any OS/device combination. The userspace need not be GNU/Linux; non-Linux kernels are out of scope.

## 0. Set up the port

Once, before the first flash. The stock system is the highest-authority research source for this board, and parts stop being readable once overwritten.

**Establish the target** (every later answer depends on it; every decision here is asked and user-confirmed — an existing repo, monorepo layer, staged OS image, prior context file or sibling device's choice near the project is a candidate to offer, not an answer; artifacts establish what is present, never what is wanted, and one target OS inferred from surrounding layout had to be superseded by the operator's actual selection):

- **Exact device identity** — marketing name, codename, SoC, regional/storage variant. Sibling variants of one name differ in panel, touch, modem; a fix researched against the wrong variant looks plausible and fails. Record sibling codenames — phase 2 enumerates their ports. On firmware-boot targets DMI/SMBIOS cannot even be trusted for the SoC generation: one family's DMI string appeared in no documentation while one marketing name spanned two SoC generations with zero portability (DT, patches, community tooling all generation-specific). Discriminate by the stock OS's CPU identity string plus per-core max frequency as tiebreaker, and verify later against the running system's board description; a "no port exists" sweep verdict must name the CPU generation it searched for, or it may be reporting the sibling's ecosystem — on one device all community port effort sat on the newer generation.
- **Engineering stage** — EVT/DVT/PVT, board identity, carrier/SKU, bootloader product + build fingerprints. Reconcile before choosing images: one presumed retail handset was an EVT board for a different product while Android reported `user/release-keys`. Build labels do not establish production hardware.
- **Target OS/platform and integration contract** — chosen release, required kernel lineage, startup/service manager, hardware-service interfaces, build/image/update model, evidence/control tools; verify against the target project's current docs. Linux-based ≠ mainline-compatible; a working Linux driver ≠ target-framework or application support.
- **Target user interface/session** — ask when unspecified; never assume. Prefer the platform's supported interface/session stack over replacing it during bring-up.
- **Boot-model class** — classify before anything else about boot; the class selects the write gate, the backup route and the board-authority artefact (boot-image DTB / ACPI tables / DT inside the vendor kernel image):
  - **Retail-unlock** — OEM unlock, boot images, A/B slots. **Unlocked bootloader only** — no unlocking steps; point at the OEM's instructions and resume when unlocked.
  - **Exploit-booted** — bootROM-vulnerable devices; no unlock state exists. Gate: a payload boot path demonstrated on this device; the payload environment is a candidate control channel and recovery path — prove both. No exploit-landing steps; point at the payload project's own instructions.
    - **Session mechanics — every naive liveness signal false-positives.** Enumeration shows the right VID:PID for a zombie session; the exploit tool's own verifier can print success without running its stage machine on a stale session; a session contaminated by dozens of failed races reads back as pwned while untrustworthy (one session burned ~3 h this way). Working gate: a USB node number different from the one recorded at last session close, a GETSTATUS-class control transfer answered within a short timeout (timeout = zombie), and the full stage trace in the exploit log — a success line with zero stages _is_ the false-positive signature. Bound exploit attempts per session; after repeated failed races, re-enter clean DFU instead of continuing on the contaminated one.
    - **The pwn exists only inside the live session.** An idle payload session dies on its own; "pwned then later gone" means the session died — check what re-enumerated before theorizing a physical decay (one such theory was refuted in seconds by a raw descriptor read). Pre-stage everything downstream so exploit-success → payload-load is seconds, not minutes (one 25-minute grind was dead ~9 minutes later); record node + verified state at session close.
    - **Success announces itself as USB re-enumeration to a payload-specific PID** — the device leaves the exploit PID entirely, and different payload builds present different PIDs; watching the wrong PID reads as "loader silently does nothing" (one attempt wrongly concluded the whole approach dead). Record the expected PID map per build beside the udev rules; cover every chain stage's VID:PID up front (each stage re-enumerates). The `uaccess` tag can silently grant nothing (logind seat mismatch) — fall back to a deterministic group rule; and a newly installed rule does not apply to the already-plugged node, which is exactly the mid-chain node — replug or manual chgrp before the session continues. Interactive payload-console clients block forever without piped stdin: drive them as `printf '…\n' | client`.
    - **Never diagnose device state from a host tool's parsed rendering of a USB string descriptor** — read the raw descriptor with an oversized buffer. Host tools, and even the exploit suite itself, have read a truncated serial with the payload marker cut off, concluded "not pwned", and re-exploited an already-pwned session on every attempt; the misdiagnosis fed a physical "pwn decay" theory for hours before a raw-byte read retired it. The wrapper-corrupts-evidence rule extends into descriptor space, and the corrupting reader can be your own tool.
    - **Mine a working project's boot scripts for the success signal and undocumented prerequisite steps** — do not derive the flow from docs alone. A sibling project's flash script yielded steps no documentation mentioned (an explicit reset between exploit and payload load, missed by every doc-derived attempt). Pre-register the falsifier for the first tethered boot: one clean-session roll with a re-enumeration-poll timeout and named fallback flows.
    - **Chain binaries get full provenance or the chain is unreproducible.** Third-party CI-built and locally built payloads that no package manager provides: per-file origin (exact feed/zip), SHA-256, local build recipe with the pinned source revision; the tool environment defaults to the hashed copies via variables so no invocation silently picks up an untracked binary.
    - **Entitlement-gated refusal is not absence.** A readable retail OS may refuse its own diagnostic surfaces — an empty registry dump or a named storage-info failure proves the surface exists but is gated; the verdict is "unknown, evidence-backed", not "not exposed". Capture stdout and stderr as sibling files so refusals are first-class evidence, run every stock-side diagnostic **before** exploit work starts (the payload environment is the fallback surface and only exists after the chain works), and pre-register standing operational facts the stock OS already revealed — one end-of-life battery (cycle count + corroborating brown-out panic) became "expect spontaneous shutdowns, keep tethered, treat uncommanded power loss as routine" before any chain work; a capacity field returning a constant is a stub, not data.
  - **Firmware-boot** — UEFI or equivalent; lock state is often irrelevant to booting external media, but record it where the chain has one — some UEFI-lineage chains gate writes through their own unlock state and service tooling; where it gates writes, apply the retail-unlock gate to it. Gate: a demonstrated boot path. The board description may be ACPI tables, not a DTB.
    - **Filesystem encryption is a phase 0 gate, before wipe/resize decisions.** Record per-volume encryption status; the other OS can never read the recovery key for Linux and a resize of an encrypted volume fails until decryption fully completes (measured 10–60 min for a half-full 256 GB SSD). Record only _that a key was exported and where_ — never the key. Record the wipe-vs-dual-boot decision and which partitions (e.g. factory recovery images) are intentionally forfeited before any destructive step.
    - **The stock OS is the pre-install control channel.** Its built-in ssh server replaces per-iteration operator hands: streaming harvests (driver/firmware stores, ACPI dumps) and even staging boot files onto the install medium, hashes verified host-side. The session's elevation follows the stock OS's own UAC configuration — know which you have. `wmic`-era tools are gone on current builds (use the OS's current management API). Do not route transfers through the install medium: its data partitions may be unreadable from the stock OS and its system partition may reject writes without elevation.
    - **Harvest the Linux firmware before the wipe.** On Windows-on-ARM targets the WLAN/BT/modem/DSP/GPU firmware lives in the driver store (`System32/DriverStore/FileRepository`) and is unobtainable afterwards — harvest filtered by SoC-number substring (one device: 99 package dirs, 726 MB / 1083 files), keep duplicate retained versions, file with dirlist + hashes + a README mapping each payload to its future firmware-path consumer, marked "reference only, not a restore source".
    - **Harvest the ACPI board authority from the stock OS** with the platform's dump/disassemble toolchain; if the toolchain mirrors are unreachable, the binaries ship inside GUI utilities. Disassemble the DSDT naming _every_ SSDT so cross-table externals resolve. Then read absences as architecture: zero battery/AC method objects means the whole power path runs through an embedded controller on a bus the SoC dtsi may not even instantiate — and a mainline driver whose binding names this SoC may never have been tested on it: bindings text ≠ hardware validation.
    - **Map physical connector → controller wiring from the stock OS before writing any DT node**: with the device you will boot from plugged into the port you intend to use, walk the device's parent chain up to the first ACPI node — that instance names the owning controller (confirm its MMIO base in the DSDT). The same pass classifies unwired controllers: present-and-healthy-with-no-devices = dead on this board. One board's convention-based port guess cost two boot rounds before the stock-OS walk resolved the ports differently.
- **Boot topology** — A/B or not, slot layout. Decode slot state from the on-disk structure, not documented layouts (one SoC kept it in GPT partition-entry attribute bits, not the documented control partition). After a crash-loop, refill the tries-remaining budget before interpreting the next failure.
- **Recovery** — custom recovery installed? Convenience, not requirement.

Vendor A/B bank names do not establish seamless-update/slot/fallback semantics — establish the implementation before manipulating banks. Empty security properties = unknown, not unlocked. Same-SoC trees/firmware/signed programmers are candidates, not certified recovery paths; an unreleased prototype's only installed system may have no compatible replacement — preserve it, restorable backup before destructive experiments.

**Raw-storage access without boot-chain control is a write/recovery route, not a boot path.** A ROM-mode firehose (Qualcomm EDL-class) on a device whose vendor offers no unlock does not confer unlocked-like status: it can write partitions while the boot chain still verifies what it loads. The class gate is met only when a non-stock image is _demonstrated to boot on this device_ through whatever chain achieves it — community "EDL ROMs" are precedent to verify, not proof. Once demonstrated, record the posture as retail-unlock-equivalent with the raw-storage route as the named recovery path; until then a permanently locked device stays out of scope for porting, and the available raw-storage channel is still recorded — it upgrades the backup and rescue posture for everything that follows (programmers are per-variant: prove the channel streams on this device before trusting it).

**Back up every readable partition except userdata** — a class that exposes no partitions is backed up through its demonstrated route (payload environment, preboot environment, full-storage image) with coverage recorded as it is:

- Userdata excluded: bulk, private — leave a DO-NOT-RESTORE note in the backup. Exclusion does not authorize its loss; before any operation that may wipe it, explain consequences + preservation options and obtain separate informed authorization.
- **Device-unique partitions — back up, never publish.** Modem NV/EFS, persist, calibration siblings carry IMEI, radio calibration, sensor trim; unreproducible; corruption leaves a phone that cannot register. Critical AND private.
- **Private calibration stays in a protected runtime path.** Validate provenance, length, integrity checks before use. Values must not enter source, DT, build inputs/outputs, arguments, logs or public reproductions (one audio integration kept them out of the build store and process args; a lens integration read them at runtime, not compiled in). Missing/invalid calibration blocks the operation — never borrow another handset's values or guess.
- Hash every image; record hashes beside the partition table. Only a hash answers "is the device still what I backed up."
- Verify restorable before the first risky flash, not after.
- **Record what the backup route excludes by design, with counts and the size expectation.** Unencrypted mobile-OS backups omit the credential store entirely and never include app binaries — a large-storage device yielding a few hundred megabytes and ~a thousand files is success, not truncation. A coverage record without the by-design exclusions gets a good backup re-diagnosed as failed.
- **EDL is the fallback backup route when boot-based channels cannot read — prove the channel, don't assume it.** A signed firehose programmer (OEM EDL package or custom-ROM bundle) grants raw storage; its rawprogram XML doubles as an independent partition-map cross-check. Programmers are per-variant; a mismatched one can configure cleanly and still not stream.
- **Bulk dump loops abort at the first zero-byte or short read.** A run whose partition-table read itself returned zero bytes still recorded ~50 void per-partition failures. Probe the smallest read (GPT) first; diagnose the loader/programmer instead of iterating a void list.

**Research value of the backup:**

- **Stock board description — boot-image DTB, ACPI tables, or the DT carried inside the vendor kernel image, per boot-model class** — board-specific configuration authority over a sibling SoC's dtsi once the variant/overlay is established; not proof of fitted silicon, physical wiring or measured voltages. Preserve conflicts until resolved.
- **Firmware blobs + load order**, **vendor kernel cmdline + boot-image layout where the class has one** (offsets, header version, args), **exact kernel version string** (`uname -a` / `/proc/version` — selects the right GPL OEM release in phase 2; vendors ship several, they differ), **vendor configs** (sensor/modem/HAL interfaces mainline must satisfy).

**Check storage health before trusting device reports:**

- Record the wear/lifetime report in phase 0 (UFS life-time %, eMMC health register). Storage is the classic misdiagnosis: I/O errors read as a dying drive; the wear % proved it healthy and one more stock-ROM flash cleared the fault. The % separates "hardware dying" from "software state bad" — opposite next steps.
- A media-failure verdict that would retire the device must survive **re-probe in a later session**: one flash region refused writes twice (cold power-off included), then passed 3/3 the next day against a full zero-error read scan. Absent wear fields = unknown; coarse values exclude nothing.

**Inventory + harvest while the stock system is present:**

- **Inventory every component the stock system names** — panel, touch, sensors, cameras, modem + RF config, WLAN/BT, charger + fuel gauge, audio — and compare against this variant's official spec sheets: `find-docs` for component docs, `wigolo` for web-found material (its cache matters across sessions); else plain web search. Sibling variants differ exactly here.
- **Evidence level** — declared / enumerated / driver-bound / exercised, no implied progression. A HAL name can reflect software ancestry, not silicon; installed firmware may cover unfitted components. One camera advertised 1080p30 in USB descriptors while Android exposed at most 1024×768 and 27 fps; no frames captured, so throughput was never measured.
- **Readable retail beats a ROM image.** On Android that is root: `getprop`, `/proc/config.gz`, mounted vendor/odm trees, HAL/sensor configs, calibration artefacts, factory field-test modes. Other retail OSes have their own readable surfaces — enumerate what this one exposes and harvest it through them. Privilege steps are the user's — no rooting or jailbreaking steps; resume when access exists.
- **Record the newest community custom OS** (on Android: custom ROM) as an optional development source: the most responsive OS ever run on one device was an unofficial recent-Android build, and its boot image shares the stock downstream lineage, so its DTB cross-checks the phase 0 extraction. A bundled EDL package also carries the signed programmer + rawprogram map.

**Keep a persistent component–wiring–document inventory**, seeded from stock, extended in phase 2, in the project's existing format. One research pack linked 134 component/subsystem records to 47 document records — not 134 fitted chips. Per component record:

| Field       | Content                                                                                                                                                                             |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identity    | Stable ID, function, manufacturer, exact/partial part; distinguish silicon, package suffix, module, flex/board marking.                                                             |
| Evidence    | Source location + applicability; measured chip ID, stock/schematic declaration, external photo, alternative, inference, unknown. Preserve contradictions + what would resolve them. |
| Connections | Bus/address, endpoints, muxes, clocks, supplies, reset/IRQ; claim → evidence link; unresolved routes marked.                                                                        |
| Documents   | Linked document IDs, pages/sections, questions answered, gaps.                                                                                                                      |
| Port status | Driver/binding/firmware dependencies; separate declared/enumerated/driver-bound/exercised results. Identity ≠ functional qualification.                                             |

Integrated functions are not separate packages; alternative populations are not extra fitted chips; a driver-family name need not equal the physical part (one controller shipped both 720×1280 and 720×1440 panel programs — the name alone did not select initialization). Keep unknowns unknown; an inventory is not permission to invent a BOM or blindly probe buses.

**Working conventions.** Resolve the device project root first. Multi-device reference layout (not a restructuring order):

```text
kernel/<version>/     Clean upstream source + generic builder
soc/<vendor-soc>/     SoC-common patches, configuration, packages
os/<platform>/        Device-neutral OS integration and tools
devices/<model>/      Device assembly, patches, calibration, state
```

Shared bases never import device policy; device facts, private captures, per-device agent context stay with the device. Mark a shared kernel tree used for experiments as patched — never pristine upstream.

- **Project layout.** Everything large/private/device-derived lives under gitignored `artifacts/` at the device project root: `private/` (device-unique; partition backup first), `firmware-harvest/` (blobs pending redaction), `android/` (stock-ROM packages, rooted captures), `debug-evidence/` (irreplaceable captures, preregistrations), `reference/` (reading copies). Publishable firmware → sibling `firmware-publishable/` repo with its own history, derived only after redacting device-unique identifiers. Device-local `logs/`: one subdir per boot/deploy + `LATEST-*` links. An `artifacts/` README maps paths to class with hashes, acquisition, coverage — provenance, not a substitute for integrity checks.
- **Publication hygiene runs at every boundary, not only upstream.** Whatever leaves the project — an upstream payload, a published repo or install guide, a status report, an issue/PR/forum reply — is swept first for operator-supplied secrets (Wi-Fi SSIDs and passwords, accounts and app passwords, tokens) and device-unique identifiers (IMEI, serial and MAC addresses, board/asset ids, calibration values, private captures, local hostnames and addresses). Redact or generalize in the payload itself, then verify by scanning the exact bytes that leave the machine — a sweep run from memory misses the value it forgot.
- **Prose holds no volatile numbers — the artifact does.** Hand-copied hashes, counts and ratios drift the moment the next flash or run lands, and a stale hand-copied hash is worse than none: it reads as a mismatch and invites a pointless reflash (one project was bitten three times; one stale pair survived six sessions after being called out). Point at the authority — the verify tool, the ledger, the build file — and query it. The same goes for derived indexes: a convenience enumeration in prose that has gone stale is deleted rather than re-synced, because a wrong index is how a wrong patch gets quoted.
- **Control channels.** Before any flash, prove reachability through the target's available control/recovery tools + at least one post-boot channel into target Linux. `adb`, fastboot, USB-gadget Ethernet, serial/ACM, ssh are examples where supported, not individually mandatory. Record working channels, selectors, supported operations — a channel assumed but never tested is discovered missing mid-wedge. Provision missing host tools through the host OS's own package route — on declarative hosts via its shell/profile mechanism; on container/WSL setups, prove USB passthrough before a workflow depends on it. Missing a tool does not waive operation-level proof or recovery prerequisites.
- **Bind every operation to the target.** Record handset↔transport mapping; select the transport explicitly per command; re-establish after re-enumeration; verify product + slot immediately before writes. A second connected phone must never become an implicit target. The binding constrains the host too: a probe must never mutate host network config — one absent gadget MAC made a control script rewrite an unrelated USB Ethernet dock's IP settings. Inspect interface identity + route first; refuse ambiguous matches; host network setup is a separate explicit command on an owned, uniquely verified interface.
- **Prove the operation, not just the connection.** Record whether required operations are actually supported before building around them: one unlocked bootloader supported neither RAM boot nor partition fetch, stranding a built rescue image. Deeper: a firehose that connects, configures and advertises the full `read` set still truncated every streamed read to 16 bytes of a 17,408-byte request — stock tool and two custom drivers alike. Advertised commands ≠ working data path; self-test-read a known-content region and verify bytes before scripting bulk transfer.
- Note which recovery path is known to work.

Write into the project's agent context files: all work touching this device goes through this ruleset; no device action outside it. Existing backup? Confirm coverage + hashes still verify.

**Keep the retail OS bootable for data gathering** (Android in the founding port; same principle wherever the class retains one). Read a working vendor driver's probe/firmware sequence live next to a mainline failure; exercise hardware mainline cannot yet drive. Never conclude mainline behaviour from retail-OS behaviour without a cross-check.

## 1. Gather evidence from the live device first

Capture the failing run's full dmesg before changing anything. Enable debug knobs before reproducing — a second reproduction costs another boot. Keep them relevant and understood: do not arm unassessed probes indiscriminately, and never increase physical stress to compensate for unverified protection telemetry.

Evidence interpretation is kernel work: invoke `linux-kernel-development` if installed, else `linux-kernel-crash-debug`, else a subsystem kernel skill; else reason from sources + `Documentation/`. Do not stall.

### Acquisition and claim boundaries

Per artifact record: origin, exact revision/build, acquisition method, coverage, access failures, integrity hashes. A running sysfs DT may include runtime fixups — not an untouched boot-image DTB (one capture: 2,740 property files preserved, 2,222 verified byte-identical, 518 redundant names accounted; all 2,839 manifest files passed — still not a restorable backup). A firmware path inventory does not establish load order.

Photographic identification: bind each observation to a named image/page/crop AND the photographed sample — not output order, not the target handset by assumption. One parallel-image association and one presumed manufacturer marking were withdrawn after original-byte inspection. Preserve uncertain transcriptions; public regional samples do not prove this handset's population.

Carry phase 0 evidence labels into findings. Record running vs deployed identities separately; service readiness ≠ end-to-end behaviour. Registered sound card + silent playback ≠ acoustic qualification; mark untested checks untested.

### Evidence channels, roughly by reliability

- **Live shell** (network/USB) — cheapest; anything the device survives.
- **Bootable retail OS** (Android where retained) — boot deliberately when a question is best answered there; cross-check before transferring conclusions.
- **pstore.** Identify the active crash-record collector, its config and archive destination. Inspect the mounted kernel interface (`/sys/fs/pstore/` where used) and any collector archive — when `systemd-pstore.service` has collected records its configured archive must be harvested too (one system moved everything to `/var/lib/systemd/pstore/` early in boot). Merely running systemd proves nothing about collection; an empty kernel directory alone proves no absence of a crash. Console region is usually a single slot — copy to host first or the next crash overwrites it.
- **ramoops.** Lossy: DRAM charge retention decays unpowered — measured ~6.5 % of bits on one device, enough to fail ECC on zone headers and lose records — and warm reboots rot it too (tens of blocks). Decay follows the power cycle. Grep fuzzily. Let a wedged device log while the console zone holds, then power-cycle and back promptly; a 4 MiB zone absorbs hours of watchdog spam first — check zone size.
- **Kernel ACM console** (`CONFIG_U_SERIAL_CONSOLE=y` + `console=ttyGS0`) — the only channel that survives a wedge, but verify it survives the reporting context: payload is emitted inside `panic()` with IRQs off and other CPUs stopped; a workqueue-deferred console never prints. Test free with a crash injector (LKDTM `/sys/kernel/debug/provoke-crash/DIRECT` where present): synthetic lockup, exact geometry, no flash. A userspace getty yields zero bytes while still enumerating. Both config halves required.
- **On compositor targets the screen is an evidence channel.** A screenshot pulled over the demonstrated channel returned the live UI state in seconds, and a loopback VNC server reached through the existing ssh tunnel added touch/keyboard injection — no new network exposure. Read the panel instead of inferring UI state from logs; the UI is part of the runtime surface.

### Rules that turn inference into measurement

- **Confirm what is flashed by reading back** — never notes or build logs. `iflag=direct` so the read doesn't perturb. Size-only comparisons prove nothing (boot images are padded). Hash the payload. But validate the readback method itself once against known content: on one host kernel `iflag=direct` against USB storage returned a deterministic _wrong_ buffer for any freshly written image — the hazard that would have condemned good media. Verify install media cross-method (drop caches, buffered `cmp` device-vs-image, plus a hash of a file read back through a mount), and condemn hardware only on physical evidence — mid-write bus dropout, re-enumeration as a different identity-less device, disagreement across independent reads — never on a hash mismatch alone.
- **Reading a debug interface can manufacture the symptom.** Sampling DRM/GPU crash-state nodes during active submission synthesised fault/recovery lines that never occurred (five reads, five fault/recover pairs; none unobserved). Check whether your own observation produced the log line.
- **Validate stimulus + observation path before interpreting silence.** Confirm the actual register protocol, effective post-init state, physical receiver route and required clocks throughout sampling. The claim that unchanged camera TX-on/off status bytes proved a static bank is **withdrawn**: the TX-off control used the wrong protocol, stream startup rewrote the setting, and earlier silence measurements targeted the wrong PHY. Unchanged bytes prove neither latch nor transmitter failure; a sensor/top-level runtime reference does not retain per-PHY clocks through teardown.
- **Capture a rejection's raw response before interpreting the mapped errno.** A firmware `EINVAL` was ambiguous — wrong encoding or refused policy — until dynamic debug exposed well-formed words carrying explicit refusal. Opposite verdicts, opposite next steps. Classify every rejection: encoding / policy / not-found. Mirror: a clean negative is positive proof — a not-found from a correct lookup validates the encoding and kills a plan branch without touching the loader.
- **A measurement racing an uncontrolled condition returns a plausible void number.** One benchmark scored 55 in two seconds — the compositor held DRM master, every scene failed. The valid 132 → 485 comparison needed the compositor stopped for both runs. Name the voiding condition; verify it held every run.
- **Control ownership through the whole measurement.** One camera reopen failed EPIPE after another client changed the media graph; configure + capture under the clients' compatible lock passed a 90-frame burst without stopping audio. Establish which clients cooperate. For power cleanup, identify open owners before calling an active supply a leak: last-owner close/open/close measured votes 0→1→0; a restored camera monitor legitimately reopened the device.
- **Measure controls on linear data, not enhanced previews.** Fixed region + processing scale, account for black level and clipping, bracket with baselines for scene drift. Exposure doubling measured 1.990×/1.995× signal; 2×/4× gain 1.984×/3.957×; baseline drift <0.23%. Accepted writes and auto-contrasted images are not proof. Physical-unit calibration, per-frame delay, autofocus, color remain separate qualifications.
- **Prove protection before increasing stress.** Enable bits + successful init ≠ limiter intervention. Establish telemetry identity, units, update behavior, failure handling before using it to justify higher output. All five retained candidate temperature values in one investigation were zero; the OEM getter ignored the return code. Never heat/drive harder to compensate for missing observability. Keep output restricted when protection is unverified — one speaker path muted output and revoked access on provisioning loss rather than falling back unprotected. A read-only debug interface is not automatically harmless: an observer-enabled boot locked up, causation unresolved.
- **Classify one-direction-dead USB networking by counters, not symptoms.** Gadget still enumerated + host ARP failing is ambiguous: ping from the host while reading the device's interface TX/RX counters over a surviving channel — counters rising only inbound proved the device could receive but not transmit. Check the rootfs first, too: a read-only remount after a failed resume kills sshd and networking with identical outward silence.
- **A host-side USB id proves neither the running OS nor a failed boot.** One enumerated vendor id plus dead ssh was repeatedly logged as an "Android fallback" while the target OS was up and responsive; host-visible signals establish channel state only. Obtain OS identity through a working channel; until then record a channel failure, not a boot verdict.
- **A session or compositor swap re-opens its layer's defaults.** The previous session claimed the power key (screen lock); the new one claimed nothing, so the logind stock handler turned one short press into a clean shutdown — journal `-b -1` shows the keypress, pstore stays empty: a shutdown masquerading as a crash. Config-variable errors can render as a near-invisible error banner instead of the UI, and autostarted input methods fail silently. After a session change verify: config loads clean (reload success), key/suspend handlers claimed, autostarts actually running, scale/orientation/idle behavior intact.
- **Record rules you withdraw.** Folklore goes stale with hardware revisions. Keep the overturned version next to the current one with what changed.
- **Attribute arch-only crashes by running the same pinned revision on the other architecture.** Same closure/revision crashing on x86_64 ⇒ upstream or packaging (miscompile class); running clean past the faulting constructor ⇒ port/display-stack specific. Strip variables before layer fixes: confirm the fault reproduces with no display stack (minimal platform), decode the core from its thread + file notes (a null vtable disassembly names the layer). Three single-layer fixes were refuted on one crash before the cross-arch run pointed at the build.
- **Decompose the compositing tax before any "GPU is slow" conclusion.** Matched-scene ladder, cheapest first: raw EGL surface → direct KMS (compositor stopped; the DRM-master voiding condition named and verified every run) → composited session. Check compositor CPU% to rule out starvation, A/B compositors, and verify direct-scanout engagement mechanically — compositor CPU time frozen while exactly one KMS plane is bound (it engages only for a single opaque fullscreen client with chrome hidden on some stacks). Perceived smoothness can be frame pacing, not throughput.
- **Long multi-session hunts need infrastructure, not memory.** Commit the smallest deterministic reproducer as a tool with a measured pass count; reconcile the incident-numbering ledger before comparing counts across sessions; pre-register predictions per experiment batch; and read back what the instrument actually received before trusting a control — one post-fix run whose gate parameter was silently dropped produced a withdrawn row (the printed received-range exposed it) and a corrected rerun reversed the conclusion. A sha256-pinned evidence manifest plus an offline replay tool that re-derives recorded metrics from the private captures closes the loop: drift is fatal, conflicts are the output.

### Host-side gotchas

- `timeout N ssh` kills the local client only; the remote process keeps running — kill it explicitly.
- `pgrep -f`/`pkill -f` over that ssh path self-match their own remote shell — filter the matcher or match by PID.
- `find` needs `-L` when the start path is a symlink, or it returns silently empty.
- Any wrapper that rewrites output — token filters, pagers, formatters — corrupts evidence, sometimes inverting a test. Use the raw command for findings.
- A detached recovery unit (`systemd-run`) runs with a minimal PATH: one script's `sleep`s silently no-opped, it raced its own readiness check, and its reboot safety net fired on a false negative — destroying the state it existed to diagnose. Absolute paths; every step logged with timestamps; a destructive fallback gated on preconditions verified inside the same script.
- **Verification harnesses: a missing answer is a FAIL, never a silent pass.** Empty output, timeout or transport failure on a required probe = FAIL (or an explicitly modeled skip); exit nonzero when required evidence is missing; capture the producer's exit status _before_ filtering (failed log acquisition is not a clean zero-match); set pipefail so a pipeline with a failing producer fails; bound cleanup so it preserves the original failure classification and reports its own uncertainty separately. Never kill by process-name pattern — a matcher can self-match or catch an unrelated process.

### Operator hands are a step, not an assumption

- **Authorization is scoped; pauses are binding.** Record the permitted action, target, limits, pause/resume prerequisite. Inspect ≠ restart-service ≠ capture ≠ actuate ≠ reboot. A paused experiment stays paused until its prerequisite is satisfied and required operator authorization confirmed; only explicitly permitted remaining work may continue. Reachable transport and an unlimited iteration budget do not expand permission (one lens session allowed same-position lifecycle checks while optical movement stayed deferred). A higher-output exception expires with its named test, not tomorrow's default.
- **Physical actions**: name the exact action + direction, then verify via an observable signal — appearance (mount, re-enum, slot change) or disappearance (power-off, USB drop, silence). Disappearance is ambiguous: watch the drop as an event, or require done-confirmation + verified post-absence probe failure. Poll briefly; no signal within a bound ⇒ re-ask.
- **Total channel loss after suspend/resume has three branches, not two.** Host-side, live-phone-w

…(truncated)
