commit a9e3c2f46182ff0dfc33dca8f4b26cbc4a6349dc Author: Codex Date: Wed Aug 26 19:56:27 2026 +0200 Dokumentiere Philips-TV-Speicherdiagnose und Patch-Engineering diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf1020b --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Never publish private/raw device artifacts. +*.apk +*.jar +*.odex +*.vdex +*.art +*.db +*.db-* +*.ab +*.img +*.bin +*.ko +*.so +*.dll +*.exe +*.whl +*.pyc +*.zip +*.7z +*.tar +*.gz + +__pycache__/ +.pytest_cache/ + +diagnostics/raw/ +diagnostics/bugreport/ +diagnostics/packages/ +diagnostics/privilege-escalation/ +diagnostics/selinux/ +diagnostics/kernel/ +diagnostics/deep-forensics/kernel/ +diagnostics/deep-forensics/selinux/ +diagnostics/deep-forensics/sqlite/ +diagnostics/deep-forensics/storage/ +diagnostics/deep-forensics/philips/*.txt + +*SECURITY-001* +*private-key* +*.key +*.p12 +*.pfx diff --git a/PUBLICATION-MANIFEST.md b/PUBLICATION-MANIFEST.md new file mode 100644 index 0000000..c6fe0eb --- /dev/null +++ b/PUBLICATION-MANIFEST.md @@ -0,0 +1,31 @@ +# Publication manifest + +Stand: 2026-08-26. Diese Git-Arbeitskopie wurde aus dem lokalen Diagnoseprojekt +für ein privates Gitea-Repository zusammengestellt. + +## Enthalten + +- finale Speicher-, EPG-, eMMC-, System- und Bootberichte; +- funktionale Philips-Bugreports BUG-001 bis BUG-003 und DESIGN-001; +- Phase-5-Reparaturzusammenfassungen ohne Datenbankkopien; +- vollständige Phase-6/7-Patch-, Signing-, OTA-, AVB- und Deploymentdokumente; +- Quelltext der Offline-Testmodelle und des read-only Health-Checks; +- Recovery-/Rollback-Runbooks; +- öffentliche Zertifikatfingerprints und unvertrauliche Baseline-Hashes. + +## Bewusst ausgeschlossen + +- vertrauliche Sicherheits- und Privilege-Escalation-/SELinux-Berichte; +- rohe Bugreports, Logs, Tombstones, Prozess-/Netzwerk-/Nutzerdaten; +- EPG- und andere private SQLite-Datenbanken; +- APK, JAR, ODEX, VDEX, ELF, Kernelmodule und Firmware-/Partitionsabbilder; +- ADB-Backups, lokale USB-/App-Daten und Recovery-Artefakte; +- alte Delta-/Diagnose-ZIPs, Python-Caches und extrahierte Toolchains; +- Zugangsdaten, Tokens, private Schlüssel und absolute Benutzerpfade. + +## Integrität + +Die dokumentierten SHA-256-Werte identifizieren lokale Stock-Artefakte, die +Binärdateien selbst sind nicht Teil des Repositorys. Die Veröffentlichung darf +nicht als flashbares Firmwarepaket oder vollständige Wiederherstellungskopie +missverstanden werden. diff --git a/README.md b/README.md new file mode 100644 index 0000000..85982f7 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Philips 43PUS7394/12 – Speicherdiagnose und Patch Engineering + +Kuratierte technische Ergebnisse zur Diagnose der wiederkehrenden Meldung +„interner Speicher fast voll“ auf einem Philips 43PUS7394/12 mit +TPM191E-Plattform und Android TV 12. + +## Ergebnis + +Die Warnung wurde durch eine auf rund 1,62 GiB angewachsene EPG-Datenbank +verursacht. Der Philips-Cleanup-Task protokollierte eine Bereinigung, führte +aber keine Löschung aus. Eine kontrollierte Reparatur reduzierte die DB auf +wenige MiB und gewann rund 1,62 GiB auf `/data`; Guide, Now/Next, Sender, +Favoriten, Aufnahmen, Timer und Reminder wurden anschließend validiert. + +Weitere bestätigte Befunde: + +- unbeschränkt wachsende GEL-WorkManager-Abhängigkeitskette; +- falscher USB-Freier-Speicher-Wert durch fehlenden `StatFs`-Fallback; +- Philips-Speicherwarnung vermischt Kapazität und eMMC-Lebensdauer; +- dauerhafte APK-Patches benötigen den Philips-Platform-Signer; +- das Gerät verwendet locked/green AVB und einen statischen A/B-OTA-Pfad; +- der sichere erste Deploymentkandidat bleibt ohne OEM-Signatur **NO-GO**. + +## Inhalt + +- `diagnostics/` – bereinigte Analyse- und Herstellerberichte; +- `patches/` – Patchpläne, Referenz-Diffs, Tests und Deploymentanalyse; +- `recovery/` – Notfall- und Rollback-Runbooks; +- `tools/epg-health-check/` – rein lesender EPG-Größen-/Snapshot-Check. + +Der Health-Check enthält keine automatische Reparaturfunktion: + +```powershell +python .\tools\epg-health-check\health_check.py --serial :5555 +``` + +## Sicherheits- und Datenschutzgrenze + +Dieses Repository ist eine kuratierte Veröffentlichung und kein vollständiges +Gerätebackup. Private Datenbanken, Bugreports, APK/JAR-/Kernel-Binaries, +Firmwareabbilder, Zugangsdaten und die vertrauliche Sicherheitsanalyse wurden +absichtlich nicht aufgenommen. Siehe `PUBLICATION-MANIFEST.md`. + +## Wichtige Entscheidungen + +- `patches/PHASE7-DECISION.md` +- `patches/phase7/DEPLOYMENT-MATRIX.md` +- `diagnostics/vendor-bugs/PHILIPS-PATCH-SUBMISSION.md` +- `recovery/README.md` + diff --git a/diagnostics/boot/boot-inventory-agent.md b/diagnostics/boot/boot-inventory-agent.md new file mode 100644 index 0000000..abdc980 --- /dev/null +++ b/diagnostics/boot/boot-inventory-agent.md @@ -0,0 +1,526 @@ +# Lokale System-, Boot- und Partitionsinventur + +Stand: 2026-08-25. Diese Auswertung wurde **ausschließlich lokal** aus dem +vorhandenen Bugreport und bereits gesicherten Textartefakten erstellt. Es gab +keine Live-ADB-Abfrage, keinen Reboot, keinen Wechsel in Recovery/Fastboot und +keine Änderung am Fernseher. + +## Bewertungsmaßstab + +- **CONFIRMED** – unmittelbar im Bugreport, in Properties, Kernel-Commandline + oder Mountinfo beobachtet. +- **LIKELY** – mehrere lokale Indizien passen zusammen, aber der entscheidende + Rohwert beziehungsweise die Implementierung fehlt. +- **POSSIBLE** – technisch mit dem Befund vereinbar, aber ohne positiven + lokalen Beleg. +- **DISPROVED** – durch die lokale Momentaufnahme direkt widerlegt. Das gilt + nur für die untersuchte Firmware und den aufgezeichneten Boot. + +## Quellenkürzel + +- **BR**: + `diagnostics/bugreport/extracted/files/01790__bugreport-PH9M_EA_5599-STT2.230526.001-2026-08-25-13-26-44.txt` +- **MI**: PID-1-Mountnamespace, + `diagnostics/bugreport/extracted/files/00018__mountinfo` +- **EA**: Zugriffsprotokoll, + `diagnostics/raw/emmc/emmc-filesystem-health-access.txt` +- **EI**: eMMC-Identitäts-/Zugriffsprotokoll, + `diagnostics/raw/emmc/emmc-identity-and-access.txt` +- **FS**: `diagnostics/boot/fstab.mt5887` +- **FL**: `diagnostics/boot/fstab.late.mt5887` +- **IA**: `diagnostics/boot/vendor-init/hw/init.abupdate.rc` +- **KI**: `diagnostics/boot/vendor-init/hw/init.mt5887.rc` +- **KC**: lokal gesichertes `/proc/config.gz`, + `diagnostics/kernel/proc-config.gz` (SHA-256 + `2C8DC79DBA98F4555684D0DED59C39232B4A4ADEAA4D92E5625FBB0C24D8F8C6`) + +Zeilenangaben hinter `BR` und `MI` beziehen sich auf diese unveränderten +lokalen Dateien. + +## Kurzfazit + +| Aussage | Bewertung | Beleg | +|---|---|---| +| Android TV 12, API 31, Patchlevel 2025-11-01 | **CONFIRMED** | BR:248208-248211 | +| Philips/TPV-Firmware `TPM191E_R.211.000.103.000` | **CONFIRMED** | BR:248246-248247 | +| Produktionsbuild `user`, `release-keys`, nicht debuggable | **CONFIRMED** | BR:248190-248200, 248219 | +| MediaTek-MT53xx-Plattform, Kernel-Machine `MT5887` | **CONFIRMED** | BR:248049, 248053, 248221-248222; KC: `CONFIG_MACH_MT5887=y`, `CONFIG_SOC_MEDIATEK_MT53XX=y` | +| OEM-Chipkennung `MT5599` | **CONFIRMED** als TPV-Property/Produktkennung, nicht als ausgelesene Silizium-ID | BR:247943; zusätzlich BR:10563-10569 | +| TPVs Kennung `MT5599` bezeichnet hier dieselbe physische Plattform/Familie wie `mt5887` | **POSSIBLE** | beide Kennungen treten auf demselben Gerät auf; eine physische Aliasbeziehung ist mangels Chip-ID/Herstellerunterlage nicht bewiesen | +| Android-Userspace ist ausschließlich 32-Bit ARM (`armeabi-v7a`, `armeabi`) | **CONFIRMED** | BR:248255-248259; Tombstones melden `ABI: 'arm'`, z. B. BR:168856 | +| Kernel/CPU-Architektur ist ARM64; Android-Userspace bleibt 32-Bit | **CONFIRMED** | KC: `CONFIG_ARM64=y`, `CONFIG_64BIT=y`, `CONFIG_COMPAT=y`; ABI-Properties in BR:248255-248259 | +| Kernel `4.9.310-ab4215`, SMP/PREEMPT, GCC 5.5/Linaro | **CONFIRMED** | BR:12 | +| Aktiver Bootslot ist A | **CONFIRMED** | BR:13, 248056 | +| Legacy/statisch partitioniertes A/B-System, keine Android-Dynamic-Partitions | **CONFIRMED** für diesen Boot | vollständige `blkdevparts=`-Tabelle in BR:13; kein `super`; direkte physische System-/Vendor-Slots | +| AVB/Verified Boot ist gesperrt, Zustand grün | **CONFIRMED** | BR:248050, 248057-248063 | +| dm-verity läuft im Enforcing-Modus | **CONFIRMED** | BR:13 und 248064 | +| System-as-root über `/dev/dm-0` | **CONFIRMED** | BR:13, 248197; MI:1 | +| Öffentliches Bootloader-Unlock / Unlock-Fähigkeit | **UNBEKANNT / nicht belegt** | kein Fastboot-Test, keine Unlock-Ability-Ausgabe und keine modellspezifische Herstellerfreigabe vorhanden | + +## Phase 1 – System- und Buildinventur + +### Geräte- und Firmwaredaten + +| Feld | Wert | Status / Fundstelle | +|---|---|---| +| Hersteller / Marke | Philips | **CONFIRMED**, BR:248254, 248263 | +| Modellproperty | `TPM191E` | **CONFIRMED**, BR:248264 | +| konkretes TV-Modell | `43PUS7394/12` | **CONFIRMED**, BR:13 (`device_name`) und BR:247942 | +| Produkt / Device / Board | `PH9M_EA_5599` | **CONFIRMED**, BR:248196, 248253, 248260, 248265 | +| TPV-Produkt-ID | `TPM191E_R` | **CONFIRMED**, BR:248246 | +| Firmware | `TPM191E_R.211.000.103.000` | **CONFIRMED**, BR:248247 | +| Android | 12 / `REL` | **CONFIRMED**, BR:248201-248209 | +| SDK | 31 | **CONFIRMED**, BR:248210 | +| First API level | 28 | **CONFIRMED**, BR:248261; dies belegt die ursprüngliche Plattformgeneration, nicht zwingend das ursprüngliche Verkaufs-ROM | +| Security Patch Level | `2025-11-01` | **CONFIRMED**, BR:248211 | +| Build-ID | `STT2.230526.001` | **CONFIRMED**, BR:248191, 248195 | +| Incremental | `4215` | **CONFIRMED**, BR:248204 | +| Buildzeit | `Tue Dec 9 11:16:50 CST 2025` | **CONFIRMED**, BR:248188 | +| Buildhost / User | `TWTPSA398-ub1404` / `jenkins` | **CONFIRMED**, BR:248194, 248200 | +| Fingerprint | `Philips/PH9M_EA_5599/PH9M_EA_5599:12/STT2.230526.001/4215:user/release-keys` | **CONFIRMED**, BR:6 und 248192 | + +Der vollständige Build-String lautet (BR:248190): + +```text +PH9M_EA_5599-user 12 STT2.230526.001 4215 release-keys +``` + +### Produktions- und ADB-Sicherheitsstatus + +| Property / Beobachtung | Wert | Bewertung | +|---|---:|---| +| `ro.build.type` | `user` | **CONFIRMED** | +| `ro.build.tags` | `release-keys` | **CONFIRMED** | +| `ro.debuggable` | `0` | **CONFIRMED** | +| `ro.secure` | `1` | **CONFIRMED** | +| `ro.adb.secure` | `1` | **CONFIRMED** | +| `init.svc.adbd` | `running` | **CONFIRMED**; BR:247638 | +| `service.adb.root` | nicht in der vollständigen Propertyliste publiziert | **CONFIRMED** als Nichtfund, nicht gleichbedeutend mit einem Wert `0`; Vendor-Init besitzt nur einen Trigger, der bei Wert `1` den USB-Gadget toggelt und `adbd` neu startet (`init.mtk_common.rc`:74-84) | +| SELinux Runtime | `Enforcing`; Shell `uid=2000`, Domain `u:r:shell:s0` | **CONFIRMED** durch `deep-forensics/selinux/access-summary.txt` und Auditzeilen mit `permissive=0`, z. B. BR:68587 | +| Build ist `eng` oder `userdebug` | – | **DISPROVED** durch `user`, `release-keys`, `ro.debuggable=0` | + +Fundstellen: BR:248041, 248198-248200, 248219, 248294. Die Kombination ist +typisch für einen Production-ADB-Kontext. Ob der Vendor dennoch einen speziell +geschützten Engineeringpfad besitzt, folgt daraus nicht. + +### Vendor-/ODM-/Treble-Merkmale + +| Merkmal | Wert | Bewertung | +|---|---|---| +| `ro.treble.enabled` | `true` | **CONFIRMED**, BR:248325 | +| Framework VNDK | 31 | **CONFIRMED**, BR:248380 | +| Vendor VNDK | 26 | **CONFIRMED**, BR:248369; älterer Vendor-Unterbau trotz Android-12-Framework | +| ODM-Build | Android 12 / SDK 31 / Incremental 4215 / release-keys | **CONFIRMED**, BR:248233-248245 | +| Vendor-Build | Android 12 / SDK 31 / Incremental 4215 / release-keys | **CONFIRMED**, BR:248327-248337 | +| Vendor SPL | leer | **CONFIRMED**, BR:248331; daraus darf kein Kernel-/Vendor-Patchlevel abgeleitet werden | +| `ro.vendor.mtk.oem.lock` | `1` | **CONFIRMED**, BR:248342; Semantik vendorintern, daher nicht allein als Bootloaderbeweis verwenden | +| `ro.crypto.state` | `unsupported` | **CONFIRMED**, BR:248217; `/data` erscheint direkt als ext4 auf `mmcblk0p37` | + +## SoC, CPU und ABI + +### Direkt beobachtete Kennungen + +```text +ro.board.platform=mt5887 +ro.boot.hardware=mt5887 +ro.hardware=mt5887 +ro.hardware.bootctrl=mt5887 +persist.odm.tpv.sys.ACR_OemChip=MT5599 +ro.product.board=PH9M_EA_5599 +``` + +Fundstellen: BR:247943, 248049, 248053, 248221-248222, 248253. + +Die Firmware enthält zudem MediaTek-TV-HALs und -Dienste; der HbbTV-User-Agent +nennt `_TV_MT5599_2019` (BR:10563-10569). Damit ist **CONFIRMED**, dass es sich +um eine MediaTek/TPV-Plattform handelt und dass TPV sie als `MT5599` ausweist. +Gleichzeitig ist **CONFIRMED**, dass Androids Board-/HAL-Plattform `mt5887` +heißt. Die Kernelkonfiguration verstärkt diesen Befund wesentlich: +`CONFIG_MACH_MT5887=y` und `CONFIG_SOC_MEDIATEK_MT53XX=y`; die alternativen +aufgeführten TV-Machines `MT5886`, `MT5893`, `MT5895` usw. sind nicht gesetzt. +Die spätere `/proc/cpuinfo`-Abfrage nennt `Hardware: MT5887` und vier +Cortex-A53-Kerne, liefert aber ebenfalls keine ausgelesene Silizium-ID. Ohne +Device-Tree-`compatible`, Chip-ID-Register oder passende Herstellerunterlage +ist die Aussage „MT5887, MT5690 und MT5599 sind exakt dasselbe Silizium“ daher +weiterhin **nicht bestätigt**. Die beste lokale Einordnung lautet: + +> **CONFIRMED:** Kernel und Android sind konkret für die MediaTek-Machine +> `MT5887` gebaut. **CONFIRMED:** TPV bezeichnet den OEM-Chip/die Produktfamilie +> gleichzeitig als `MT5599`. **POSSIBLE:** Beide Kennungen können Ebenen +> derselben Plattformfamilie bezeichnen. Die exakte Alias-/Siliziumbeziehung +> bleibt offen und darf nicht aus der Namenskorrelation abgeleitet werden. + +### Architektur + +```text +ro.product.cpu.abi=armeabi-v7a +ro.product.cpu.abi2=armeabi +ro.product.cpu.abilist=armeabi-v7a,armeabi +ro.product.cpu.abilist32=armeabi-v7a,armeabi +ro.product.cpu.abilist64= +``` + +Dies belegt einen ausschließlich 32-Bit-fähigen Android-Userspace. Native +Crashdumps melden `ABI: 'arm'`. Das inzwischen lokal gesicherte +`/proc/config.gz` bestätigt jedoch ausdrücklich `Linux/arm64 4.9.310`, +`CONFIG_ARM64=y`, `CONFIG_64BIT=y` und `CONFIG_COMPAT=y`. Damit sind ein +64-Bit-ARM-Kernel und die 32-Bit-Kompatibilitätsschicht **CONFIRMED**. Der +Fernseher verwendet also einen 64-Bit-Kernel mit reinem 32-Bit-Android- +Userspace. `CONFIG_NR_CPUS=4` bestätigt, dass dieser Kernel für maximal vier +CPUs gebaut ist. Eine spätere rein lesende `/proc/cpuinfo`-Abfrage hat vier +Prozessoren mit `CPU part 0xd03` direkt gezählt und damit vier Cortex-A53-Kerne +**CONFIRMED**. Das ist eine CPU-Kernidentifikation, keine Silizium-ID für die +offene MT5887/MT5690/MT5599-Aliasfrage. + +## Kernelinventur + +Direkte Bugreport-Ausgabe (BR:12): + +```text +Linux version 4.9.310-ab4215 (jenkins@TWTPSA398-ub1404) +(gcc version 5.5.0 (Linaro GCC 5.5-2017.10)) +#1 SMP PREEMPT Tue Dec 9 11:04:39 CST 2025 +``` + +| Merkmal | Bewertung | +|---|---| +| Kernelrelease `4.9.310-ab4215` | **CONFIRMED** | +| SMP und PREEMPT | **CONFIRMED** | +| Compiler GCC 5.5.0 / Linaro 2017.10 | **CONFIRMED** | +| Kernelbuild vom 2025-12-09 | **CONFIRMED** | +| `ro.kernel.version=4.9` | **CONFIRMED**, BR:248228 | +| exakte Kernelkonfiguration | **CONFIRMED verfügbar** als KC; Header: `Linux/arm64 4.9.310 Kernel Configuration` | +| Android-SPL belegt alle Kernel-CVE-Backports | **DISPROVED als Schlussfolgerung**; SPL und Kernelpatchstand sind getrennte Evidenz | + +Der zugängliche Bugreport-Kernelabschnitt war leer beziehungsweise durch +`klogctl` beschränkt. `dmesg`, `/proc/cmdline` und `/proc/partitions` waren der +ADB-Shell direkt nicht zugänglich (EA:18-26). Dumpstate hat die Kernel- +Commandline dennoch im Bugreportkopf gesichert. + +### Relevante Kernelkonfiguration + +| Option | Wert | Bedeutung für diese Inventur | +|---|---:|---| +| `CONFIG_ARM64`, `CONFIG_64BIT`, `CONFIG_COMPAT` | `y` | **CONFIRMED** ARM64-Kernel mit 32-Bit-Kompatibilität | +| `CONFIG_PREEMPT`, `CONFIG_SMP` | `y` | entspricht der Versionszeile | +| `CONFIG_IKCONFIG`, `CONFIG_IKCONFIG_PROC` | `y` | erklärt den jetzt gesicherten `/proc/config.gz`-Export | +| `CONFIG_CMDLINE_PARTITION` | `y` | Kernel unterstützt das beobachtete `blkdevparts=`-Layout | +| `CONFIG_CMDLINE="loop.max_part=7"`, `CONFIG_CMDLINE_EXTEND=y` | gesetzt | eingebaute Minimal-Commandline wird um die vom Bootpfad gelieferte Commandline erweitert | +| `CONFIG_EFI_PARTITION` | `y` | GPT-Unterstützung ist einkompiliert; das beweist nicht, dass dieses konkrete Layout von GPT stammt | +| `CONFIG_DM_VERITY`, `CONFIG_DM_VERITY_FEC` | `y` | bestätigt dm-verity/FEC-Unterstützung | +| `CONFIG_DM_CRYPT` | `y` | erklärt unter anderem Device-Mapper-Verschlüsselung für adopted storage; `/data` ist trotzdem direkt ext4 gemountet | +| `CONFIG_EXT4_FS`, `CONFIG_SQUASHFS` | `y` | deckt die beobachteten Dateisysteme ab | +| `CONFIG_MMC`, `CONFIG_MMC_BLOCK`, `CONFIG_MMC_MTK` | `y` | MediaTek-MMC-Treiberpfad bestätigt | +| `CONFIG_MMC_BLOCK_MINORS` | `64` | passt exakt zur beobachteten Hauptnode plus p1–p63 | +| `CONFIG_MACH_MT5887`, `CONFIG_SOC_MEDIATEK_MT53XX` | `y` | konkrete Kernel-Machine und SoC-Familie bestätigt | +| `CONFIG_NR_CPUS` | `4` | Kernelobergrenze vier logische CPUs; spätere `/proc/cpuinfo`-Abfrage zählt vier Cortex-A53-Kerne direkt | +| `CONFIG_SECURITY_SELINUX`, `CONFIG_DEFAULT_SECURITY_SELINUX` | `y` | SELinux ist Standard-LSM | +| `CONFIG_SECURITY_SELINUX_BOOTPARAM_VALUE` | `1` | Default ist enforcing; Runtime-Audit meldet ebenfalls `permissive=0` | +| `CONFIG_SECCOMP`, `CONFIG_SECCOMP_FILTER` | `y` | Seccomp-Filter verfügbar | +| `CONFIG_RANDOMIZE_BASE`, `CONFIG_CC_STACKPROTECTOR_STRONG`, `CONFIG_HARDENED_USERCOPY` | `y` | relevante Kernelhärtungen aktiv | +| `CONFIG_USER_NS` | nicht gesetzt | unprivilegierte User-Namespaces nicht verfügbar | +| `CONFIG_KEXEC` | nicht gesetzt | kein einkompilierter kexec-Bootpfad | +| `CONFIG_DEVMEM`, `CONFIG_DEVKMEM` | nicht gesetzt | keine klassischen `/dev/mem`-/`/dev/kmem`-Interfaces | +| `CONFIG_MODULES`, `CONFIG_MODULE_UNLOAD` | `y` | Kernelmodule sind unterstützt | +| `CONFIG_MODULE_SIG` | nicht gesetzt | keine Kernel-Modulsignaturprüfung durch diese Option; dies verleiht der Shell **keine** Ladeberechtigung | + +Die Konfiguration erlaubt außerdem SELinux-Development-/Disable-Codepfade +(`CONFIG_SECURITY_SELINUX_DEVELOP=y`, `..._DISABLE=y`). Im **tatsächlichen** +Boot wurde aber kein Disable-/Permissive-Argument beobachtet; AVB ist locked, +die Runtime ist enforcing. Aus der einkompilierten Option folgt daher kein +nutzbarer Shell-Schalter. + +## Phase 2 – Boot- und Verified-Boot-Architektur + +### Relevante `ro.boot.*`-Momentaufnahme + +| Property | Wert | Status | +|---|---|---| +| `ro.boot.bootreason` | `reboot` | **CONFIRMED**, BR:248051; OEM-Properties führen daneben `Cold Boot`/`CrashBoot`, deren Semantik ist nicht identisch | +| `ro.boot.hardware` | `mt5887` | **CONFIRMED**, BR:248053 | +| `ro.boot.slot_suffix` | `_a` | **CONFIRMED**, BR:248056 | +| `ro.boot.dtbo_idx` | `0` | **CONFIRMED**, BR:248052 | +| `ro.boot.serialno` | `000070af24c031e7` | **CONFIRMED**, BR:248055 | +| `ro.boot.phandle` | leer | **CONFIRMED**, BR:248054 | +| `ro.boot.vbmeta.device_state` | `locked` | **CONFIRMED**, BR:248059 | +| `ro.boot.verifiedbootstate` | `green` | **CONFIRMED**, BR:248063 | +| `ro.boot.veritymode` | `enforcing` | **CONFIRMED**, BR:248064 | + +`ro.boot.flash.locked`, eine Unlock-Ability und ein Rollback-Index werden in +den vorhandenen Properties nicht ausgegeben und bleiben **UNBEKANNT**. + +### Rekonstruierter Normalboot + +```text +eMMC / mmcblk0 + -> U-Boot-artige Bootloaderpartitionen (uboot/ubootB + Umgebungen) [LIKELY] + -> aktiver Slot _a [CONFIRMED] + -> vbmeta_a = mmcblk0p8 [CONFIRMED] + -> boot_a = mmcblk0p14 [CONFIRMED als Partitionszuordnung] + -> system_a = mmcblk0p15 -> dm-verity -> dm-0 -> / [CONFIRMED] + -> vendor_a = mmcblk0p35 -> vendor-verity -> dm-1 -> /vendor [LIKELY für p35-Ziel, + CONFIRMED für dm-1/verity] + -> linux_rootfs_a = p17 -> dm-2 -> squashfs unter /mnt/vendor/linux_rootfs [LIKELY für p17-Ziel] + -> 3rd_a = p19 -> dm-3 -> squashfs unter /mnt/vendor/3rd [LIKELY für p19-Ziel] + -> userdata = p37 -> ext4 -> /data [CONFIRMED] +``` + +Die Bootloaderart ist **LIKELY U-Boot-basiert**, weil die Partitionen wörtlich +`uboot`, `ubootB`, `uboot_env` und `uboot_envB` heißen. Eine ausführbare +Bootloader-Versionsausgabe fehlt; `ro.bootloader=unknown` ist **CONFIRMED** +(BR:7 und 248065). Daher ist weder eine konkrete U-Boot-Version noch deren +Kommando-/Unlockoberfläche bestätigt. + +Die lokal gesicherte Fstab bestätigt die Slot- und AVB-Zuordnung unabhängig +von der Commandline: `system` und `vendor` sind `avb,slotselect,recoveryonly` +(FS:6-7); `linux_rootfs`, `3rd` und `factory` sind ebenfalls `avb,slotselect` +(FS:12-14). `boot`, `vbmeta`, `dtbo`, `misc` und `tzbp` werden als rohe +`emmc`-Bereiche mit `slotselect` beschrieben (FS:16-20). Die separate +`recovery`-Zeile ist auskommentiert (FS:15). + +### System-as-root + +Die Commandline enthält (BR:13): + +```text +skip_initramfs root=/dev/dm-0 androidboot.slot_suffix=_a +``` + +Zusätzlich gilt `ro.build.system_root_image=true` (BR:248197). PID 1 sieht `/` +read-only als ext4 auf Major/Minor `254:0`, `/dev/root` (MI:1). Die dm-Tabelle +in der Commandline nennt als Daten- und Hashgerät `179:15`, also +`mmcblk0p15 = system_a`. Damit sind System-as-root und die Zuordnung von +`dm-0` zu `system_a` **CONFIRMED**. `/system` ist in diesem Namespace kein +separater Mount, sondern Teil dieses Root-Dateisystems. + +### AVB und dm-verity + +| Feld | Wert | Bewertung | +|---|---|---| +| `ro.boot.avb_version` | `1.2` | **CONFIRMED**, BR:248050 | +| `ro.boot.vbmeta.avb_version` | `1.0` | **CONFIRMED**, BR:248057 | +| vbmeta block device | `179:8` = `mmcblk0p8` = `vbmeta_a` | **CONFIRMED**, BR:13, 248058 | +| Device state | `locked` | **CONFIRMED**, BR:248059 | +| Verified boot state | `green` | **CONFIRMED**, BR:248063 | +| Verity mode | `enforcing` | **CONFIRMED**, BR:248064 | +| vbmeta hash algorithm | SHA-256 | **CONFIRMED**, BR:248061 | +| vbmeta digest | `66eae984496730f2339e5e5531f513645ce32263ac24b95cd80d4f787d701dc6` | **CONFIRMED**, BR:248060 | +| vbmeta size | 17,280 Byte | **CONFIRMED**, BR:248062 | +| System verity hash tree | SHA-1, 4096-Byte-Blöcke, FEC | **CONFIRMED**, BR:13 | +| Fehlerverhalten | `restart_on_corruption`, `ignore_zero_blocks`, `use_fec_from_device` | **CONFIRMED**, BR:13 | +| Vendor-Verity | dm-Gerät `vendor-verity` vorhanden | **CONFIRMED**, BR:7325; `/vendor` ist dm-1 ro (MI:8) | + +`locked + green + enforcing` widerlegt für diesen Boot einen bereits +entsperrten/orangen Verified-Boot-Zustand. Ein beliebig gepatchtes `boot_a` +würde im gegenwärtigen Zustand deshalb **LIKELY** an Signatur-/Verified-Boot- +Prüfungen scheitern. Ob Philips/MediaTek einen autorisierten Signaturpfad oder +einen Unlockmechanismus implementiert, ist aus den lokalen Daten nicht +entscheidbar. Der konkrete AVB-Hashdescriptor für `boot`, dessen Public Key +und Rollback-Indizes wurden nicht exportiert; diese Details bleiben bis zur +rein lokalen Analyse eines originalen `vbmeta`-/`boot`-Images **UNBEKANNT**. + +## Partitionierung + +### Schema + +Die Kernel-Commandline definiert die gesamte User-Area mit +`blkdevparts=mmcblk0:...` (BR:13). Das ist ein statisches, per Bootargument +benanntes Partitionslayout. Die 63 angegebenen Größen summieren sich auf +14.809 MiB beziehungsweise 14,462 GiB. Ein nominelles 16-GB-eMMC ist damit +**LIKELY**, seine exakte Rohgröße bleibt wegen gesperrtem +`/sys/block/mmcblk0/size` und `blockdev --getsize64` unbekannt (EI). + +Es gibt kein `super` in der vollständigen Liste und System/Vendor liegen auf +festen physischen A/B-Partitionen. Android Dynamic Partitions beziehungsweise +ein logisches `super`-Layout sind für diesen Boot daher **DISPROVED**. +Ob auf den ersten eMMC-Sektoren zusätzlich eine GPT- oder MBR-Tabelle liegt, +ist hingegen **UNBEKANNT**: KC enthält sowohl `CONFIG_MSDOS_PARTITION=y` als +auch `CONFIG_EFI_PARTITION=y`, der Rohblock war nicht lesbar. Die tatsächlich +vom laufenden Kernel verwendete Namens-/Größenzuordnung ist aber direkt durch +`CONFIG_CMDLINE_PARTITION=y` plus `blkdevparts=` **CONFIRMED**. + +Die generischen Android-12-Dateien `snapuserd.rc` und `lpdumpd.rc` ändern diese +Bewertung nicht: beide Dienste sind `disabled`; `lpdumpd` enthält nur den +AOSP-Defaultnamen `${ro.boot.super_partition:-super}`. Weder eine +`ro.boot.super_partition`-Property noch ein `super`-Blockgerät ist im +Gerätebefund vorhanden. + +### Vollständige `mmcblk0`-Zuordnung + +Die folgende Tabelle ist in der Reihenfolge aus BR:13 rekonstruiert. Größen +sind die wörtlichen `m`-Angaben der Kernel-Commandline. + +| Block | Größe | Name | Block | Größe | Name | +|---|---:|---|---|---:|---| +| p1 | 2 MiB | `uboot` | p2 | 2 MiB | `ubootB` | +| p3 | 2 MiB | `uboot_env` | p4 | 2 MiB | `uboot_envB` | +| p5 | 10 MiB | `perm` | p6 | 8 MiB | `dtbo_a` | +| p7 | 8 MiB | `dtbo_b` | p8 | 1 MiB | `vbmeta_a` | +| p9 | 1 MiB | `vbmeta_b` | p10 | 1 MiB | `ciplus` | +| p11 | 2 MiB | `dvbsDB_a` | p12 | 2 MiB | `eeprom_A` | +| p13 | 1 MiB | `misc` | p14 | 40 MiB | `boot_a` | +| p15 | 2390 MiB | `system_a` | p16 | 40 MiB | `factory_b` | +| p17 | 128 MiB | `linux_rootfs_a` | p18 | 1 MiB | `basic_a` | +| p19 | 368 MiB | `3rd_a` | p20 | 256 MiB | `3rd_rw` | +| p21 | 1 MiB | `Panel_Table_b` | p22 | 1 MiB | `channelA` | +| p23 | 1 MiB | `channelB` | p24 | 30 MiB | `pq_a` | +| p25 | 5 MiB | `aq_a` | p26 | 1 MiB | `bootlogo` | +| p27 | 1 MiB | `al_a` | p28 | 4 MiB | `tzbp_a` | +| p29 | 8 MiB | `adsp_a` | p30 | 1 MiB | `part_29` | +| p31 | 1 MiB | `part_30` | p32 | 1352 MiB | `upgrade` | +| p33 | 40 MiB | `dolby` | p34 | 1 MiB | `SCHEDPVR` | +| p35 | 460 MiB | `vendor_a` | p36 | 1 MiB | `odm_a` | +| p37 | 5115 MiB | `userdata` | p38 | 1 MiB | `al_b` | +| p39 | 40 MiB | `factory_a` | p40 | 1 MiB | `Panel_Table_a` | +| p41 | 4 MiB | `r20_a` | p42 | 4 MiB | `r21_a` | +| p43 | 4 MiB | `3dlut_a` | p44 | 2 MiB | `dvbsDB_b` | +| p45 | 40 MiB | `boot_b` | p46 | 2390 MiB | `system_b` | +| p47 | 128 MiB | `linux_rootfs_b` | p48 | 1 MiB | `basic_b` | +| p49 | 368 MiB | `3rd_b` | p50 | 30 MiB | `pq_b` | +| p51 | 5 MiB | `aq_b` | p52 | 1000 MiB | `user_setting` | +| p53 | 8 MiB | `tpv_misc_a` | p54 | 4 MiB | `tzbp_b` | +| p55 | 8 MiB | `adsp_b` | p56 | 460 MiB | `vendor_b` | +| p57 | 1 MiB | `odm_b` | p58 | 1 MiB | `Panel_Table_b` | +| p59 | 4 MiB | `r20_b` | p60 | 4 MiB | `r21_b` | +| p61 | 4 MiB | `3dlut_b` | p62 | 1 MiB | `reserved2` | +| p63 | 8 MiB | `tpv_misc_b` | | | | + +Die doppelte Bezeichnung `Panel_Table_b` bei p21 und p58 steht **wörtlich** in +der Commandline und wurde nicht korrigiert. Welche davon aktiv/legacy ist, +bleibt unbekannt. + +Zusätzlich sind `/sys/block/mmcblk0boot0` und `/sys/block/mmcblk0rpmb` +**CONFIRMED** vorhanden (BR:156599-156600, 156627-156628). Ein `boot1`-Knoten +wurde vom Bugreport nicht aufgeführt; das ist kein Beweis, dass die eMMC- +Hardware keinen zweiten Bootbereich besitzt. + +### A/B-OTA + +`ro.build.ab_update=true` (BR:248186) und `ro.boot.slot_suffix=_a` belegen ein +A/B-System. Die deklarierte OTA-Liste lautet (BR:248252): + +```text +boot,system,vbmeta,vendor,dtbo,linux_rootfs,factory,3rd,tzbp,pq,aq,adsp, +tpv_misc,uboot,uboot_env,ubootB,uboot_envB,dolby,r20,r21,3dlut +``` + +Nicht jede physische Partition ist geslottet; `userdata`, `upgrade`, +`user_setting`, `3rd_rw`, `misc`, `perm` und weitere Daten-/Konfigurations- +bereiche sind gemeinsam. `update_engine` lief im Bugreport als root-Prozess +(BR:159499, 161329) und `init.svc.update_engine=running` (BR:247719). + +`IA:4-21` legt zur Bootzeit die generischen `by-name`-Symlinks auf den +aktuellen `${ro.boot.slot_suffix}` um. `IA:24-57` erzeugt für tatsächlich +ungeslottete Bereiche künstliche `_a`- und `_b`-Aliase auf denselben Block; +besonders wichtig sind hier `misc`, `upgrade`, `userdata`, `uboot` und deren +Umgebungen. Die bloße Existenz eines `_a`/`_b`-Alias beweist bei diesem Vendor +daher **nicht**, dass zwei physische Kopien existieren. + +Zusätzlich ist ein root laufender Boot-Control-HAL registriert +(`android.hardware.boot@1.0::IBootControl default` in +`android.hardware.boot@1.0-service.rc`:1-5). Dies bestätigt die A/B- +Slotverwaltung. Der Dienst `update_engine` wird in +`system-init/update_engine.rc`:8-9 ausdrücklich bei vorhandenem +`ro.boot.slot_suffix` aktiviert. + +### Beobachtete Mounts und Dateisysteme + +| Mountpoint | Quelle | FS / Optionen | Status | +|---|---|---|---| +| `/` | `/dev/root`, Major/Minor 254:0 = dm-0 | ext4, ro | **CONFIRMED**, MI:1 | +| `/vendor` | `/dev/block/dm-1` | ext4, ro | **CONFIRMED**, MI:8 | +| `/data` | `.../by-name/userdata`, 179:37 | ext4, rw, `data=ordered` | **CONFIRMED**, MI:46 | +| `/perm` | `.../by-name/perm`, 179:5 | ext4, ro | **CONFIRMED**, MI:49 | +| `/data/ota_package` | `.../by-name/upgrade`, 179:32 | ext4, rw | **CONFIRMED**, MI:51 | +| `/data/vendor/user_setting` | `.../by-name/user_setting`, 179:52 | ext4, rw | **CONFIRMED**, MI:61 | +| `/data/vendor/3rd_rw` | `.../by-name/3rd_rw`, 179:20 | ext4, rw | **CONFIRMED**, MI:62 | +| `/mnt/vendor/linux_rootfs` | dm-2 | squashfs, ro | **CONFIRMED**, MI:47 | +| `/mnt/vendor/3rd` | dm-3 | squashfs, ro | **CONFIRMED**, MI:48 | +| Adopted USB unter `/mnt/expand/...` | dm-4 | ext4, rw | **CONFIRMED**, MI:64 | +| `/storage/emulated` | `/dev/fuse` über adopted storage | FUSE/sdcardfs-Schichten | **CONFIRMED**, MI:77-93 | + +Der df-Snapshot bestätigt `/` mit 2.370.764 KiB, `/vendor` mit 434.492 KiB +und `/data` mit 5.089.920 KiB Dateisystemgröße (BR:248515-248525). + +### Vorhandene beziehungsweise fehlende Standardpartitionen + +| erwarteter Bereich | Befund | Bewertung | +|---|---|---| +| `boot` | `boot_a` p14, `boot_b` p45 | **CONFIRMED** | +| `system` | `system_a` p15, `system_b` p46 | **CONFIRMED** | +| `vendor` | `vendor_a` p35, `vendor_b` p56 | **CONFIRMED** | +| `odm` | kleine `odm_a` p36 / `odm_b` p57, aber kein eigener `/odm`-Mount | Partitionen **CONFIRMED**, Inhalt/Funktion **UNBEKANNT** | +| `userdata` | p37, `/data` | **CONFIRMED** | +| `misc` | p13 | **CONFIRMED**, nicht normal gemountet | +| `vbmeta` | p8/p9 | **CONFIRMED** | +| `dtbo` | p6/p7 | **CONFIRMED** | +| `recovery` | keine so benannte Partition; Fstab-Zeile ist auskommentiert | klassische separate Recoverypartition **DISPROVED**; Recovery in `boot`/`factory` bleibt **POSSIBLE** | +| `cache` | keine so benannte Partition und kein eigener Mount | separate Cachepartition **DISPROVED**; `/data/cache` ist nur ein Datenverzeichnis | +| `metadata` | keine so benannte Partition / kein Mount | separate Android-Metadatapartition **DISPROVED** in diesem Layout | +| `persist` | keine so benannte Partition | **DISPROVED** als Name; `perm` darf ohne weitere Evidenz nicht mit `persist` gleichgesetzt werden | +| `super` | nicht vorhanden | Dynamic-Partition-Schema **DISPROVED** | +| `factory` | `factory_a`/`factory_b`; Fstab beschreibt `/factory` als AVB-geschütztes, geslottetes SquashFS | Abbildformat/-schutz **CONFIRMED**, genaue Recovery-/Service-Rolle **LIKELY**, aber nicht vollständig belegt | +| `upgrade` | p32, unter `/data/ota_package` gemountet | **CONFIRMED** | + +## Recovery-, Fastboot- und Service-Modi + +| Modus/Fähigkeit | lokale Evidenz | Bewertung | +|---|---|---| +| Android RecoverySystem/OTA | RecoverySystemService startet (BR:8008); `upgrade`-Partition, UpdateEngine, `uncrypt` und BCB-Dienste vorhanden | **CONFIRMED** als Update-/Recovery-Infrastruktur | +| separate klassische Recoverypartition | fehlt in vollständiger Tabelle | **DISPROVED** | +| Recovery steckt in `boot_a/b` | mit Layout und fehlender Recoverypartition vereinbar | **POSSIBLE**, nicht verifiziert | +| `factory_a/b` enthält ein OEM-Serviceabbild; eine Recovery-Rolle ist möglich | Fstab: AVB-geschütztes SquashFS auf `/factory`; `factory_service.rc` definiert `/factory/bin/factory_service` oneshot in `u:r:vold:s0` | Serviceabbild/-definition **CONFIRMED**; Recovery-Rolle **POSSIBLE**, tatsächlicher Start/Inhalt/Bedienschnittstelle nicht beobachtet | +| Vendor-Recovery-Installationshook | `vendor_flash_recovery.rc` definiert `/vendor/bin/install-recovery.sh` als `class main`, oneshot | Definition **CONFIRMED**, tatsächlicher Lauf sowie Ziel/Aktion des nicht gesicherten Skripts **UNBEKANNT** | +| BCB-Steuerung | `uncrypt.rc` definiert `setup-bcb` und `clear-bcb`, jeweils disabled/oneshot | **CONFIRMED** als vorhandene, aber nicht ausgelöste Infrastruktur | +| TPV-spezifischer One-button-/USB-Recoverypfad | `factory`, `upgrade`, Install-Recovery-Hook und OEM-Pakete sind damit vereinbar, belegen aber keinen konkreten Bedienpfad | **POSSIBLE**, nicht gestartet oder positiv nachgewiesen | +| Android-Rebootziel `bootloader` | `system-init/apexd.rc`:15 enthält als Fehleraktion `reboot,bootloader,...` | Zielstring/Anforderung **CONFIRMED**; Verhalten des Bootloaders **UNBEKANNT** | +| Bootloader/Fastboot-Transport | keine Sitzung, Bootloader-USB-ID oder Fastbootausgabe im Korpus | **POSSIBLE / UNBEKANNT**; der Rebootzielstring allein beweist kein Fastboot-Protokoll | +| `fastbootd` | kein positiver lokaler Beleg; kein `super`/Dynamic-Partitions-Schema | **POSSIBLE**, aber eher **UNLIKELY**; nicht als abwesend bewiesen | +| serielle Android-Konsole | Commandline `console=ttyMT0,115200n1`; KI:39-40 startet den Console-Service früh; `system-init/hw/init.rc`:1223-1230 definiert `/system/bin/sh` als User `shell`, Domain `u:r:shell:s0` | Konfiguration **CONFIRMED**; kein Root, keine Pinbelegung/Spannung und kein physischer Zugriff geprüft | +| Bootloader ist derzeit entsperrt | AVB meldet locked/green | **DISPROVED** | +| Unlock-Fähigkeit | `get_unlock_ability`/`oem device-info` nie sicher abgefragt | **UNBEKANNT** | + +Weder Fastboot noch Recovery sollte allein zum Nachweis neu gebootet werden, +solange kein modellgenauer Rückweg und keine sichere Eingabemethode bestätigt +sind. Der lokale Befund genügt nur für die Architektur, nicht für die +Erreichbarkeit dieser Modi. + +## Offene Punkte und sinnvolle spätere Nur-Lese-Nachweise + +Diese Befehle wurden von diesem Agenten **nicht ausgeführt**. Falls der +Hauptauftrag sie später live erlaubt und die Shell sie freigibt, würden sie +die verbleibenden Lücken ohne Flashen schließen: + +```text +getprop ro.boot.flash.locked +getprop ro.boot.dynamic_partitions +getprop ro.boot.super_partition +bootctl get-current-slot +bootctl hal-info +ls -lZ /dev/block/bootdevice/by-name +cat /proc/device-tree/compatible +ls -l /system/bin/fastbootd /system/bin/bootctl /system/bin/update_engine +``` + +Direkte Reads von `/proc/partitions`, `/proc/cmdline`, +`/sys/block/mmcblk0/size` und dem Rohblock wurden bereits als `Permission +denied` dokumentiert; wiederholtes Probieren verspricht ohne geänderten +Kontext keinen Erkenntnisgewinn. Das Einsteigen in Fastboot/Recovery wäre kein +reiner Dateilesezugriff und ist nicht Teil dieser lokalen Auswertung. + +## Lokal ausgeführte Auswertungsschritte + +Es wurden nur Host-Dateien gelesen und Text verarbeitet: + +```powershell +rg --files +Get-Content diagnostics\bugreport\bugreport-metadata.txt +rg -n +Get-Content -First 30 +Get-Content diagnostics\bugreport\extracted\files\00018__mountinfo +Import-Csv -Delimiter "`t" diagnostics\bugreport\extracted\manifest.tsv +``` + +Die Partitionsnummern und die Größensumme wurden lokal deterministisch aus dem +`blkdevparts=mmcblk0:`-Abschnitt bis `blkdevpartenc=` geparst. Kein Befehl in +dieser Phase kommunizierte mit `:5555`. diff --git a/diagnostics/bugreport-analysis.md b/diagnostics/bugreport-analysis.md new file mode 100644 index 0000000..ff3342b --- /dev/null +++ b/diagnostics/bugreport-analysis.md @@ -0,0 +1,158 @@ +# Bugreport-Analyse – Philips 43PUS7394/12 + +> **Nachtrag 2026-08-25:** Eine spätere, rein lesende Abfrage des noch +> vorhandenen Android-Eventpuffers lieferte +> `storaged_emmc_info: [emmc 5.1,1,10,10]`. Die geräteeigene Event-Tagdefinition +> weist die Felder als `(mmc_ver,eol,lifetime_a,lifetime_b)` aus. Damit ist +> `PRE_EOL_INFO=0x01` (Normal) entgegen den damaligen, nur auf den extrahierten +> Haupttext bezogenen Nichtfund-Aussagen inzwischen **bestätigt**. Der Bericht +> unterscheidet unten zwischen dem ursprünglichen Haupttext-Nichtfund, dem +> späteren Event-Export und dem weiterhin gesperrten direkten Rohbyte. + +Stand: 2026-08-25. Der Bugreport wurde mit dem normalen, autorisierten ADB- +Zugang erzeugt. Alle Aktionen am Fernseher waren lesend; Entpacken und Suchen +erfolgten ausschließlich im lokalen Workspace. + +## Erzeugung und Vollständigkeit + +| Merkmal | Ergebnis | +|---|---| +| Datei | `bugreport-PH9M_EA_5599-STT2.230526.001-2026-08-25-13-26-44.zip` | +| lokale Größe | 6.420.496 Byte (6,12 MiB) | +| lokaler Zeitstempel | 2026-08-25 13:29:05 +02:00 | +| ZIP-Einträge | 1.790 | +| extrahierte Dateien | 1.792, einschließlich zweier Einträge aus einem eingebetteten ZIP | +| extrahierte Nutzdaten | 61.268.602 Byte | +| Hauptbericht | 46.511.209 Byte | +| Textsuche | 1.787 Dateien geprüft, 5 Binärdateien ausgelassen, 2.450 Trefferzeilen | + +Windows `Expand-Archive` konnte Android-Dateinamen mit `::` und einzelne sehr +lange cgroup-Pfade nicht abbilden. Deshalb wurde das ZIP mit +`extract_bugreport.py` verlustfrei in flach nummerierte Dateien extrahiert. +`extracted/manifest.tsv` ordnet jede lokale Datei ihrem ursprünglichen +ZIP-Pfad zu. Dieser Fehler betraf nur die lokale Extraktion, nicht den +Bugreport auf dem Fernseher. + +## Dumpstate-Einschränkungen + +Der Hauptbericht ist verwertbar, aber nicht vollständig privilegiert: + +- `perfetto --save-for-bugreport` wird auf diesem `user`-Build nicht unterstützt; +- der historische `logcat -L`-Abruf scheiterte; +- `iotop` ist nicht installiert; +- mehrere HIDL-Debug-Aufrufe, darunter TPV-/MediaTek-Dienste, antworteten mit + `FAILED_TRANSACTION`; +- `dumpstate` meldet ausdrücklich `No IDumpstateDevice implementation`; +- deshalb existiert weder `dumpstate_board.bin` noch `dumpstate_board.txt`; +- der Kernel-Log-Abschnitt enthält nur `Unexpected klogctl return value: -1`; +- `/proc/last_kmsg` fehlt; der Recovery-`last_kmsg`-Eintrag ist leer; +- Dienst-Dumps von UpdateEngineService scheiterten, die separaten + Update-Engine-Logdateien sind jedoch vorhanden und erfolgreich. + +`IDumpstateDevice` ist eine optionale OEM-Schnittstelle. Ihr Fehlen bedeutet, +dass dieser Build keinen zusätzlichen Philips/TPV-Board-Dump in den +Standard-Bugreport einspeist. Die leeren `lshal-debug`-Dateien liefern daher +keine weitere eMMC-Gesundheitsinformation. Siehe +[AOSP: Scoped vendor logging](https://source.android.com/docs/core/tests/debug/scoped-vendor-logging). + +## eMMC-Befunde + +Der Bugreport bestätigt unabhängig von den vorherigen Einzelabfragen: + +```text +[persist.odm.tpv.sys.emmc_lifetime]: [0x0a 0x0a] +[init.svc.emmclifereaderd]: [stopped] +[ro.boottime.emmclifereaderd]: [11750142162] +OnBootLogger: logEMMCDetails devLifeTime = 0x0a 0x0a +dev_lt_a:"a", dev_lt_b:"a" +TVStorageMonitorService: propvalue = 0x0a 0x0a +TVStorageMonitorService: EmmcLifetime 90%-100% device lifetime used +``` + +Die ursprüngliche native Boot-Zeile +`D eMMCLifeReader: eMMC life time 0x0a 0x0a` ist im neuen Bugreport nicht mehr +im aktuellen Log-Ring enthalten; sie bleibt in den zuvor gesicherten Bootlogs +belegt. Der neue Bericht wiederholt denselben Wert über Property, OnBootLogger +und TVStorageMonitorService über mehrere Stunden. + +Neu ist die genaue Debugfs-Quelle, auf die Androids `storaged` zugreifen wollte: + +```text +root 2644 storaged: avc: denied { read } +path="/sys/kernel/debug/mmc0/mmc0:0001/ext_csd" +scontext=u:r:storaged:s0 +tcontext=u:object_r:debugfs_mmc:s0 +``` + +Damit sind Gerät `mmc0:0001`, ein vorhandener EXT_CSD-Debugfs-Knoten und die +SELinux-Sperre direkt belegt. Bemerkenswert ist: Auch der Prozess `storaged` +mit Linux-UID root wurde blockiert. Ein bloßer UID-0-Zugang würde den Wert +daher nicht automatisch lesbar machen; zusätzlich wäre ein geeigneter +SELinux-Domain-/Policy-Zugang erforderlich. + +Für `life_time` und `pre_eol_info` enthält der extrahierte Hauptbericht die +Audit-Denials der ADB-Shell. Dort erscheint kein ausgeschriebener +`PRE_EOL_INFO`-Wert. Die spätere Eventlog-Abfrage hat den Wert jedoch als +`eol=1` bestätigt. Weiterhin fehlen ein vollständiger EXT_CSD-Rohdump, CID und +eine Hersteller- oder Modellkennung. + +## Life Time A, Life Time B und PRE_EOL + +| Feld | Befund | Status | +|---|---|---| +| Life Time A | Philips speichert `0x0a`; OnBootLogger meldet `dev_lt_a="a"` | **belegt: 90–100 % verbraucht** | +| Life Time B | Philips speichert `0x0a`; OnBootLogger meldet `dev_lt_b="a"`; TVStorageMonitor wertet diesen zweiten Wert aus | **belegt: 90–100 % verbraucht** | +| PRE_EOL_INFO | direkter Pfad für Shell gesperrt; späterer Storaged-Event meldet `eol=1` | **bestätigt: `0x01` (Normal)**; Rohbyte nicht direkt gelesen | + +Die Interpretation von `0x0a` stützt sich auf die JEDEC-Wertetabelle, wie sie +beispielsweise in Herstellerunterlagen wiedergegeben wird. Der genaue +Verschleiß innerhalb des 10-%-Bandes ist nicht bestimmbar. + +## Prüfung auf Storage-Fehler + +Die automatisierte Suche erfasste Hauptbericht, ANRs, Recovery-Logs, +Incident-Report, Update-Engine-Logs und alle eingebetteten Textdateien. Danach +wurden die Treffer manuell klassifiziert. + +Nicht gefunden wurden: + +- `EXT4-fs error`, Journal-Fehler oder schreibgeschützte Remounts; +- `Buffer I/O error`, Block-I/O-Fehler oder `blk_update_request`; +- MMC-Timeouts, CRC-/ECC-Fehler, Controller-Resets oder Command-Fehler; +- `disk I/O error`, SQLite-`malformed` oder nachgewiesene DB-Korruption; +- OTA-Schreibfehler oder erfolglose Update-Engine-Aktionen; +- ein unerwarteter Neustart mit belegtem Storage-Bezug. + +Die vier automatischen Treffer der Kategorie `storage_errors` waren: + +1. ein DNS/SSL-`write error` mit `Try again`; +2. ein TLS-Lesefehler nach `Connection reset by peer`; +3. ein TLS-Shutdown mit `Broken pipe`; +4. `incidentd: Buffer corrupted` beim Lesen eines Incident-Puffers. + +Keiner dieser Treffer ist einem Blockgerät oder Dateisystem zuzuordnen. Alle +sechs enthaltenen Update-Engine-Logs enden mit `ErrorCode::kSuccess` und +`Processing Done`. PackageManager-Meldungen, dass ältere Systemkopien wegen +vorhandener neuerer Updates nicht ausgewählt wurden, sind normale +Paket-Selektion und keine Schreibfehler. + +Das Fehlen sichtbarer Fehler ist ausdrücklich **kein Gesundheitsnachweis**: +Kernel-`dmesg`, EXT4-Fehlerzähler und OEM-Board-Dumps fehlen. Es bedeutet nur, +dass im verfügbaren Diagnosefenster kein Storage-Fehler nachweisbar ist. + +## Ergebnis + +Der Bugreport bestätigt die bisherige Speicherursache und die beiden +Philips-eMMC-Werte, liefert aber keinen dritten Health-Wert. Sein stärkster +neuer Beitrag ist die direkte Identifikation des EXT_CSD-Knotens und der +SELinux-Barriere. Ein unmittelbarer Medienausfall ist nicht belegt; die beiden +hohen Life-Time-Bänder bleiben dennoch ein ernstzunehmender Verschleißhinweis. + +## Lokale Artefakte + +- `bugreport/original/`: unverändertes ZIP +- `bugreport/extracted/manifest.tsv`: vollständige Pfadzuordnung +- `bugreport/analysis/search-hits.tsv`: Treffer mit Quelle, Zeile und Kontext +- `bugreport/analysis/search-summary.tsv`: Treffer je Datei/Kategorie +- `bugreport/analysis/search-totals.tsv`: Summen je Kategorie +- `bugreport/analysis/binary-skipped.tsv`: ausgelassene Binärdateien diff --git a/diagnostics/deep-forensics/emmc/emmc-kernel-agent.md b/diagnostics/deep-forensics/emmc/emmc-kernel-agent.md new file mode 100644 index 0000000..d806a22 --- /dev/null +++ b/diagnostics/deep-forensics/emmc/emmc-kernel-agent.md @@ -0,0 +1,335 @@ +# Tiefe eMMC-/Kernel-Forensik – lokaler Artefaktbefund + +Stand: 2026-08-25 +Gerät: Philips 43PUS7394/12, TPM191E, Android TV 12 +Arbeitsweise: ausschließlich lokale, lesende Auswertung bereits gesicherter +Bugreport-, Log- und Dokumentationsdateien. Es wurden **keine neuen +ADB-Gerätebefehle** ausgeführt und am Fernseher keinerlei Änderungen +vorgenommen. + +## Wichtiger Nachtrag – PRE_EOL inzwischen unabhängig belegt + +Eine spätere, rein lesende Live-Abfrage des bereits vorhandenen Android- +Eventlogs hat die zentrale damalige Lücke geschlossen. Sie lieferte: + +```text +storaged_emmc_info: [emmc 5.1,1,10,10] +``` + +Die geräteeigene Definition +`storaged_emmc_info (mmc_ver),(eol),(lifetime_a),(lifetime_b)` beweist die +Feldreihenfolge. Damit meldet Android Storaged für diesen Boot eMMC 5.1, +`PRE_EOL_INFO=0x01` (Normal), Lifetime A `0x0a` und Lifetime B `0x0a`. +AOSPs Android-12-Implementierung gewinnt diese Werte aus den MMC-sysfs- +Attributen beziehungsweise als Fallback aus EXT_CSD-Debugfs. Dieser Befund +ist unabhängig von Philips' persistenter Property. Die nachfolgenden Tabellen +sind auf diesen Gesamtstand aktualisiert; gesperrt bleiben nur der direkte +Shell-Read, der vollständige EXT_CSD-Rohdump, CID/CSD und die +Herstelleridentität. + +Primärquellen: +[AOSP Storaged Android 12](https://android.googlesource.com/platform/system/core/+/refs/heads/android12-gsi/storaged/storaged_info.cpp), +[AOSP EventLogTags](https://android.googlesource.com/platform/system/core/+/master/storaged/EventLogTags.logtags). + +## Kurzfazit + +Philips hat beim Booten zwei eMMC-Lebensdauerwerte `0x0a 0x0a` gemeldet; kurz +danach liegt derselbe Wert in einer persistenten Property vor. Mehrere voneinander +getrennte Philips-Komponenten bestätigen beide Felder als Life Time A und B; +`TVStorageMonitorService` ordnet den zweiten Wert dem Band „90–100 % device +lifetime used“ zu. Diese Philips-interne A/B-Aussage ist **direkt und mehrfach +belegt**. Nicht unabhängig belegt sind die aktuellen Rohbytes 268/269 des +EXT_CSD, weil alle direkten Pfade gesperrt sind. + +Der direkte sysfs-/EXT_CSD-Rohzugriff auf `PRE_EOL_INFO` bleibt der ADB-Shell verwehrt. +Der logische Wert ist inzwischen dennoch unabhängig bestätigt: Android +Storaged meldet `eol=1`, also `PRE_EOL_INFO=0x01` (Normal). Auch der genaue +Debugfs-EXT-CSD-Knoten existiert: +`/sys/kernel/debug/mmc0/mmc0:0001/ext_csd`. Androids `storaged` versuchte ihn +als Unix-UID `root` zu lesen und wurde von SELinux abgewiesen. Das ist der +stärkste direkte Nachweis dafür, dass „UID 0“ auf diesem Produktions-Build +allein keinen eMMC-Rohzugriff garantiert. + +Im durchsuchbaren Diagnosefenster gibt es **keinen nachweisbaren MMC-, Block- +I/O-, EXT4- oder Journalfehler**. Alle vier automatisch als Storage-Fehler +erkannten Zeilen sind Netzwerk- beziehungsweise Incident-Pufferfehler. Diese +negative Aussage ist jedoch stark eingeschränkt: `dmesg`, PStore/Last-Kmsg, +EXT4-Fehlerzähler und ein OEM-Board-Dump fehlen. Aus dem Nichtfund darf daher +weder „eMMC gesund“ noch „kein historischer Medienfehler“ abgeleitet werden. + +## Quellen und Auswertungsumfang + +Primär ausgewertet wurden: + +- `diagnostics/bugreport/extracted/files/01790__bugreport-...txt` + (46.511.209 Byte, Standard-Bugreport); +- `diagnostics/bugreport/extracted/manifest.tsv` mit 1.790 äußeren Einträgen + und zwei zusätzlich extrahierten inneren Dateien; +- `diagnostics/bugreport/analysis/search-hits.tsv` und + `storage-error-review.txt`; +- die sechs Dateien `00012__update_engine...` bis + `00017__update_engine...`; +- `diagnostics/raw/emmc/emmc-property-init-and-bootlog.txt`; +- `diagnostics/raw/emmc/emmc-identity-and-access.txt`; +- `diagnostics/raw/emmc/emmc-filesystem-health-access.txt`; +- die vorhandenen Berichte `emmc-analysis.md`, `bugreport-analysis.md` und + `privilege-analysis.md`. + +Die vorhandene reproduzierbare Analyse hat 1.787 textartige Dateien geprüft, +fünf Binärdateien wegen ihres NUL-Anteils ausgelassen und 2.450 kategorisierte +Trefferzeilen erzeugt. Zusätzlich wurden die ernsthaften MMC-/EXT4-Muster +nochmals exakt über den extrahierten Textkorpus geprüft. + +## Evidenzklassen + +| Kennung | Bedeutung | Anwendung in diesem Bericht | +|---|---|---| +| **E1 – direkt belegt** | Wortlaut, Pfad, Property, Audit oder Prozesszustand liegt im lokalen Artefakt vor | zum Beispiel `0x0a 0x0a`, AVC-Denial für `ext_csd`, Prozess-UID/-Domain | +| **E2 – stark gestützte Schlussfolgerung** | mehrere E1-Indizien bilden eine konsistente Kette, ein entscheidender Rohzugriff fehlt aber | der Boot-Reader gewinnt A/B sehr wahrscheinlich aus dem eMMC-EXT_CSD | +| **E3 – begrenzter Nichtfund** | im tatsächlich verfügbaren Textfenster nicht gefunden; fehlende privilegierte Quellen begrenzen die Aussage | keine sichtbaren MMC-/EXT4-Fehler | +| **U – unbekannt/nicht prüfbar** | kein Wert oder keine ausreichende Quelle vorhanden | CID/Hersteller, vollständiger EXT_CSD-Rohdump, vollständiger historischer Kernelzustand | + +E3 ist ausdrücklich keine schwächere Form eines Gesundheitsnachweises, +sondern eine Aussage über den untersuchten Korpus. + +## Physische und logische Speicherzuordnung + +Folgende Zuordnung ist aus den gesicherten Rohdaten direkt belegt: + +```text +MediaTek-Host: 1006d000.MSDC0 +eMMC-Gerät: mmc0:0001 +Blockgerät: mmcblk0 +/data: /dev/block/mmcblk0p37, ext4, rw +Debugfs: /sys/kernel/debug/mmc0/mmc0:0001/ext_csd +sysfs: /sys/block/mmcblk0/device -> .../mmc0:0001 +Blocknode: /dev/block/mmcblk0, brw------- root root, major:minor 179:0 +``` + +Der Kernel-Commandline-Eintrag enthält eine statische +`blkdevparts=mmcblk0:...`-Partitionstabelle und bestätigt unter anderem die +`userdata`-Partition. Die rohe Blockgröße blieb dagegen unlesbar; aus der +Partitionstabelle wird deshalb hier weder Modell noch exakte physische +Kapazität abgeleitet. + +## Status der eMMC-Gesundheitsinformationen + +| Information | Zugriff/Beobachtung | Blockade | Benötigte Rechte bzw. Funktion | Legitimer risikoarmer Weg | Evidenz | +|---|---|---|---|---|---| +| Life Time A | Philips-Property `0x0a 0x0a`; OnBootLogger `dev_lt_a="a"`; Storaged-Event `lifetime_a=10` | direkter EXT-CSD-Bytezugriff fehlt | lesender Zugriff auf EXT_CSD-Byte 268 für einen Rohdump | autorisierter TPV/Philips-Serviceexport | exportierter Wert `0x0a` **E1**; Rohbyte weiterhin **U** | +| Life Time B | dieselbe Property; `dev_lt_b="a"`; TVStorageMonitor wertet zweiten Wert als 90–100 % aus; Storaged-Event `lifetime_b=10` | direkter EXT-CSD-Bytezugriff fehlt | lesender Zugriff auf EXT_CSD-Byte 269 für einen Rohdump | autorisierter Service-/Engineering-Dump | exportierter Wert `0x0a` **E1**; Rohbyte weiterhin **U** | +| `PRE_EOL_INFO` | sysfs-Knoten existiert, direkter Shell-Read erhält AVC-Denial; Storaged-Event meldet `eol=1` | nur der direkte Rohbytezugriff bleibt gesperrt | passende SELinux-Domain nur für unabhängigen Rohdump von EXT-CSD-Byte 267 nötig | signierter OEM-Diagnosedienst bzw. autorisierter Engineering-Build | logischer Wert `0x01` (Normal) **E1**; Rohbyte weiterhin **U** | +| vollständiger EXT_CSD | exakter Debugfs-Knoten ist durch Audit belegt | `storaged:s0` wird trotz UID root an `debugfs_mmc:s0` abgewiesen; Shell schon am Verzeichnis | Verzeichnissuche plus `open/read/getattr` für `debugfs_mmc`; ein hierfür erlaubter OEM-/Kernelkontext | Philips/TPV-Servicewerkzeug, das nur lesend exportiert | **E1** für Existenz/Blockade, **U** für Inhalt | +| CID | sysfs-Knoten existiert, `cat` durch `shell:s0` abgewiesen | SELinux; zusätzlich Rohblock-DAC ist restriktiv | erlaubter sysfs-Lesekontext | OEM-Board-/Service-Dump | **U** | +| CSD | sysfs-Knoten existiert, `cat` durch `shell:s0` abgewiesen | SELinux | erlaubter sysfs-Lesekontext | OEM-Board-/Service-Dump | **U** | +| Hersteller/Modell/Serienkennung | `manfid`, `oemid`, `name`, `serial` und weitere Attribute wurden angefragt | AVC-Denials beziehungsweise `Permission denied` | erlaubter sysfs-Lesekontext | OEM-Serviceausgabe; alternativ offizielle Firmware-/BOM-Unterlage | **U** | +| Rohblock `/dev/block/mmcblk0` | Node ist `brw------- root root`; `blockdev --getsize64` scheiterte | Unix-DAC und voraussichtlich SELinux-Blockdevice-Policy | Root-DAC **und** erlaubte SELinux-Domain; nur lesender Blockzugriff | OEM-Werkstatttool; kein allgemeiner Root-/Exploitversuch | **E1** für Blockade, **U** für Rohinhalt | +| EXT4-Fehlerzähler für `/data` | `/sys/fs/ext4/mmcblk0p37/{errors_count,first_error_time,last_error_time,lifetime_write_kbytes,session_write_kbytes}` sind bekannt | alle Leseversuche `Permission denied` | erlaubter sysfs-/ext4-Diagnosekontext | privilegierter, signierter Read-only-Dump | **U** | +| Kernel-Ringpuffer | Bugreport-Sektion: `Unexpected klogctl return value: -1`; direktes `dmesg` war nicht erlaubt | Kernel-/SELinux-Loggingrechte | Dumpstate-/Kernelkontext mit zulässigem `klogctl`, typischerweise zusätzlich entsprechende Capability/Policy | OEM-Bugreport/Engineering-Dump | **U** für nicht sichtbare Kernelereignisse | +| PStore/Ramoops/Last-Kmsg | `/proc/last_kmsg` fehlt; Incidentd wird an `pstorefs` abgewiesen und kann `console-ramoops*` nicht öffnen | fehlender Pfad und SELinux-`pstorefs`-Denial | erlaubter PStore-Lesekontext, sofern Daten existieren | autorisierter Crash-/Board-Dump | **U** | +| OEM-Board-Dump | `No IDumpstateDevice implementation`; keine `dumpstate_board.bin/.txt` | HAL nicht im VINTF-Manifest implementiert | passende OEM-`IDumpstateDevice`-Implementierung | zukünftige offizielle Philips-Firmware oder Hersteller-Servicebuild | **U** | +| Implementierung `emmclifereader` | Init-RC, Root-Start, Dateilabel und Ausgabe sind bekannt | Shell darf an `tpvdebug_exec:s0` weder `getattr` noch `read` | autorisierte Systemimage-/Quellcodeanalyse oder erlaubter Servicekontext | offizielle Firmwaredatei/TPV-Servicepaket lokal analysieren | **E1** für äußere Kette, **U** für internen Codepfad | + +## Beweiskette für `0x0a 0x0a` + +Die gesicherten Artefakte bilden folgende Kette: + +```text +init: service emmclifereaderd /system/bin/emmclifereader + oneshot, user root, group system + | + | Boot-Log: eMMCLifeReader: eMMC life time 0x0a 0x0a + v +persist.odm.tpv.sys.emmc_lifetime = 0x0a 0x0a + | + +--> OnBootLogger: dev_lt_a="a", dev_lt_b="a" + | + +--> TVStorageMonitorService: + propvalue = 0x0a 0x0a + EmmcLifetime 90%-100% device lifetime used +``` + +Direkt belegt sind Init-Definition, Root-Anforderung, Boot-Ausgabe, persistente +Property und beide Philips-Verbraucher. Der Dienst ist beim Bugreport bereits +`stopped`, passend zu `oneshot`; `ro.boottime.emmclifereaderd` ist gesetzt. +Das ausführbare Objekt trägt das Label `u:object_r:tpvdebug_exec:s0`. + +Nicht direkt belegt sind: + +- die Laufzeit-SELinux-Domain des sehr kurz laufenden Readers; +- welcher seiner möglichen Kernelpfade tatsächlich geöffnet wurde; +- die im Moment des Bugreports vorhandenen Rohbytes 268/269. + +Die Formulierung „Philips hat beim Boot A/B als `0x0a/0x0a` ermittelt“ ist +daher E1. Die engere Formulierung „der Reader hat genau +`/sys/kernel/debug/mmc0/mmc0:0001/ext_csd` geöffnet und daraus unmittelbar die +Bytes 268/269 gelesen“ bleibt E2. Dass der persistente Wert bloß eine +Belegungsanzeige sei, ist durch die getrennte A/B-Telemetrie und die Philips- +Zuordnung widerlegt. + +## Unix- und SELinux-Barrieren + +Die Befunde zeigen vier getrennte Zugriffsebenen: + +1. **Unix-DAC:** Der rohe Blocknode ist nur für `root:root` lesbar. Eine + normale ADB-Shell scheitert bereits hier. +2. **SELinux Type Enforcement:** UID root hebt die Policy nicht auf. + `storaged` läuft als root, aber in `u:r:storaged:s0`; der Zielknoten ist + `u:object_r:debugfs_mmc:s0`, und `{ read }` wird verweigert. +3. **Domain-spezifische Debugfs-Rechte:** `dumpstate:s0` darf einzelne generisch + gelabelte Binder-/DMA-Buf-Debugfs-Dateien lesen. Daraus folgt nicht, dass es + auch `debugfs_mmc:s0` lesen darf. Die getrennten Zieltypen sind gerade der + Zweck der Policy. +4. **fehlende Exports/Schnittstellen:** Selbst mit einem geeigneten Prozess + muss eine Diagnosefunktion den Wert ausgeben. Der Standard-Bugreport hat + keine OEM-`IDumpstateDevice`-Implementierung und exportiert deshalb keinen + Board-Health-Dump. + +Weitere direkt beobachtete SELinux-Beispiele stützen diese Einordnung: + +- `shell:s0 -> sysfs:s0`: `life_time`, `pre_eol_info`, CID, CSD, `manfid`, + `oemid` und `serial` werden beim Lesen abgewiesen; +- `shell:s0 -> debugfs_mmc:s0`: schon `read/search` am Verzeichnis `mmc0` + wird abgewiesen; +- `shell:s0 -> tpvdebug_exec:s0`: `getattr` und `read` am Reader-Binary werden + abgewiesen; +- `incidentd:s0 -> pstorefs:s0`: Verzeichnissuche wird abgewiesen; +- `tpvdebug:s0 -> proc_uid_io_stats:s0`: TPVs root-Prozess + `storagedcapture` erhält wiederholt `getattr`-Denials. + +Damit wäre die Aussage „Root genügt“ technisch unpräzise. Erforderlich ist +eine Kombination aus passender Unix-UID/GID beziehungsweise Capability, +einer von SELinux zugelassenen Domain, zugänglichen Zieltypen und einer +read-only implementierten Ausgabefunktion. + +## Privilegierte und speichernahe Prozesse + +| Prozess/Dienst | beobachtete Unix-Identität | SELinux-Domain | Speicherbezug im Artefakt | Forensische Grenze | +|---|---|---|---|---| +| `emmclifereaderd` | Init fordert `root`, Gruppe `system`; beim Bugreport beendet | Laufzeitdomain nicht beobachtet; Binary `tpvdebug_exec:s0` | erzeugt direkt beobachtbar die Boot-Logausgabe; steht zeitlich vor der gleichlautenden Property | Property-Schreibcode, Binary und exakter Quellpfad gesperrt; kein PRE_EOL-Export | +| `storaged` PID 2644 | `root` | `u:r:storaged:s0` | publiziert eMMC 5.1, EOL 1 und A/B 10/10; versucht zusätzlich `/sys/kernel/debug/mmc0/mmc0:0001/ext_csd` zu lesen | Eventwerte **E1**; zusätzlicher Debugfs-Rohread durch AVC blockiert | +| `storagedcapture` PID 2613 | `root` | `u:r:tpvdebug:s0` | TPV-Prozess, liest periodisch Storage-/UID-I/O-Daten und schreibt laut Log eigene Capturedateien | sogar `/proc/uid_io/stats` ist per SELinux gesperrt; keine EXT-CSD-Ausgabe im Bugreport | +| `vold` PID 1442 | `root` | `u:r:vold:s0` | Mount-, fscrypt- und Volumeverwaltung; e2fsck des adoptierten `dm-4` | kein eMMC-Health-Export; `Can't find service: vold` im Dumpstate-Log betrifft den Dumpsys-Lookup, nicht den laufenden Daemon | +| `installd` PID 2630 | `root` | `u:r:installd:s0` | erkennt Quota-Mounts auf `userdata`, `user_setting` und adoptiertem Volume | Paket-/Quota-Verwaltung, kein roher MMC-/EXT4-Health-Dump | +| `system_server` PID 3007 | `system` (UID 1000) | `u:r:system_server:s0` | `StorageManagerService`, `DeviceStorageMonitor` und Paket-/Quota-Koordination | liefert logische Speicherzustände, keine EXT-CSD-/PRE_EOL-Rohwerte | +| `TVStorageMonitorService` PID 5636 | `system` (UID 1000) | `u:r:system_app:s0` | liest die TPV-Property etwa alle 30 Sekunden und klassifiziert Life Time B | Property-Verbraucher, kein unabhängiger Hardwareleser | +| `OnBootLogger`/`candeebug` PID 4186 | `system` (UID 1000) | `u:r:system_app:s0` | teilt die Property in `dev_lt_a` und `dev_lt_b` und protokolliert Dateisystemtelemetrie | bestätigt A/B semantisch, aber ebenfalls nur abgeleiteter Wert | +| Standard-`dumpstate` PID 21293 | `shell` | `u:r:dumpstate:s0` | sammelt den Bugreport und darf ausgewählte Debugfs-Typen lesen | Kernel-Log scheitert; kein `IDumpstateDevice`; kein MMC-spezifischer Board-Dump | +| TPV `ddg` PID 2609 | `root` | `u:r:dumpstate:s0` | laufender Debug-Dump-Daemon mit Thread `uart-dumper`; Log meldet nur `ddg alive` | keine eMMC-Healthdaten oder dokumentierte sichere Schnittstelle im Artefakt | +| TPV `tpv_top_service` PID 2610 | `root` | `u:r:dumpstate:s0` | TPV-Diagnoseprozess | keine eMMC-/EXT4-Ausgabe im Bugreport | +| TPV `errlogservice` PID 2578 | `system` | `u:r:hal_tv_errlogservice_default:s0` | möglicher OEM-Fehlerlogdienst | `lshal debug` endet mit `FAILED_TRANSACTION`; Inhalt nicht verfügbar | + +Die Existenz von Root-Diagnosedaemons ist kein Beleg dafür, dass deren +Schnittstellen den gesuchten Wert exportieren oder gefahrlos interaktiv +aufrufbar sind. Sie wurden in dieser Untersuchung nicht angesprochen. + +## Prüfung sämtlicher Storage-Fehlertreffer + +Die Kategorie `storage_errors` enthält genau vier Zeilen: + +| Treffer | tatsächlicher Kontext | MMC-/EXT4-Relevanz | +|---|---|---| +| `resolv: SSL_connect write error ... Try again` | DNS-/TLS-Netzwerkpfad | keine | +| `NativeCrypto: ... I/O error ... Connection reset by peer` | TLS-Socket wurde vom Gegenüber zurückgesetzt | keine | +| `NativeCrypto: SSL shutdown ... I/O error ... Broken pipe` | TLS-Socket beim Shutdown geschlossen | keine | +| `incidentd: Buffer corrupted: expect 145 bytes, read 148 bytes` | Serialisierung beziehungsweise interner Incident-Puffer | keine belegte Blockgeräte- oder Dateisystemzuordnung | + +Zusätzliche exakte Volltextprüfungen ergaben keinen Treffer für: + +- `EXT4-fs` mit Error/Warning/Abort/Corruption; +- `Buffer I/O error`, `blk_update_request`, Block-I/O-/End-Request-Fehler; +- MMC-Timeout, MMC-CRC/ECC, MMC-Reset oder MMC-Command-Error; +- JBD2-/Journalfehler oder Journal-Abbruch; +- Remount eines Dateisystems als read-only; +- SQLite `disk I/O error`, `database disk image is malformed`; +- `ENOSPC` beziehungsweise `No space left on device`. + +Die sechs Update-Engine-Logs enden jeweils mit +`ErrorCode::kSuccess` und `Processing Done`. Die davor erscheinende Meldung +über nicht aktivierbare Binder-Oneway-Spam-Erkennung (`Invalid argument`) ist +kein OTA-Schreibfehler. + +`vold` führte `e2fsck` auf `/dev/block/dm-4` aus und meldete dieses **adoptierte +USB-Volume** als `clean`. Das ist kein Check von `/data` auf `mmcblk0p37` und +darf nicht als interner eMMC-/EXT4-Gesundheitsbeleg verwendet werden. + +Ein separat gesicherter älterer Bootlog enthält einmal +`Icing: Array storage bad crc ...`. Das ist ein beschädigter +Google-App-Suchindexdatensatz. Der Ausdruck `crc` stammt hier aus der +Anwendungsdatenstruktur, nicht aus dem MMC-Controller. Ohne begleitenden +Block-I/O-, EXT4- oder MMC-Fehler bleiben Softwareabbruch, unvollständiger +Schreibvorgang, Anwendungsfehler und Medienfehler ununterscheidbar. Auch der +vorhandene Google-Icing-Tombstone zeigt einen +`Check failed: is_initialized()`-Abort, keinen Blockgerätefehler. + +## Warum der Nichtfund nur E3 ist + +Vier besonders wichtige Quellen fehlen: + +1. Die Kernel-Logsektion ist leer (`klogctl` Rückgabewert `-1`). +2. `/proc/last_kmsg` existiert nicht; PStore/Ramoops konnte von Incidentd nicht + gelesen werden. +3. Die EXT4-Zähler von `mmcblk0p37` sind für Shell gesperrt. +4. Mangels `IDumpstateDevice` gibt es keinen OEM-Board-Dump; mehrere TPV-/MTK- + HIDL-Debugaufrufe scheitern mit `FAILED_TRANSACTION`. + +Deshalb sind folgende Aussagen **nicht** zulässig: + +- „Die eMMC ist gesund.“ +- „Es gab nie MMC-/EXT4-Fehler.“ +- „Der normale PRE_EOL-Wert beweise insgesamt eine gesunde eMMC.“ +- „Ein Ausfall ist ausgeschlossen.“ + +Für die Fehlerhistorie zulässig ist nur: Im verfügbaren Android-/Bugreport- +Textfenster ist kein Speichergeräte- oder Dateisystemfehler nachweisbar. Der +separat bestätigte PRE_EOL-Wert `0x01` bewertet nur die Reserveblockachse. + +## Forensische Gesamtbewertung + +| Aussage | Bewertung | +|---|---| +| Philips hat beim erfassten Boot beide Life-Time-Felder als `0x0a` gemeldet | **E1 – direkt belegt** | +| Philips versteht A und B als getrennte eMMC-Life-Time-Felder | **E1 – direkt belegt** durch `dev_lt_a`/`dev_lt_b` und DEX-/Laufzeitbefund | +| Beide `0x0a` entsprechen dem Band 90–100 % geschätzter Lebensdauer verbraucht | **E1** für Philips' eigene B-Zuordnung; zusätzlich durch die vorhandene JEDEC-/Herstelleranalyse gestützt | +| Der Reader liest die Werte aus dem eMMC-EXT_CSD | **E2 – sehr wahrscheinlich**, aber nicht durch Binary oder Rohdump geschlossen | +| Der Reader verwendet genau den beobachteten Debugfs-Pfad | **E2**, nicht direkt beobachtet | +| `PRE_EOL_INFO` ist normal/warning/urgent | **E1: `0x01` (Normal)** über Storaged-Event und lokale Felddefinition; direkter Rohread bleibt **U** | +| CID, Hersteller und konkretes eMMC-Modell | **U** | +| Es gibt aktuell sichtbare MMC-/EXT4-/Block-I/O-Fehler | **E3-Nichtfund** im verfügbaren Fenster | +| Es gab historisch keine solchen Fehler | **U**, weil Kernel-/PStore-/EXT4-Quellen fehlen | +| Die eMMC fällt unmittelbar aus | **nicht belegt** | +| Die eMMC weist einen ernstzunehmenden Verschleißindikator auf | **E1**, da A und B im höchsten regulären 10-%-Band stehen | + +## Legitime nächste Beweisquelle + +Der technisch präziseste nächste Schritt wäre kein allgemeiner Root-Zugang, +sondern ein **autorisierter Philips/TPV-Diagnosekontext**, der ausschließlich +lesend folgende Werte exportiert: + +1. EXT-CSD-Bytes 267, 268 und 269 samt vollständigem Rohdump; +2. CID/CSD und Herstellerkennung; +3. `/sys/fs/ext4/mmcblk0p37/errors_count`, `first_error_time`, + `last_error_time` und `lifetime_write_kbytes`; +4. Kernel-/PStore-Zeilen zu `mmc0`, `mmcblk0`, `ext4` und `jbd2`; +5. Zeitstempel und Rückgabestatus des Readers, damit ein persistenter Altwert + sicher ausgeschlossen wird. + +Legitime Wege wären ein signierter Engineering-/Servicebuild, ein offizielles +TPV-Werkstatttool oder eine zukünftige Firmware mit korrekt implementiertem +`IDumpstateDevice`. Auch eine lokale statische Analyse einer **offiziell +bezogenen** passenden Firmware kann die Reader-Implementierung schließen, +ohne das Gerät zu verändern. Nicht erforderlich und nicht angemessen sind +Root-Exploit, Bootloader-Entsperrung, Recovery, Flashen oder das manuelle +Starten privilegierter TPV-Dienste. + +## Reproduzierbarkeit und Sicherheitsgrenze + +Die entscheidenden lokalen Rohfundstellen wurden gegen eMMC-, Init-, Bootlog- +und Bugreport-Ausgaben geprüft. Diese Rohdateien sind aus Datenschutzgründen +nicht veröffentlicht. Enthalten bleibt die +[Bugreport-Gesamtanalyse](../../bugreport-analysis.md). + +Diese Vertiefung beruht ausschließlich auf bereits vorhandenen lokalen +Artefakten. Es wurden keine neuen Geräteabfragen, keine Prozessaufrufe am TV, +keine Einstellungsänderungen und keine Speicherzugriffe außerhalb der bereits +gesicherten Daten vorgenommen. diff --git a/diagnostics/deep-forensics/philips/dex_disasm.py b/diagnostics/deep-forensics/philips/dex_disasm.py new file mode 100644 index 0000000..07a0f90 --- /dev/null +++ b/diagnostics/deep-forensics/philips/dex_disasm.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Small Dalvik disassembler for a selected locally defined method. + +It builds on dex_probe.py and is intentionally limited to textual forensic +inspection. It does not execute or modify the APK. +""" + +from __future__ import annotations + +import argparse +import re +import struct +import zipfile +from pathlib import Path + +from dex_probe import Dex, iter_instructions, u16, u32 + + +NAMES = { + 0x00: "nop", 0x01: "move", 0x02: "move/from16", 0x03: "move/16", + 0x04: "move-wide", 0x05: "move-wide/from16", 0x06: "move-wide/16", + 0x07: "move-object", 0x08: "move-object/from16", 0x09: "move-object/16", + 0x0A: "move-result", 0x0B: "move-result-wide", 0x0C: "move-result-object", + 0x0D: "move-exception", 0x0E: "return-void", 0x0F: "return", + 0x10: "return-wide", 0x11: "return-object", 0x12: "const/4", + 0x13: "const/16", 0x14: "const", 0x15: "const/high16", + 0x16: "const-wide/16", 0x17: "const-wide/32", 0x18: "const-wide", + 0x19: "const-wide/high16", 0x1A: "const-string", 0x1B: "const-string/jumbo", + 0x1C: "const-class", 0x1D: "monitor-enter", 0x1E: "monitor-exit", + 0x1F: "check-cast", 0x20: "instance-of", 0x21: "array-length", + 0x22: "new-instance", 0x23: "new-array", 0x24: "filled-new-array", + 0x25: "filled-new-array/range", 0x26: "fill-array-data", 0x27: "throw", + 0x28: "goto", 0x29: "goto/16", 0x2A: "goto/32", + 0x2B: "packed-switch", 0x2C: "sparse-switch", + 0x2D: "cmpl-float", 0x2E: "cmpg-float", 0x2F: "cmpl-double", + 0x30: "cmpg-double", 0x31: "cmp-long", 0x32: "if-eq", 0x33: "if-ne", + 0x34: "if-lt", 0x35: "if-ge", 0x36: "if-gt", 0x37: "if-le", + 0x38: "if-eqz", 0x39: "if-nez", 0x3A: "if-ltz", 0x3B: "if-gez", + 0x3C: "if-gtz", 0x3D: "if-lez", +} + +for i, name in enumerate(("aget", "aget-wide", "aget-object", "aget-boolean", "aget-byte", "aget-char", "aget-short", + "aput", "aput-wide", "aput-object", "aput-boolean", "aput-byte", "aput-char", "aput-short"), 0x44): + NAMES[i] = name +for i, name in enumerate(("iget", "iget-wide", "iget-object", "iget-boolean", "iget-byte", "iget-char", "iget-short", + "iput", "iput-wide", "iput-object", "iput-boolean", "iput-byte", "iput-char", "iput-short"), 0x52): + NAMES[i] = name +for i, name in enumerate(("sget", "sget-wide", "sget-object", "sget-boolean", "sget-byte", "sget-char", "sget-short", + "sput", "sput-wide", "sput-object", "sput-boolean", "sput-byte", "sput-char", "sput-short"), 0x60): + NAMES[i] = name +for i, name in enumerate(("invoke-virtual", "invoke-super", "invoke-direct", "invoke-static", "invoke-interface"), 0x6E): + NAMES[i] = name +for i, name in enumerate(("invoke-virtual/range", "invoke-super/range", "invoke-direct/range", "invoke-static/range", "invoke-interface/range"), 0x74): + NAMES[i] = name + +BIN = [ + "add-int", "sub-int", "mul-int", "div-int", "rem-int", "and-int", "or-int", "xor-int", "shl-int", "shr-int", "ushr-int", + "add-long", "sub-long", "mul-long", "div-long", "rem-long", "and-long", "or-long", "xor-long", "shl-long", "shr-long", "ushr-long", + "add-float", "sub-float", "mul-float", "div-float", "rem-float", "add-double", "sub-double", "mul-double", "div-double", "rem-double", +] +for i, name in enumerate(BIN, 0x90): + NAMES[i] = name + +ADDR = [ + "add-int/2addr", "sub-int/2addr", "mul-int/2addr", "div-int/2addr", "rem-int/2addr", "and-int/2addr", "or-int/2addr", "xor-int/2addr", "shl-int/2addr", "shr-int/2addr", "ushr-int/2addr", + "add-long/2addr", "sub-long/2addr", "mul-long/2addr", "div-long/2addr", "rem-long/2addr", "and-long/2addr", "or-long/2addr", "xor-long/2addr", "shl-long/2addr", "shr-long/2addr", "ushr-long/2addr", + "add-float/2addr", "sub-float/2addr", "mul-float/2addr", "div-float/2addr", "rem-float/2addr", "add-double/2addr", "sub-double/2addr", "mul-double/2addr", "div-double/2addr", "rem-double/2addr", +] +for i, name in enumerate(ADDR, 0xB0): + NAMES[i] = name + +for i, name in enumerate(("add-int/lit16", "rsub-int", "mul-int/lit16", "div-int/lit16", "rem-int/lit16", "and-int/lit16", "or-int/lit16", "xor-int/lit16"), 0xD0): + NAMES[i] = name +for i, name in enumerate(("add-int/lit8", "rsub-int/lit8", "mul-int/lit8", "div-int/lit8", "rem-int/lit8", "and-int/lit8", "or-int/lit8", "xor-int/lit8", "shl-int/lit8", "shr-int/lit8", "ushr-int/lit8"), 0xD8): + NAMES[i] = name + + +def s8(v: int) -> int: + return struct.unpack("b", bytes([v & 0xFF]))[0] + + +def s16(v: int) -> int: + return struct.unpack(" int: + return struct.unpack(" tuple[int, int]: + return (unit >> 8) & 0xF, (unit >> 12) & 0xF + + +def fmt(d: Dex, pc: int, op: int, u: list[int]) -> str: + name = NAMES.get(op, f"op_{op:02x}") + a4, b4 = ab(u[0]) + aa = u[0] >> 8 + + if op in (0x01, 0x04, 0x07) or 0xB0 <= op <= 0xCF: + return f"{name} v{a4}, v{b4}" + if op in (0x02, 0x05, 0x08): + return f"{name} v{aa}, v{u[1]}" + if op in (0x03, 0x06, 0x09): + return f"{name} v{u[1]}, v{u[2]}" + if op in (0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x1D, 0x1E, 0x27): + return f"{name} v{aa}" + if op in (0x0E,): + return name + if op == 0x12: + return f"{name} v{a4}, #{s8(b4 if b4 < 8 else b4 - 16)}" + if op in (0x13, 0x16): + return f"{name} v{aa}, #{s16(u[1])}" + if op in (0x14, 0x17): + return f"{name} v{aa}, #{s32(u[1] | u[2] << 16)}" + if op in (0x15, 0x19): + return f"{name} v{aa}, #0x{u[1]:04x}0000" + if op == 0x18: + val = u[1] | u[2] << 16 | u[3] << 32 | u[4] << 48 + return f"{name} v{aa}, #0x{val:016x}" + if op == 0x1A: + return f"{name} v{aa}, {d.strings[u[1]]!r}" + if op == 0x1B: + idx = u[1] | u[2] << 16 + return f"{name} v{aa}, {d.strings[idx]!r}" + if op in (0x1C, 0x1F, 0x22): + return f"{name} v{aa}, {d.types[u[1]]}" + if op == 0x20: + return f"{name} v{a4}, v{b4}, {d.types[u[1]]}" + if op == 0x21: + return f"{name} v{a4}, v{b4}" + if op == 0x23: + return f"{name} v{a4}, v{b4}, {d.types[u[1]]}" + if op in (0x24, 0x6E, 0x6F, 0x70, 0x71, 0x72): + count = b4 + g = a4 + regs = [u[2] & 0xF, (u[2] >> 4) & 0xF, (u[2] >> 8) & 0xF, (u[2] >> 12) & 0xF, g][:count] + target = d.types[u[1]] if op == 0x24 else d.methods[u[1]] + return f"{name} {{{', '.join('v'+str(x) for x in regs)}}}, {target}" + if op in (0x25, 0x74, 0x75, 0x76, 0x77, 0x78): + count = aa + regs = list(range(u[2], u[2] + count)) + target = d.types[u[1]] if op == 0x25 else d.methods[u[1]] + return f"{name} {{{', '.join('v'+str(x) for x in regs)}}}, {target}" + if op in (0x26, 0x2B, 0x2C): + off = s32(u[1] | u[2] << 16) + return f"{name} v{aa}, :{pc + off:04x}" + if op == 0x28: + return f"{name} :{pc + s8(aa):04x}" + if op == 0x29: + return f"{name} :{pc + s16(u[1]):04x}" + if op == 0x2A: + return f"{name} :{pc + s32(u[1] | u[2] << 16):04x}" + if 0x2D <= op <= 0x31 or 0x44 <= op <= 0x51 or 0x90 <= op <= 0xAF: + return f"{name} v{aa}, v{u[1] & 0xFF}, v{u[1] >> 8}" + if 0x32 <= op <= 0x37: + return f"{name} v{a4}, v{b4}, :{pc + s16(u[1]):04x}" + if 0x38 <= op <= 0x3D: + return f"{name} v{aa}, :{pc + s16(u[1]):04x}" + if 0x52 <= op <= 0x5F: + return f"{name} v{a4}, v{b4}, {d.fields[u[1]]}" + if 0x60 <= op <= 0x6D: + return f"{name} v{aa}, {d.fields[u[1]]}" + if 0xD0 <= op <= 0xD7: + return f"{name} v{a4}, v{b4}, #{s16(u[1])}" + if 0xD8 <= op <= 0xE2: + return f"{name} v{aa}, v{u[1] & 0xFF}, #{s8(u[1] >> 8)}" + return name + " " + " ".join(f"0x{x:04x}" for x in u) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("apk", type=Path) + ap.add_argument("--method", required=True, help="regex matched against full DEX method name") + args = ap.parse_args() + rx = re.compile(args.method, re.I) + with zipfile.ZipFile(args.apk) as zf: + for name in sorted(n for n in zf.namelist() if re.fullmatch(r"classes\d*\.dex", n)): + d = Dex(zf.read(name), f"{args.apk}!/{name}") + for em in d.encoded_methods: + method = d.methods[em.method_idx] + if not rx.search(method): + continue + print(f"\n### {d.source}\n{method}") + print(f"registers={u16(d.data, em.code_off)} ins={u16(d.data, em.code_off+2)} outs={u16(d.data, em.code_off+4)}") + for pc, op, units in iter_instructions(d.data, em.code_off): + print(f"{pc:04x}: {fmt(d, pc, op, units)}") + + +if __name__ == "__main__": + main() diff --git a/diagnostics/deep-forensics/philips/epg-reverse-agent.md b/diagnostics/deep-forensics/philips/epg-reverse-agent.md new file mode 100644 index 0000000..bebeb2e --- /dev/null +++ b/diagnostics/deep-forensics/philips/epg-reverse-agent.md @@ -0,0 +1,363 @@ +# Statische Reverse-Analyse von `EpgDatabase.db` + +Stand: 2026-08-25 +Zielplattform: Philips TPM191E, Firmware `TPM191E_R.211.000.103.000`, Android TV 12 +Hauptartefakt: `diagnostics/raw/storage/channels.apk` + +## Umfang und Beweisgrade + +Diese Untersuchung war ausschließlich lokal und lesend. Es wurden keine ADB-Gerätebefehle und keine Reparatur-, Lösch-, Reset- oder Rebuild-Aktion auf dem Fernseher ausgeführt. Untersucht wurden die vorhandenen APK-/DEX-Artefakte, deren binäres Manifest und Ressourcen sowie bereits zuvor abgelegte Diagnoseausgaben. + +Die Kennzeichnungen bedeuten: + +- **BEWIESEN**: direkt aus Manifest, DEX-Bytecode, SQL-Literal oder vorhandener Rohdiagnose ableitbar. +- **SEHR WAHRSCHEINLICH**: mehrere direkte Belege ergeben dieselbe Erklärung; eine verbleibende Laufzeitunsicherheit besteht. +- **PLAUSIBEL**: technisch gut passende Interpretation, deren Entwicklerabsicht oder Laufzeitwirkung nicht direkt belegt ist. +- **NICHT VERIFIZIERBAR**: mit den lokal vorhandenen Artefakten nicht entscheidbar. + +## Kurzfazit + +1. **BEWIESEN:** `org.droidtv.channels` verwaltet `EpgDatabase.db` über `org.droidtv.channels.epg.bcepg.EpgDatabaseHelper`, eine Unterklasse von `android.database.sqlite.SQLiteOpenHelper`. Der Konstruktor übergibt den Datenbanknamen `EpgDatabase.db` und die Schemaversion **6**. +2. **BEWIESEN:** Die vorgesehene zeitbasierte Stale-Cleanup-Kette endet in `EpgProvider$deleteStaleDataRunnable.run()`. Dieses Runnable berechnet zwei Grenzwerte und protokolliert sie, führt aber **keinerlei Löschung** aus. Es gibt darin weder `SQLiteDatabase.delete/execSQL` noch `ContentResolver.delete/applyBatch` noch einen Aufruf der vorhandenen echten Zeit-Löschmethoden. +3. **BEWIESEN:** Der beim Provider-Start ausgeführte Logpfad wird nach 120.000 ms ausgelöst. Die vorhandenen Boot-Logs passen exakt zum Bytecode, beweisen aber keine gelöschte Zeile. +4. **BEWIESEN:** Die Rebuild-Pfade löschen Zeilen, komprimieren die SQLite-Datei jedoch nicht. In allen vier DEX-Dateien von `channels.apk` fehlen Literale beziehungsweise Referenzen für `VACUUM`, `PRAGMA auto_vacuum`, `incremental_vacuum` und `wal_checkpoint`. +5. **BEWIESEN:** Die bisher kleinen Zeilenzahlen betreffen nur `indextable`, `nowtable`, `nexttable` und `fastipnownexttable`; sie begrenzen weder `scheduletable` noch die beiden FVP-Tabellen. Eine lesende Abfrage des Join-Endpunkts `antenna/scheduletable` lief länger als 120 Sekunden und musste clientseitig abgebrochen werden. Aktive Schedule-/FVP-Daten können daher einen wesentlichen Teil der 1,62 GiB belegen. +6. **SEHR WAHRSCHEINLICH:** Der wirkungslose Stale-Cleanup ist ein konkreter Mechanismus, durch den alte Schedule-Daten anwachsen können. Ob die Dateigröße überwiegend aus lebenden Schedule-/FVP-Zeilen, freien Seiten oder interner Fragmentierung besteht, ist ohne `freelist_count`/`dbstat` beziehungsweise DB-Kopie offen. +7. **BEWIESEN:** Es gibt interne mutierende Rebuild-URIs, aber keine gefundene Benutzer-Activity, Service-Aktion oder UI-Zeichenkette „EPG zurücksetzen/löschen“. Das Umschalten der TV-Guide-Quelle ist ausdrücklich keine harmlose Reset-Funktion: Ressourcen warnen vor Verlust beziehungsweise Abbruch von Aufnahmen, Erinnerungen und programmierten Aufnahmen. + +## Analysierte Artefakte + +| Artefakt | SHA-256 | Relevanz | +|---|---|---| +| `diagnostics/raw/storage/channels.apk` | `3A44BC648624F0CC9CD9FEB4CFC3FD33F41323597718E4FCCFC25FE35504D34F` | EPG-Provider, Datenbankhelper, Guide-UI, Aufnahme-/Reminder-Verknüpfungen | +| `diagnostics/raw/tvstrings.apk` | `A25724A08369FBF1AE31884044816441C4ABCD9FC0F0116BCDB563FEC44AE923` | lokalisierte TV-Guide-Warntexte | +| `diagnostics/raw/TPTvService.apk` | `75F492EE89906C8C267D685D2B7D0CE02BB09F3A6B4CC8456F8B096B9E441ECD` | Quersuche nach externen Rebuild-/Cleanup-Aufrufern | +| `diagnostics/raw/eum.apk` | `3B27806AC60B946C47CCAC1BE7EDAD46938CAE10C43C3F2E1F3EF4F9D2D30B98` | Quersuche | +| `diagnostics/raw/storage/gel.apk` | `29A68E6B09700ED4CCDF312C0B99EEE0C3F67E329A537FE87ED4341A71D383D7` | Quersuche | +| `diagnostics/raw/tvwidgets2k15.apk` | `93876A5FDC637B304BD6EE988A7F7523D957A9B0135AEFD62E3C13A6AC06E496` | Quersuche | +| `diagnostics/raw/emmc/usagelogger.apk` | `7416D0F7017B56F2980A4F3042B49A4A6954D9FBACC7FF995509FB8BD9BF6A15` | Quersuche | + +`channels.apk` enthält vier DEX-Dateien: `classes.dex`, `classes2.dex`, `classes3.dex` und `classes4.dex`. Die hier relevante Implementierung liegt überwiegend in `channels.apk!/classes4.dex`. + +Für die reproduzierbare Extraktion wurden die lokalen, dependency-freien Hilfsprogramme `dex_probe.py`, `axml_dump.py` und `resource_strings.py` in diesem Verzeichnis verwendet. Sie dekompilieren nicht heuristisch, sondern lesen DEX-Index-/Klassendaten, Bytecode-Referenzen, binäres Android-Manifest und Resource-Stringpools. + +## Datenbankname, Helper und Version + +### Helper + +- **BEWIESEN:** Klasse `Lorg/droidtv/channels/epg/bcepg/EpgDatabaseHelper;` +- **BEWIESEN:** Superklasse `Landroid/database/sqlite/SQLiteOpenHelper;` +- **BEWIESEN:** Konstruktor `EpgDatabaseHelper.(android.content.Context)` lädt das Literal `EpgDatabase.db`, setzt die CursorFactory auf `null` und den Versionsparameter auf `6`, bevor `SQLiteOpenHelper.(Context,String,CursorFactory,int)` aufgerufen wird. +- **BEWIESEN:** `onCreate(SQLiteDatabase)` ruft ausschließlich `createTables(SQLiteDatabase)` auf. +- **BEWIESEN:** In der Helper-Klasse gibt es keine Overrides von `onConfigure()` oder `onOpen()`. + +### Schema v6 + +`EpgDatabaseHelper.createTables(SQLiteDatabase)` enthält folgende physischen Tabellen: + +| Tabelle | Exakte Spalten aus dem DDL | +|---|---| +| `mappingtable` | `mappingid INTEGER PRIMARY KEY AUTOINCREMENT`, `presetid INTEGER`, `svlid INTEGER`, `svlrecid INTEGER` | +| `indextable` | `dbitindex INTEGER PRIMARY KEY AUTOINCREMENT`, `satnum`, `sid`, `tsid`, `onid`, `pfshd`, `sdtguidanceinfo TEXT`, `dpsid`, `dptsid`, `pkgid`, `presetnumber`, `presetid`, `favoriteNumber`, `channelname TEXT`, `logoid`, `svlid`, `svlrecid`, `browsable` (nicht als TEXT markierte Felder jeweils `INTEGER`) | +| `nowtable` | `_id INTEGER PRIMARY KEY AUTOINCREMENT`, `eventid`, `presetid`, `eventname TEXT`, `version`, `starttime`, `endtime`, `shortinfo TEXT`, `extendedinfo TEXT`, `languageid`, `linktype`, `targetindex`, `genre`, `scrambled`, `rating`, `guidanceinfo TEXT` | +| `nexttable` | identisch zu `nowtable` | +| `scheduletable` | `_id INTEGER PRIMARY KEY AUTOINCREMENT`, `eventid`, `presetid`, `eventname TEXT`, `version`, `starttime`, `endtime`, `shortinfo TEXT`, `extendedinfo TEXT`, `languageid`, `genre`, `scrambled`, `rating`, `guidanceinfo TEXT` | +| `fvpscheduletable` | `channelid`, `onid`, `sid`, `eventid`, `crid`, Start-/Endzeit, Titel-/Synopsis-, Genre-/Rating-, Bild-/On-Demand-, Accessibility-, Delivery-, Video- und Expiry-Felder; zusammengesetzter Primärschlüssel `(channelid,starttime,endtime)` | +| `fvpnownexttable` | wie FVP-Schedule mit zusätzlichem `nownext`, `restartstatus` und FVP-Now/Next-Feldern; zusammengesetzter Primärschlüssel `(channelid,starttime,endtime)` | +| `channelmapversiontable` | `id INTEGER PRIMARY KEY`, `majorversion INTEGER`, `minorversion INTEGER`, `favouriteversion INTEGER` | +| `fastipnownexttable` | `_id INTEGER PRIMARY KEY AUTOINCREMENT`, `channelid`, `eventid`, `programId`, `nownext`, Start-/Endzeit, Titel-/Synopsis-, Genre-/Rating-, Audio-/Video-, `lastmodified`- und `expiretime`-Felder | + +Die vollständigen, ungekürzten DDL-Literale stehen in `epg-dex-context.txt`, insbesondere im Kontext von `EpgDatabaseHelper.createTables()`. + +Definierte Indizes: + +- `mappingindex(svlid,svlrecid)` +- `indexdbit(satnum,sid,tsid,onid)` +- `indexpresetid(presetid,presetnumber)` +- `indexnowendtime(endtime)`, `indexnowevent(presetid)` +- `indexnextendtime(endtime)`, `indexnextevent(presetid)` +- `indexschedendtime(endtime)`, `indexschedstarttime(starttime)`, `indexschedpresetid(presetid)`, `indexschedeventid(presetid,eventid)` +- `ixfvpschedpresetidstet(channelid,starttime,endtime)` +- `ixfvpnownext(channelid,starttime,endtime)` + +**BEWIESEN:** `nownexttable` ist trotz des URI-Pfads keine durch `createTables()` angelegte physische Tabelle. `EpgProvider.query()` bildet diesen Endpunkt über den Join + +`indextable AS Dbit LEFT JOIN nowtable AS Now USING(presetid) LEFT JOIN nexttable AS Next USING(presetid)` + +ab. Frühere Rohnotizen, die `nownexttable` als physische Tabelle bezeichnen, sind insofern zu präzisieren. + +## `onUpgrade`, `onDowngrade` und vermeintliche Drop-Funktionen + +### `onUpgrade(SQLiteDatabase,int,int)` + +**BEWIESEN:** Der Bytecode vergleicht den Parameter `newVersion` mit `4`, `5` und `6`: + +- Zielversion 4, 5 oder 6: nur `createTables(db)`; wegen `IF NOT EXISTS` eine additive/idempotente Schemaanlage. +- Jede andere Zielversion: `DROP TABLE IF EXISTS` für `indextable`, `nowtable`, `nexttable`, `scheduletable`, `channelmapversiontable` und `fastipnownexttable`, anschließend `createTables(db)`. + +Der Fallback droppt weder `mappingtable` noch `fvpscheduletable` noch `fvpnownexttable`. + +### `onDowngrade(SQLiteDatabase,int,int)` + +**BEWIESEN:** Die Methode protokolliert lediglich `onDowngrade allowed ` und nimmt keine Schemaänderung vor. + +### `resetInstance()` + +**BEWIESEN:** `EpgDatabaseHelper.resetInstance()` setzt nur das statische Singleton-Feld `mEpgDbHelper` auf `null`. Die Methode löscht weder Datei noch Tabellen oder Zeilen. + +### `dropTables()` und `dropIndexTable()` + +Die Namen sind irreführend: + +- **BEWIESEN:** `dropIndexTable()` führt innerhalb einer Transaktion `SQLiteDatabase.delete("indextable", null, null)` aus; die Tabelle selbst bleibt bestehen. +- **BEWIESEN:** `dropTables()` führt `SQLiteDatabase.delete(table,null,null)` für `indextable`, `nowtable`, `nexttable`, `scheduletable`, `fvpnownexttable`, `fvpscheduletable`, `channelmapversiontable` und `fastipnownexttable` aus. +- **BEWIESEN:** `mappingtable` wird von `dropTables()` nicht geleert. `syncOnChannelTableChange()` leert und rekonstruiert sie später separat aus der kanonischen Senderliste. +- **BEWIESEN:** Keine der beiden Methoden führt anschließend `VACUUM` oder eine andere Dateikompaktierung aus. + +## Stale-/Cleanup-Logik + +### Tatsächliche Aufrufketten + +Es existieren zwei Einstiege in dasselbe Runnable: + +1. **Provider-Start** + - `EpgProvider$1.run()` führt zunächst `dropIndexTable()`, `syncChannelTable()` und `syncOnChannelTableChange()` aus. + - Danach postet die Methode `mDeleteStaleDataRunnable` mit `Handler.postDelayed(..., 120000)`. + - Das Literal und die Logmeldung `Posting stale data deletion after 2 mins in onCreate` stimmen überein. + +2. **Alarm/Receiver-Pfad** + - Manifest-Receiver: `org.droidtv.channels.epg.bcepg.EpgStaleDataCleanupAlarmReceiver` + - Intent-Action: `org.droidtv.channels.epg.bcepg.EpgStaleDataCleanupAlarmReceiver.CLEAR_STALE_DATA` + - Data-Scheme: `epgStaleDataCleanup` + - `EpgDatabaseHelper.setEpgStaleDataCleanUpAlarm()` erstellt einen expliziten Pending-Broadcast und setzt per `AlarmManager.setExact()` einen RTC-Alarm auf `currentTimeMillis + 43.200.000 ms`, also 12 Stunden. + - `EpgStaleDataCleanupAlarmReceiver.onReceive()` ruft `EpgInfoSingleton.cleanEpgStaleData()` auf. + - `EpgInfoSingleton$8.run()` führt `ContentResolver.delete(content://org.droidtv.epgdvb/antenna/staledatacleanup,null,null)` aus. + - `EpgProvider.delete()` postet dafür `EpgProvider$deleteStaleDataRunnable`. + +**BEWIESEN:** In allen vier DEX-Dateien von `channels.apk` gibt es keinen Invoke-Verweis auf `EpgDatabaseHelper.setEpgStaleDataCleanUpAlarm()`. Der Receiver plant den Alarm nach Empfang nicht erneut. Damit ist die Methode in den lokal sichtbaren direkten Java-Aufrufpfaden verwaist. Ein reflektiver oder aus einem nicht vorhandenen Artefakt kommender Aufruf bleibt **NICHT VERIFIZIERBAR**. + +### Das Cleanup-Runnable löscht nichts + +`Lorg/droidtv/channels/epg/bcepg/EpgProvider$deleteStaleDataRunnable;->run()V` enthält im Uhr-definiert-Zweig exakt folgende relevante Operationen: + +1. `System.currentTimeMillis()` +2. Subtraktion des Wide-Literals `1800` +3. bei verfügbarem FreeView zusätzlich Subtraktion des Wide-Literals `604800` +4. Ausgabe des Ergebnisses über `Log.d("EpgProvider", "Deleting stale data for time " + threshold)` + +**BEWIESEN:** Danach folgt keine Datenoperation. Die vollständige Methodenreferenzliste enthält ausschließlich Clock-/FreeView-Abfragen, StringBuilder/Logging und im Uhr-nicht-definiert-Zweig `Handler.postDelayed()` mit `30000` ms. Sie enthält insbesondere nicht: + +- `SQLiteDatabase.delete`, `execSQL`, `rawQuery` oder eine andere SQLite-Mutation, +- `ContentResolver.delete`, `applyBatch`, `update` oder `insert`, +- `EpgProgramPump.deleteChannelProgrambytime()` oder `deleteScheduleProgramsByTime()`, +- eine native oder sonstige in dieser Methode sichtbare Löschfunktion. + +Damit ist der als Stale-Cleanup bezeichnete Uhr-definiert-Pfad in dieser Firmware funktional ein **No-op mit Logausgabe**. + +### Einheitenfehler ist zusätzlich sichtbar + +- **BEWIESEN:** Der Code verwendet `System.currentTimeMillis()`, subtrahiert die Literale aber direkt als Millisekunden. +- **PLAUSIBEL:** `1800` war als 1.800 Sekunden = 30 Minuten und `604800` als 604.800 Sekunden = 7 Tage gemeint; die fehlende Multiplikation mit 1.000 würde daraus 1,8 Sekunden beziehungsweise 10,08 Minuten machen. +- **BEWIESEN:** Der vorhandene Boot-Log bestätigt die tatsächliche Einheit: Logzeit `2026-08-25 11:11:48.657`, protokollierter Grenzwert `1787649106837` = lokal `2026-08-25 11:11:46.837`, also rund 1,82 Sekunden früher. +- Wegen der vollständig fehlenden Löschoperation ändert selbst dieser falsche Grenzwert derzeit keine Zeilen. + +### Vorhandene echte Zeit-Löschmethoden sind nicht verdrahtet + +Folgende Methoden könnten tatsächlich alte EPG-Zeilen entfernen: + +- `EpgProgramPump.deleteChannelProgrambytime(long)`: drei `ContentProviderOperation.newDelete()`-Operationen mit `endtime < ?`, danach `applyBatch("org.droidtv.epgdvb", ...)`; die drei `mContentUri`-Ziele sind Now, Next und Schedule. +- `EpgProgramPump.deleteScheduleProgramsByTime(long)`: `ContentResolver.delete()` für Schedule mit `endtime < ?`. +- `EpgSyncThread.deleteScheduleProgramsByTime(long)`: delegiert an die vorige Methode. + +**BEWIESEN:** Es gibt in den vier DEX-Dateien keinen direkten Invoke-Aufrufer für diese drei öffentlichen Methoden. Insbesondere ruft `deleteStaleDataRunnable.run()` keine davon auf. Ein externer/reflektiver Aufruf ist **NICHT VERIFIZIERBAR**, aber für die sichtbare Cleanup-Kette nicht vorhanden. + +Normale EPG-Synchronisation enthält dagegen selektive Lösch-/Update-Pfade wie `deleteProgramsInWindow()` und kanalbezogene Abgleiche. Das erklärt, warum aktuelle Tabellen klein sein können, obwohl der globale Zeit-Cleanup wirkungslos ist. + +## `VACUUM`, `auto_vacuum` und Dateigröße + +- **BEWIESEN:** Quersuche über alle vier `channels.apk`-DEX-Dateien fand kein Literal beziehungsweise keine Referenz für `VACUUM`, `PRAGMA`, `auto_vacuum`, `incremental_vacuum` oder `wal_checkpoint`. +- **BEWIESEN:** `dropTables()` und beide Rebuild-Runnables löschen Zeilen, rufen aber selbst weder `VACUUM` noch ein Auto-Vacuum-PRAGMA auf. +- **BEWIESEN, aber nicht gerätespezifisch:** Die AOSP-libsqlite-Buildkonfiguration ist mindestens im Android-11-Branch mit `-DSQLITE_DEFAULT_AUTOVACUUM=1` dokumentiert, also Default `FULL`. Der Android-12-Quellbaum besitzt dieselbe Buildstruktur. Dieser Primärquellenhinweis stammt aus der parallelen Plattformanalyse, nicht aus dem Philips-APK. +- **SEHR WAHRSCHEINLICH:** Falls TP Vision die AOSP-Option unverändert übernommen und die DB sie bei Anlage geerbt hat, kann SQLite freie Endseiten bei Commits automatisch zurückgeben. Die bloße Abwesenheit eines appseitigen `VACUUM` beweist deshalb **nicht**, dass ein großer Freelist-Anteil bestehen muss. +- **NICHT VERIFIZIERBAR:** Die konkrete Vendor-Buildoption sowie der tatsächliche Headerwert von `PRAGMA auto_vacuum`, `page_count`, `freelist_count`, Seitengröße und WAL-Zustand der laufenden Datenbank, da keine lokale Kopie von `EpgDatabase.db` vorliegt und diese Teilanalyse keine Gerätezriffe ausgeführt hat. + +Vorhandene lokale Rohdiagnose (`diagnostics/raw/storage/epg-content-evidence.txt`): + +| Objekt | aktuelle Zeilen | beobachtete IDs | +|---|---:|---:| +| `indextable` | 358 | `dbitindex` 1.267.806 bis 1.268.163 | +| `nowtable` | 195 | `_id` 4.431.681 bis 4.451.328 | +| `nexttable` | 162 | `_id` 4.026.589 bis 4.027.685 | +| Join-Endpunkt `nownexttable` | 358 | kein physischer AUTOINCREMENT-Schlüssel | +| `fastipnownexttable` | 0 | keine aktuellen Zeilen | + +Die Datenbankdatei wurde dort mit `1.742.053.376` Bytes (ca. 1,62 GiB) protokolliert. Diese Tabelle ist ausdrücklich unvollständig: `scheduletable`, `fvpscheduletable` und `fvpnownexttable` wurden damit nicht gezählt. + +- **BEWIESEN:** AUTOINCREMENT-Werte in Millionenhöhe bei nur wenigen hundert aktuellen Zeilen zeigen massiven historischen Insert/Delete-Churn. +- **BEWIESEN:** Daraus folgt nicht, dass Millionen Zeilen gleichzeitig noch vorhanden sind, aber ebenso wenig, dass die nicht gemessenen Schedule-/FVP-Tabellen klein sind. +- **SEHR WAHRSCHEINLICH:** Der defekte Stale-Cleanup kann insbesondere `scheduletable` über lange Zeit mit abgelaufenen Ereignissen anwachsen lassen. +- **NICHT VERIFIZIERBAR:** Der exakte Anteil lebender Schedule-/FVP-Daten, freier Seiten und interner Fragmentierung ohne belastbare Counts beziehungsweise `page_count`, `freelist_count`, `dbstat` oder eine DB-Kopie. + +Die frühere Aussage in `epg-content-evidence.txt`, der Boot-Log zeige eine erfolgte Löschung, wird durch die statische Bytecode-Analyse korrigiert: Er zeigt nur die Ausführung des No-op-Runnables. + +## Query-/URI-Semantik und Grenzen einer risikoarmen Zählung + +### Warum `antenna/scheduletable --projection _id` sehr lange läuft + +**BEWIESEN:** `content://org.droidtv.epgdvb/antenna/scheduletable` wird nicht direkt auf die physische Tabelle abgebildet. `EpgProvider.query()` ersetzt den Tabellenausdruck durch: + +`indextable AS Dbit LEFT JOIN scheduletable AS Schedule USING(presetid)` + +Für eine Client-Projektion `_id` ersetzt der Provider die Spalte ausdrücklich durch `Dbit.dbitindex AS _id`. Die Abfrage zählt oder limitiert damit nicht, sondern liefert eine Join-Zeile pro passendem Schedule-Datensatz. Eine Laufzeit von mehr als 120 Sekunden ist daher mit einer sehr großen Schedule-Menge vereinbar. Sie kann zusätzlich durch Join-/Provider-/SQLite-Latenz beeinflusst sein und ist allein noch kein exakter Rowcount. + +Die physische Tabelle ist auch über `antenna/scheduletablesync` erreichbar. Dieser Pfad vermeidet den Dbit-Join, besitzt aber ebenfalls keinen eingebauten Count- oder Limit-Modus. + +### Projektion und Selection + +- **BEWIESEN:** Für `nownexttable` und `scheduletable` baut der Provider Join-Ausdrücke und schreibt Projektionsnamen mit Unterstrich in qualifizierte Spalten um, zum Beispiel `Schedule_eventid` zu `Schedule.eventid AS Schedule_eventid`. +- **BEWIESEN:** Die Sonderprojektion `_id` wird immer zu `Dbit.dbitindex AS _id`. Das ist bei Join-Endpunkten beabsichtigt, bei `fvpscheduletable` aber ungültig, weil dort kein Alias `Dbit` existiert. +- **BEWIESEN:** `fvpscheduletable` und `fvpnownexttable` besitzen schemabedingt keine `_id`-Spalte; ihr Primärschlüssel ist `(channelid,starttime,endtime)`. +- **BEWIESEN:** Bestimmte Selection-Strings sind keine SQL-Where-Klauseln, sondern Provider-Kommandos: `GetNowNext`, `GetNowNextNoHidden`, `GetSchedule` und `getFastIpNowNextForChannel`. Bei `GetSchedule` setzt der Provider selbst die Bedingung `Schedule_presetid = ? AND Schedule_endtime > ? AND Schedule_starttime < ?` und sortiert nach `Schedule_starttime`. +- **BEWIESEN:** Für die Join-Endpunkte tokenisiert der Provider eine normale Selection an Whitespace und ersetzt Unterstriche in Tokens durch Punkte. Komplexe Ausdrücke sind daher nicht verlässlich transparent. +- **BEWIESEN:** Zwei Distinct-Temp-XML-AIT-Endpunkte bauen SQL sogar per Stringverkettung auf. Sie sind für die Tabellenzählung nicht relevant und keine empfehlenswerte Diagnosefläche. + +### Sortierung und Limit + +- **BEWIESEN:** Der fünfte Legacy-Query-Parameter `sortOrder` wird zu Beginn nur auf den Sonderwert `PF_EVENT` geprüft. Dieser Wert löst eine EPG-Sync-Notification aus und liefert keinen Cursor. +- **BEWIESEN:** Der Client-Sortstring wird danach nicht als freie Sortierung an die abschließende SQLite-Abfrage weitergereicht. Einige interne Pfade setzen stattdessen feste Sortierungen (`Dbit_presetnumber`, `Schedule_starttime`, `starttime`). +- **BEWIESEN:** Der letzte Parameter der verwendeten 8-Argument-Überladung `SQLiteDatabase.query(..., orderBy, limit)` wird immer auf `null` gesetzt. Es gibt in diesem Provider keinen generischen URI-Queryparameter oder Legacy-Pfad für `LIMIT 1`. +- **BEWIESEN:** Ein `LIMIT` im Sortstring wäre hier nicht nur unerwünschte SQL-Einschleusung, sondern wegen der Providerlogik auch kein belastbarer Weg. + +### Gibt es einen legitimen Count-Weg? + +- **BEWIESEN:** Der Provider implementiert keinen eigenen Count-Endpunkt, kein `DatabaseUtils.queryNumEntries()` und kein festes `SELECT COUNT(*)` für Schedule/FVP. +- **NICHT VERIFIZIERBAR:** Ob ein Aggregate-Ausdruck wie `COUNT(*) AS row_count` als Client-Projektion auf jedem URI akzeptiert wird. Die Provider-Umschreibung verwirft oder verändert Projektionen abhängig vom Pfad; auf dem normalen `scheduletable`-Join ist dies deshalb kein dokumentierter, verlässlicher Vertrag. +- **PLAUSIBEL:** Die Spezialpfade `antenna/fvpbackwardepg`, `antenna/fvpnownexttable` und `antenna/fastipnownexttable` reichen Projektionen direkter an SQLite weiter; dort könnte ein Aggregate-Count rein lesend funktionieren. Das ist statisch nicht als Philips-API zugesichert und sollte, falls später überhaupt getestet, nur mit kurzer externer Zeitbegrenzung erfolgen. +- **BEWIESEN:** Ein garantiert maximal einzeiliger, aber nicht zählender Leseweg besteht über eine eindeutige Schlüssel-Selection: bei `scheduletablesync` `_id = `, bei FVP `(channelid,starttime,endtime)` mit bekannten Werten. Das beantwortet nur Existenz/Einzeldaten, nicht die Gesamtgröße. + +Risikoarme weitere Diagnose bedeutet daher: keine unbeschränkte Ausgabe des Schedule-Joins, keine Sort-/Selection-Injection und keine mutierenden URI-Endpunkte. Sinnvoll sind ausschließlich zeitlich eng begrenzte, indexgestützte Leseselektionen auf der physischen `scheduletablesync`-Tabelle (`starttime`/`endtime`) sowie kurz begrenzte Aggregate-Experimente nur dort, wo die Projektion statisch unverändert durchgereicht wird. Ein vollständiger, verlässlicher Tabellen-/Seitenbefund erfordert letztlich SQLite-Metadaten oder eine lesbare DB-Kopie. + +## Interne Reset-/Rebuild-Flächen + +### ContentProvider + +Manifest-Komponente: + +- Klasse: `org.droidtv.channels.epg.bcepg.EpgProvider` +- Authority: `org.droidtv.epgdvb` +- `android:exported="true"` +- Im Provider-Element ist keine `readPermission` oder `writePermission` deklariert. + +Die folgenden URI-Pfade werden in `EpgUriMatcher` erkannt. **Alle werden über `ContentResolver.delete()` angestoßen und sind deshalb mutierend; sie wurden nicht ausgeführt.** + +| URI | Runnable/Funktion | Tatsächliche Wirkung | +|---|---|---| +| `content://org.droidtv.epgdvb/antenna/staledatacleanup` | `EpgProvider$deleteStaleDataRunnable` | derzeit nur Grenzwertberechnung/Log; keine Löschung | +| `content://org.droidtv.epgdvb/antenna/rebuilddb` | `EpgProvider$rebuildDbRunnable` | `dropIndexTable()`, `syncChannelTable()`, `syncOnChannelTableChange()`, Acquisition-Notification; selektiver Reindex/Mapping-Abgleich | +| `content://org.droidtv.epgdvb/antenna/rebuilddbonnewchannellist` | `EpgProvider$rebuildDbOnNewChannelListRunnable` | `dropTables()`, zusätzlich `dropIndexTable()`, danach Kanal-/Mapping-Neuaufbau und Notification; umfassende Zeilenleerung der EPG-Ereignistabellen, aber keine Dateikompaktierung | + +`EpgProgramPump.rebuildEpgDataBase()` ruft den regulären `rebuilddb`-URI über `ContentResolver.delete()` auf. **BEWIESEN:** Auch diese Methode hat innerhalb der vier DEX-Dateien keinen direkten Invoke-Aufrufer. + +Der tatsächlich verdrahtete interne Rebuild erfolgt in `EpgInfoSingleton$MyRunnableHandleMessage.run()`: + +- bei Senderlisten-Update-Ende und Installationsabbruch über `REBUILD_CONTENT_URI` (`rebuilddb`), +- bei `CHANNEL_INSTALL_COMPLETE` über `REBUILD_ONNEWCHLIST_CONTENT_URI` (`rebuilddbonnewchannellist`). + +Das ist ein interner Lifecycle-Pfad der Senderinstallation, keine gefundene Benutzerfunktion zum Freigeben von Speicher. + +### Manifest-Activities, Services und Intent-Actions + +Gefundene, EPG-nahe Benutzerkomponenten sind: + +- `org.droidtv.channels.ChannelsActivity`: `org.droidtv.action.GUIDE`, `org.droidtv.action.DUKTPGUIDE`, `org.droidtv.intent.action.SHOW_PLANNER`, `org.droidtv.intent.action.CHANLIST` +- `org.droidtv.channels.epg.bcepg.epgui.RecordingsManager`: `org.droidtv.ACTION.SHOW_RECORDINGS_LIST` +- `org.droidtv.channels.epg.bcepg.epgui.NowNextOverview`: `org.droidtv.ACTION.SHOW_NOWNEXT_OVERVIEW` +- `org.droidtv.channels.epg.bcepg.epgui.ScheduleReminder`: `org.droidtv.ACTION.SET_REMINDER` +- `org.droidtv.channels.epg.bcepg.epgui.ScheduleManualRecording`: `org.droidtv.ACTION.SCHEDULE_MANUAL_RECORDING` +- `org.droidtv.channels.epg.bcepg.epgui.ScheduleEventRecording`: `org.droidtv.ACTION.SCHEDULE_EVENT_RECORDING` +- `org.droidtv.channels.epg.bcepg.epgui.ScheduleRecording`: `org.droidtv.ACTION.SCHEDULE_RECORDING` +- `org.droidtv.channels.epg.bcepg.RecordingsUpdateService`: Aktion `org.droidtv.bcepg.RecordingsUpdateService`, ausdrücklich `android:exported="false"` +- `org.droidtv.channels.epg.search.EPGSearchProvider`: Authority `org.droidtv.channels.epg.search`; Suchprovider, kein Reset-Pfad + +**BEWIESEN:** Keine dieser Manifest-Aktionen ist als EPG-Datenbank-Reset implementiert. In Manifest- und Ressourcenquersuche wurde keine Activity, kein Service und kein expliziter Intent mit der Bedeutung „EPG/Guide database reset/clear/rebuild“ gefunden. + +`EpgStaleDataCleanupAlarmReceiver` hat ein Intent-Filter, aber kein explizites `android:exported`-Attribut. Bei `targetSdkVersion=30` ergibt sich auf der untersuchten Plattform aus dem Intent-Filter typischerweise Exportierbarkeit; die genaue vom PackageManager auf diesem Gerät aufgelöste Export-Eigenschaft ist in dieser rein lokalen Teilanalyse **NICHT VERIFIZIERBAR**. Für den Befund ist dies unerheblich, weil der Empfänger letztlich nur den No-op-Cleanup auslöst. + +### Quersuche in weiteren lokalen Philips-APKs + +**BEWIESEN:** `TPTvService.apk`, `eum.apk` und `usagelogger.apk` enthalten gemeinsame EPG-Vertragskonstanten für normale EPG-Datenabfragen, aber keine Treffer für `rebuilddb`, `rebuilddbonnewchannellist`, `staledatacleanup` oder `EpgDatabase.db`. `gel.apk`, `tvstrings.apk` und `tvwidgets2k15.apk` enthalten ebenfalls keinen solchen Aufrufer. Ein Aufrufer aus einem nicht lokal vorliegenden Paket bleibt **NICHT VERIFIZIERBAR**. + +## Abhängigkeiten und Risikoabgrenzung + +### Senderliste + +- **BEWIESEN:** Die kanonische Senderliste liegt außerhalb von `EpgDatabase.db`. `EpgDatabaseHelper.syncChannelTable()` fragt `android.media.tv.TvContract.Channels.CONTENT_URI` ab. +- **BEWIESEN:** `indextable` ist ein EPG-interner Spiegel von Sender-/Presetdaten einschließlich `presetid`, `presetnumber`, `svlid`, `svlrecid`, Name, `browsable` und `favoriteNumber`. +- **BEWIESEN:** `mappingtable` ordnet `presetid` zu `svlid/svlrecid` zu. +- **BEWIESEN:** Die Rebuild-Runnables löschen die kanonische `TvContract.Channels`-Tabelle nicht; sie lesen sie und bauen EPG-Index/Mapping neu auf. +- **NICHT VERIFIZIERBAR:** Ob ein konkreter Laufzeitfehler während des Abgleichs zu sichtbaren temporären Kanal-/EPG-Zuordnungsproblemen führen würde. + +### Favoriten + +- **BEWIESEN:** Favoritenlisten werden über einen separaten Provider `org.droidtv.tv.provider.favorite` angesprochen, unter anderem `content://org.droidtv.tv.provider.favorite/FavoriteList` sowie Mixed-/Terrestrial-/Cable-/Satellite-Listen. +- **BEWIESEN:** `EpgDatabase.db` enthält nur Spiegel-/Versionsfelder (`favoriteNumber`, `favouriteversion`), nicht die externen Favoritentabellen selbst. +- **BEWIESEN:** Die EPG-Rebuild-Runnables enthalten keinen direkten Delete-Aufruf gegen den Favoriten-Provider. +- **PLAUSIBEL:** Der Reindex kann vorübergehend Darstellung oder Zuordnung beeinflussen, bis die Spiegelwerte erneut synchronisiert sind. + +### Aufnahmen, geplante Aufnahmen, Erinnerungen und Timer + +Die persistenten Aufnahme-/Reminder-Daten liegen ebenfalls außerhalb von `EpgDatabase.db`: + +- Authority: `org.droidtv.playtv.provider.recordremind` +- Datenbank: `RecordingsRemindersList.db` +- Aufnahmen: `.../RecordingsTable` +- Erinnerungen: `.../RemindersTable` +- gemeinsame View: `.../RecRemView` + +- **BEWIESEN:** Diese Tabellen kommen weder im v6-Schema von `EpgDatabaseHelper` noch in `dropTables()` vor. +- **BEWIESEN:** `EpgProgramPump` registriert einen `ContentObserver` auf `IRecordingsContract.CONTENT_URI_Recordings` und korreliert Aufnahmen über Felder wie `EventId` und `ChannelId`; Methoden wie `syncRecordingDBbyEvent()` und `RecDynamicEventUpdateHandler` aktualisieren Aufnahme-Metadaten bei EPG-Ereignisänderungen. +- **BEWIESEN:** Ein URI-basierter EPG-Rebuild besitzt keinen direkten Codepfad, der `RecordingsTable` oder `RemindersTable` löscht. +- **NICHT VERIFIZIERBAR:** Vollständige Nebenwirkungsfreiheit auf Darstellung, Event-ID-Verknüpfung, Folgeaufnahmen oder Scheduler nach Reakquisition. Das externe Persistieren verhindert nicht, dass neu vergebene Event-/Channel-IDs Beziehungen vorübergehend oder dauerhaft beeinflussen könnten. + +### TV-Guide-Quelle ist kein sicherer Reset + +`tvstrings.apk` enthält unter anderem die deutsche Warnung: + +> Wenn Sie zu TV-Guide-Informationen aus dem Internet wechseln, gehen alle Aufnahmen verloren, die Sie mit Informationen der Sendeanstalt aufgenommen haben. Alle Erinnerungen und programmierten Aufnahmen werden abgebrochen. + +Die Gegenrichtung warnt ebenfalls vor Verlust von Aufnahmen und Abbruch programmierter Aufnahmen. + +- **BEWIESEN:** Das Umschalten zwischen Broadcaster- und Internet-Guide ist laut Philips-Ressourcen eine ausdrücklich destruktive Benutzeraktion für Aufnahme-/Timerdaten. +- **BEWIESEN:** Es ist nicht mit den internen `rebuilddb`-URI-Pfaden gleichzusetzen und sollte nicht als Speicherbereinigung vorgeschlagen werden. + +## Gesamteinordnung zur Speicherwarnung + +Die statische Implementierung liefert eine konsistente Ursache für die ungewöhnlich große EPG-Datei: + +1. EPG-Synchronisation erzeugt sehr hohen Insert/Delete-Churn; die hohen AUTOINCREMENT-Werte beweisen Millionen historische Inserts. +2. Der als periodischer Stale-Cleanup bezeichnete Kernpfad löscht in dieser Firmware tatsächlich nichts. +3. Der sichtbare 12-Stunden-Alarm-Setter hat keinen direkten Caller und wäre selbst bei Ausführung nur ein einmaliger Einstieg in dasselbe wirkungslose Runnable. +4. Interne Rebuilds leeren Zeilen, verwenden aber selbst kein `VACUUM`; ob ein Vendor-/DB-seitig aktives Full-Auto-Vacuum die Datei dabei verkleinert, ist offen. +5. Nur Now/Next/Index wurden als klein gemessen. Der unbeschränkte Schedule-Join lief länger als 120 Sekunden; Schedule/FVP können daher einen großen lebenden Datenanteil enthalten. + +**BEWIESEN:** `EpgDatabase.db` ist mit 1,62 GiB der konkrete große Speicherverbraucher. **SEHR WAHRSCHEINLICH:** Der defekte Stale-Cleanup trägt wesentlich zu ihrem Wachstum bei, insbesondere durch nicht entfernte Schedule-Daten. **NICHT VERIFIZIERBAR:** Ob die aktuelle Dateigröße überwiegend aus lebenden Schedule-/FVP-Zeilen oder aus freien/fragmentierten Seiten besteht. + +**NICHT VERIFIZIERBAR:** Der genaue Beitrag eines ungewöhnlichen SQLite-Headerzustands, WAL-/Journalresten oder Dateisystembesonderheiten. Dafür wären ausschließlich weitere lesende Datenbank-/Header-Messungen erforderlich; sie waren nicht Teil dieser lokalen APK-Teilanalyse. + +## Keine Reparaturfreigabe aus diesem Bericht + +Die gefundenen `ContentResolver.delete()`-Endpunkte und Rebuild-Pfade sind zustandsverändernd. Sie wurden nicht ausgeführt und dieser Bericht empfiehlt keinen konkreten Ausführungsbefehl. Insbesondere ist der umfassende New-Channel-List-Rebuild nicht gleichbedeutend mit einer Dateikompaktierung und eine vollständige Nebenwirkungsfreiheit für EPG-Verknüpfungen zu Aufnahmen/Timern ist nicht bewiesen. + +## Reproduzierbare Belegdateien + +- `diagnostics/deep-forensics/philips/epg-dex-context.txt` – Helper, DDL, Lifecycle, Provider und Query-Joins +- `diagnostics/deep-forensics/philips/epg-runnable-context.txt` – vollständige Referenzkontexte der Provider-Runnables +- `diagnostics/deep-forensics/philips/epg-cleanup-context.txt` – Cleanup- und echte Delete-Methoden +- `diagnostics/deep-forensics/philips/epg-stale-context.txt` – Alarm-/Receiver-/URI-Kette +- `diagnostics/deep-forensics/philips/epg-sync-thread-context.txt` – SyncThread-Methoden +- `diagnostics/deep-forensics/philips/epg-recording-deps-context.txt` – Aufnahme-/Reminder-Verträge und Interaktionen +- `diagnostics/deep-forensics/philips/epg-delete-rebuild-string-context.txt` – interne Rebuild-Auslösung bei Senderlistenereignissen +- `diagnostics/deep-forensics/philips/channels-manifest.txt` – dekodiertes binäres Manifest +- `diagnostics/deep-forensics/philips/tvstrings-epg-matches.txt` – relevante TV-Guide-Ressourcen +- `diagnostics/raw/storage/epg-content-evidence.txt` – bereits vorhandene Laufzeit-Zeilenzahlen/IDs/Dateigröße +- `diagnostics/bugreport/extracted/files/01790__bugreport-PH9M_EA_5599-STT2.230526.001-2026-08-25-13-26-44.txt` – Boot-Log, insbesondere Zeilen um 32807–32812 diff --git a/diagnostics/diagnosis.md b/diagnostics/diagnosis.md new file mode 100644 index 0000000..fa50a30 --- /dev/null +++ b/diagnostics/diagnosis.md @@ -0,0 +1,192 @@ +# Schlussdiagnose: Philips 43PUS7394/12 – interne Speicherwarnung + +Stand: 2026-08-25. Plattform TPM191E, Firmware +`TPM191E_R.211.000.103.000`, Android TV 12, Kernel `4.9.310-ab4215`. + +Dies ist die aktuelle Kurzfassung. Die vertrauliche Deep-Forensics-Fassung mit +Angriffsflächenanalyse ist nicht Teil der kuratierten Veröffentlichung. + +**Phase-4-Update:** Eine konsistente UID1000-Kopie der EPG-DB schließt die +letzte zentrale Lücke. `scheduletable` hält 3.071.250 vollständig abgelaufene +Zeilen; 97,81 % sind älter als 30 Tage. Die Tabelle belegt 1.436,98 MiB, ihre +vier Indizes 222,06 MiB; `freelist_count=0`, `auto_vacuum=FULL`, +`integrity_check=ok`. Damit ist jahrelang nicht gelöschte Schedule-Historie die +bewiesene Hauptursache. Die veröffentlichten Details stehen in +[`phase4/epg-database-forensics.md`](phase4/epg-database-forensics.md). + +## Ergebnis + +Die Warnung ist kein Beleg dafür, dass Androids `/data`-Dateisystem unmittelbar +voll ist. Die letzte Momentaufnahme zeigte: + +| Wert | Ergebnis | +|---|---:| +| `/data` gesamt | 5.089.920 KiB = 4,85 GiB | +| belegt | 3.831.944 KiB = 78 % | +| verfügbar | 1.136.840 KiB = 1,084 GiB | +| Inodes | 7 % belegt | +| Android-Status | `NORMAL` | + +Philips verwendet jedoch einen eigenen Monitor in der Nähe von 1 GiB freiem +`/data`. Zur 1-GiB-Marke blieben zuletzt nur 88.264 KiB beziehungsweise +86,2 MiB. Normale Schwankungen reichen deshalb aus, die Philips-Warnung +wiederholt auszulösen, obwohl Android selbst noch keinen Low-Storage-Zustand +meldet. + +Eine nach dem späteren Aufwachen sichtbare Philips-Zeile mit rund 762 MB frei +misst nicht `/data`, sondern die separate 1000-MiB-ext4-Partition +`/data/vendor/user_setting`. TvProvider warnt dort erst unter rund 13 % und +10 %; aktuell sind bezogen auf die Rohpartitionsgröße rund 72,7 % frei. Diese +zweite Messung ist daher unauffällig und nicht die Ursache der Meldung. + +Der klare Hauptverursacher ist eine Philips-EPG-Datenbank: + +| Verursacher | Größe | +|---|---:| +| `org.droidtv.channels/.../EpgDatabase.db` | 1.742.053.376 B = 1,622 GiB | +| `org.droidtv.gel/.../androidx.work.workdb` | 229.453.824 B = 218,824 MiB | +| sichtbarer Code von 18 aktualisierten System-Apps | 686,195 MiB | +| statbare Basis-ODEX/VDEX dieser Apps | 226,381 MiB | + +Insgesamt sind konservativ 2.830,530 MiB beziehungsweise rund 75–76 % der +belegten `/data`-Kapazität konkret erklärt. Rund 0,88–0,89 GiB bleiben wegen +App-Sandbox, DAC und SELinux nicht einzeln aufteilbar; dieser Rest wurde nicht +als frei oder unbenutzt behandelt. + +## EPG-Datenbank: Ursache und offene Größe + +Für `EpgDatabase.db` sind folgende Werte direkt bewiesen: + +- Seitengröße 4096 Byte; +- `page_count` 425.306; +- Dateigröße exakt `425.306 × 4096 = 1.742.053.376` Byte; +- nur 358 sichtbare Index-, 195 Now- und 162 Next-Zeilen, aber + AUTOINCREMENT-IDs im Millionenbereich; +- eine unbeschränkte lesende Schedule-Abfrage war nach mehr als 120 Sekunden + noch nicht vollständig und wurde clientseitig abgebrochen. + +Die APK-Analyse erklärt einen konkreten Wachstumsmechanismus: Der als +Stale-Cleanup benannte Runnable berechnet Grenzwerte und schreibt Logmeldungen, +führt aber keine Löschoperation aus. Zusätzlich sind die Zeitkonstanten dort +offenbar als Millisekunden statt als die beabsichtigten längeren Intervalle +verwendet. Vorhandene echte Zeit-Löschmethoden sind nicht mit diesem globalen +Cleanup-Pfad verbunden. + +Philips' interne `rebuilddb`-Pfade sind ebenfalls kein physischer +Datenbank-Neuaufbau. Sie löschen beziehungsweise synchronisieren Zeilen in +derselben Datei und rufen weder `VACUUM` noch einen Dateiersatz auf. + +Wichtig ist die Grenze der Aussage: `freelist_count`, der persistierte +`auto_vacuum`-Headerwert und die Seitennutzung der großen Schedule-/FVP- +Tabellen sind nicht zugänglich. AOSP baut Plattform-SQLite langjährig mit +`SQLITE_DEFAULT_AUTOVACUUM=1` (`FULL`); da die Philips-App keinen anderen +Modus setzt, ist FULL eine starke Inferenz, aber kein Gerätebeweis. Deshalb +ist eine sehr große Freelist **nicht** als Hauptursache bewiesen. Die Datei +kann zu wesentlichen Teilen aus noch lebenden Schedule-Daten, freien Seiten +und/oder interner Fragmentierung bestehen. Der tatsächlich rückgewinnbare +Platz ist derzeit nicht seriös bezifferbar. + +## GEL-WorkManager-Datenbank + +GELs `androidx.work.workdb` ist rund 2.155-mal größer als typische +WorkManager-Datenbanken auf demselben Gerät. Die APK erzeugt wiederholt +`GEL_WORK` mit `APPEND_OR_REPLACE`; eine lang gewachsene Work-Kette ist daher +sehr wahrscheinlich. Die Hauptdatei hat 4096-Byte-Seiten und 56.019 Seiten; +der Live-Stand umfasst wegen aktivem WAL 56.020 Seiten. WAL und SHM belegen +zusammen nur 0,531 MiB und erklären den Ausreißer nicht. Tabellen- und +Freelist-Anteile bleiben ohne autorisierten Owner-Zugriff unbekannt. + +## Adoptierter Speicher + +Das adoptierte 112-GiB-Volume ist korrekt gemountet und praktisch leer. Die +18 Pakete unter `/data/app` sind jedoch ausnahmslos Updates von System-Apps; +auch Channels und GEL sind Systempakete. Android verschiebt solche Pakete und +ihre privaten Daten nicht auf das adoptierte Volume. Der große freie +USB-Speicher kann die beiden Datenbanken daher nicht automatisch aufnehmen. + +Ein Philips-spezifischer `mFreeSize 0`-Fehler für öffentliche/adoptierte +Volumes ist in Logs vorhanden. Androids eigener StorageManager misst diese +Volumes korrekt. Ein Einfluss des Fehlers auf einzelne Philips-Oberflächen ist +plausibel, aber er erklärt weder die reale `/data`-Belegung noch ist er als +Auslöser des hier dekompilierten Warnpfads bewiesen. + +## eMMC-Gesundheit + +Philips hat beim Boot beide eMMC-Life-Time-Felder als `0x0a/0x0a` ermittelt, +protokolliert und in `persist.odm.tpv.sys.emmc_lifetime` gespeichert. Nach der +von KIOXIA mit JEDEC-Genehmigung wiedergegebenen Tabelle bedeutet `0x0a` für +beide Felder die grobe Klasse 90–100 % der geschätzten Lebensdauer verbraucht. +Das ist keine exakte Restlebensdauer und beweist keinen gegenwärtigen I/O- +Fehler. + +`PRE_EOL_INFO` beschreibt getrennt den Verbrauch reservierter Ersatzblöcke. +Eine spätere rein lesende Eventlog-Abfrage hat diesen Wert unabhängig von der +Philips-Property sichtbar gemacht: +`storaged_emmc_info: [emmc 5.1,1,10,10]`. Die installierte Tagdefinition ordnet +die Felder als Version, EOL, Lifetime A und Lifetime B zu. Damit sind eMMC 5.1, +`PRE_EOL_INFO=0x01` (**Normal**) und A/B `0x0a/0x0a` für diesen Boot bestätigt. +Nur der direkte Rohdump sowie CID, CSD, Hersteller und Modell bleiben auf +diesem Production-Build durch SELinux gesperrt. `storaged` wurde beim +zusätzlichen Lesen des EXT_CSD-Debugfs-Knotens als UID 0 durch SELinux +abgewiesen; Root allein wäre daher weiterhin keine vollständige Zugriffs- oder +Reparaturlösung. + +Im verfügbaren Bugreport-/Logfenster wurden keine echten MMC-, Block-I/O-, +EXT4-, JBD2-, ENOSPC- oder Read-only-Remount-Fehler gefunden. Weil dmesg, +PStore, EXT4-Fehlerzähler und ein OEM-Board-Dump fehlen, ist das ein begrenzter +Nichtfund und kein Gesundheitszertifikat. + +Quellen: [KIOXIA Wear Leveling](https://www.kioxia.com/content/dam/kioxia/shared/business/memory/mlc-nand/asset/productbrief/KIOXIA_Understanding_Wear_Leveling_Tech_Brief.pdf), +[KIOXIA Bad Block Management](https://tw.kioxia.com/content/dam/kioxia/shared/business/memory/mlc-nand/asset/productbrief/KIOXIA_Understanding_Bad_Block_Tech_Brief.pdf), +[AOSP EXT_CSD-Auswertung](https://android.googlesource.com/platform/frameworks/native/+/c93afd5/cmds/dumpstate/utils.cpp), +[AOSP SQLite-Buildoptionen](https://android.googlesource.com/platform/external/sqlite/+/refs/heads/android11-qpr3-s1-release/dist/Android.bp) +und [SQLite VACUUM](https://www.sqlite.org/lang_vacuum.html). + +## Bewertung möglicher Maßnahmen – nichts davon ausgeführt + +| Option | Bewertung | +|---|---| +| Philips-dokumentierter, geordneter EPG-Dateirebuild | technisch größter gezielter Hebel und voraussichtlich schreibärmer als `VACUUM`; wurde aber weder als UI-/Supportweg gefunden noch auf Nebenwirkungen verifiziert | +| internes `rebuilddb` / `rebuilddbonnewchannellist` | kein bestätigter Dateirebuild; Speichergewinn abhängig vom unbekannten Auto-Vacuum und mit EPG-/Verknüpfungsrisiken | +| manuelles Löschen von `EpgDatabase.db` | nicht vertretbar bei laufendem Provider, offenen FDs und unbekannten Labels/Abhängigkeiten | +| `VACUUM` | vermeiden: hohe Schreiblast; kann bis zu ungefähr eine zusätzliche DB-Größe benötigen, während nur rund 1,1 GiB frei sind | +| `pm clear org.droidtv.channels` | zu grob; löscht das gesamte Paketprofil und kann Spiegel/Einstellungen betreffen | +| Werksreset/Firmware-Flash | unverhältnismäßig, schreibintensiv und ohne Beleg für dauerhafte Fehlerbehebung | +| adoptierter Speicher | für die bestehenden Systempaketdaten kein gangbarer Hebel | + +Die risikoärmste Folgerung lautet daher weiterhin: **keine spontane Mutation**. +Sinnvoll wäre ein von Philips/TPV ausdrücklich dokumentierter Ablauf, der den +Channels-Prozess geordnet schließt, nur die EPG-Datenbank konsistent neu +erzeugt, Sender/Favoriten/Timer schützt und die Nebenwirkungen benennt. Eine +solche Prozedur wurde in der Firmware und den vorhandenen Supportartefakten +nicht gefunden. + +## Standby-Nachprüfung + +Der während der Analyse beobachtete Aus-Zustand war kein Absturz und kein +Neustart. Nach dem manuellen Einschalten lief die Uptime weiter. Die Logs +belegen `power off by auto power down`, den Übergang nach +`POWER_STATE_SEMI_STANDBY` mit `detail=SWITCH_OFF_TIMER` und später das +Aufwachen per `WAKE_REASON_WAKE_KEY`. Die Kontrollmessung danach blieb bei +78 % `/data`-Belegung und Android-Status `NORMAL`. Im erhaltenen aktuellen +Logfenster trat nach dem Aufwachen kein `MEMORY_CRITICAL`, `CLEANUP_APPS` oder +Android-Low-/Full-Storage-Ereignis auf. + +## Nachvollziehbarkeit und Sicherheit + +Am Fernseher wurden keine Dateien gelöscht oder verändert, keine Einstellungen +oder Pakete geändert und kein Reboot, Recovery-, Flash-, Root- oder +Reparaturvorgang ausgeführt. Lange Provider-Leseabfragen wurden nur auf dem PC +abgebrochen. Transparenz-Ausnahme: Das vom Auftrag ausdrücklich verlangte +`dumpsys diskstats` meldete selbst einen `[512B Data Write]`-Latenztest; dieser +Android-Test wurde danach nicht wiederholt. + +Detailberichte: + +- [Speicher-/SQLite-Analyse](storage-analysis.md) +- [EPG-Reverse-Engineering](epg-analysis.md) +- [eMMC-Analyse](emmc-analysis.md) +- [Bugreport-Analyse](bugreport-analysis.md) + +Rohbefehle, private Zustandsdaten sowie Privilegien-/SELinux-Berichte sind aus +dieser Veröffentlichung ausgeschlossen. diff --git a/diagnostics/emmc-analysis.md b/diagnostics/emmc-analysis.md new file mode 100644 index 0000000..46b3c1b --- /dev/null +++ b/diagnostics/emmc-analysis.md @@ -0,0 +1,295 @@ +# eMMC-Analyse – Philips 43PUS7394/12 + +Stand: 2026-08-25. Alle Geräteabfragen waren lesend. Der Grundbericht wurde +aus lokal gesicherten Artefakten erstellt; der unten hervorgehobene +Storaged-Nachtrag stammt aus einer späteren rein lesenden Live-Abfrage des +bereits vorhandenen Android-Eventlogs. + +## Ergebnis in einem Satz + +**BEWIESEN:** Zwei voneinander unabhängige Android-/Philips-Pfade berichten +für Lifetime A/B jeweils `0x0a`, also das höchste noch nicht überschrittene +JEDEC-Band **90–100 % der geschätzten Lebensdauer verbraucht**. Der +Android-Eventlog enthält außerdem `storaged_emmc_info: [emmc 5.1,1,10,10]`; +die geräteeigene Tagdefinition ordnet dies eindeutig als eMMC 5.1, +`PRE_EOL_INFO=0x01`, Lifetime A `0x0a`, Lifetime B `0x0a` zu. `0x01` bedeutet +für den Reserveblockindikator **Normal**. Die Rohbytes, CID, CSD, Hersteller +und Modell bleiben gesperrt. Hoher Lifetime-Bucket und normaler PRE_EOL-Wert +sind gleichzeitig möglich und bilden unterschiedliche Gesundheitsachsen; +zusammen belegen sie weiterhin keinen unmittelbar bevorstehenden Ausfall. + +## Nachtrag: unabhängiger Android-Storaged-Wert + +Die rein lesende Abfrage des vorhandenen Eventpuffers lieferte: + +```text +01-01 01:00:17.642 2644 2644 I storaged_emmc_info: [emmc 5.1,1,10,10] +``` + +Die auf genau diesem Gerät installierte Event-Tag-Definition lautet: + +```text +2733 storaged_emmc_info (mmc_ver|3),(eol|1),(lifetime_a|1),(lifetime_b|1) +``` + +Damit sind Feldreihenfolge und Werte **BEWIESEN**. AOSPs Android-12- +`storaged_info.cpp` liest dafür `rev`, `pre_eol_info` und `life_time` aus dem +MMC-sysfs und fällt bei Bedarf auf EXT_CSD-Debugfs zurück; der Kernel erzeugt +die sysfs-Werte aus den entsprechenden EXT_CSD-Feldern. Dieser Storaged-Pfad +ist von Philips' persistenter Property unabhängig. Die Übereinstimmung beider +Lifetime-Werte schließt deshalb die frühere Hauptunsicherheit „möglicherweise +nur veralteter Philips-Cache“ für diesen Boot praktisch aus. Der vollständige +EXT_CSD-Rohdump bleibt dennoch nicht zugänglich. + +Primärquellen: + +- [AOSP Android 12: `storaged_info.cpp`](https://android.googlesource.com/platform/system/core/+/refs/heads/android12-gsi/storaged/storaged_info.cpp) +- [AOSP: Storaged-Event-Tagdefinition](https://android.googlesource.com/platform/system/core/+/master/storaged/EventLogTags.logtags) +- [Android-Kernel: MMC-sysfs aus EXT_CSD](https://android.googlesource.com/kernel/common/+/60a02c0d815e1df9bb8e6dd14105c31d8c298f2d/drivers/mmc/core/mmc.c) + +## Beweiskette + +### BEWIESEN + +1. Der Boot-Log enthält die Ausgabe der nativen Philips-Komponente: + + ```text + D eMMCLifeReader: eMMC life time 0x0a 0x0a + ``` + +2. Die persistente Eigenschaft enthält aktuell denselben Wert: + + ```text + persist.odm.tpv.sys.emmc_lifetime = 0x0a 0x0a + ``` + +3. Die dazugehörige Init-Definition startet `/system/bin/emmclifereader` als + `root`, `oneshot`. Der Dienst ist nach dem Boot erwartungsgemäß `stopped`. + +4. Philips' `OnBootLogger` liest die Property, zerlegt sie in zwei Werte und + übermittelt beide separat als `dev_lt_a="a"` und `dev_lt_b="a"`. + +5. `TVStorageMonitorService.getEmmcLifetime()` liest dieselbe Property. Die + Bytecode-Analyse zeigt, dass dieser konkrete Warndienst nur den Text nach + dem letzten `x` auswertet, also den zweiten Wert (Typ B). Er ordnet `0x0a` + selbst der Stufe `90%-100% device lifetime used` zu. + +### SEHR WAHRSCHEINLICH, aber nicht vollständig direkt verifizierbar + +Die zeitliche Boot-Ausgabe des `eMMCLifeReader`, seine privilegierte +One-shot-Init-Definition, die anschließend identische persistente Property und +zwei unabhängige Philips-Verbraucher bilden eine starke Kette dafür, dass der +native Reader die eMMC-EXT_CSD-Felder beim Boot liest und die Property befüllt. +Die lokal gesicherte SELinux-Policy schließt einen weiteren Teil der Kette: +`/system/bin/emmclifereader` trägt `tpvdebug_exec`, und ein +`typetransition init ... tpvdebug_exec process tpvdebug` führt den von Init +gestarteten Prozess in die Domain `tpvdebug`. Diese Domain darf TPV-Properties +setzen. Die konkrete Property-Zuordnung, der native Schreibcode und der vom +Reader geöffnete Kernelpfad bleiben aber unsichtbar. Deshalb ist die Herkunft +aus EXT_CSD **SEHR WAHRSCHEINLICH**, nicht unabhängig **BEWIESEN**. + +## JEDEC-/Herstellerinterpretation + +Die Felder `DEVICE_LIFE_TIME_EST_TYP_A` (EXT_CSD-Byte 268) und Typ B (Byte 269) +geben eine herstellerspezifisch ermittelte, mittlere Abnutzung relativ zur +maximal geschätzten Lebensdauer an. Eine KIOXIA-Fachinformation reproduziert +die Tabelle aus JEDEC JESD84-B51: `0x0a` bedeutet 90–100 % verbraucht, `0x0b` +erst „maximale geschätzte Lebensdauer überschritten“. Kingston erläutert +zusätzlich, dass die Werte nur in 10-%-Bändern berichtet werden und je ein Feld +unterschiedliche Speicherarten/-Konfigurationen repräsentiert. + +Quellen: + +- [KIOXIA: Understanding Wear Leveling in NAND Flash Memory](https://tw.kioxia.com/content/dam/kioxia/shared/business/memory/mlc-nand/asset/productbrief/KIOXIA_Understanding_Wear_Leveling_Tech_Brief.pdf) +- [KIOXIA: Understanding Bad Block Management in NAND Flash Memory](https://www.kioxia.com/content/dam/kioxia/shared/business/memory/mlc-nand/asset/productbrief/KIOXIA_Understanding_Bad_Block_Tech_Brief.pdf) +- [Kingston: eMMC Life-Cycle Estimating, Validating & Monitoring](https://www.kingston.com/en/blog/embedded-and-industrial/emmc-lifecycle) +- [Macronix eMMC-Datenblatt mit vollständiger A/B-Wertetabelle](https://www.macronix.com/Lists/Datasheet/Attachments/9163/MX52LM08A11%2C%203V%2C%208GB%2C%20v1.7.pdf) + +`0x0a` ist daher nicht „10 % Zustand“ und auch keine Belegungsanzeige. Es ist +das Band 90–100 % der vom eMMC-Controller geschätzten Schreib-/Löschlebensdauer. +Der genaue Punkt innerhalb dieses Bandes ist nicht bestimmbar. + +`PRE_EOL_INFO` ist ein davon getrennter Indikator für verbrauchte Reserveblöcke. +Die KIOXIA-Unterlage zum Bad-Block-Management ordnet EXT_CSD-Byte 267 zu: +`0x01` normal, `0x02` Warnung (80 % der Reserveblöcke verbraucht) und `0x03` +dringend. Der direkte sysfs-Read bleibt der Shell durch SELinux verwehrt; der +unabhängige Storaged-Boot-Event exportiert jedoch `eol=1`. Für diesen Boot ist +`PRE_EOL_INFO=0x01` deshalb **BEWIESEN**. Wichtig: Dieser Wert wurde unabhängig +gemessen; er wird nicht aus `0x0a/0x0a` abgeleitet. + +## Identität und Kapazität + +Das interne Gerät ist eindeutig eine eMMC 5.1 als `mmcblk0` am MediaTek-Host +`1006d000.MSDC0`; `/data` liegt auf `mmcblk0p37`. Der Zugriff auf `cid`, `name`, +`manfid`, `oemid`, `serial`, CSD und die rohe Blockgröße wird durch Rechte/SELinux +verweigert. Hersteller, Modell und Seriennummer können daher nicht seriös +bestimmt werden. Eine Android-/Philips-Oberfläche berichtet rund 8 GB +**nutzbaren internen Speicher**; das ist keine Rohkapazitätsangabe der eMMC. +Die vollständige statische Partitionstabelle summiert sich bereits auf +14.809 MiB (14,462 GiB) und spricht deshalb stark für eine nominelle +16-GB-eMMC. Die exakte Rohsektorzahl ließ sich am Blockgerät weiterhin nicht +unabhängig prüfen. + +## Exakte DAC-/SELinux-Barrieren + +Die lokal gesicherten Audit-, Dateikontext- und Policy-Daten trennen die +Barrieren präzise: + +| Ziel | Unix-/DAC-Zustand | beobachtete SELinux-Barriere | Ergebnis | +|---|---|---|---| +| `/dev/block/mmcblk0` | `brw------- root root`, Major:Minor `179:0` | ein geeigneter Blockdevice-Zieltyp wäre zusätzlich erforderlich | Shell scheitert bereits an DAC; Rohgröße und -inhalt **NICHT VERIFIZIERBAR** | +| `/sys/block/mmcblk0/device/life_time` | sysfs-Knoten vorhanden | `shell:s0 -> sysfs:s0` `{ read }` verweigert; Policy gewährt Shell nur `getattr` auf generischem sysfs | Rohwert **NICHT VERIFIZIERBAR** | +| `/sys/block/mmcblk0/device/pre_eol_info` | sysfs-Knoten vorhanden | dieselbe AVC-Barriere | direkter Shell-Read gesperrt; Wert `0x01` über unabhängigen Storaged-Event **BEWIESEN** | +| CID/CSD/`manfid`/`oemid`/`serial` | sysfs-Knoten vorhanden | konkrete `{ read }`-AVCs gegen `sysfs:s0` | Identität **NICHT VERIFIZIERBAR** | +| `/sys/kernel/debug/mmc0/mmc0:0001/ext_csd` | Debugfs-Pfad und Datei durch Audit direkt belegt | `storaged:s0 -> debugfs_mmc:s0` `{ read }` verweigert, obwohl Prozess UID root besitzt; Shell wird schon an `mmc0` abgewiesen | Inhalt **NICHT VERIFIZIERBAR** | +| `/system/bin/emmclifereader` | Systemdatei, von Init als root gestartet | Shell erhält an `tpvdebug_exec:s0` weder `getattr` noch `read`; Init wechselt beim Start nach `tpvdebug` | äußere Reader-Kette **BEWIESEN**, Implementierung **NICHT VERIFIZIERBAR** | +| `/sys/fs/ext4/mmcblk0p37/*` | Health-Knoten wie `errors_count` und `lifetime_write_kbytes` vorhanden | alle direkten Leseversuche `Permission denied` | interne EXT4-Historie **NICHT VERIFIZIERBAR** | + +Die verfügbare Vendor-Policy gewährt `dumpstate` am Typ `debugfs_mmc` nur +Verzeichnissuche, nicht den benötigten Datei-Read. Sie erlaubt `dumpstate` +zugleich Zugriffe auf andere, generisch gelabelte Debugfs-Bereiche. Ein +erfolgreicher Binder-/DMA-Buf-Debugfs-Dump sagt daher nichts über MMC-Zugriff +aus. Der reale `storaged`-AVC beweist zusätzlich, dass Unix-UID 0 die +SELinux-Type-Enforcement-Regel nicht überstimmt. + +## Prozess- und Rechtematrix + +| Prozess | UID / Domain | Speicherfunktion | belegte Grenze | Einordnung | +|---|---|---|---|---| +| `emmclifereaderd` | Init: root/system; Policy-Transition nach `u:r:tpvdebug:s0` | einmaliger Philips-Life-Time-Reader | Prozess beim Bugreport bereits beendet; Binary/Property-Schreibcode nicht lesbar | äußere Kette **BEWIESEN**; EXT_CSD als Rohquelle **SEHR WAHRSCHEINLICH**; genau dieser Debugfs-Pfad **PLAUSIBEL** | +| `storaged` PID 2644 | root / `u:r:storaged:s0` | Android-I/O-Statistik und Boot-Healthpfad | Debugfs-Read wird durch AVC blockiert; dennoch publiziert der sysfs-/Fallback-Pfad eMMC 5.1, EOL 1, A/B 10/10 | Eventwerte und Blockade **BEWIESEN** | +| TPV `storagedcapture` PID 2613 | root / `u:r:tpvdebug:s0` | periodische Storage-/UID-I/O-Erfassung | sogar `getattr` auf `/proc/uid_io/stats` wird abgewiesen | root allein genügt nicht, **BEWIESEN** | +| `vold` PID 1442 | root / `u:r:vold:s0` | Mount-, fscrypt- und Volumeverwaltung | kein EXT-CSD-/PRE_EOL-Export; der sichtbare `e2fsck` galt nur dem adoptierten `dm-4` | interner eMMC-Healthwert **NICHT VERIFIZIERBAR** | +| `installd` PID 2630 | root / `u:r:installd:s0` | Quota-/Paket-Speicherverwaltung | erkennt `/data`-Mount, liefert keine MMC-Healthdaten | logische Speicherfunktion **BEWIESEN**, Hardwarezustand unbekannt | +| `system_server` PID 3007 | system / `u:r:system_server:s0` | StorageManager und DeviceStorageMonitor | logische Schwellwerte, keine Rohregister | Belegung **BEWIESEN**, eMMC-Rohzustand nicht | +| `TVStorageMonitorService` PID 5636 | system / `u:r:system_app:s0` | liest TPV-Property regelmäßig und klassifiziert Typ B | nur Property-Verbraucher | Zuordnung **BEWIESEN**, keine unabhängige Messung | +| Standard-`dumpstate` PID 21293 | shell / `u:r:dumpstate:s0` | Bugreport-Sammlung | leeres Kernel-Log, keine `IDumpstateDevice`-HAL, kein Board-Dump | privilegierte Lücke **BEWIESEN** | +| TPV `ddg` / `tpv_top_service` | root / `u:r:dumpstate:s0` | OEM-Diagnosedaemons | im Artefakt kein eMMC-Health-Export | Fähigkeit dazu **NICHT VERIFIZIERBAR** | +| TPV `errlogservice` | system / `u:r:hal_tv_errlogservice_default:s0` | OEM-Fehlerlogdienst | `lshal debug` endet mit `FAILED_TRANSACTION` | Inhalt **NICHT VERIFIZIERBAR** | + +Die AOSP-Historie enthält inzwischen einen Storaged-Fix mit genau derselben +AVC-Zeile und demselben Pfad. Der Commit erklärt den Zugriff als veralteten +Storaged-Init-Control-File-Pfad und beseitigt ihn, statt `storaged` pauschal +MMC-Debugfs-Rechte zu geben. Das bestätigt die technische Einordnung des Audits +als reale SELinux-Sperre, beweist aber nicht, dass Philips' +`emmclifereader` denselben Pfad verwendet. Quelle: +[AOSP storaged: Fix SELinux denial with debugfs_mmc](https://android.googlesource.com/platform/system/core/%2B/65b5858a9e18b38fe8c142ac7852fb010b889ab5). + +## Fehler- und Korruptionslage + +**BEWIESEN** ist, dass die direkt zugänglichen Android-Logs zeigen: + +- keinen `EXT4-fs error`, `Buffer I/O error`, `blk_update_request`, `ENOSPC` oder + fehlgeschlagenen Update-Engine-Vorgang; +- einen einzelnen `Icing`-Fehler `Array storage bad crc` in einem App-Index; +- erfolgreichen aktuellen Dateisystembetrieb: `/data` ist `rw` eingehängt und + das vom Auftrag ausdrücklich verlangte `dumpsys diskstats` meldete einen + `[512B Data Write]`-Latenztest. Dieser interne 512-Byte-Test ist die + dokumentierte Transparenz-Ausnahme und wurde nicht wiederholt. + +Der isolierte Icing-CRC-Fehler belegt einen beschädigten Indexdatensatz, ordnet +die Ursache aber nicht dem eMMC-Medium zu. Softwareabbruch, unvollständiger +Schreibvorgang und Medienfehler sind damit nicht unterscheidbar. + +Wesentliche Einschränkung: `dmesg`, Kernel-Log, EXT_CSD, `errors_count`, +`first_error_time`, `last_error_time` und `lifetime_write_kbytes` sind dem +ADB-Shell-Kontext verwehrt. Deshalb bedeutet „keine zugänglichen I/O-Fehler +gefunden“ ausdrücklich **nicht**, dass Kernel oder eMMC nie Fehler registriert +haben. Der Nichtfund ist nur **BEWIESEN innerhalb des sichtbaren +Diagnosefensters**; die vollständige historische Fehlerlage ist +**NICHT VERIFIZIERBAR**. + +## Evidenzbewertung + +| Aussage | Stufe | Begründung | +|---|---|---| +| Philips' nativer Reader meldete beim Boot `0x0a 0x0a` | **BEWIESEN** | native Boot-Logzeile | +| Property und Telemetrie enthalten beide A/B-Werte `0x0a` | **BEWIESEN** | Property, OnBootLogger, `dev_lt_a/b` | +| Beide Werte bedeuten 90–100 % Lebensdauer verbraucht | **BEWIESEN** | JEDEC-reproduzierende Herstellerunterlagen und Philips-Zuordnung | +| TVStorageMonitor bewertet Typ B als 90–100 % verbraucht | **BEWIESEN** | DEX-Analyse und wiederholter Laufzeitlog | +| Init startet den Reader als root in der SELinux-Domain `tpvdebug` | **BEWIESEN** | Init-RC, File-Context und Policy-`typetransition` | +| Android Storaged meldet eMMC 5.1, PRE_EOL `0x01`, A/B `0x0a/0x0a` | **BEWIESEN** | Boot-Event plus lokale Felddefinition; AOSP-Code ordnet sysfs/EXT_CSD zu | +| Philips-Property spiegelt den aktuellen Hardwarewert dieses Boots | **SEHR WAHRSCHEINLICH** | unabhängiger Storaged-Wert stimmt für A/B exakt überein; Philips-Binary bleibt gesperrt | +| Reader verwendet genau den beobachteten Debugfs-Pfad | **PLAUSIBEL** | passendes Gerät und EXT-CSD-Pfad vorhanden; kein Reader-Open-Audit | +| `PRE_EOL_INFO=0x01` (Normal) | **BEWIESEN** | `storaged_emmc_info` und geräteeigene Event-Tagdefinition | +| CID, CSD, Hersteller und Modell | **NICHT VERIFIZIERBAR** | direkte sysfs-/Debugfs-Reads werden verweigert; kein OEM-Export | +| Keine MMC-/EXT4-Fehler im sichtbaren Textfenster | **BEWIESEN** | exakte Volltextsuche; alle vier automatischen Treffer fachlich verworfen | +| Historisch keine MMC-/EXT4-Fehler | **NICHT VERIFIZIERBAR** | Kernel-Log, PStore und EXT4-Zähler fehlen | +| eMMC fällt unmittelbar aus | **NICHT VERIFIZIERBAR** | `0x0a` ist ein grobes Band; PRE_EOL ist noch normal, Fehlerzähler fehlen | +| eMMC ist gesund | **nicht haltbar** | beide Lebensdauerfelder stehen bereits im höchsten noch nicht überschrittenen Band | + +## Ergänzung aus dem vollständigen Bugreport + +Der vollständige Bugreport bestätigt Property, OnBootLogger und die wiederholte +`TVStorageMonitorService`-Auswertung. Zusätzlich enthält er einen Boot-Audit des +Android-Dienstes `storaged`: + +```text +root 2644 storaged: avc: denied { read } +path="/sys/kernel/debug/mmc0/mmc0:0001/ext_csd" +scontext=u:r:storaged:s0 +tcontext=u:object_r:debugfs_mmc:s0 +``` + +Damit ist der vorhandene EXT_CSD-Knoten für das konkrete Gerät `mmc0:0001` +direkt belegt. Der Audit beweist aber auch, dass selbst ein Prozess mit +Linux-UID root nicht genügt: SELinux blockiert die `storaged`-Domain. Für eine +Direktmessung wäre ein autorisierter OEM-/Engineering-Kontext mit passender +Policy nötig. + +Der gespeicherte Haupttext des Bugreports enthält keinen ausgeschriebenen +`PRE_EOL_INFO`-Wert; die später rein lesend abgefragte Eventlog-Ringpuffer- +Ausgabe enthält ihn jedoch als `eol=1`. Die Kernel-Log-Sektion ist +wegen `klogctl`-Fehler leer, und `dumpstate` meldet +`No IDumpstateDevice implementation`; ein Philips-/TPV-Board-Dump wurde nicht +erzeugt. In allen verfügbaren Textartefakten wurden keine MMC-Timeouts, +CRC-/ECC-Fehler, Controller-Resets, Block-I/O-Fehler, EXT4-/Journal-Fehler oder +Read-only-Remounts gefunden. Die Treffer mit „I/O error“ waren TLS-/Netzwerk- +Fehler. Das ist nur eine Aussage zum verfügbaren Diagnosefenster, kein +Gesundheitsbeweis. + +AOSP dokumentiert `IDumpstateDevice` als optionale, vom OEM zu implementierende +HAL. Der OEM bestimmt, welche Boarddaten sie in einen Standard-Bugreport +aufnimmt; die Implementierung benötigt regelmäßig passende SEPolicy-Regeln. +Das Fehlen der HAL auf diesem Build erklärt daher konkret, weshalb `dumpstate` +keinen privilegierten TPV-Health-Dump nachliefert. Quelle: +[AOSP: Scoped vendor logging / IDumpstateDevice](https://source.android.com/docs/core/tests/debug/scoped-vendor-logging). + +Die Beweiskette ist damit unabhängig doppelt: Android Storaged berichtet +eMMC 5.1/EOL 1/A 10/B 10, während Philips separat A/B `0x0a/0x0a` persistiert +und auswertet. Nur der genaue native Philips-Readerpfad und der vollständige +EXT_CSD-Rohdump bleiben unlesbar. + +Weitere Details: [Bugreport-Analyse](bugreport-analysis.md). Die vollständige +lokale Rechte-/Prozessmatrix ist nicht veröffentlicht; die technische +eMMC-/Kernel-Auswertung steht in +[Tiefe eMMC-/Kernel-Forensik](deep-forensics/emmc/emmc-kernel-agent.md). + +## Rohdaten + +- `raw/emmc/emmc-identity-and-access.txt` +- `raw/emmc/emmc-property-init-and-bootlog.txt` +- `raw/emmc/emmc-filesystem-health-access.txt` +- `raw/emmc/usagelogger.apk` + +## Legitimer nächster Beweisweg + +Für die Speicherwarnungsursache ist keine Privilegieneskalation nötig. Für die +noch offene eMMC-Ausfallprognose wäre der risikoärmste, technisch vollständige +Weg ein **autorisierter Philips/TPV-Service- oder Engineering-Export**, der +ausschließlich lesend liefert: + +1. EXT-CSD-Bytes 267, 268 und 269 sowie den vollständigen EXT-CSD-Dump zur + forensischen Gegenprüfung der bereits exportierten Werte; +2. CID/CSD und Herstellerkennung; +3. EXT4-Zähler für `mmcblk0p37`; +4. Kernel-/PStore-Zeilen zu `mmc0`, `mmcblk0`, `ext4` und `jbd2`; +5. Reader-Zeitstempel und Rückgabestatus, um einen persistenten Altwert sicher + auszuschließen. + +Geeignete legitime Mittel sind ein signierter, exakt passender OEM-Servicebuild, +ein Philips/TPV-Werkstatttool oder eine zukünftige Firmware mit korrekt +implementiertem `IDumpstateDevice`. Ebenfalls ungefährlich wäre die lokale +statische Analyse einer offiziell bezogenen passenden Firmware. Root-Exploit, +Bootloader-Entsperrung, Recovery oder Flashen sind dafür weder erforderlich +noch angemessen. diff --git a/diagnostics/epg-analysis.md b/diagnostics/epg-analysis.md new file mode 100644 index 0000000..2fb9f04 --- /dev/null +++ b/diagnostics/epg-analysis.md @@ -0,0 +1,320 @@ +# Konsolidierte Analyse von `EpgDatabase.db` + +Stand: 2026-08-25 +Gerät: Philips 43PUS7394/12, TPM191E, Android TV 12 +Firmware: `TPM191E_R.211.000.103.000` + +## Ergebnis in einem Satz + +`/data/user/0/org.droidtv.channels/databases/EpgDatabase.db` ist mit +1.742.053.376 Bytes der konkrete große Speicherverbraucher. Phase 4 hat die +früher offene Verteilung direkt an einer hashidentischen, integritätsgeprüften +Kopie entschieden: `scheduletable` enthält 3.071.250 Zeilen, alle zum Snapshot +abgelaufen; Tabelle plus vier Indizes belegen rund 1.659 MiB. +`freelist_count=0`, `auto_vacuum=FULL`. Die 1,62 GiB sind daher fast vollständig +echte jahrelange Schedule-Altlast, nicht Freelist oder FVP. Details: +[`phase4/epg-database-forensics.md`](phase4/epg-database-forensics.md). + +## Beweisgrade + +- **BEWIESEN**: direkt aus lokalem Manifest, DEX-Bytecode, SQL-Literal, Bugreport oder abgelegter Rohdiagnose. +- **SEHR WAHRSCHEINLICH**: mehrere unabhängige Belege ergeben dieselbe Erklärung, eine Laufzeitvariable bleibt offen. +- **PLAUSIBEL**: technisch passende Interpretation ohne direkten Beleg der Entwicklerabsicht oder konkreten Laufzeitwirkung. +- **NICHT VERIFIZIERBAR**: mit dem derzeitigen lesenden Zugriff beziehungsweise den lokalen Artefakten nicht entscheidbar. + +Diese Fassung konsolidiert ausschließlich Diagnose. Es wurde kein Cleanup, Delete, Rebuild, App-Reset oder sonstiger Reparaturpfad ausgeführt. + +## 1. Datenbank und Schema + +### Eigentümer und Version + +- **BEWIESEN:** Paket `org.droidtv.channels` +- **BEWIESEN:** Helper `org.droidtv.channels.epg.bcepg.EpgDatabaseHelper` +- **BEWIESEN:** Superklasse `android.database.sqlite.SQLiteOpenHelper` +- **BEWIESEN:** Dateiname `EpgDatabase.db` +- **BEWIESEN:** Schemaversion **6**, als Integerargument im Helper-Konstruktor + +### Physische Tabellen + +`EpgDatabaseHelper.createTables()` legt an: + +- `mappingtable` +- `indextable` +- `nowtable` +- `nexttable` +- `scheduletable` +- `fvpscheduletable` +- `fvpnownexttable` +- `channelmapversiontable` +- `fastipnownexttable` + +`mappingtable`, `indextable`, `nowtable`, `nexttable`, `scheduletable` und `fastipnownexttable` besitzen Integer-Primärschlüssel; mehrere davon sind `AUTOINCREMENT`. Die FVP-Tabellen verwenden den zusammengesetzten Primärschlüssel `(channelid,starttime,endtime)` und haben keine `_id`-Spalte. + +**BEWIESEN:** Der URI-Name `nownexttable` bezeichnet keine zusätzliche physische Tabelle. Der Provider erzeugt dafür den Join: + +`indextable AS Dbit LEFT JOIN nowtable AS Now USING(presetid) LEFT JOIN nexttable AS Next USING(presetid)` + +Die vollständigen DDL-Literale und Indizes sind in `diagnostics/deep-forensics/philips/epg-reverse-agent.md` sowie `epg-dex-context.txt` dokumentiert. + +## 2. Bisher gemessene Größe und Churn + +Vorhandene lesende Rohdaten: + +| Objekt | aktuelle Zeilen | beobachtete IDs | +|---|---:|---:| +| `indextable` | 358 | 1.267.806 bis 1.268.163 | +| `nowtable` | 195 | 4.431.681 bis 4.451.328 | +| `nexttable` | 162 | 4.026.589 bis 4.027.685 | +| Join-Endpunkt `nownexttable` | 358 | keine eigene physische ID | +| `fastipnownexttable` | 0 | keine aktuellen Zeilen | + +- **BEWIESEN:** Die hohen AUTOINCREMENT-Werte bei kleinen aktuellen Tabellen zeigen millionenfachen historischen Insert/Delete-Churn. +- **BEWIESEN:** Diese Messung umfasst nicht `scheduletable`, `fvpscheduletable` und `fvpnownexttable`. +- **BEWIESEN:** Eine unbeschränkte lesende Abfrage von `antenna/scheduletable` mit Projektion `_id` lief länger als 120 Sekunden und wurde clientseitig abgebrochen. +- **SEHR WAHRSCHEINLICH:** Die Schedule-Menge ist sehr groß oder die Schedule-Abfrage leidet unter einer schwerwiegenden Provider-/SQLite-Latenz. Der Lauf beweist keine genaue Zeilenzahl und keinen konkreten Byteanteil. + +Die frühere Kurzannahme „wenige aktuelle Zeilen, also fast alles Freelist“ ist damit nicht haltbar. Die kleinen Now/Next-Werte sagen nichts über Schedule/FVP aus. + +## 3. Der Stale-Cleanup ist ein No-op + +### Aufrufkette + +Beim Provider-Start: + +1. `EpgProvider$1.run()` synchronisiert Index und Kanalmapping. +2. Es postet `EpgProvider$deleteStaleDataRunnable` nach exakt 120.000 ms. +3. Die Logmeldung lautet `Posting stale data deletion after 2 mins in onCreate`. + +Alternativer Alarmweg: + +1. `EpgDatabaseHelper.setEpgStaleDataCleanUpAlarm()` würde einen RTC-Alarm auf `currentTimeMillis + 43.200.000 ms` setzen. +2. Empfänger: `EpgStaleDataCleanupAlarmReceiver` +3. Action: `org.droidtv.channels.epg.bcepg.EpgStaleDataCleanupAlarmReceiver.CLEAR_STALE_DATA` +4. Der Receiver ruft `EpgInfoSingleton.cleanEpgStaleData()` auf. +5. `EpgInfoSingleton$8.run()` ruft den Provider-Delete-Endpunkt `antenna/staledatacleanup` auf. +6. Der Provider postet dasselbe `deleteStaleDataRunnable`. + +**BEWIESEN:** In den vier DEX-Dateien von `channels.apk` existiert kein direkter Invoke-Aufrufer für `setEpgStaleDataCleanUpAlarm()`. Der Receiver plant sich nicht erneut ein. Ein externer oder reflektiver Aufruf ist nicht belegt. + +### Inhalt des Runnables + +`EpgProvider$deleteStaleDataRunnable.run()` macht bei definierter TV-Uhr: + +1. `System.currentTimeMillis()` lesen +2. `1800` direkt subtrahieren +3. bei FreeView zusätzlich `604800` direkt subtrahieren +4. `Deleting stale data for time ` protokollieren + +**BEWIESEN:** Danach folgt keine Mutation. In der vollständigen Methode fehlen: + +- `SQLiteDatabase.delete()` oder `execSQL()` +- `ContentResolver.delete()` oder `applyBatch()` +- `EpgProgramPump.deleteChannelProgrambytime()` +- `EpgProgramPump.deleteScheduleProgramsByTime()` +- irgendein anderer sichtbarer Löschaufruf + +Ist die Uhr nicht definiert, wird nur nach 30.000 ms erneut gepostet. + +**BEWIESEN:** Der Boot-Log belegt exakt diesen No-op-Pfad, keine gelöschte Zeile: + +- Logzeit: `2026-08-25 11:11:48.657` +- ausgegebener Grenzwert: `1787649106837` +- Grenzwert als Lokalzeit: `2026-08-25 11:11:46.837` + +Die Differenz von rund 1,82 Sekunden passt zum direkt als Millisekunden subtrahierten Literal `1800`. + +### Sichtbarer Einheitenfehler + +- **BEWIESEN:** Basis ist Millisekundenzeit, die Konstanten werden ohne Faktor 1.000 subtrahiert. +- **PLAUSIBEL:** `1800` sollte 30 Minuten in Sekunden und `604800` sieben Tage in Sekunden bedeuten. +- Selbst bei korrekter Einheit bliebe der aktuelle Pfad wirkungslos, weil kein Delete folgt. + +### Vorhandene, aber nicht verdrahtete Löschmethoden + +- `EpgProgramPump.deleteChannelProgrambytime(long)` löscht Now, Next und Schedule mit `endtime < ?` per Batch. +- `EpgProgramPump.deleteScheduleProgramsByTime(long)` löscht Schedule mit `endtime < ?`. +- `EpgSyncThread.deleteScheduleProgramsByTime(long)` delegiert an die vorige Methode. + +**BEWIESEN:** Keine dieser Methoden hat in den vier DEX-Dateien einen direkten Caller. Das Cleanup-Runnable ruft keine davon auf. + +## 4. Rebuild-Semantik + +Der Provider `org.droidtv.channels.epg.bcepg.EpgProvider` ist im Manifest mit Authority `org.droidtv.epgdvb` und `android:exported="true"` eingetragen. Das Provider-Element deklariert keine eigene Read-/Write-Permission. + +Die drei relevanten Endpunkte werden über `ContentResolver.delete()` aktiviert und sind deshalb mutierend. Sie sind hier nur dokumentiert: + +| Endpunkt | Codepfad | Wirkung | +|---|---|---| +| `antenna/staledatacleanup` | `deleteStaleDataRunnable` | aktuell nur Log/No-op | +| `antenna/rebuilddb` | `EpgProvider$rebuildDbRunnable` | Index leeren, Senderindex und Mapping abgleichen, selektiv EPG-Zeilen anpassen | +| `antenna/rebuilddbonnewchannellist` | `EpgProvider$rebuildDbOnNewChannelListRunnable` | EPG-Ereignis-/Index-/Versionszeilen leeren, Senderindex/Mapping neu aufbauen | + +`EpgDatabaseHelper.dropTables()` droppt entgegen seinem Namen keine Tabellen. Es führt `SQLiteDatabase.delete(table,null,null)` für Index, Now, Next, Schedule, beide FVP-Tabellen, Channelmap-Version und Fast-IP-Now/Next aus. `mappingtable` wird anschließend in `syncOnChannelTableChange()` separat neu aufgebaut. + +**BEWIESEN:** Die Rebuild-Runnables führen kein explizites `VACUUM` aus. + +Interne Rebuild-Aufrufer: + +- Senderlisten-Update-Ende und Installationsabbruch: regulärer `rebuilddb` +- `CHANNEL_INSTALL_COMPLETE`: umfassender `rebuilddbonnewchannellist` + +Das sind Lifecycle-Pfade der Senderinstallation, kein gefundener Benutzer-Reset. + +## 5. Kein Philips-UI-Reset gefunden + +Gefundene EPG-nahe Aktionen öffnen Guide, Planner, Now/Next, Aufnahmeliste oder Aufnahme-/Reminder-Dialoge. Keine Manifest-Activity und kein Service besitzt eine Reset-/Clear-/Rebuild-Aktion für die EPG-Datenbank. + +Auch die Ressourcenquersuche fand keine Benutzerzeichenkette für „EPG-Datenbank zurücksetzen/löschen“. + +Das Umschalten der TV-Guide-Quelle ist kein Ersatz. `tvstrings.apk` warnt ausdrücklich: + +> Wenn Sie zu TV-Guide-Informationen aus dem Internet wechseln, gehen alle Aufnahmen verloren, die Sie mit Informationen der Sendeanstalt aufgenommen haben. Alle Erinnerungen und programmierten Aufnahmen werden abgebrochen. + +Die Gegenrichtung enthält eine entsprechende Warnung. + +**BEWIESEN:** Ein Wechsel Broadcaster ↔ Internet kann Aufnahme-/Timerdaten beeinträchtigen und darf nicht als harmlose Speicherbereinigung behandelt werden. + +## 6. Abhängigkeiten + +### Senderliste + +- Kanonische Quelle: `android.media.tv.TvContract.Channels.CONTENT_URI` +- `indextable` spiegelt Preset-/Senderdaten einschließlich `favoriteNumber`. +- `mappingtable` verbindet `presetid` mit `svlid/svlrecid`. +- Rebuild-Runnables lesen die Senderliste und bauen EPG-Spiegel neu auf; sie löschen die kanonische `TvContract.Channels`-Tabelle nicht direkt. + +### Favoriten + +- Separater Provider: `org.droidtv.tv.provider.favorite` +- Unter anderem `FavoriteList`, Mixed-, Terrestrial-, Cable- und Satellite-Listen +- `EpgDatabase.db` speichert nur Spiegel-/Versionswerte. +- Die Rebuild-Runnables enthalten keinen direkten Delete gegen den Favoritenprovider. + +### Aufnahmen, Erinnerungen und Timer + +Persistenz außerhalb der EPG-Datenbank: + +- Authority `org.droidtv.playtv.provider.recordremind` +- DB `RecordingsRemindersList.db` +- `RecordingsTable` +- `RemindersTable` +- `RecRemView` + +Diese Tabellen stehen weder im EPG-Schema noch in `dropTables()`. + +**BEWIESEN:** Ein EPG-Rebuild löscht diese externen Tabellen nicht direkt. `EpgProgramPump` korreliert Aufnahmen jedoch über `EventId` und `ChannelId` und aktualisiert Metadaten bei EPG-Ereignisänderungen. + +**NICHT VERIFIZIERBAR:** Vollständige Nebenwirkungsfreiheit eines Rebuilds auf Event-ID-Verknüpfung, Darstellung, Folgeaufnahmen oder Scheduler. Externe Persistenz schützt vor direktem Tabellen-Delete, nicht zwingend vor Neuverknüpfungsproblemen. + +## 7. `auto_vacuum` und Dateikompaktierung + +- **BEWIESEN:** In allen vier `channels.apk`-DEX-Dateien fehlen `VACUUM`, `PRAGMA auto_vacuum`, `incremental_vacuum` und `wal_checkpoint`. +- **BEWIESEN:** Die App setzt den Modus nicht selbst und benutzt `SQLiteOpenHelper`/Plattform-SQLite. +- **BEWIESEN, aber nicht gerätespezifisch:** Die AOSP-libsqlite-Buildkonfiguration nutzt mindestens im Android-11-Branch `-DSQLITE_DEFAULT_AUTOVACUUM=1`, also Default Full-Auto-Vacuum. Für Android 12 ist dieselbe Buildstruktur vorhanden. +- **SEHR WAHRSCHEINLICH:** Der Philips-Vendor-Build könnte diese AOSP-Vorgabe übernommen haben. +- **NICHT VERIFIZIERBAR:** Ob TP Vision die Option verändert hat, mit welchem Default die konkrete DB ursprünglich angelegt wurde und welcher Modus im DB-Header steht. + +Folge: Fehlendes appseitiges `VACUUM` beweist nicht automatisch eine riesige Freelist. Bei aktivem Full-Auto-Vacuum kann SQLite freie Endseiten bei Transaktionsende zurückgeben. Umgekehrt kann eine alte, anders angelegte DB den Modus beibehalten. + +Erforderliche lesende Metadaten für eine Entscheidung wären: + +- `auto_vacuum` +- `page_size` +- `page_count` +- `freelist_count` +- möglichst `dbstat` oder eine lesbare DB-Kopie + +Diese Werte sind im unprivilegierten Providervertrag nicht als URI exponiert. + +## 8. Query-Semantik und der Schedule-Hänger + +### `antenna/scheduletable` + +Dieser URI fragt nicht direkt `scheduletable` ab, sondern: + +`indextable AS Dbit LEFT JOIN scheduletable AS Schedule USING(presetid)` + +Bei Projektion `_id` erzeugt der Provider `Dbit.dbitindex AS _id`. Es gibt kein Count und kein Limit. Der Client muss jede Join-Zeile entgegennehmen. + +Darum kann der >120-s-Lauf durch sehr viele Schedule-Zeilen entstehen. Er kann keine genaue Anzahl oder Byteverteilung liefern. + +### Physischer Schedule-Pfad + +`antenna/scheduletablesync` wird auf die physische `scheduletable` gemappt. Dieser Pfad vermeidet den Dbit-Join, besitzt aber keinen dokumentierten Count- oder Limit-Modus. + +Die Tabelle hat Indizes auf `endtime`, `starttime`, `presetid` sowie `(presetid,eventid)`. Enge zeitliche oder presetbezogene Selektionen sind deshalb die risikoärmste Möglichkeit, kleine Stichproben zu lesen. + +### FVP-Pfade + +`fvpscheduletable` und `fvpnownexttable` haben keine `_id`; ihr Schlüssel ist `(channelid,starttime,endtime)`. Der generische Provider-Projektionsrewriter ersetzt `_id` mit `Dbit.dbitindex`, was auf einer direkten FVP-Tabelle ungültig ist und den beobachteten Mappingfehler erklärt. + +Spezialpfade wie `fvpbackwardepg` reichen Projektionen direkter an SQLite weiter. Ein Aggregate-Count könnte dort rein lesend funktionieren, ist aber nicht als offizieller Philips-Count-Vertrag implementiert. + +### Selection, Sortierung, Limit + +- `GetNowNext`, `GetNowNextNoHidden`, `GetSchedule` und `getFastIpNowNextForChannel` sind Provider-Kommandos im Selection-Feld, keine freien Where-Ausdrücke. +- Normale Join-Selektionen werden an Whitespace zerlegt und Unterstriche werden in Punkte umgeschrieben. +- `sortOrder` wird nur auf den Sonderwert `PF_EVENT` geprüft; dieser löst Sync aus und liefert keinen Cursor. +- Interne Querypfade setzen eigene Sortierungen wie `Dbit_presetnumber`, `Schedule_starttime` oder `starttime`. +- Der `limit`-Parameter der abschließenden 8-Argument-`SQLiteDatabase.query()` ist immer `null`. + +**BEWIESEN:** Es gibt keinen generischen, legitimen `LIMIT 1`-Pfad und keinen eigenen Count-Endpunkt. Ein `LIMIT` im Sortstring wäre weder zuverlässig noch eine akzeptable Diagnosemethode. + +Ein Aggregate-Ausdruck `COUNT(*) AS row_count` als Projektion ist je nach URI möglicherweise technisch ausführbar, wird aber auf Join-/generischen Pfaden umgeschrieben oder verworfen. Er ist daher ohne enges Timeout nicht belastbar. Eine eindeutige Schlüssel-Selection liefert garantiert höchstens einen Datensatz, zählt aber die Tabelle nicht. + +## 9. Aktuelle Ursachenbewertung + +### Sicher + +- `EpgDatabase.db` belegt 1,62 GiB und ist der große konkrete Speicherverbraucher. +- Historischer EPG-Churn liegt im Millionenbereich. +- Der als Stale-Cleanup bezeichnete Kernpfad löscht nichts. +- Schedule/FVP sind bisher nicht zuverlässig gezählt. +- Der Schedule-Join ist unbeschränkt und lief länger als 120 Sekunden. +- Appseitiges explizites `VACUUM` fehlt. + +### Sehr wahrscheinlich + +- Der No-op-Cleanup ermöglicht das langfristige Anwachsen abgelaufener Schedule-Daten. +- Schedule und eventuell FVP enthalten einen erheblichen lebenden Datenbestand oder verursachen zumindest den dominanten Abfrageaufwand. +- Die Philips-Speicherwarnung wird durch die große EPG-Datei ausgelöst beziehungsweise wesentlich verstärkt. + +### Offen + +- Lebende Bytes je Tabelle +- Freelist-/Fragmentierungsanteil +- tatsächlicher Auto-Vacuum-Modus +- WAL-/Journalbeitrag +- exakte Rebuild-Nebenwirkungen auf Aufnahme-/Timer-Verknüpfungen + +## 10. Risikoarme weitere Strategie + +Weiterhin ausschließlich lesend: + +1. Keine weitere unbeschränkte Ausgabe des `scheduletable`-Joins starten. +2. Schedule nur über `scheduletablesync` in sehr engen, indexgestützten Zeit- oder Presetfenstern beproben und jeden Clientlauf kurz begrenzen. +3. FVP nur über seine tatsächlichen Schlüsselspalten beziehungsweise geeignete Spezialpfade untersuchen; `_id` nicht verwenden. +4. Aggregate-Projektionen nur als kurz begrenztes, rein lesendes Experiment auf Pfaden erwägen, die Projektionen statisch unverändert weiterreichen. Ein Fehlschlag darf nicht durch SQL-Injection umgangen werden. +5. Nach Möglichkeit den DB-Header beziehungsweise `auto_vacuum`, `page_count`, `freelist_count` und `page_size` rein lesend beschaffen. Ohne diese Werte keine Aussage „überwiegend Freelist“ oder „überwiegend lebende Zeilen“ treffen. +6. Vor jeder späteren mutierenden Maßnahme Senderlisten-, Favoriten-, Aufnahme- und Timer-Abhängigkeiten erneut prüfen und eine ausdrückliche Freigabe einholen. + +Nicht ohne Freigabe ausführen: + +- `staledatacleanup`, `rebuilddb` oder `rebuilddbonnewchannellist` +- App-Daten-/Cache-Löschung +- Guide-Quellenwechsel +- Datenbank-, Datei- oder Paketmanipulation + +## Belege + +- `diagnostics/deep-forensics/philips/epg-reverse-agent.md` +- `diagnostics/deep-forensics/philips/epg-dex-context.txt` +- `diagnostics/deep-forensics/philips/epg-runnable-context.txt` +- `diagnostics/deep-forensics/philips/epg-cleanup-context.txt` +- `diagnostics/deep-forensics/philips/epg-stale-context.txt` +- `diagnostics/deep-forensics/philips/epg-recording-deps-context.txt` +- `diagnostics/deep-forensics/philips/epg-delete-rebuild-string-context.txt` +- `diagnostics/deep-forensics/philips/channels-manifest.txt` +- `diagnostics/deep-forensics/philips/tvstrings-epg-matches.txt` +- `diagnostics/raw/storage/epg-content-evidence.txt` +- `diagnostics/bugreport/extracted/files/01790__bugreport-PH9M_EA_5599-STT2.230526.001-2026-08-25-13-26-44.txt` diff --git a/diagnostics/phase4/epg-database-forensics.md b/diagnostics/phase4/epg-database-forensics.md new file mode 100644 index 0000000..0f0da17 --- /dev/null +++ b/diagnostics/phase4/epg-database-forensics.md @@ -0,0 +1,123 @@ +# EPG-Datenbank-Forensik + +## Kurzfazit + +`EpgDatabase.db` ist 1,62 GiB groß, weil Philips seit Februar 2023 nahezu alle +Schedule-Ereignisse akkumuliert hat. Die Datei enthält **3.071.250 reale +`scheduletable`-Zeilen**. Zum Snapshotzeitpunkt waren **100 % abgelaufen**; +97,81 % waren älter als 30 Tage und 79,03 % älter als ein Jahr. Es handelt +sich nicht um eine große Freelist und nur zu 0,91 % um exakte Dubletten. + +Das bestätigt die bereits statisch rekonstruierte Ursache: Der vorgesehene +Philips-Stale-Cleanup wird gestartet und protokolliert einen Grenzwert, führt +im Runnable aber keine SQLite-/Provider-Löschung aus. **CONFIRMED / VALIDATED +LIVE plus Offline-DB-Beweis.** + +## Konsistenter Snapshot + +Original: +`/data/user/0/org.droidtv.channels/databases/EpgDatabase.db` + +- Prozess: `org.droidtv.channels`, PID 7267/31859 je Prozessneustart; +- Connection-Pool: eine Primary-Connection, beim Precheck read-only und nicht + acquired; Rollback-Journalmodus, kein WAL/SHM; +- Journal vor und nach akzeptierter Kopie: 0 Byte; +- Originalgröße/inode/mtime während Kopie und beider Hashläufe unverändert; +- Original, adopted-USB-Kopie und PC-Kopie: + `71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba`. + +Ein erster Versuch wurde wegen gleichzeitig geändertem Original und +Hash-Mismatch verworfen. Der zweite Lauf erfüllte alle Gates. Die Original-DB +wurde nie geschrieben. + +## SQLite-Metadaten + +| PRAGMA/Check | Ergebnis | +|---|---:| +| `page_size` | 4.096 B | +| `page_count` | 425.306 | +| Dateigröße | 1.742.053.376 B = 1.661,35 MiB | +| `freelist_count` | **0** | +| `auto_vacuum` | **1 / FULL** | +| `journal_mode` | `delete` | +| `user_version` | 6 | +| `schema_version` | 23 | +| `quick_check` | `ok` | +| `integrity_check` | `ok` | + +Die frühere offene Freelist-/Auto-Vacuum-Frage ist damit geschlossen. Die +Datei ist intakt und enthält keine freie Seitenreserve. FULL-Auto-Vacuum ist +im persistenten DB-Header direkt bewiesen. + +## Zeilen und Seiten + +| Objekt | Typ | Seiten | MiB | Zeilen | +|---|---|---:|---:|---:| +| `scheduletable` | Tabelle | 367.868 | 1.436,98 | 3.071.250 | +| `indexschedendtime` | Index | 16.827 | 65,73 | – | +| `indexschedstarttime` | Index | 16.818 | 65,69 | – | +| `indexschedeventid` | Index | 13.183 | 51,50 | – | +| `indexschedpresetid` | Index | 10.019 | 39,14 | – | +| alle übrigen Objekte zusammen | Tabelle/Index | ca. 76 | <0,3 | 919 + Metadaten | + +Schedule-Tabelle plus vier Indizes belegen rund **1.659,04 MiB** und damit +praktisch die ganze 1.661,35-MiB-Datei. Die etwa 2,03-MiB-Differenz besteht +vorwiegend aus FULL-Auto-Vacuum-Pointer-Map-/sonstigen Nicht-`dbstat`-Seiten. + +Die Schedule-Tabellen-Payload beträgt 1.079,65 MiB. Allein `shortinfo` +enthält 961.046.485 Zeichen (ca. 916,5 Mi-Zeichen), `eventname` 67.158.355. +`extendedinfo` und `guidanceinfo` sind in diesem Bestand leer. Teilweise +unbenutzter Platz innerhalb aktiver B-Tree-Seiten ist keine Freelist. + +## Altlasten + +Cutoff war der Original-mtime-Zeitpunkt `1787680625` (2026-08-25). + +| Altersklasse nach `endtime` | Zeilen | Anteil | +|---|---:|---:| +| aktuell/zukünftig | 0 | 0 % | +| abgelaufen 0–1 Tag | 1 | <0,001 % | +| 1–7 Tage | 8.887 | 0,289 % | +| 7–30 Tage | 58.280 | 1,898 % | +| 30–365 Tage | 576.718 | 18,778 % | +| älter als 365 Tage | 2.427.353 | 79,035 % | +| Zeitwert 0 | 11 | <0,001 % | + +Nichtnull-Daten reichen von 2023-02-08 bis 2026-08-24. Es gibt 196 Presets; +die größten halten je 30.000–63.888 historische Events. Alle Schedule-Presets +existieren in `indextable`: **0 orphan rows**. + +Logische Schlüssel-Dubletten `(presetid,eventid,starttime,endtime)` erklären +28.001 überzählige Zeilen; exakte Vollzeilen-Dubletten 27.974, jeweils nur +0,91 %. Der dominante Fehler ist daher fehlende Retention, nicht Duplikation. + +## Abgleich mit dem Cleanup-Code + +Live um 19:57/19:59: + +- `Posting stale data deletion after 2 mins in onCreate`; +- `deleteStaleDataRunnable: Triggering stale data cleanup`; +- `Deleting stale data for time 1787680743361`. + +Der geloggte Wert ist Millisekunden-nahe, während DB-Zeiten Sekunden sind. +Entscheidender ist der statische Bytecodebeweis: Das Runnable enthält keinen +`SQLiteDatabase.delete/execSQL`, `ContentResolver.delete/applyBatch` oder +Aufruf einer echten Löschmethode. Zwei Minuten später war die 3,07-Mio.-Zeilen- +Historie weiterhin vollständig vorhanden. Das ist die direkte Ursache des +langjährigen Wachstums. + +## Was wäre rückgewinnbar? + +- Echte Live-Nutzlast in `scheduletable`: zum Cutoff **0 zukünftige/aktuelle + Zeilen**; Now/Next sind separat klein. +- Historische Altlast: praktisch die gesamten 1.437 MiB Tabelle plus 222 MiB + Schedule-Indizes. +- Gelöscht, aber nicht ans Dateisystem zurückgegeben: **0 Freelist-Seiten**. +- Fragmentierung/unused auf aktiven Seiten: vorhanden, aber sekundär. + +Eine tatsächliche Bereinigung wird hier bewusst **nicht** ausgeführt. Bei +FULL-Auto-Vacuum kann ein millionenfaches `DELETE` erhebliche Page-Moves, +Journaling und eMMC-Schreiblast auslösen. Ein app-kontrollierter Neuaufbau kann +konzeptionell write-ärmer sein, muss aber Sender-/Favoriten-/Aufnahme-/Timer- +Abhängigkeiten und den exakten Philips-Workflow berücksichtigen. + diff --git a/diagnostics/phase4/usb-volume-uid1000-analysis.md b/diagnostics/phase4/usb-volume-uid1000-analysis.md new file mode 100644 index 0000000..cd07249 --- /dev/null +++ b/diagnostics/phase4/usb-volume-uid1000-analysis.md @@ -0,0 +1,49 @@ +# USB-/Adopted-Storage-Analyse unter UID 1000 + +## Live-Messung + +| Volume | Philips `mTotalSize` | Philips `mFreeSize` | UID1000 `df` frei | +|---|---:|---:|---:| +| Public FAT `/storage/A5F7-1E0B` | 62.495.129.600 B | 0 | 44.214.720 KiB | +| Adopted private `/mnt/expand/0ca9…60fc` | 123.060.879.360 B | 0 | 116.529.660 KiB | + +UID1000 kann beide Pfade statten und mit `df` korrekt messen. Es existiert in +ContentExplorer, EPGData, Settings oder TPTvService keine gefundene private +Volume-Datenbank, die den Nullwert persistiert. **A und B sind damit stark +widerlegt; C als allgemeines Dateisystem-Permissionproblem ist widerlegt.** + +## Konkreter Codepfad + +`/system/framework/tvfw.jar`, SHA-256 +`a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad`, +wurde lokal read-only disassembliert. In +`UsbVolumeListerImpl.getVolumes(String)`: + +1. `IStorageManager.getVolumes()` liefert korrekte `VolumeInfo`-Objekte. +2. `mRawDiskSize` wird aus `DiskInfo.size` gesetzt; daher sind Totalwerte korrekt. +3. `mFreeDiskSize` wird **nur** aus `DeviceManager.getMountPoint(...).mFreeSize` + gesetzt, wenn `SystemProperties.get("sys.mtk.dmremoteservice.ready")` + exakt `"1"` ist. +4. Live ist `sys.mtk.dmremoteservice.ready` leer. Vorhanden ist stattdessen + `vendor.mtk.dmremoteservice.ready=0`; der HAL-Prozess + `vendor.mediatek.tv.mtkdmservice@1.0-service` läuft. +5. Der Initialwert des Java-`long` bleibt deshalb 0 und wird so geloggt. + +Beim adopted Volume ist `VolumeInfo.internalPath=null`, aber der Code nimmt es +trotzdem in die Liste auf. Das erklärt einen zusätzlichen Pfadmangel für D. +Da selbst das public Volume mit gültigem `path` und `internalPath` denselben +Nullwert erhält, ist die gemeinsame Property-/DeviceManager-Gate-Logik der +entscheidende Fehler. + +## Klassifikation A–E + +| Hypothese | Urteil | +|---|---| +| A: nur zur Laufzeit falsch | **CONFIRMED** | +| B: Nullwert in DB/Preference gespeichert | **DISPROVED** im inventarisierten relevanten Privatbestand | +| C: StatFs-/Pfad-Permission unter UID1000 | **DISPROVED** | +| D: falscher/fehlender Pfad | **PARTIALLY CONFIRMED** nur für adopted `internalPath`; erklärt public nicht | +| E: gemeinsame Firmwarebibliothek | **CONFIRMED**: Fehler liegt in `tvfw.jar`-`UsbVolumeListerImpl` | + +Keine Property wurde verändert und kein Dienst neu gestartet. + diff --git a/diagnostics/phase5/adopted-app-move-matrix.md b/diagnostics/phase5/adopted-app-move-matrix.md new file mode 100644 index 0000000..4e1297c --- /dev/null +++ b/diagnostics/phase5/adopted-app-move-matrix.md @@ -0,0 +1,24 @@ +# Adopted-storage app move matrix + +Live state 25.08.2026: install location `0[auto]`; no package code path appears +under `/mnt/expand/.../app`. All listed update APKs are currently in internal +`/data/app`; their base packages remain system/product/vendor apps. + +| Package | Current code | Kind | installLocation evidence | Official move? | Reason | +|---|---|---|---|---|---| +| `com.google.android.gms` | `/data/app` update; base `/system/product/priv-app` | updated privileged system app | manifest value not exposed; global auto | **No** | PackageManager blocks system packages; data stays internal | +| `com.netflix.ninja` | `/data/app` update; base `/vendor/app` | updated system app | not exposed | **No** | still flagged SYSTEM/UPDATED_SYSTEM_APP | +| `com.google.android.webview` | `/data/app` update; product base | updated system app | not exposed | **No** | system WebView is boot/runtime critical | +| `com.google.android.tts` | `/data/app` update; product base | updated system app | not exposed | **No** | SYSTEM/UPDATED_SYSTEM_APP | +| `com.google.android.youtube.tv` | `/data/app` update; product base | updated system app | not exposed | **No** | SYSTEM/UPDATED_SYSTEM_APP | +| `com.android.vending` | `/data/app` update; privileged product base | updated privileged system app | not exposed | **No** | system/privileged package | +| `org.droidtv.channels` | `/system/priv-app` | Philips privileged system app | fixed system | **No** | code and shared UID/system integration; app data internal | +| `org.droidtv.gel` | `/system/app` | Philips system app | fixed system | **No** | system/shared UID integration | +| `org.droidtv.playtv` | `/system/app` | Philips system app | fixed system | **No** | system TV component | +| `org.droidtv.epgdata` | `/system/priv-app` | Philips privileged system app | fixed system | **No** | privileged TV component | + +This is a capability conclusion, not a move attempt. `pm move-package` was not +executed. Ordinary non-system apps that declare/permit external placement may +be movable through Android, but none of the dominant listed updated-system +packages is a safe candidate. Even a movable APK does not imply every app-data +directory or device-protected data moves. diff --git a/diagnostics/phase5/adopted-storage-analysis.md b/diagnostics/phase5/adopted-storage-analysis.md new file mode 100644 index 0000000..d52eb14 --- /dev/null +++ b/diagnostics/phase5/adopted-storage-analysis.md @@ -0,0 +1,51 @@ +# Adopted USB architecture + +## Confirmed live layout + +- `disk:8,0` is a 123.060.879.360-byte adoptable USB disk. +- `private:8,2` is mounted ext4 at + `/mnt/expand/0ca93141-9ebf-42fa-9123-dfb75fff60fc` through `/dev/block/dm-4`; + free 114.828.268 KiB. Device-mapper plus vold's private-volume model confirms + an encrypted adopted mapping; the exact cipher/key metadata is not readable + to shell and remains **UNKNOWN**. +- The volume is Android `PRIVATE`, and its UUID is the **primary storage UUID**. + Its `media/` directory backs the primary emulated storage through sdcardfs and + FUSE at `/storage/emulated/0`. +- `/data` remains `/dev/block/mmcblk0p37`, ext4, 5.089.920 KiB. Therefore a + 112-GiB `/storage/emulated` does not enlarge internal `/data`. +- The adopted root contains standard `app`, `user`, `user_de`, `media`, `misc` + directories, but no package code path currently resolves below its `app`. +- A second `disk:8,16` is public VFAT (`A5F7-1E0B`). + +## Options for EPG placement + +| Variant | Classification | Main issues | +|---|---|---| +| App patch using an app-private adopted `Context` | **POSSIBLE WITH PATCH; preferred selective experiment** | Channels is a system/shared-UID app and must choose a private volume explicitly; boot/unmount fallback, migration and locking required | +| DB path directly under `/mnt/expand//user...` | **POSSIBLE WITH PATCH, brittle** | volume UUID/readiness, sandbox/fscrypt/SELinux paths and OTA compatibility | +| Bind-mount only EPG DB directory | **HIGH-RISK HACK** | root/init, vold timing, mount namespace, SELinux, USB loss, recovery | +| Symlink | **NOT PRACTICAL** | Android data-directory traversal/SELinux and emulated storage SQLite locking; FUSE target is wrong | +| init mount unit for EPG | **HIGH-RISK HACK** | creates early-boot/vold dependency and recovery complexity | +| Move all Channels appdata | **POSSIBLE WITH PLATFORM PATCH, not recommended** | boot-critical shared UID consumers, DE/CE split, failures affect TV UI | + +Never put SQLite on `/storage/emulated`/FUSE: use the private ext4 view, preserve +atomic rename/fsync and fail closed to a small internal DB when USB is absent. + +## Entire `/data` on USB + +**NOT PRACTICAL / unacceptable boot risk.** `/data` is mounted by early init +before vold adopts USB; FBE/fscrypt user setup, SELinux labels, zygote, Package +Manager and recovery depend on it. USB enumeration/power/UUID failure would +prevent a normal boot. Required fstab/init/recovery/format/OTA changes intersect +locked AVB and static partitions. The boot log already shows transient zygote +failures while the adopted `user` path was not yet present—evidence that making +the whole boot depend on USB would worsen a real ordering edge. + +## Ranking + +1. Keep `/data` internal and repair EPG/GEL leaks – best reliability/OTA/boot. +2. Selectively place large, reconstructible, non-boot-critical payloads on + private adopted ext4 with an internal fallback. +3. Android-supported movement of genuinely movable user apps – low risk but + little gain here. +4. Entire `/data` or bind-mount hacks – worst recovery and boot safety. diff --git a/diagnostics/phase5/epg-dependency-matrix.md b/diagnostics/phase5/epg-dependency-matrix.md new file mode 100644 index 0000000..f882f8d --- /dev/null +++ b/diagnostics/phase5/epg-dependency-matrix.md @@ -0,0 +1,54 @@ +# Phase 5A – EPG-Abhängigkeiten vor Reparatur + +Quelle ist ausschließlich der verifizierte Snapshot mit SHA-256 +`71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba`. + +## Tabellenmatrix + +| Tabelle | Zeilen | Größe | Schlüssel/Indizes | Zweck und Abhängigkeit | Kandidat | +|---|---:|---:|---|---|---| +| `android_metadata` | 1 | 4 KiB | keine PK/FK | SQLite-Locale; keine TV-Nutzdaten | exakt übernehmen | +| `channelmapversiontable` | 0 | 4 KiB | `id` PK | Versionscache für Sender-/Favoritenspiegel | exakt übernehmen (leer) | +| `indextable` | 358 | 24 KiB | `dbitindex` PK; `(satnum,sid,tsid,onid)`, `(presetid,presetnumber)` | EPG-interner Spiegel von Sender/Preset, Name, `favoriteNumber`, `svlid/svlrecid`; kanonische Sender liegen außerhalb | **exakt übernehmen** | +| `mappingtable` | 203 | 4 KiB | `mappingid` PK; `(svlid,svlrecid)` | Preset-zu-SVL-Mappingcache | **exakt übernehmen** | +| `nowtable` | 195 | 84 KiB plus 2 kleine Indizes | `_id` PK | reines Now-EPG; alle Werte im Snapshot veraltet | leeren | +| `nexttable` | 162 | 76 KiB plus 2 kleine Indizes | `_id` PK | reines Next-EPG; alle Werte veraltet | leeren | +| `scheduletable` | 3.071.250 | 1.436,98 MiB plus 222,06 MiB Indizes | `_id` PK; End-/Startzeit, Preset, `(presetid,eventid)` | ausschließlich Broadcast-Schedule; 100 % abgelaufen | **leeren** | +| `fastipnownexttable` | 0 | 4 KiB | `_id` PK | Fast-IP Now/Next | leer lassen | +| `fvpnownexttable` | 0 | 4 KiB plus Auto-/Sekundärindex | PK `(channelid,starttime,endtime)` | FVP Now/Next | leer lassen | +| `fvpscheduletable` | 0 | 4 KiB plus Auto-/Sekundärindex | PK `(channelid,starttime,endtime)` | FVP Schedule | leer lassen | + +## Relationen und persistente TV-Daten + +- `PRAGMA foreign_key_list` ist für **jede Tabelle leer**. +- Es gibt **keine Trigger und keine Views**. +- Die kanonische Senderliste ist `TvContract.Channels` in einem separaten + Provider. `indextable` ist nur Spiegel/Cache, wird aber vorsichtshalber exakt + erhalten. +- Favoriten liegen in `org.droidtv.tv.provider.favorite`; EPG speichert nur + Spiegelwerte wie `favoriteNumber`. +- Aufnahmen/Timer/Reminder liegen in separater + `/data/vendor/user_setting/rw/RecordingsRemindersList.db` mit externem + Provider. Keine ihrer Tabellen kommt im EPG-Schema vor. +- Der Philips-Rebuild liest die Senderquelle neu und löscht die externen + Sender-/Favoriten-/Recordingtabellen nicht direkt. +- Event-ID-Verknüpfungen können während einer leeren EPG-Phase vorübergehend + keine Metadaten auflösen. Im Snapshot existiert aber ohnehin **kein einziges + aktuelles oder zukünftiges Schedule-Event**. + +## Öffnen, Erstellen und Rebuild + +`EpgDatabaseHelper` erbt `SQLiteOpenHelper`, Dateiname `EpgDatabase.db`, +Version 6. `createTables()` legt exakt die obigen Tabellen und Indizes an. +`dropTables()` ist irreführend benannt und führt große zeilenweise +`SQLiteDatabase.delete(...,null,null)`-Operationen aus. Der +`rebuilddbonnewchannellist`-Pfad leert EPG-Tabellen und baut Index/Mapping aus +`TvContract.Channels` neu. Ein echtes Datei-Neuanlegen oder `VACUUM` ist dort +nicht implementiert. + +**Ergebnis:** Eine schemaidentische kompakte DB mit erhaltenem +`indextable`/`mappingtable` und leeren EPG-Payloadtabellen zerstört keine +kanonische Sender-, Favoriten-, Aufnahme- oder Timerpersistenz. Dieses Urteil +ist für direkte Tabellenlöschung **CONFIRMED**; visuelle und Event-ID-bezogene +Folgewirkungen bleiben bis zur Live-Prüfung `USER VALIDATION REQUIRED`. + diff --git a/diagnostics/phase5/epg-repair-options.md b/diagnostics/phase5/epg-repair-options.md new file mode 100644 index 0000000..2b92292 --- /dev/null +++ b/diagnostics/phase5/epg-repair-options.md @@ -0,0 +1,37 @@ +# Phase 5B – Vergleich der EPG-Reparaturstrategien + +| Methode | eMMC-Schreiblast / Peak-Space | Datenrisiko | Rollback | Gewinn / EPG-Neuladen | Urteil | +|---|---|---|---|---|---| +| A: `DELETE` von 3.071.250 Zeilen | sehr hoch: Indexupdates, FULL-AutoVacuum-Page-Moves und DELETE-Journal; Peak potenziell hunderte MiB bis problematisch nahe aktuellem Freiraum | geringe semantische Selektivität bei korrektem Cutoff, aber lange Transaktion/Lock/Stromausfallrisiko | Originalsnapshot vorhanden, Live-Rollback teuer | etwa 1,6 GiB; App behält Schema | **nicht empfohlen** | +| B: `DROP/CREATE scheduletable` + Indizes | deutlich weniger SQL-Operationen als A, aber DDL/Journaling und FULL-AutoVacuum auf der 1,6-GiB-Datei; App besitzt keinen validierten Live-DDL-Endpunkt | nur Schedule, aber Schema-/Connectioncache- und offener-DB-Risiko | Snapshot; eigenes DDL fehleranfällig | etwa 1,6 GiB; Nachladen wahrscheinlich | möglich, unnötig komplex | +| C: kompakte Ersatz-DB | intern nur ca. 160 KiB neue Datei plus atomarer Rename; kein zweiter 1,6-GiB-Stand auf `/data` | behält Senderindex/Mapping exakt, leert nur EPG-Payload; Owner/Label/Open-Connection müssen stimmen | hashgeprüfter PC- und USB-Snapshot; atomarer Rücktausch möglich | erwartet 1.701.068 KiB; leerer Now/Next/Schedule erzwingt frische Befüllung | **eindeutige Empfehlung** | +| D: Philips `rebuilddbonnewchannellist` | sehr hoch: intern mehrere `SQLiteDatabase.delete` über Millionen Zeilen plus FULL-AutoVacuum | kanonische Sender/Favoriten nicht direkt gelöscht, aber Mapping komplett rekonstruiert; Eventverknüpfungsrisiko | Snapshot, aber laufender Appworkflow schwerer zu kontrollieren | wahrscheinlich ähnlich, jedoch unnötige Writes | offiziell app-kontrolliert, aber für verschlissene eMMC nicht optimal | + +## Empfehlung + +**Methode C** ist bei diesem konkreten Datenbestand schreibärmster, +rückrollbarster und semantisch konservativster. Die Kandidaten-DB wird aus dem +Originalschema erstellt, übernimmt ausschließlich: + +- `android_metadata`; +- `channelmapversiontable`; +- `indextable` inklusive Sender-/Favoritenspiegel; +- `mappingtable`. + +Now/Next/Schedule/FVP/FastIP bleiben leer. Das ist konservativer als ein +vollständiger Philips-Rebuild, weil die bestehenden Sender- und +Preset-Spiegelzeilen unverändert bleiben. + +## Lokal validierter Kandidat + +- Größe: **159.744 B / 39 Seiten**; +- SHA-256: `1ae978c7ca9333c6a0528ad535a732f116deb7213321e9d0bd9c0d377a25aa5b`; +- exakt gleiche `sqlite_master`-Objekte und SQL-Definitionen; +- `page_size=4096`, `auto_vacuum=FULL`, `journal_mode=delete`, `user_version=6`; +- alle vier übernommenen Tabellen besitzen identische kanonische Zeilenhashes; +- `quick_check=ok`, `integrity_check=ok`, `foreign_key_check=[]`; +- alle Foreign-Key-Listen leer; keine Trigger/Views. + +Erwarteter unmittelbarer Dateigewinn: **1.741.893.632 B = 1.701.068 KiB = +1.661,20 MiB = 1,6223 GiB**. + diff --git a/diagnostics/phase5/epg-repair.md b/diagnostics/phase5/epg-repair.md new file mode 100644 index 0000000..c81c5fa --- /dev/null +++ b/diagnostics/phase5/epg-repair.md @@ -0,0 +1,120 @@ +# Phase 5 – kontrollierte EPG-Reparatur + +Stand: 2026-08-25, Live-Austausch und technische Sofortvalidierung abgeschlossen. + +## Gewählte Methode + +**Methode C: atomarer Austausch gegen eine offline gebaute kompakte DB.** Das +Schema, alle Indizes und `user_version=6` sind identisch. Die persistenten +Tabellen `android_metadata`, `indextable` und `mappingtable` wurden zeilengleich +übernommen; `channelmapversiontable` war und bleibt leer. Ausschließlich die +EPG-Nutzlasttabellen (Schedule, Now/Next, FVP und FastIP) beginnen leer. + +Diese Methode vermeidet 3.071.250 Einzel-Deletes, die Auto-Vacuum- +Seitentransporte und eine große temporäre Schreiblast. Der Austausch erfolgt +erst nach Prozess-Stopp und erneuter Hash-/DAC-/SELinux-Prüfung über eine +159.744-Byte-Temporärdatei im originalen Datenbankverzeichnis. + +## BEFORE + +- `/data`: 5.089.920 KiB gesamt, 3.613.240 KiB benutzt, 1.355.544 KiB frei, + 73 % belegt. +- Original: 1.742.053.376 Byte, Inode 131613, Owner/Group `system:system`, + Mode `660`, Label `u:object_r:system_app_data_file:s0`. +- Original-SHA-256: + `71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba`. +- Schedule: 3.071.250 Zeilen; zum Snapshotzeitpunkt vollständig abgelaufen. +- Journal: vorhanden, 0 Byte; kein offener Channels-Prozess und kein durch + `lsof` sichtbarer EPG-Handle im abschließenden Preflight. + +## Rollback-Gates + +| Gate | Ergebnis | +|---|---| +| PC-Snapshot | PASS – Größe und SHA-256 verifiziert | +| adopted-storage-Snapshot | PASS – gleicher SHA-256 | +| Original integer | PASS – `integrity_check` und `quick_check` = `ok` | +| Kandidat lokal getestet | PASS – Schema identisch, FK-Check leer | +| Abhängigkeiten | PASS – Sender/Favoriten und Recordings/Reminder extern; benötigte Mappings erhalten | +| Prozess stoppbar/offene Verbindung | PASS – Preflight ohne PID/Handle; vor Commit erneut geprüft | +| Owner/Group/Mode/Label | PASS – bekannt; Tempdatei muss exakt entsprechen | +| freier Peak-Speicher | PASS – Kandidat benötigt nur 159.744 Byte intern | +| Rücksicherung | PASS – vorab definierter, hash-gegatterter atomarer Restore vom USB-Snapshot | + +Alle 14 vom Auftrag geforderten Gates sind erfüllt. Bei jeder Abweichung +bricht das Swap-Skript vor dem Rename ab und entfernt nur seine exakt benannte +Temporärdatei. + +## ACTION + +Ausgeführt: `org.droidtv.channels` kontrolliert gestoppt; PID-Freiheit geprüft; +Original- und Kandidatenhash erneut prüfen; Kandidat in dasselbe Verzeichnis +kopieren; Mode, Owner, Group, SELinux-Label und Hash prüfen; unmittelbar vor +dem atomaren `mv` nochmals PID-Freiheit prüfen; danach `sync` und Live-Hash. + +## AFTER + +- `COMMIT_OK`; neue DB 159.744 Byte, Inode 140395, `system:system`, Mode 660, + Label `u:object_r:system_app_data_file:s0`. +- Commit-SHA-256 entsprach exakt dem offline geprüften Kandidaten: + `1ae978c7ca9333c6a0528ad535a732f116deb7213321e9d0bd9c0d377a25aa5b`. +- Nach normalem Providerstart führte Channels den erwarteten Mapping-Sync aus; + die konsistente Nachkopie hat SHA-256 + `67f8834a7e56d3b84b47f0c75b3781703915b0d1f93e93c42312c69796e6cb36`. +- SQLite: `quick_check=ok`, `integrity_check=ok`, FK-Check leer, Version 6, + 39 Seiten, Freelist 0, Auto-Vacuum FULL. +- Zeilen: Schedule 0; Now/Next 0; `indextable` 358 und `mappingtable` 203. +- Zwei konsistente Nachmessungen mehrere Minuten auseinander waren bytegleich. +- Die unabhängigen Hashes von `RecordingsRemindersList.db`, `tv.db` und + `list.db` blieben gegenüber dem Preflight unverändert. + +### Speicher + +| Messwert | Vorher | Nachher | Differenz | +|---|---:|---:|---:| +| `/data` frei (`df -k`) | 1.355.512 KiB | 3.056.912 KiB | **+1.701.400 KiB = 1.661,52 MiB = 1,62258 GiB** | +| `/data` Belegung | 73 % | 39 % | −34 Prozentpunkte | +| EPG-Datei | 1.742.053.376 B | 159.744 B | **−1.741.893.632 B = 1.701.068 KiB = 1.661,20 MiB** | +| Schedule-Zeilen | 3.071.250 | 0 | −3.071.250 | + +## ROLLBACK + +`phase5-epg-rollback.json` stellt ausschließlich die EPG-DB aus dem +verifizierten USB-Snapshot über eine neue Tempdatei und atomaren Rename wieder +her. Auch dieser Pfad prüft PID, Hash, Mode, Owner/Group und SELinux-Label vor +dem Commit. Er wird nur bei fehlgeschlagener Funktionsvalidierung ausgeführt. + +## VALIDATION + +**VALIDATED LIVE (softwareseitig):** ADB blieb erreichbar; WLAN blieb +`CONNECTED/VALIDATED`; Android meldet internes und adopted Volume `NORMAL`; +Channels, EPGData, PlayTV und TVInput liefen. Der Provider öffnete die DB und +führte erfolgreiche Queries sowie den Sync von 358 Channels/203 Mappings aus. +Keine SQLite-Exception, Korruptionsmeldung, ANR oder Crashloop trat auf. Der +einzige neue Exit war der dokumentierte `USER REQUESTED` Force-Stop. + +Der nach zwei Minuten geloggte Philips-Stale-Cleanup lief erneut und blieb ohne +Mutation – reproduzierbarer Beleg für BUG-001. Im Einstellungsmodus loggte der +Sync zunächst `Mode Guide/WatchTV: false`. Nach echtem TV-/Guide-Betrieb wuchs +die DB kontrolliert auf 806.912 Byte. Die dritte konsistente Momentaufnahme +enthielt 59 Now-, 48 Next- und 1.041 Schedule-Zeilen; Schedule reichte lokal vom +25.08.2026 19:00 bis 01.09.2026 02:50. `integrity_check=ok`, 197 Seiten, +Freelist 0. Aktuelle EPG-Neubefüllung ist damit **VALIDATED LIVE**. + +Der normale kanonische Sync reduzierte den internen Spiegel im späteren +Snapshot von 358/203 auf 357/202 Index-/Mapping-Zeilen. Da der Kandidat anfangs +exakt 358/203 enthielt und die App selbst den späteren Sync ausführte, ist dies +kein Beleg für blind gelöschte Senderdaten; die sichtbare Senderliste bleibt +dennoch zu prüfen. + +Folgende Punkte bleiben **USER VALIDATION REQUIRED**: + +1. Live-TV-Bild/Ton und Senderwechsel prüfen. +2. Senderliste und Favoriten öffnen und stichprobenartig vergleichen. +3. Im bereits neu befüllten Guide Now/Next und zukünftige Sendungen visuell + stichprobenartig prüfen. +4. Vorhandene Aufnahmen/Timer/Reminder anzeigen (nichts neu programmieren nötig). +5. Prüfen, ob die Speicherwarnung verschwunden ist. + +USB-Snapshot und vorab geprüfte Rollback-Konfiguration bleiben bis zum Abschluss +dieser Sichtprüfung erhalten. diff --git a/diagnostics/phase5/phase5-summary.md b/diagnostics/phase5/phase5-summary.md new file mode 100644 index 0000000..cc7be7e --- /dev/null +++ b/diagnostics/phase5/phase5-summary.md @@ -0,0 +1,22 @@ +# Phase 5 summary + +**Repair: VALIDATED LIVE (software), visual UI validation pending.** A +schema-identical compact DB preserved 358 channel mirror and 203 mapping rows, +removed 3,071,250 expired schedule rows and recovered 1,701,400 KiB according +to `df`. `/data` changed from 73% to 39% used. + +After real TV/Guide activity the healthy DB grew to 806,912 bytes with 59 Now, +48 Next and 1,041 Schedule rows spanning 2026-08-25 19:00 through 2026-09-01 +02:50 local time. Current/future EPG reacquisition is proven. + +Confirmed functional vendor bugs: ineffective EPG stale cleanup, pathological +GEL WorkManager history, USB free-size zero. The Philips warning threshold is a +design issue; the GEL command path is a separate confidential security report. + +Storage conclusion: adopted USB is valid private ext4 and primary emulated +storage, but it does not enlarge `/data`; no package is currently installed on +it. Keep `/data` internal. Prefer leak fixes and, only in a future patched +firmware, selective private-volume placement with an internal fallback. + +Root was not required for the repair. Root/OEM signing/unlock is required only +for APK/JAR/system/SELinux/init/AVB changes or mount hacks. diff --git a/diagnostics/phase7/commands.md b/diagnostics/phase7/commands.md new file mode 100644 index 0000000..3daf514 --- /dev/null +++ b/diagnostics/phase7/commands.md @@ -0,0 +1,63 @@ +# Phase 7 command record + +Date: 2026-08-26. TV operations were read-only. `adb pull` copied files to the +PC and did not change the TV. No install/uninstall/reboot/recovery/flash/remount/ +settings write was issued. + +## Live ADB command families + +Target was always `:5555`: + +```text +adb devices -l +adb shell pm path +adb shell dumpsys package +adb shell dumpsys package android +adb shell dumpsys package packages +adb shell cmd package help +adb shell service list +adb shell ps -A -o USER,PID,PPID,NAME,ARGS,LABEL +adb shell getprop +adb shell mount +adb shell cat /proc/cmdline +adb shell cat /proc/self/mountinfo +adb shell ls -laZ +adb shell stat +adb shell sha256sum +adb shell dumpsys dbinfo org.droidtv.channels -v +adb shell df -k /data +``` + +Targeted local forensic acquisition: + +```text +adb pull /system/priv-app/oad/oad.apk diagnostics/phase7/original-apks/oad.apk +adb pull /system/app/btota/btota.apk diagnostics/phase7/original-apks/btota.apk +adb pull /system/app/USBService/USBService.apk diagnostics/phase7/original-apks/USBService.apk +adb pull /system/framework/framework-res.apk diagnostics/phase7/original-apks/framework-res.apk +adb pull /system/etc/security/otacerts.zip diagnostics/phase7/otacerts.zip +``` + +The pulled APKs are multi-megabyte reference evidence and are intentionally +excluded from the Phase-7 delta ZIP. + +## Offline command/tool families + +- `rg`/PowerShell inventory and exact-string searches in local bugreport, + extracted init/fstab, DEX reports and prior phase documentation. +- SHA-256 and size checks for baseline/live-copied artifacts. +- `keytool -printcert` and APK signing-block parser for public X.509/v1/v2/v3 + metadata; no private-key search. +- ZIP entry inspection and local DEX method/string analysis for Settings/OAD/ + USB update components. +- `python tools/epg-health-check/health_check.py` in live read-only mode. +- The same tool against the immutable historical EPG snapshot for validation. + +## Important latest outputs + +- `dumpsys dbinfo`: `EpgDatabase.db` 8,724,480 bytes. +- `df`: userdata 5,089,920 KiB total, 3,047,232 KiB available, 39% used. +- Health tool: `OK`, no repair attempted. +- Historical snapshot test: 1,742,053,376 bytes, 3,071,250 schedule rows, + 3,004,958 older than 30 days, `quick_check=ok`, + `MAINTENANCE_RECOMMENDED`. diff --git a/diagnostics/storage-analysis.md b/diagnostics/storage-analysis.md new file mode 100644 index 0000000..3914f58 --- /dev/null +++ b/diagnostics/storage-analysis.md @@ -0,0 +1,259 @@ +# Speicheranalyse – `/data` und adoptierter Speicher + +Stand: 2026-08-25, vertieft bis 14:20 Uhr. Die TV-Abfragen waren bis auf die +vom Auftrag ausdrücklich verlangte Android-Diagnose `dumpsys diskstats` +lesend. Dieser Dienst meldet selbst einen 512-Byte-Latenztest (`[512B Data +Write]`); er wird deshalb nicht nochmals verwendet. + +## Kurzfazit + +Die interne `/data`-Belegung ist jetzt zu rund **75,7 %** konkret erklärt. +Der größte Einzelverursacher ist eine Philips-EPG-Datenbank mit **1,62 GiB**. +Daneben ist eine Philips-WorkManager-Datenbank mit **218,8 MiB** extrem +auffällig. Aktualisierte vorinstallierte Apps samt direkt messbaren +Basiskompilaten beanspruchen mindestens weitere **912,6 MiB**. Ob und wie viel +der beiden SQLite-Hauptdateien intern frei ist, bleibt ohne +`freelist_count` unbekannt. + +Das 112-GiB-adoptierte Volume ist korrekt als privater und primärer Speicher +eingebunden, bleibt aber fast leer. Es kann diese Hauptverursacher nicht +automatisch aufnehmen: Sie gehören zu Systempaketen, und Android verbietet das +Verschieben von System-Apps. Auch alle 18 Pakete unter `/data/app` sind trotz +ihres Pfades keine normalen Drittanbieter-Apps, sondern aktualisierte +System-Apps. + +## Objektive Lage + +| Bereich | Größe/Belegung | Bewertung | +|---|---:|---| +| `/data` gesamt | 4.970,6 MiB | kleines internes Dateisystem | +| `/data` belegt | zuletzt 3.831.944 KiB = 3.742,1 MiB (78 %) | real belegt; laufende TV-Daten schwanken | +| `/data` verfügbar | zuletzt 1.136.840 KiB = 1.110,2 MiB | Android `NORMAL` | +| Abstand zur Philips-Schwelle | zuletzt 88.264 KiB = 86,2 MiB | sehr geringe Schwankungsreserve | +| adoptiert, privat | 112,3 GiB, nur 1,36 MiB belegt | technisch intakt, praktisch leer | +| öffentliches USB-Volume | 58,2 GiB, 42,2 GiB frei | unkritisch | + +Inodes sind nicht knapp: `/data` nutzt 7 % seiner Inodes. Der Android- +`DeviceStorageMonitor` meldet `NORMAL`; dessen Low-Schwelle liegt bei rund +249 MiB. Die Warnung entsteht weiterhin an Philips' separater 1-GiB-Schwelle. + +## Konkrete Belegungsverursacher + +| Struktur | Messwert | Anteil an aktueller `/data`-Belegung | Evidenz | +|---|---:|---:|---| +| `org.droidtv.channels/.../EpgDatabase.db` | 1.661,352 MiB = 1,622 GiB | 44,5 % | `dumpsys dbinfo`/`meminfo` | +| `org.droidtv.gel/.../androidx.work.workdb` | 218,8 MiB | 5,9 % | `dumpsys dbinfo` | +| alle weiteren von `dbinfo` gelisteten DB-Dateien/Sidecars | 29,1 MiB zusätzlich | 0,8 % | dienstvermittelte Dateiliste, dedupliziert | +| 18 aktualisierte System-Apps: sichtbarer Code | 686,2 MiB | 18,4 % | `du` bekannte Codepfade | +| deren statbare Basis-ODEX/VDEX | 226,4 MiB | 6,1 % | `stat` exakte OAT-Dateien | +| `/data/anr` und kleine zusätzliche Bugreport-Artefakte | 8,7 MiB | 0,2 % | direkt/Bugreport | +| **konservativ konkret zugeordnet** | **2.830,5 MiB = 2,764 GiB** | **75,7 %** | synchronisierte Abschlussbilanz | +| **verbleibend, nicht einzeln messbar** | **ca. 905–912 MiB = 0,88–0,89 GiB** | **ca. 24,3 %** | zeitabhängige Differenz | + +Der Rest umfasst unter anderem weitere Split-OAT-Dateien, private Nicht-DB- +Appdaten, Dalvik-Cache, `/data/system`, `/data/misc` und Dateisystem-Overhead. +Wegen DAC und SELinux ist eine vollständige `du`-Aufteilung auf diesem +Produktions-Build nicht möglich. Die genannten 75,7 % sind deshalb keine +Schätzung nach Kategorien, sondern eine konservative, deduplizierte Summe +dienstvermittelter oder direkt statbarer Werte. Der spätere `df`-Stand wich um +rund 6 MiB ab; deshalb wird das Ergebnis sinnvoll als 75–76 % angegeben. + +### 1. Philips EPG-Datenbank + +Pfad: + +```text +/data/user/0/org.droidtv.channels/databases/EpgDatabase.db +``` + +Androids Datenbankdienst meldet 1.742.053.376 Byte; das Rollback-Journal ist +0 Byte, und es existieren in der Momentaufnahme weder WAL noch SHM. `dumpsys +meminfo 7267` belegt `pgsz=4` KiB und `dbsz=1.701.224` KiB. Damit sind +`page_size=4096` Byte und `page_count=425.306` exakt bewiesen +(`425.306 × 4096 = 1.742.053.376`). Eine physische Blockzählung wurde der +Shell verweigert; die fast vollständige Übereinstimmung mit `df` macht eine +weitgehend reale Blockbelegung sehr wahrscheinlich, Sparse-Anteile sind aber +nicht separat ausgeschlossen. + +Der ContentProvider erlaubt eine weitere, rein lesende Prüfung: + +| Tabelle | aktuelle Zeilen | beobachtete AUTOINCREMENT-IDs | +|---|---:|---:| +| `indextable` | 358 | 1.267.806–1.268.163 | +| `nowtable` | 195 | 4.431.681–4.451.328 | +| `nexttable` | 162 | 4.026.589–4.027.685 | +| `nownexttable` | 358 | korrespondierende aktuelle IDs | +| `fastipnownexttable` | 0 | – | + +Damit sind millionenfache historische Inserts bei wenigen aktuellen +Index-/Now-/Next-Zeilen direkt nachgewiesen. Die statische DEX-Analyse +korrigiert jedoch einen früheren Schluss: `deleteStaleDataRunnable.run()` +berechnet nur Grenzwerte und protokolliert „Deleting stale data“, führt aber +**keine Löschung** aus. Zusätzlich werden `1800` und `604800` direkt von +`currentTimeMillis()` subtrahiert; die protokollierte 1,8-Sekunden-Differenz +bestätigt den Einheitenfehler. Die echten Zeit-Löschmethoden sind in den vier +DEX-Dateien nicht an diesen Pfad verdrahtet. + +Die Philips-`rebuilddb`-Pfade sind ebenfalls kein physischer Rebuild: +`dropTables()` führt `SQLiteDatabase.delete()` auf Tabellen aus, ersetzt die +Datei aber nicht und ruft kein `VACUUM` auf. Eine explizite `PRAGMA +auto_vacuum`-Anweisung fehlt im APK. Zugleich baut AOSP seine Plattform- +SQLite-Bibliothek langjährig mit `SQLITE_DEFAULT_AUTOVACUUM=1` (FULL). Da +dieses Gerät `/system/lib/libsqlite.so` Version 3.32.2 verwendet, ist FULL für +eine normal durch `SQLiteOpenHelper` neu angelegte Datenbank eine starke +Inferenz, **aber kein gemessener Headerwert dieses Philips-Vendor-Builds**. + +Eine lesende Vollabfrage von `scheduletable` war nach 120 Sekunden noch nicht +beendet und wurde abgebrochen. Das beweist keine Zeilenzahl, zeigt aber, dass +aktive Schedule-Daten keinesfalls als vernachlässigbar angenommen werden +dürfen. Deshalb gilt nun streng: + +- `freelist_count`: **NICHT VERIFIZIERBAR**; +- intern freier Platz: **NICHT BERECHENBAR** (`freelist_count × 4096`); +- `auto_vacuum`: **NICHT direkt verifiziert**, FULL ist AOSP-basiert + plausibel bis sehr wahrscheinlich; +- „fast die gesamte Datei ist leer“: **nicht belegt**. + +SQLite dokumentiert sowohl das Freelist-Verhalten als auch, dass `VACUUM` die +DB in eine temporäre Datei kopiert und bis zum Zweifachen der Ursprungsgröße an +zusätzlichem freien Platz benötigen kann. Bei nur rund 1,1 GiB frei ist das +für eine 1,62-GiB-Datei nicht sicher durchführbar und bleibt ausdrücklich +verboten. Quellen: [SQLite VACUUM](https://www.sqlite.org/lang_vacuum.html), +[PRAGMA auto_vacuum](https://www.sqlite.org/pragma.html#pragma_auto_vacuum) +und [AOSP SQLite-Buildoptionen](https://android.googlesource.com/platform/external/sqlite/+/refs/heads/android11-qpr3-s1-release/dist/Android.bp). + +### 2. Philips GEL-WorkManager-Datenbank + +Pfad: + +```text +/data/user/0/org.droidtv.gel/no_backup/androidx.work.workdb +``` + +Die Datei hat 229.453.824 Byte. Android meldet `page_size=4096` Byte und je +nach Momentaufnahme `page_count=56.019–56.020`. Dazu kommen nur 524.288 Byte +WAL und 32.768 Byte SHM; der Ausreißer steckt also in der Hauptdatei und nicht +in einem außer Kontrolle geratenen WAL. Übliche WorkManager-Dateien auf +demselben Gerät liegen bei rund 98–106 KiB; GEL ist ungefähr **2.155-mal +größer**. `dbinfo` +zeigt aktuelle Inserts in `WorkSpec`, `Dependency`, `WorkTag` und `WorkName`. + +Die statische APK-Analyse belegt: + +- `GatheringService` baut für jeden Start einen `OneTimeWorkRequest`; +- der eindeutige Name lautet `GEL_WORK`; +- der optimierte Bytecode verwendet die Konfliktpolitik + `APPEND_OR_REPLACE` (unboxed enum value 4); +- der Dienst läuft im langlebigen Systemprozess `org.droidtv.candeebug`. + +Android beschreibt, dass `APPEND_OR_REPLACE` neue Arbeit an die Blätter einer +vorhandenen Kette anhängt. Das stimmt mit den beobachteten +`Dependency`-Insertionen überein. Quelle: +[Android ExistingWorkPolicy](https://developer.android.com/reference/androidx/work/ExistingWorkPolicy). + +Bewertung: Eine über Jahre gewachsene WorkManager-Kette oder daraus entstandene +freie SQLite-Seiten ist sehr wahrscheinlich. Ohne privilegierten SQL-Lesezugriff +sind Tabellenzählung und Freelist auch hier nicht endgültig trennbar. + +### 3. Aktualisierte System-Apps + +`pm list packages -3` ist leer. Dennoch liegen 18 aktive Codepfade unter +`/data/app`; bei allen meldet PackageManager die Flags `SYSTEM` und +`UPDATED_SYSTEM_APP` sowie eine ältere, unveränderliche Ursprungskopie unter +`/system/product` oder `/vendor`. + +Die sichtbaren APK-/Split-/DM-Anteile summieren sich auf 686,2 MiB. Durch +gezieltes `stat` bekannter `oat/arm/base.odex`- und `base.vdex`-Pfade kommen +mindestens 226,4 MiB hinzu. Größte Mindestbelegungen: + +| Paket | mindestens | +|---|---:| +| Google Play Services | 243,0 MiB | +| Netflix | 105,4 MiB | +| Google Text-to-Speech | 96,6 MiB | +| Android WebView | 93,3 MiB | +| Play Store | 63,0 MiB | +| YouTube | 52,1 MiB | + +Split-Kompilate und private Daten fehlen in diesen Mindestwerten. Die Updates +belegen zusätzlich Platz, während ihre Werksversionen im schreibgeschützten +Systemimage unverändert vorhanden bleiben. + +## Warum der adoptierte Speicher leer bleibt + +Die Konfiguration ist objektiv korrekt: + +- privates ext4-Volume `0ca93141-...` ist `MOUNTED`; +- das zugehörige emulierte Volume trägt `PRIMARY|VISIBLE`; +- `sm get-primary-storage-uuid` liefert dessen UUID; +- `/storage/emulated/0` zeigt bereits auf dessen `media`-Bereich; +- Installationsmodus ist `0[auto]`. + +Androids AOSP-Dokumentation erklärt, dass adoptierter Speicher Apps und private +Daten nur für unterstützte Apps aufnehmen kann und die Migration primär den +gemeinsamen `/sdcard`-Inhalt betrifft. Androids PackageManager lehnt das +Verschieben von Systempaketen ausdrücklich mit `MOVE_FAILED_SYSTEM_PACKAGE` ab. +Quellen: +[AOSP Adoptable Storage](https://source.android.com/docs/core/storage/adoptable) und +[AOSP Android-12 PackageManager-Logik](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android12-gsi/core/java/android/app/ApplicationPackageManager.java). + +Genau diese Einschränkung trifft hier zu: Es gibt keine Drittanbieterpakete, +alle 18 aktualisierten Pakete sind weiterhin System-Apps, und auch +`org.droidtv.channels` sowie `org.droidtv.gel` sind Systempakete mit UID 1000. +Das adoptierte Volume dient daher aktuell fast nur als primärer gemeinsamer +Medienspeicher; dort liegen lediglich etwa 1,2 MiB. + +## Separate Philips-Partition `user_setting` + +Eine beim Aufwachen sichtbare TvProvider-Zeile meldete 762.183.680 Byte +verfügbar. Das ist **kein** neuer `/data`-Messwert. Die Mounttabelle belegt: + +```text +/dev/block/mmcblk0p52 on /data/vendor/user_setting type ext4 +``` + +Die rekonstruierte Kernel-Partitionsdefinition weist `mmcblk0p52` 1000 MiB +zu. Statische Analyse von `TvProvider.apk` zeigt, dass +`checkDBSizeOverThreshold()` für exakt `/data/vendor/user_setting` ein +`StatFs` erzeugt und `blockSize * availableBlocks` protokolliert. Der Provider +setzt seine interne Stufe 2 unter 10 % frei und Stufe 1 unter +`totalBytes / 7,69`, also rund 13,0039 % frei. + +762.183.680 Byte entsprechen 726,875 MiB und rund 72,6875 % der rohen +1000-MiB-Partition. Weil die StatFs-Gesamtgröße den ext4-Overhead bereits +abzieht, liegt der wirkliche freie Prozentsatz des Dateisystems eher noch +etwas höher. Im vorherigen Bugreport blieb der Wert über mindestens 90 Minuten +nahezu konstant; `USER_SETTING_10_FREE`- oder Insert-Blockiermeldungen wurden +nicht gefunden. Die Partition enthält unter anderem `rw/tv.db` und `list.db`, +ist derzeit aber klar nicht knapp. + +Direktes `df`, Listing und das Lesen der p52-Sysfs-Größenknoten wurden der +ADB-Shell verweigert. Diese Grenze wurde akzeptiert. Der Befund beruht daher +auf der direkt sichtbaren Mountzuordnung, der Commandline-Größe, dem laufenden +UID-1000-StatFs-Log und der lokalen DEX-Analyse. + +## Schlussfolgerung zur Warnung + +Die Meldung ist kein bloßer falscher Zähler, aber auch kein Android-Low-Storage- +Alarm. `/data` hat real nur rund 1,08–1,11 GiB verfügbar und schwankt knapp um Philips' +eigene 1-GiB-Warnschwelle. Die beiden ungewöhnlich großen Philips-Datenbanken +belegen zusammen rund 1,84 GiB und sind die wichtigsten konkreten Ursachen für +diesen knappen Zustand. Die EPG-Datenbank allein ist größer als der gesamte +aktuell freie Platz. Ihre genaue reclaimbare Teilmenge ist noch unbekannt; +Ursache und Rückgewinn dürfen nicht gleichgesetzt werden. + +Der eMMC-Wert `0x0a 0x0a` verändert nicht die Belegung, sorgt aber dafür, dass +Philips eine Unterschreitung der 1-GiB-Schwelle als `MEMORY_CRITICAL` darstellt. +Damit sind Belegungsursache und Warnschärfe getrennt erklärt. + +## Rohdaten + +- `raw/storage/filesystem-overview.txt` +- `raw/storage/data-access-map.txt` +- `raw/storage/updated-system-apps.tsv` +- `raw/storage/adopted-storage.txt` +- `raw/storage/database-outliers.txt` +- `raw/storage/epg-content-evidence.txt` +- `raw/storage/final-validation.txt` +- `raw/storage/channels.apk` +- `raw/storage/gel.apk` diff --git a/diagnostics/storage/phase3-storage-repair.md b/diagnostics/storage/phase3-storage-repair.md new file mode 100644 index 0000000..4781897 --- /dev/null +++ b/diagnostics/storage/phase3-storage-repair.md @@ -0,0 +1,207 @@ +# Phase-3-Speicherreparatur + +Stand: 2026-08-25. Jede Änderung wird vor Ausführung ergänzt. + +## Ausgangslage + +- `/data`: 5.089.920 KiB gesamt, 3.832.668 KiB benutzt, + 1.136.116 KiB frei, 78 %. +- GEL `androidx.work.workdb`: 229.453.824 Byte plus 524.288-Byte-WAL und + 32.768-Byte-SHM. +- Die DB liegt unter + `/data/user/0/org.droidtv.gel/no_backup/`; Androids regulärer App-Backup- + Mechanismus schließt dieses Verzeichnis absichtlich aus. +- `org.droidtv.gel` hat `ALLOW_BACKUP` und `ALLOW_CLEAR_USER_DATA`. +- Die normale Actions-DB ist nur 20.480 Byte groß. Der Actions-Satz ist nach + bestätigter Philips-Leerkonfiguration leer; alle Phase-3-Testdateien sind + entfernt. +- Live belegt sind mehrminütige Unique-Work-Graphtraversierungen, dauerhafte + lineare Aufarbeitung alter BGM-Status-WorkSpecs und massive Verzögerung neuer + GEL-Intents. Die WorkDB ist daher nicht nur groß, sondern funktional + pathologisch. + +## Reparaturstufe 1 – normale GEL-Daten sichern, vor Ausführung + +**Hypothese:** Ein Android-Full-Backup von `org.droidtv.gel` sichert die +wiederherstellbaren normalen App-Daten (Preferences, `files/`, Actions-DB), +während der explizit nicht backupfähige WorkManager-Schedulerzustand +ausgeschlossen bleibt. Nach erfolgreicher Sicherung kann genau dieser +ephemere Zustand neu aufgebaut werden. + +**Aktionen:** + +1. `am force-stop org.droidtv.gel`, um Service, DB-Writes und die laufende + Work-Kette für eine konsistente Sicherung anzuhalten. +2. `adb backup -f diagnostics/phase3/gel-normal-data-before-clear.ab -noapk + -noobb -noshared org.droidtv.gel`; Bestätigung am Fernseher ohne Passwort. +3. Nur ein Archiv mit gültigem `ANDROID BACKUP`-Header **und mindestens einem + Tar-Eintrag mit App-Nutzdaten** gilt als Erfolg. Vorher erfolgt kein + `pm clear`. + +**Mögliche Nebenwirkungen:** GEL und der gemeinsame candeebug-Prozess werden +beendet; Systemereignisse können ihn wieder starten. Während der Backup- +Bestätigung erscheint kurz eine System-UI. Die Sicherung schreibt nur auf den +PC; die App-Daten werden gelesen, nicht geändert. + +**Rückkehrweg:** Vor einer Bereinigung ist kein Datenrollback nötig; GEL kann +durch einen normalen Service-/Eventstart wieder anlaufen. Die Backup-Datei +kann später mit `adb restore` (erneute UI-Bestätigung) zurückgespielt werden. + +### Ergebnis des Sicherungsversuchs + +- `am force-stop org.droidtv.gel` wurde ausgeführt. Der gemeinsame Prozess + `org.droidtv.candeebug` wurde durch eine andere UID-1000-Komponente wieder + gestartet, GEL selbst blieb jedoch `stopped=true`; ein GEL-Service lief + nicht. +- Die Backup-Bestätigung wurde am Fernseher angenommen und der ADB-Befehl + ordnungsgemäß beendet. +- Das erzeugte Archiv + `diagnostics/phase3/gel-normal-data-before-clear.ab` ist nur 47 Byte groß. + Es enthält den Header `ANDROID BACKUP`, Kompression `zlib`, danach aber ein + leeres Tar-Archiv: **keine einzige GEL-Datei wurde gesichert**. +- Manifest und PackageManager bestätigen zwar `allowBackup=true`, aber GEL + läuft als Shared-/Core-UID `android.uid.system` / UID 1000, besitzt keinen + deklarierten `backupAgent` und erscheint nicht unter den Teilnehmern von + `dumpsys backup`. Das ist die belastbare Laufzeiterklärung für das leere + Archiv; die genaue interne Eligibility-Regel ist daraus nur als sehr + wahrscheinliche Framework-Inferenz abgeleitet. +- Unabhängig davon liegt die 229-MiB-WorkDB unter `no_backup/` und wäre auch + bei einem funktionierenden normalen App-Backup absichtlich ausgeschlossen. + +Status: **BACKUP GATE NICHT ERFÜLLT. Kein `pm clear` ausgeführt.** Das +47-Byte-Archiv bleibt als Beleg erhalten, darf aber nicht als +Wiederherstellungspunkt behandelt werden. + +## Reparaturstufe 2 – ausdrücklich freigegeben + +Das ursprüngliche Backup-/Restore-Konzept ist auf diesem Build nicht +realisierbar. Ein Package-Daten-Reset würde die pathologische WorkDB sicher +entfernen, zugleich aber auch kleine normale GEL-Daten, für die jetzt kein +dateibasierter Rückkehrweg existiert. Bestätigt risikomindernd sind: die +Actions-Liste ist leer, die Philips-Konfiguration wird beim App-Start wieder +vom kanonischen Dienst bezogen, das System-APK bleibt unverändert und Daten +anderer Packages werden nicht gelöscht. Unbekannt bleiben einzelne GEL- +Preferences bzw. sonstige interne Dateien. + +Der Eigentümer hat am 2026-08-25 den Verlust unbekannter kleiner GEL- +Preferences/private Appdaten ausdrücklich akzeptiert und exakt +`pm clear --user 0 org.droidtv.gel` freigegeben. + +### Unmittelbarer BEFORE-Snapshot + +- `/data`: 5.089.920 KiB gesamt, 3.836.812 KiB benutzt, + **1.131.972 KiB frei**, 78 %. +- GEL: installiertes System-APK Version 7.2.0.56, UID/sharedUser 1000 / + `android.uid.system`, `stopped=true`, Datenpfad + `/data/user/0/org.droidtv.gel`. +- Kein laufender GEL-Service. Der gemeinsame Prozess + `org.droidtv.candeebug`, PID 23775, läuft als UID system in + `u:r:system_app:s0`; er verwendet seine eigene Package-Datenbank, nicht die + gestoppte GEL-WorkDB. +- Letzter exakt gemessener WorkDB-Zustand vor dem Stop: + 229.453.824 Byte Hauptdatei + 524.288 Byte WAL + 32.768 Byte SHM. +- JobScheduler-Historie bestätigt für GEL 62 Pending-/60 Active-Ereignisse + und fortlaufende erfolgreiche Jobs mit Nummern bis mindestens 257839; + aktuell ist nach dem Force-Stop keine GEL-Queue aktiv. +- WorkSpec-/Dependency-Zeilenzahlen bleiben **UNKNOWN**; die Datenbank ist für + ADB shell weder direkt lesbar noch durch einen offenen GEL-Prozess abfragbar. + +### Vorab dokumentierte Aktion + +**Hypothese:** Der Package-Reset entfernt den 229-MiB-WorkManager-Zustand und +die historische serielle Queue. Die System-App rekonstruiert anschließend +einen kleinen sauberen Zustand aus der kanonischen Philips-Konfiguration. + +**Aktion:** ausschließlich +`pm clear --user 0 org.droidtv.gel`. + +**Erwartung:** `Success`, rund 219 MiB mehr freier `/data`-Platz, kein Eingriff +in APK/Firmware/andere Packages. + +**Nebenwirkung:** alle privaten User-0-Daten von GEL werden gelöscht; ein +dateibasierter Restore ist wegen des nachweislich leeren ADB-Backups nicht +möglich. + +**Rückkehrweg:** GEL-System-APK bleibt vorhanden; normale Neuinitialisierung +und erneuter Abruf der Philips-Konfiguration. Kein Restore des bewusst +verworfenen Schedulergraphs. + +### Unmittelbares Ergebnis + +- `pm clear --user 0 org.droidtv.gel` antwortete exakt **`Success`**. +- `/data` unmittelbar danach: 3.611.820 KiB benutzt, + **1.356.964 KiB frei**, 73 %. +- Speichergewinn: **224.992 KiB = 219,72 MiB = 0,2146 GiB**. +- Die zuletzt gemessenen WorkManager-Dateien erklären 224.620 KiB davon. + Die Differenz von nur 372 KiB ist eine Obergrenze aus sonstigen GEL-Daten + plus Dateisystem-/Messschwankungen. Der Reset traf damit praktisch exakt den + identifizierten WorkManager-Verbraucher. +- GEL blieb zunächst `stopped=true`; kein GEL-Service und kein GEL-Prozess. + `org.droidtv.candeebug` wurde für seine eigene CollectionService-Nutzung als + neuer PID 26108 wieder gestartet. +- JobScheduler protokollierte `Can't cancel all jobs for system uid`. Das ist + kein Clear-Fehler, sondern eine Schutzschranke gegen das pauschale Löschen + sämtlicher Jobs der Shared-UID 1000. Der Package-Clear selbst war erfolgreich, + es gab keine aktuelle GEL-Pending-/Active-Queue; die angezeigten 62/60-Werte + sind historische Statistik. +- Nach 30 Sekunden erfolgte kein automatischer GEL-Lifecycle-Start. Deshalb + wird einmalig der bereits verstandene normale Produktpfad + `DAC_START_GEL` an `GatheringService` verwendet. + +### Vorab: normale GEL-Neuinitialisierung + +**Aktion:** Expliziter Start von +`org.droidtv.gel/.gathering.GatheringService` mit +`org.droidtv.tv.intent.action.DAC_START_GEL`. + +**Erwartung:** neuer kleiner WorkManager-Zustand, GELWorker-Ausführung, +kanonischer Konfigurationsabruf und stabiler Service. + +**Nebenwirkung:** normale neue GEL-Appdaten/WorkDB und ein legitimer +GEL_WORK-Eintrag. + +**Rückkehrweg:** Bei Crashloop `am force-stop org.droidtv.gel`; keine weitere +Datenlöschung. + +### Ergebnis der Neuinitialisierung + +- Service-Start von ADB shell wurde akzeptiert; GEL läuft im gemeinsamen + Prozess PID 26108, UID 1000, `u:r:system_app:s0`. +- `GELWorker` für `DAC_START_GEL` startete sofort und endete mit `SUCCESS`. +- GEL forderte die kanonische Konfiguration an; `ConfigLoader` meldete + `No actions found in Config`. Die neue Actions-DB ist 20.480 Byte groß und + `SELECT uuid FROM Actions` blieb leer. +- Neue WorkDB unmittelbar danach: 4.096 Byte; WAL 206.032 Byte; SHM + 32.768 Byte, zusammen **242.896 Byte = 237,20 KiB**. +- Gegenüber 229.453.824 + 524.288 + 32.768 Byte vorher ist der private + WorkManager-Dateisatz um rund **219,49 MiB kleiner**. +- Keine Connection-Waiter, keine SQLite-Exception und keine Crashloop. Ein + legitimer GEL-Job/Alarm wurde neu angelegt; die historische serielle Kette + ist nicht zurückgekehrt. + +Bewertung: **GEL-WorkManager-Pathologie BEREINIGT / VALIDATED LIVE.** + +### Mehrminütige Stabilitätsbeobachtung + +- WorkDB-Hauptdatei nach mehreren Alarmzyklen: 102.400 Byte; SHM 32.768 Byte; + WAL 461.472 Byte. Kein annähernd lineares Wachstum in Richtung des alten + 219-MiB-Zustands. +- GATHERING_ALARM_INTENT lief im 60-Sekunden-Takt jeweils sofort und mit + Worker result SUCCESS; kein serieller Rückstau. +- Keine GEL-Crashloop, ANR, SQLiteException, SQLiteDiskIOException oder + malformed-Meldung im Beobachtungsfenster. +- /data blieb bei 73 % und rund 1.356.336 KiB frei. +- ADB erreichbar; WLAN-Netz 100 CONNECTED/VALIDATED. Channels-, EPGData-, + PlayTV-, MediaTek-TVInput- und GEL/candeebug-Prozesse liefen. + +Zur quantitativen Bestätigung des kleinen Work-Satzes wird der bereits +verstandene AndroidX DiagnosticsReceiver genau einmal aufgerufen. Er erzeugt +selbst einen kleinen, prune-fähigen Diagnose-WorkSpec, liest anschließend nur +WorkManager-Metadaten und endet regulär mit SUCCESS. + +**Ergebnis:** Der Diagnoseworker listete elf abgeschlossene GEL_WORK-Zeilen, +alle SUCCEEDED. Running enthielt nur den Diagnoseworker selbst, der unmittelbar +SUCCESS meldete. WorkSpec-Zahl im sichtbaren aktuellen Fenster damit 12 +einschließlich Diagnose-Work; die exakte gesamte Tabellenzeilenzahl und +Dependency-Zahl bleiben ohne sqlite3 **UNKNOWN**. Entscheidend ist, dass keine +Pending-/Running-Altkette mehr vorhanden war und die DB klein/stabil blieb. diff --git a/diagnostics/system/system-inventory.md b/diagnostics/system/system-inventory.md new file mode 100644 index 0000000..4d99bbe --- /dev/null +++ b/diagnostics/system/system-inventory.md @@ -0,0 +1,149 @@ +# Phase 1 – System-, Security-, Kernel- und SoC-Inventar + +Stand: 2026-08-25. Ziel: Philips 43PUS7394/12 unter +`:5555`. Alle Geräteabfragen waren lesend; APK-, Fstab-, Init- +und Kernel-Config-Pulls schrieben nur Kopien in den Host-Workspace. Es gab +keinen Reboot, keine Property-/Settings-Änderung, kein `adb root`, keinen +Fastboot-/Recovery-Wechsel und keine Exploit-Ausführung. + +Bewertung: **CONFIRMED** = direkt beobachtet, **LIKELY** = starke Indizkette, +**POSSIBLE** = offen/vereinbar, **DISPROVED** = im untersuchten Build direkt +widerlegt. + +## Identität und Build + +| Merkmal | Wert | Bewertung | +|---|---|---| +| Modell | Philips 43PUS7394/12; Android-Modellproperty `TPM191E` | **CONFIRMED** | +| ADB-Produkt / Device | `PH9M_EA_5599` | **CONFIRMED** | +| Firmware | `TPM191E_R.211.000.103.000` | **CONFIRMED** | +| Android / SDK | Android TV 12 / API 31; First API Level 28 | **CONFIRMED** | +| Security Patch Level | `2025-11-01` | **CONFIRMED** als Property, nicht als Binärbeweis jedes Vendor-Backports | +| Build-ID / Incremental | `STT2.230526.001` / `4215` | **CONFIRMED** | +| Buildzeit | 2025-12-09 11:16:50 CST | **CONFIRMED** | +| Fingerprint | `Philips/PH9M_EA_5599/PH9M_EA_5599:12/STT2.230526.001/4215:user/release-keys` | **CONFIRMED** | +| Buildtyp / Tags | `user` / `release-keys` | **CONFIRMED** | +| Debug/Secure | `ro.debuggable=0`, `ro.secure=1`, `ro.adb.secure=1` | **CONFIRMED** | +| Priv-App-Permissions | `ro.control_privapp_permissions=enforce` | **CONFIRMED** | +| Treble / VNDK | Treble `true`, Framework-VNDK 31, Vendor-VNDK 26 | **CONFIRMED** | + +Seriennummern, Netzwerkadressen und andere eindeutige Gerätekennungen aus der +vollständigen Propertyliste werden in diesem Bericht bewusst nicht wiederholt. + +## CPU, SoC und ABI + +| Ebene | Befund | Bewertung | +|---|---|---| +| CPU | vier ARMv8-Cortex-A53-Kerne (`CPU part 0xd03`) | **CONFIRMED** | +| Kernelarchitektur | ARM64/AArch64 (`CONFIG_ARM64=y`, `CONFIG_64BIT=y`) | **CONFIRMED** | +| Android-Userspace | ausschließlich 32-Bit ARM: `armeabi-v7a,armeabi`, `zygote32`, keine 64-Bit-ABI | **CONFIRMED** | +| Android-/Kernelplattform | `mt5887`; `CONFIG_MACH_MT5887=y`, `CONFIG_SOC_MEDIATEK_MT53XX=y` | **CONFIRMED** | +| Android-SoC-Property | Hersteller `Mediatek`, Modell `MT5690` | **CONFIRMED** als Property | +| TPV-OEM-Chipproperty | `MT5599` | **CONFIRMED** als OEM-Kennung | +| Physische Aliasbeziehung MT5887/MT5690/MT5599 | nicht durch Chip-ID oder Herstellerunterlage bewiesen | **POSSIBLE**, nicht gleichsetzen | + +Die belastbare Formulierung lautet daher: MediaTek-TV-Plattform mit +`mt5887`-BSP und den zusätzlichen TPV/Android-Kennungen `MT5599`/`MT5690`. + +## Kernel und Härtung + +```text +Linux localhost 4.9.310-ab4215 #1 SMP PREEMPT +Tue Dec 9 11:04:39 CST 2025 armv8l +Compiler: Linaro GCC 5.5.0 (2017.10) +``` + +Der 32-Bit-`armv8l`-Maschinenstring kommt vom 32-Bit-Userspace; die gesicherte +`/proc/config.gz` beweist einen ARM64-Kernel. + +| Merkmal | Befund | Bewertung | +|---|---|---| +| SELinux | `Enforcing`; Development-/Disable-Code einkompiliert, aber nicht aktiv | **CONFIRMED** | +| User namespaces | nicht einkompiliert | **CONFIRMED** | +| Userfaultfd | nicht einkompiliert | **CONFIRMED** | +| Kexec, `/dev/mem`, `/dev/kmem` | nicht einkompiliert | **CONFIRMED** | +| PAN / SW-TTBR0-PAN | aktiv | **CONFIRMED** | +| KASLR / relocatable kernel | `CONFIG_RANDOMIZE_BASE=y`; kein `nokaslr` in der beobachteten Commandline, tatsächliche Entropie nicht separat gemessen | **CONFIRMED** als Buildoption / **LIKELY** zur Laufzeit | +| Stack protector / hardened usercopy | stark / aktiv | **CONFIRMED** | +| Seccomp / Filter | einkompiliert; ADB-Shell-Prozess meldete aktuell `Seccomp: 0` | **CONFIRMED** | +| Perf | `/proc/sys/kernel/perf_event_paranoid=3` | **CONFIRMED** | +| Module | Laden/Entladen einkompiliert; `CONFIG_MODULE_SIG` nicht gesetzt | **CONFIRMED**; Shell besitzt dennoch weder `CAP_SYS_MODULE` noch einen nachgewiesenen Ladepfad | +| BPF | BPF und BPF-Syscall einkompiliert; Runtimewert für unprivilegiertes BPF durch SELinux verborgen | **CONFIRMED** / offen | +| Debugfs | einkompiliert und gemountet; sensible Bereiche durch SELinux begrenzt | **CONFIRMED** | + +Geladene proprietäre Module umfassen unter anderem `dtv_driver`, `mtk_core`, +`mali_kbase`, `fusion`, `tpvinput`, Audio-, WLAN- und Videomodule. Das aktive +Mali-Kernelmodul enthält `r32p0`-Buildpfade als starkes Basisindiz und den +Vermagic-String +`4.9.310-ab4215 ... aarch64`; SurfaceFlinger meldet separat einen Mali-G51- +Userspace-Stack `r32p1`. Die separate Sicherheits-/CVE-Triage ist aus dieser +kuratierten Veröffentlichung ausgeschlossen. + +## Aktueller ADB-/Privilegkontext + +| Merkmal | Befund | Bewertung | +|---|---|---| +| Transport | `:5555 device` | **CONFIRMED** | +| UID/GID | UID/GID 2000 `shell` | **CONFIRMED** | +| Gruppen | unter anderem `shell`, `input`, `log`, `adb`, `inet`, `readproc`, `uhid`; kein `root`/`system` | **CONFIRMED** | +| SELinux-Domain | Shellkommando `u:r:shell:s0`; adbd `u:r:adbd:s0` | **CONFIRMED** | +| Shell-Capabilities | Effective/Permitted/Inheritable/Ambient jeweils 0; Bounding Set `0xc0` | **CONFIRMED** | +| adbd | UID/GID 2000, keine effektiven/permitted Capabilities | **CONFIRMED** | +| PID 1 | UID/GID 0 und voller Capabilitysatz; nur Statuslesbarkeit, keine Kontrolle durch Shell | **CONFIRMED** | +| `su` | nicht in den geprüften üblichen System-/Vendorpfaden gefunden | **CONFIRMED** als begrenzter Nichtfund | +| ADB liefert derzeit einen Root-Kontext | nein | **DISPROVED** | + +Die Init-Definition startet adbd mit der Option +`--root_seclabel=u:r:su:s0`, und `service.adb.root=1` würde USB zurücksetzen +und adbd neu starten. Das ist noch kein Rootpfad: der Production-Build hat +`ro.debuggable=0`, und adbd läuft tatsächlich als Shell ohne Capabilities. +`adb root` wurde nicht ausgeführt, weil die lokale Init-Datei einen +Verbindungs-/Daemon-Neustart belegt und der Test damit nicht garantiert rein +beobachtend ist. + +## Boot- und Verified-Boot-Kurzbild + +| Merkmal | Befund | Bewertung | +|---|---|---| +| aktiver Slot | `_a` | **CONFIRMED** | +| A/B | physische A/B-Partitionen, `ro.build.ab_update=true` | **CONFIRMED** | +| System-as-root | `/dev/dm-0` auf `/`, ext4 read-only | **CONFIRMED** | +| AVB | AVB 1.2; vbmeta 1.0; Device State `locked`; Verified Boot `green` | **CONFIRMED** | +| dm-verity | `enforcing`; System/Vendor sowie mehrere TPV-Images verifiziert | **CONFIRMED** | +| Dynamic Partitions | kein `super`, feste physische Partitionen | **DISPROVED** | +| separate `recovery`, `cache`, `metadata`, `persist` | keine entsprechenden Partitionsnamen | **DISPROVED** für dieses Layout | +| Recovery-Infrastruktur | `uncrypt`, RecoverySystem-Service, BCB/`misc`, UpdateEngine, `factory_a/b` vorhanden | **CONFIRMED**; genaue Recovery-Startart offen | +| Fastboot/Fastbootd | keine entsprechenden Binaries im gemounteten laufenden Android-System gefunden und kein Transport sicher getestet | **POSSIBLE / unbekannt** im Bootloader; fastbootd-basierter Dynamic-Partition-Pfad nicht belegt | +| öffentliches OEM-Unlock | keine OEM-Lock-Binderinstanz, keine Unlock-Properties, keine offizielle Methode belegt | **POSSIBLE**, aber aktuell nicht nachgewiesen | + +Die vollständige 63-Partitionen-Tabelle, Fstab-Auswertung und Mountmatrix steht +in [boot-inventory-agent.md](../boot/boot-inventory-agent.md). + +## Aktueller Speicher- und eMMC-Snapshot + +| Bereich | Befund | Bewertung | +|---|---|---| +| `/data` | 5.089.920 KiB gesamt, 3.832.452 KiB benutzt, 1.136.332 KiB verfügbar, 78 % | **CONFIRMED** | +| Android DeviceStorageMonitor | `NORMAL`; low 260.603.904 Byte | **CONFIRMED** | +| adoptierter USB | 117.749.032 KiB gesamt, 116.529.664 KiB verfügbar, 1 % | **CONFIRMED** | +| Installationsmodus | `0[auto]`; adopted private Volume gemountet | **CONFIRMED** | +| Philips-Lifetime-Property | `0x0a 0x0a` | **CONFIRMED** | +| unabhängiger Android-Wert | `storaged_emmc_info: [emmc 5.1,1,10,10]` | **CONFIRMED** | +| eMMC-Gesundheit | eMMC 5.1; PRE_EOL `0x01` normal; Lifetime A/B `0x0a/0x0a` = je 90–100-%-Bucket | **CONFIRMED** | + +Der neue Storaged-Wert beweist: Der hohe Lifetime-Bucket ist kein bloßer +Philips-Cache, aber der Reserveblockindikator steht noch auf Normal. Das ist +kein Beweis eines unmittelbar bevorstehenden Hardwareausfalls. Details: +[emmc-analysis.md](../emmc-analysis.md). + +## Phase-1-Grenzen + +- Direkte Reads von EXT_CSD, CID/CSD, `/proc/partitions`, Kernel-Dmesg und + mehreren sysfs-/procfs-Sicherheitswerten werden durch DAC/SELinux blockiert. +- SUID-/SGID-/world-writable-Scans liefen nur in für die Shell durchsuchbaren + Bereichen; viele Executable-Labels verweigern bereits `getattr`. Es wurde + kein nutzbarer SUID-/SGID-Pfad gefunden, eine mathematisch vollständige + Abwesenheitsbehauptung ist aber nicht zulässig. +- Kein echter Exploit und kein schreibender IOCTL-Test wurde durchgeführt. +- Das späte Framework-SPL ist ein starkes nominelles Patchindiz, aber kein + Binärnachweis des proprietären Kernel-/Mali-/MediaTek-Backportstands. diff --git a/diagnostics/vendor-bugs/BUG-001-epg-stale-cleanup.md b/diagnostics/vendor-bugs/BUG-001-epg-stale-cleanup.md new file mode 100644 index 0000000..3b12fbc --- /dev/null +++ b/diagnostics/vendor-bugs/BUG-001-epg-stale-cleanup.md @@ -0,0 +1,76 @@ +# EPG stale-data cleanup logs deletion but performs none + +## Klassifikation + +**CONFIRMED BUG – High.** Betroffen: Philips 43PUS7394/12, +`TPM191E_R.211.000.103.000`, Android TV 12/API 31; Komponente +`/system/priv-app/channels/channels.apk`, Klasse +`org.droidtv.channels.epg.bcepg.EpgProvider`, Methode/Runnable +`EpgProvider$deleteStaleDataRunnable.run()`. + +## Benutzerwirkung + +Die EPG-DB wuchs auf 1.742.053.376 Byte und verdrängte auf der nur rund +4,85-GiB-großen `/data`-Partition andere Appdaten. Philips warnte deshalb vor +fast vollem Speicher. Der kontrollierte Neuaufbau gewann 1.701.400 KiB auf +`/data`; die Belegung sank von 73 % auf 39 %. Nach realem Betrieb war die DB +~806.912 Byte groß und enthielt 1.041 aktuelle Schedule-Einträge. Guide, +Now/Next, Sender, Favoriten, Aufnahmen, Timer und Reminder funktionierten; die +Speicherwarnung war verschwunden. + +## Erwartetes und tatsächliches Verhalten + +Erwartet: der nach Providerstart angekündigte Cleanup entfernt Schedule-Daten +außerhalb eines definierten Retentionsfensters. Tatsächlich berechnet das +Runnable lediglich `now-1800` und optional einen weiteren Grenzwert, schreibt +Logs und kehrt ohne `delete`, `execSQL`, `ContentResolver.delete`, `applyBatch` +oder Aufruf der vorhandenen Zeit-Löschmethoden zurück. + +## Reproduktion + +1. Provider starten und zwei Minuten warten. +2. Logs `Posting stale data deletion...`, `Triggering stale data cleanup` und + `Deleting stale data for time ...` beobachten. +3. DB-Zeilenzahl und Größe davor/danach vergleichen. + +Live am 25.08.2026 wiederholt: Logs um 21:09/21:11; keine DB-Änderung. + +## Technischer Beweis + +- Snapshot: 3.071.250 `scheduletable`-Zeilen, **100 % abgelaufen**, 97,81 % + älter als 30 Tage, 79,03 % älter als ein Jahr. +- Tabelle ca. 1.436,98 MiB, vier Indizes ca. 222,06 MiB; `freelist_count=0`, + `auto_vacuum=FULL`, `integrity_check=ok`. +- Bytecode: [epg-analysis.md](../epg-analysis.md) und + [epg-reverse-agent.md](../deep-forensics/philips/epg-reverse-agent.md). +- Aggregat-/Reparaturbeleg: [epg-repair.md](../phase5/epg-repair.md). + +## Root Cause + +Das Runnable ist unvollständig verdrahtet. Funktionsfähige Methoden +`EpgProgramPump.deleteChannelProgrambytime(long)` und +`deleteScheduleProgramsByTime(long)` existieren, werden von ihm aber nicht +aufgerufen. Zusätzlich fehlt eine robuste periodische Retention. Phase 6 hat +die exakte Methode in `classes4.dex` bei `code_off=0xe298c` bestätigt. + +## Workaround und dauerhafter Fix + +Workaround: kontrollierter kompakter DB-Neuaufbau mit Erhalt von Index/Mapping. +Dauerhaft: Clock in Millisekunden lesen, einmal in die tatsächlich gespeicherten +Epoch-Sekunden konvertieren und `scheduletable` indexgestützt in höchstens +2.000-Zeilen-Transaktionen mit sieben Tagen Retention bereinigen. Der Offline- +Test auf einer Kopie des großen Snapshots löschte 3.063.863 Zeilen, erhielt +7.387, bestand `integrity_check` und schrumpfte die FULL-auto-vacuum DB auf +4.059.136 Byte; der zweite Lauf änderte nichts. Details: +[IMPLEMENTATION.md](../../patches/epg-stale-cleanup/IMPLEMENTATION.md) und +[TEST-RESULTS.md](../../patches/epg-stale-cleanup/TEST-RESULTS.md). + +## Regression Tests / Kompatibilität + +0/7/30-Tage-Daten, Lock, Stromausfall, Doppellauf, Senderwechsel und EPG-Refresh +testen. Sender-, Favoriten- und Recording-Provider dürfen nicht mutiert werden. + +## Evidenz-Hashes + +`channels.apk` SHA-256: +`3A44BC648624F0CC9CD9FEB4CFC3FD33F41323597718E4FCCFC25FE35504D34F`. diff --git a/diagnostics/vendor-bugs/BUG-002-gel-workmanager-growth.md b/diagnostics/vendor-bugs/BUG-002-gel-workmanager-growth.md new file mode 100644 index 0000000..bb369e0 --- /dev/null +++ b/diagnostics/vendor-bugs/BUG-002-gel-workmanager-growth.md @@ -0,0 +1,53 @@ +# GEL WorkManager database grows into a long serial history + +## Klassifikation + +**CONFIRMED BUG / CONFIRMED enqueue-policy root cause – Medium.** Gerät und +Firmware wie BUG-001. Komponente `/system/app/gel/gel.apk`, AndroidX +WorkManager, `GELWorker`, Unique-Name `GEL_WORK`. + +## Benutzerwirkung + +`androidx.work.workdb` erreichte 229.453.824 Byte plus 524.288-Byte-WAL und +32.768-Byte-SHM. Der feste Unique-Work-Graph benötigte Minuten, um seine +Abhängigkeiten zu traversieren; neue GEL-Intents wurden merklich verzögert. + +## Erwartetes und tatsächliches Verhalten + +Abgeschlossene Telemetriearbeiten sollten begrenzt und prune-fähig bleiben. +Tatsächlich bestand eine jahrelange serielle `GEL_WORK`-Kette. Nach dem +autorisierten Paketdatenreset begann die DB bei 4.096 Byte; später blieb sie bei +ca. 102.400 Byte (WAL ca. 461 KiB). Der offizielle DiagnosticsWorker zeigte nur +elf sichtbare, erfolgreiche GEL-Arbeiten und keinen Rückstau. + +## Reproduktion / Beweis + +`dumpsys dbinfo org.droidtv.gel -v`, WorkManager-Diagnosebroadcast und GEL-Logs +verwenden; keine privaten Payloads ausgeben. Belege: +[phase3-storage-repair.md](../storage/phase3-storage-repair.md); der interne +Live-Validierungsbericht ist aus der kuratierten Veröffentlichung ausgeschlossen. + +## Root Cause + +**CONFIRMED:** `GatheringService.onStartCommand` in `classes.dex`, +`code_off=0x50bf0`, baut aus jedem Intent einen OneTimeWorkRequest, lädt +`GEL_WORK`, Policy-Integer `4` und ruft `WorkContinuationImpl.enqueue()` auf. +Der eingebettete AndroidX-Code bestätigt Wert 4 als `APPEND_OR_REPLACE`: neue +Arbeit hängt an den aktuellen Blättern. Das erzeugt die jahrelange serielle +Abhängigkeitsgeschichte und verteuert die Blatttraversierung. + +## Workaround und Fix + +Workaround: gezielter GEL-Datenreset nach Backup/Risikoabwägung. Ein bloßes +Ändern von Policy 4 auf KEEP ist nicht sicher, weil `GELWorker` unterschiedliche +Intents verarbeitet und dabei Ereignisse verloren gingen. Fix: persistente, +begrenzte FIFO/Outbox, Koaleszierung nur bewiesener State-Alarme, ein KEEP- +Drain-Worker und KEEP-Watchdog, Ack erst nach Dispatch. Siehe +[ROOT-CAUSE.md](../../patches/gel-workmanager/ROOT-CAUSE.md). + +## Regression Tests / Hash + +Das Phase-6-Modell bestand 10.000 koaleszierbare Alarme und 10.000 FIFO- +Ereignisse bei O(1) aktivem WorkSpec, begrenzter Queue und Crash-Retry. +`gel.apk` SHA-256: +`29A68E6B09700ED4CCDF312C0B99EEE0C3F67E329A537FE87ED4341A71D383D7`. diff --git a/diagnostics/vendor-bugs/BUG-003-usb-free-size-zero.md b/diagnostics/vendor-bugs/BUG-003-usb-free-size-zero.md new file mode 100644 index 0000000..04ba96f --- /dev/null +++ b/diagnostics/vendor-bugs/BUG-003-usb-free-size-zero.md @@ -0,0 +1,45 @@ +# UsbVolumeLister reports zero free bytes for mounted USB volumes + +## Klassifikation + +**CONFIRMED BUG – Medium.** Komponente `/system/framework/tvfw.jar`, Klasse +`UsbVolumeListerImpl`, Methode `getVolumes(String)`. + +## Benutzerwirkung + +Philips-Komponenten erhalten für ein öffentliches USB-Volume und das private +adopted Volume `mFreeSize=0`, obwohl `df` 44,2 bzw. 114,8 Millionen KiB frei +meldet. Dadurch sind falsche Speicheranzeigen und fehlerhafte Entscheidungen +möglich. + +## Erwartetes / tatsächliches Verhalten + +Für jedes `MOUNTED`-Volume muss die nutzbare Kapazität seines realen Pfades +geliefert werden. Der Code setzt `mFreeDiskSize` jedoch nur, wenn +`SystemProperties.get("sys.mtk.dmremoteservice.ready")` exakt `"1"` ist. +Live ist diese Property leer; vorhanden ist +`vendor.mtk.dmremoteservice.ready=0`. Der Java-Initialwert 0 wird ungeprüft als +echter Messwert weitergegeben. + +## Reproduktion und Beweis + +Ein public- und ein adopted-USB-Volume mounten, Philips-UsbVolumeLister-Log mit +`df`/`StatFs` desselben Pfades vergleichen. Details und Bytecodekette: +[usb-volume-uid1000-analysis.md](../phase4/usb-volume-uid1000-analysis.md). + +## Root Cause / Fix + +Eine optionale Remote-Service-Bereitschaft wird fälschlich zum einzigen +Messpfad. Exakt lokalisiert: `classes.dex`, `getVolumes(String)`, +`code_off=0x17b5e0`, Property-Branch PC `0x0133`. Der MediaTek-Produzent liefert +KiB; `UsbVolume` publiziert nach weiterer Division MiB. Robust ist: +DeviceManager-Wert nur verwenden, wenn ready und plausibel; sonst +`StatFs(volumeInfo.path)` in MiB. Ein unbekannter Wert darf nicht als echte Null +erscheinen. Details: [IMPLEMENTATION.md](../../patches/usb-freesize/IMPLEMENTATION.md). + +## Regression Tests / Hash + +Der Offline-Test bestand ready/unready, plausible/unplausible DM-Werte, +StatFs-Fallback, >2 TiB, echte Null und unknown. Live folgen public/adopted, +Mount/Unmount/Reinsert und Permission-Fälle. `tvfw.jar` SHA-256: +`a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad`. diff --git a/diagnostics/vendor-bugs/BUG-MATRIX.md b/diagnostics/vendor-bugs/BUG-MATRIX.md new file mode 100644 index 0000000..818d7c6 --- /dev/null +++ b/diagnostics/vendor-bugs/BUG-MATRIX.md @@ -0,0 +1,11 @@ +# Philips / TP Vision issue matrix + +| ID | Problem | Status | Component | Root Cause | User Impact | Workaround | Permanent Fix | Complexity | Regression | Report ready? | +|---|---|---|---|---|---|---|---|---|---|---| +| BUG-001 | EPG stale cleanup | CONFIRMED + live repair validated | channels.apk | Runnable mutiert nicht | High: 1,62 GiB Leak | kompakter DB-Neuaufbau | 7-Tage/2.000er Retention | Medium | offline full snapshot PASS | **yes** | +| BUG-002 | GEL WorkDB growth | CONFIRMED exact callsite | gel.apk/WorkManager | `APPEND_OR_REPLACE` unter `GEL_WORK` | Medium: 219 MiB + Latenz | gezielter GEL reset | bounded FIFO/outbox + KEEP drain | Medium | 10k+10k model PASS | **yes** | +| BUG-003 | USB free size zero | CONFIRMED exact method | tvfw.jar | optionales Property-Gate ohne Fallback | Medium: falsche Kapazität | `df` statt Philips-Anzeige | validated DM + StatFs fallback | Low–Medium | offline matrix PASS | **yes** | +| DESIGN-001 | 1-GiB storage warning + lifetime | DESIGN ISSUE, code path confirmed | TPTvService.apk | Kapazität und Wear werden vermischt | Medium: alarmierende Meldung | Leaks beseitigen | Android thresholds, hysteresis, separate health | Medium | policy model PASS | **yes as design report** | + +Separate confidential security finding available. It is intentionally excluded +from this public issue matrix and vendor functional-bug package. diff --git a/diagnostics/vendor-bugs/DESIGN-001-storage-monitor-threshold.md b/diagnostics/vendor-bugs/DESIGN-001-storage-monitor-threshold.md new file mode 100644 index 0000000..f41c552 --- /dev/null +++ b/diagnostics/vendor-bugs/DESIGN-001-storage-monitor-threshold.md @@ -0,0 +1,43 @@ +# Philips storage-warning policy diverges from Android + +## Klassifikation + +**DESIGN ISSUE / QUESTIONABLE POLICY – Medium user impact, not classified as a +confirmed implementation bug.** Komponente +`org.droidtv.tv.tptvservice.tvstoragemonitor.TVStorageMonitorService` in +`TPTvService.apk`. + +## Verhalten und Beweis + +Android `DeviceStorageMonitor` war `NORMAL`; seine `/data`-Low-Grenze beträgt +260.603.904 Byte. Philips verwendet zusätzlich etwa 1 GiB frei und koppelt die +Warnstufe an `persist.odm.tpv.sys.emmc_lifetime=0x0a 0x0a`. Der zweite Wert wird +als 90–100 % der geschätzten Lebensdauer verbraucht klassifiziert. Bei nur rund +4,85 GiB `/data` ist 1 GiB eine hohe absolute Schwelle. Adopted Storage mit +über 100 GiB frei beseitigt die interne `/data`-Knappheit nicht. + +Nach der EPG-Reparatur meldete Android weiterhin `NORMAL`; `/data` war nur noch +39 % belegt. Die visuelle Prüfung bestätigte, dass die Speicherwarnung +verschwunden ist. + +Phase 6 lokalisierte die exakte Runnable-Methode bei `code_off=0x2ad380` und die +Konstanten 524.288.000/1.073.741.824 Byte. Der Code koppelt Kapazität und +eMMC-Lifetime tatsächlich an Cleanup/UI. Außerdem überschreibt er eine +300-Sekunden-Zuweisung unmittelbar mit 30 Sekunden. + +## Empfehlung + +Androids `StorageManager`-Schwellen als Basis, zusätzliche Prozent-/Reserve- +Grenze mit Hysterese, reclaimable Bytes und Ursache anzeigen. eMMC-Lifetime als +separate Health-Warnung behandeln, nicht als Multiplikator einer Kapazitäts- +Meldung. EPG-/GEL-Leaks komponentennah begrenzen. Adopted freien Speicher nur +für tatsächlich dort platzierte Daten anrechnen. +Der getestete Zustandsentwurf trennt beide Achsen und ergänzt Debounce, +Hysterese, Übergangsbenachrichtigung und Cooldown; siehe +[POLICY.md](../../patches/storage-monitor/POLICY.md). + +## Tests / Hash + +Grenzübergänge, Hysterese, Reboot, adopted vorhanden/entfernt, Lifetime-Buckets +und Android/Philips-Zustände gegeneinander testen. `TPTvService.apk` SHA-256: +`75F492EE89906C8C267D685D2B7D0CE02BB09F3A6B4CC8456F8B096B9E441ECD`. diff --git a/diagnostics/vendor-bugs/PHILIPS-PATCH-SUBMISSION.md b/diagnostics/vendor-bugs/PHILIPS-PATCH-SUBMISSION.md new file mode 100644 index 0000000..2de8fe5 --- /dev/null +++ b/diagnostics/vendor-bugs/PHILIPS-PATCH-SUBMISSION.md @@ -0,0 +1,63 @@ +# Philips/TPVision patch submission – TPM191E + +Target: Philips 43PUS7394/12, `TPM191E_R.211.000.103.000`, Android TV 12/API +31. This submission contains functional reliability bugs only. Confidential +security material is deliberately excluded. + +## BUG-001 – EPG stale cleanup is a no-op + +| Field | Detail | +|---|---| +| Affected binary | `/system/priv-app/channels/channels.apk`, SHA-256 `3a44bc648624f0cc9cd9feb4cfc3fd33f41323597718e4fccfc25fe35504d34f` | +| Exact target | `classes4.dex`, `org.droidtv.channels.epg.bcepg.EpgProvider$deleteStaleDataRunnable.run()V`, `code_off=0xe298c` | +| Root cause | Runnable computes/logs `now-1800` (and optional `-604800`) but performs no delete/applyBatch/execSQL or call to existing time-delete routines | +| Proposed fix | normalize milliseconds to epoch seconds once; delete `scheduletable` entries older than 7 days using indexed predicates in transactions of at most 2,000 rows; schedule bounded periodic cleanup | +| Patch specification | `patches/epg-stale-cleanup/PATCH.diff`, `IMPLEMENTATION.md`, `SMALI-PATCH.md` | +| Full snapshot test | 1,742,053,376-byte DB; 3,063,863 stale rows deleted, 7,387 retained, 1,532 bounded transactions; result 4,059,136 bytes; `integrity_check=ok` | +| Idempotency | second run deletes zero rows and leaves identical counts/size | +| Real repair evidence | prior repair reclaimed 1,701,400 KiB on `/data`, 73%→39%; subsequent live EPG 59 Now/48 Next/1,041 Schedule and normal Guide/recording/timer/favorite behavior | + +Required regression tests: 0/7/30-day boundaries, seconds/milliseconds inputs, +clock change, duplicate run, concurrent EPG ingest/lock retry, abrupt power loss, +channel rescan, standby wake, all tuner sources, favorites, recordings, reminders +and no mutations outside EPG schedule rows. + +## BUG-002 – GEL WorkManager dependency history is unbounded + +| Field | Detail | +|---|---| +| Affected binary | `/system/app/gel/gel.apk`, SHA-256 `29a68e6b09700ed4ccdf312c0b99eee0c3f67e329a537fe87ed4341a71d383d7` | +| Exact target | `classes.dex`, `GatheringService.onStartCommand`, `code_off=0x50bf0` | +| Root cause | unique name `GEL_WORK` uses policy integer 4 = `APPEND_OR_REPLACE`; each event extends a serial dependency history | +| Observed effect | WorkManager DB 229,453,824 bytes plus WAL/SHM; dependency traversal delays | +| Proposed fix | bounded persistent FIFO/outbox, coalesce only proven state events, one KEEP drain worker plus watchdog, acknowledge after dispatch; do not blindly switch to KEEP and lose intents | +| Patch specification | `patches/gel-workmanager/PATCH.diff`, `ROOT-CAUSE.md`, `TEST-PLAN.md` | +| Test evidence | 10,000 coalescible and 10,000 FIFO events pass with bounded queue, O(1) active WorkSpec and crash retry | + +Required regression tests: ordering/loss/duplication across crash and reboot, +network offline/online, queue cap/backpressure, WorkManager prune/upgrade, all +intent classes and telemetry privacy/consent semantics. + +## BUG-003 – USB free capacity incorrectly reported as zero + +| Field | Detail | +|---|---| +| Affected binary | `/system/framework/tvfw.jar`, SHA-256 `a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad` | +| Exact target | `classes.dex`, `UsbVolumeListerImpl.getVolumes(String)`, `code_off=0x17b5e0`, property branch PC `0x0133` | +| Root cause | size is assigned only when `sys.mtk.dmremoteservice.ready == "1"`; live property is empty while `vendor.mtk.dmremoteservice.ready=0`, so constructor default zero is returned as a real measurement | +| Proposed fix | accept DeviceManager KiB only when service-ready and `total>0`, `0<=free<=total`; otherwise use `StatFs(VolumeInfo.path).getAvailableBytes()/1048576` with `long`; keep inaccessible/racy path UNKNOWN, not zero | +| Patch specification | `patches/usb-freesize/PATCH.diff`, `IMPLEMENTATION.md`, `SMALI-PATCH.md` | +| Test evidence | ready/unready, valid/invalid DeviceManager, StatFs fallback, >2 TiB, genuine zero and unknown all pass offline | + +Required device tests: public FAT/exFAT and adopted private ext4, mount/unmount/ +reinsertion, permission denial/removal race, truly full media, >2 TiB arithmetic, +UI/service consumers and class-loader/preopt compatibility. + +## Integration/signing request + +All three APK targets share the Philips Platform signer and UID1000; `tvfw.jar` +requires a coherent system image and ART preopt. Please build these changes from +the matching `TPM191E_R` source tree, sign with the existing approved keys, run +the tests above, and ship as an official A/B update or supply a separately +Platform-signed Channels test APK plus full model-specific rollback procedure. + diff --git a/diagnostics/vendor-bugs/PHILIPS-TECHNICAL-REPORT-DE.md b/diagnostics/vendor-bugs/PHILIPS-TECHNICAL-REPORT-DE.md new file mode 100644 index 0000000..e6f441c --- /dev/null +++ b/diagnostics/vendor-bugs/PHILIPS-TECHNICAL-REPORT-DE.md @@ -0,0 +1,67 @@ +# Technischer Bericht – Speichererschöpfung auf Philips TPM191E + +## Kurzfassung + +Gerät: Philips 43PUS7394/12, Firmware `TPM191E_R.211.000.103.000`, Android TV +12/API 31. Drei reproduzierbare Funktionsfehler und eine problematische +Warnpolicy wurden bis zu den konkreten Binärmethoden isoliert. Der dominante +EPG-Fehler ist durch eine kontrollierte Live-Reparatur und einen vollständigen +Offline-Patchtest besonders stark belegt. Ein separater vertraulicher +Security-Befund ist auf Anfrage verfügbar und nicht Bestandteil dieses Pakets. + +## Wirkung und erfolgreicher Live-Nachweis + +Vor der Reparatur war `EpgDatabase.db` 1.742.053.376 Byte groß und enthielt +3.071.250 abgelaufene Schedule-Einträge. `/data` war zu 73 % belegt und Philips +zeigte die Warnung „interner Speicher fast voll“. + +Nach dem schema-identischen kompakten Replacement war die DB zunächst 159.744 +Byte und nach echter EPG-Neuerfassung etwa 806.912 Byte groß. Sie enthielt 59 +Now-, 48 Next- und 1.041 Schedule-Einträge vom 25.08.2026 19:00 bis 01.09.2026 +02:50; `integrity_check=ok`. Freier `/data`-Platz stieg um 1.701.400 KiB +(1,62258 GiB), Belegung sank auf 39 %. Live TV, Bild/Ton, Senderwechsel, +Senderliste, Favoriten, Guide/Now-Next, Aufnahmen, Timer und Reminder wurden +visuell bestätigt; die Warnung verschwand. + +## Befunde und dauerhafte Fixes + +1. **BUG-001, EPG stale cleanup (High, CONFIRMED).** + `EpgProvider$deleteStaleDataRunnable.run()` in `channels.apk!/classes4.dex` + (`code_off=0xe298c`) berechnet/loggt eine Grenze, führt aber keine Mutation + aus. Der Phase-6-Fix konvertiert die Millisekunden-Clock einmal in die real + gespeicherten Epoch-Sekunden und löscht `scheduletable` mit sieben Tagen + Retention in indexgestützten 2.000er-Transaktionen. Auf einer Kopie der + 1,62-GiB-DB wurden 3.063.863 Zeilen gelöscht, 7.387 erhalten, die DB auf + 4.059.136 Byte reduziert und ein identischer zweiter Lauf bestätigt. +2. **BUG-002, GEL WorkManager (Medium, CONFIRMED).** + `GatheringService.onStartCommand` (`code_off=0x50bf0`) hängt jeden Intent + unter `GEL_WORK` mit Policy 4 `APPEND_OR_REPLACE` an den bestehenden + Blattgraphen. Das erklärt 229.453.824 Byte WorkDB und lange Enqueue-Latenz. + KEEP allein würde unterschiedliche Intents verlieren. Empfohlen ist eine + begrenzte persistente FIFO/Outbox, gezielte Koaleszierung, ein KEEP- + Drain-Worker und Watchdog. Das Modell bestand 10.000+10.000 Ereignisse. +3. **BUG-003, USB free size zero (Medium, CONFIRMED).** + `UsbVolumeListerImpl.getVolumes(String)` in `tvfw.jar` (`code_off=0x17b5e0`) + setzt Größen nur bei `sys.mtk.dmremoteservice.ready=1` und publiziert sonst + den Default Null. Der Fix validiert den KiB-DeviceManager-Wert und verwendet + sonst `StatFs` am echten Mountpfad, um MiB zu liefern; Messfehler bleiben + UNKNOWN. Die Offline-Matrix einschließlich >2 TiB bestand. +4. **DESIGN-001, Speicherwarnpolicy (Medium).** + `TVStorageMonitorService$2.run()` (`code_off=0x2ad380`) verwendet feste + 500-MB/1-GiB-Grenzen und vermischt freien Platz mit eMMC-Lifetime. Der + Ersatz nutzt Androids Volume-Schwellen, Debounce/Hysterese/Cooldown und + komponentennahe Bereinigung; Flash-Health bekommt eine separate Meldung. + +## Auslieferungsgrenze + +Channels, GEL und TPTvService verwenden `android.uid.system` und denselben +Philips-Plattform-Signer. Ein `/data/app`-Update benötigt dessen privaten +Schlüssel/akzeptierte Signierlinie. `tvfw.jar` ist eine registrierte Shared- +Library und benötigt kohärenten DEX-/ART-/Client- und AVB-Systemimage-Build. +Das Gerät ist locked/green. Deshalb liegen validierte Patchspezifikationen und +Tests, aber bewusst keine zufällig signierten APKs oder unsicheren Images bei. +Empfohlen wird zunächst ein Philips-signiertes Channels-Update, danach USB, +GEL und Warnpolicy jeweils einzeln mit geprüftem Rollback. + +Datenschutzbereinigte Einzelberichte und Hashes stehen in diesem Verzeichnis; +Implementierung und Tests unter `patches/`. diff --git a/diagnostics/vendor-bugs/PHILIPS-TECHNICAL-REPORT-EN.md b/diagnostics/vendor-bugs/PHILIPS-TECHNICAL-REPORT-EN.md new file mode 100644 index 0000000..6b166ea --- /dev/null +++ b/diagnostics/vendor-bugs/PHILIPS-TECHNICAL-REPORT-EN.md @@ -0,0 +1,66 @@ +# Technical report – storage exhaustion on Philips TPM191E + +## Executive summary + +Device: Philips 43PUS7394/12, firmware `TPM191E_R.211.000.103.000`, Android TV +12/API 31. Three reproducible functional defects and one problematic warning +policy were isolated to concrete binary methods. The dominant EPG defect has +exceptionally strong evidence from a controlled live repair and a complete +offline patch test. A separate confidential security finding is available on +request and is not part of this package. + +## Impact and successful live validation + +Before repair, `EpgDatabase.db` was 1,742,053,376 bytes and contained 3,071,250 +expired schedule records. `/data` was 73% used and the Philips low-internal- +storage warning was visible. + +After a schema-identical compact replacement, the DB was initially 159,744 +bytes and about 806,912 bytes after real EPG reacquisition. It contained 59 +Now, 48 Next and 1,041 Schedule rows spanning 25 Aug 2026 19:00 through 1 Sep +2026 02:50; `integrity_check=ok`. Free `/data` increased by 1,701,400 KiB +(1.62258 GiB), reducing use to 39%. Live TV, picture/audio, zapping, channels, +favorites, Guide/Now-Next, recordings, timers and reminders were visually +confirmed; the warning disappeared. + +## Findings and durable fixes + +1. **BUG-001, EPG stale cleanup (High, CONFIRMED).** + `EpgProvider$deleteStaleDataRunnable.run()` in `channels.apk!/classes4.dex` + (`code_off=0xe298c`) calculates/logs a boundary but performs no mutation. + The Phase 6 fix converts the millisecond clock once to the database's actual + epoch-second fields and deletes `scheduletable` in indexed 2,000-row + transactions with seven-day retention. A copy of the 1.62-GiB DB lost + 3,063,863 rows, retained 7,387, shrank to 4,059,136 bytes and produced an + identical no-op second run. +2. **BUG-002, GEL WorkManager (Medium, CONFIRMED).** + `GatheringService.onStartCommand` (`code_off=0x50bf0`) appends every Intent + below unique name `GEL_WORK` with policy 4, `APPEND_OR_REPLACE`. This explains + the 229,453,824-byte WorkDB and long enqueue traversal. KEEP alone would lose + distinct Intents. Use a bounded persistent FIFO/outbox, verified selective + coalescing, one KEEP drain worker and a watchdog. The model passed + 10,000+10,000 events. +3. **BUG-003, USB free size zero (Medium, CONFIRMED).** + `UsbVolumeListerImpl.getVolumes(String)` in `tvfw.jar` (`code_off=0x17b5e0`) + assigns size only when `sys.mtk.dmremoteservice.ready=1`, otherwise + publishing default zero. Validate the KiB DeviceManager result and fall back + to `StatFs` on the real mount path, publishing MiB; failures remain UNKNOWN. + The offline matrix, including >2 TiB, passed. +4. **DESIGN-001, storage warning policy (Medium).** + `TVStorageMonitorService$2.run()` (`code_off=0x2ad380`) uses fixed 500-MB/ + 1-GiB thresholds and mixes capacity with eMMC lifetime. Replace it with + Android volume thresholds, debounce/hysteresis/cooldown and owner-specific + cleanup. Flash health must have separate maintenance wording. + +## Delivery boundary + +Channels, GEL and TPTvService use `android.uid.system` and the same Philips +platform signer. A `/data/app` update needs its private key/accepted lineage. +`tvfw.jar` is a registered shared library requiring coherent DEX, ART/client +and AVB system-image integration. The device is locked/green. Therefore the +package contains validated specifications and tests, deliberately not randomly +signed APKs or unsafe images. Recommended order: Philips-signed Channels update, +then USB, GEL and warning policy separately with proven rollback. + +Privacy-safe individual reports and hashes are in this directory; implementation +and tests are under `patches/`. diff --git a/patches/BUILD-TOOLCHAIN.md b/patches/BUILD-TOOLCHAIN.md new file mode 100644 index 0000000..f4d3f16 --- /dev/null +++ b/patches/BUILD-TOOLCHAIN.md @@ -0,0 +1,42 @@ +# Reproducible Phase 6 build toolchain + +## Installed and missing + +Present: Temurin OpenJDK 21.0.11 (`java`, `javac`, `jar`, `keytool`), Python +3.12.6 and ADB 37.0.1. Missing from PATH: apktool, jadx, smali/baksmali, d8/r8, +zipalign and apksigner. No Philips source tree, Android 12 API-31 build stubs, +vendor build configuration, platform private key, AVB private key or accepted +firmware packer is available. Tools must be version-pinned and hashed before a +binary build; none should be downloaded or trusted blindly. + +## Per-component pipeline + +| Target | Golden input | Controlled pipeline | Output gate | +|---|---|---|---| +| Channels | `diagnostics/raw/storage/channels.apk` | verify hash → decode four DEX/resources → apply EPG source/smali change → assemble DEX → rebuild preserving manifest/resources → zipalign → sign with exact accepted platform signer | APK/DEX parse, only target methods changed, signer lineage exact, install/image test, regenerate ART | +| GEL | `diagnostics/raw/storage/gel.apk` | verify hash → implement outbox and schema migration in source/DEX → rebuild → align → platform-sign | migration/action matrix, bounded WorkDB, signer/shared UID, ART | +| TPTvService | `diagnostics/raw/TPTvService.apk` | verify hash → replace monitor state machine/resources → rebuild → align → platform-sign | core/persistent start, policy tests, UI strings, ART | +| tvfw | `diagnostics/phase4/reference-binaries/tvfw.jar` | verify hash → unpack one DEX → patch exact method → rebuild JAR deterministically | DEX reparse, registered-library client linkage, image integration, regenerated preopt | + +Never overwrite golden files. Build in a new versioned directory and record +tool hashes, command lines, input/output hashes and SOURCE_DATE_EPOCH. Java +pseudodiffs are specifications; a release build must integrate them into +Philips source or a fully verified DEX assembly. + +## Signing, ART and image integration + +All target APKs use `android.uid.system` and the same Philips platform signer. +Only that accepted private key/signing lineage can produce an updated package. +Signing with a new key or copying META-INF/signature blocks does not work: any +content change invalidates v1/v2/v3 cryptographic digests. + +Do not ship golden ODEX/VDEX next to changed DEX. Use the matching vendor build +to regenerate ARM32 preopt artifacts, or an explicitly tested image policy that +omits them and allows ART compilation. For `tvfw`, validate every client +class-loader context. It is a registered shared library, not bootclasspath. + +System-image changes require filesystem image regeneration and an AVB/vbmeta +chain signed with a key accepted by the locked/green device. A root shell alone +does not solve verified boot or platform signing. No binary was built in Phase +6 because the required private material and vendor build/recovery path are +absent; producing a randomly signed artifact would be misleading and unsafe. diff --git a/patches/DEPLOYMENT-MATRIX.md b/patches/DEPLOYMENT-MATRIX.md new file mode 100644 index 0000000..d49455a --- /dev/null +++ b/patches/DEPLOYMENT-MATRIX.md @@ -0,0 +1,27 @@ +# Deployment-path comparison + +| Path | Privilege / signer | AVB and SELinux | Persistence / rollback | Rating | +|---|---|---|---|---| +| A. ordinary APK update | exact Philips platform signer; no root | no AVB change; existing app domain | persistent in `/data/app`; OEM-tested uninstall-system-update rollback | **BEST FIRST TEST** for APK targets when signed | +| B. updated-system-app in `/data/app` | same as A; Package Manager-managed | no image change; shared UID/signer still mandatory | OTA/factory-reset interactions testable; base APK remains | **BEST FIRST TEST**; technically the mechanism behind A | +| C. runtime instrumentation | root/engineering/instrumentation authority | may hit SELinux; no durable image | temporary, fragile, timing-dependent | **HIGH RISK**, diagnostic prototype only | +| D. bind mount changed file | root every boot plus compatible binary | underlying AVB unchanged, but labels/ART/package signature remain problems | nonstandard persistence; remove mount/reboot rollback | **NOT PRACTICAL** for APK/shared library release | +| E. system partition/image patch | vendor build or root+unlocked accepted image; platform signer for APKs | filesystem and AVB/vbmeta must be regenerated; labels exact | persistent; A/B image rollback if proven | **GOOD LONG-TERM**, currently blocked/high risk | +| F. vendor partition patch | accepted vendor image and policy coordination | cannot correctly relocate current `/system` targets; split-policy risk | OTA-coupled | **NOT PRACTICAL** for these four targets | +| G. complete modified firmware | Philips source, platform/AVB/container keys and recovery | coherent SELinux, preopt and AVB | strongest reproducibility and A/B rollback when OEM-supported | **GOOD LONG-TERM**, unavailable today | + +## Exact root boundary + +| Operation | UID1000 enough? | Root? | Bootloader/AVB? | Reason | +|---|---:|---:|---:|---| +| one-time EPG data repair | yes | no | no | provider-owned app data; already validated | +| install accepted platform-signed APK update | Package Manager path | no | no | `/data/app` overlay mechanism | +| persistent Channels/GEL/TPTv replacement without OEM signer | no | not sufficient | image/accepted trust chain | shared UID signature remains enforced | +| replace `tvfw.jar` temporarily | no | yes | no for volatile bind; still unsafe | root mount plus shared-library/ART restart | +| replace `tvfw.jar` persistently | no | image path | yes/accepted AVB | verified `/system` and preopt coupling | +| bind mount | no | yes | no if volatile | mount capability and process restart | +| modify system/vendor image | no | build/flash authority | yes | locked green verified boot | +| selective adopted OEM directory/policy | app code plus vold/SELinux integration | engineering image, not runtime root as design goal | accepted image | new policy and lifecycle code | + +Root is a deployment tool, not a fix. It neither reconstructs Philips private +keys nor makes a modified image acceptable to locked AVB. diff --git a/patches/MODIFIED-FIRMWARE-ROADMAP.md b/patches/MODIFIED-FIRMWARE-ROADMAP.md new file mode 100644 index 0000000..719c6f9 --- /dev/null +++ b/patches/MODIFIED-FIRMWARE-ROADMAP.md @@ -0,0 +1,53 @@ +# Modified firmware roadmap (design only – no flashing performed) + +## Phase 6 status + +Exact stock methods and root causes are now confirmed for EPG, GEL, USB and the +storage monitor. Source-level patch specifications and offline harnesses pass. +No deployable APK/JAR/image exists: Philips platform signing, vendor build +configuration, accepted AVB signing and a proven recovery path remain blockers. +See `PHASE6-DECISION.md`, `DEPLOYMENT-MATRIX.md` and `VALIDATION.md`. The next +permitted live candidate is a Philips-signed Channels updated-system APK only; +no system modification was made in Phase 6. + +| Target | Original path / hash | Class/method | Patch | Layer | Signing / AVB / risk | +|---|---|---|---|---|---| +| EPG | `/system/priv-app/channels/channels.apk`; `3A44BC...04D34F` | `EpgProvider$deleteStaleDataRunnable.run`, helper/pump | typed 7-day batched retention | APK-internal + system | platform signature/shared UID; rebuild APK/DEX/odex; system AVB; medium boot risk, high TV-function regression risk | +| GEL queue | `/system/app/gel/gel.apk`; `29A68E...D383D7` | enqueue site for `GELWorker`/`GEL_WORK` | bounded/coalesced unique work + prune | APK-internal + system | platform shared UID; system AVB; medium | +| USB size | `/system/framework/tvfw.jar`; `a0061795...8d11ad` | `UsbVolumeListerImpl.getVolumes` | property-compatible `StatFs` fallback | registered shared Java library | DEX/oat and client class-loader consistency; system AVB; **high functional/boot risk** | +| Storage policy | TPTvService APK; `75F492...41ECD` | `TVStorageMonitorService` | API-based threshold, hysteresis, separate health warning | privileged APK | platform signature/system AVB; medium | +| Selective adopted data | Channels APK plus optional vold-aware helper | DB-open/migration layer | private-volume DB with internal fallback | APK/platform integration | SELinux policy and lifecycle changes; high functional risk, must not become boot dependency | + +The shortened hashes above are display labels; full hashes are in the +individual vendor reports. + +## Build/repack sequence + +1. Reproduce the exact Android 12 32-bit ARM DEX/minSdk environment and preserve + package names, sharedUserId and manifest permissions. +2. Patch from decompiled Java only as a specification; implement against smali + or source, rebuild, align and sign with the firmware's platform key. A random + replacement signature breaks shared UID and privileged grants. +3. For `tvfw.jar`, update matching shared-library preopt artifacts or force + correct first-boot compilation in a test image. Verify all `uses-library` + client class-loader contexts and hidden-API/class linkage. +4. Integrate into inactive-slot system image, recompute filesystem/AVB metadata + and sign vbmeta with an accepted key. The current locked/green production + chain does not accept arbitrary images. +5. Test on a sacrificial board or fully recoverable OEM engineering path before + this TV. Do not mix binary changes with storage migration in the first build. + +## SELinux and OTA + +The three functional fixes need no new SELinux access. A selective private- +volume design does: define a narrow type for reconstructible EPG data and allow +only Channels database operations after vold readiness. Never grant generic +`mnt_expand_file` access. Every system/vendor change invalidates stock AVB and +may be overwritten by OTA; maintain reproducible patches per firmware build. + +## Root boundary + +The completed EPG repair and diagnosis required no root. Root/engineering image +access is genuinely required for replacing APK/JAR files, policy/fstab/init, +bind mounts, private-volume ownership outside app APIs, compiled-artifact +management and AVB-compatible image deployment. None was attempted. diff --git a/patches/PHASE6-DECISION.md b/patches/PHASE6-DECISION.md new file mode 100644 index 0000000..aea7230 --- /dev/null +++ b/patches/PHASE6-DECISION.md @@ -0,0 +1,59 @@ +# Phase 6 decision + +## Outcome + +The four defects have actionable fixes and reproducible offline evidence. Three +exact source-level patch specifications and a storage-policy implementation +plan were produced; all local harnesses pass. No deployable APK/JAR/firmware +binary was built because the cryptographic and vendor-build prerequisites do +not exist in the workspace. This is the technically correct stopping point. + +## Answers to the Phase 6 decision questions + +1. **Real patchability:** BUG-001, BUG-002 and BUG-003 are patchable at their + exact methods; DESIGN-001 is patchable in TPTvService. BUG-002 requires a + bounded durable outbox, not a one-byte KEEP edit. +2. **Successfully built:** guarded EPG cleanup implementation, exact diffs, + GEL/USB/storage models, static verifiers, baseline and central harness. + All pass. No signed binary was built. +3. **Signing blocker:** Channels, GEL and TPTvService share + `android.uid.system` and the Philips platform signer. Its private key or an + accepted signer service is required even for `/data/app` updates. +4. **AVB blocker:** persistent `tvfw` or `/system` changes require a regenerated + image/vbmeta accepted by the locked green AVB chain. Root alone is not + enough. +5. **UID1000 capability:** one-time app-data EPG repair is sufficient and was + proven. UID1000 cannot sign/update its own APK or alter verified `/system`. +6. **Root need:** temporary instrumentation/bind mount and loose system-file + experiments need root. Those are not production deployment solutions. +7. **Bootloader/firmware access:** needed for persistent system/shared-library + images unless Philips supplies signed OTA/firmware packaging. A/B/recovery + capability exists architecturally but an accessible accepted restore path + is not proven. +8. **First live patch:** BUG-001 as a Philips-platform-signed Channels + updated-system APK, one patch only, after rollback is proven. It has the + clearest measured benefit. No live test should begin today. +9. **Selective adopted storage:** not presently justified. Correct retention + changed the live EPG DB from 1.62 GiB to ~0.8 MiB and restored 1.62 GiB of + `/data` capacity without USB dependency. Architecture C (internal retained + DB) wins. +10. **Robust long-term architecture:** Philips source-integrated fixes, exact + platform signing, rebuilt ART artifacts, narrow/no new SELinux permissions, + and a complete OEM A/B image/OTA signed by accepted AVB keys. Use bounded + retention/outboxes and keep capacity and eMMC health separate. + +## Status by patch + +| Patch | Root cause | Offline implementation | Tests | Deployment | +|---|---|---|---|---| +| EPG stale cleanup | CONFIRMED exact Runnable logs without mutation | READY as source/SQL spec | PASS including 1.62-GiB snapshot | BLOCKED on signed Channels build/recovery | +| GEL WorkManager | CONFIRMED exact APPEND_OR_REPLACE callsite | READY as bounded-outbox design/model | PASS | BLOCKED on source/schema integration and signer | +| USB free size | CONFIRMED property-gated zero default | READY as exact method spec | PASS | BLOCKED on tvfw rebuild, clients/preopt and AVB | +| Storage monitor | CONFIRMED fixed thresholds + health conflation | READY as state-machine plan | PASS | BLOCKED on TPTvService build/signer | + +## Final recommendation + +Send the vendor package with exact evidence and request either Philips-signed +updated APKs (Channels first) or a complete TPM191E maintenance firmware. Keep +the current repaired television state unchanged. Preserve the phase-5 rollback +snapshots until normal use and a longer observation period remain clean. diff --git a/patches/PHASE7-DECISION.md b/patches/PHASE7-DECISION.md new file mode 100644 index 0000000..ef43857 --- /dev/null +++ b/patches/PHASE7-DECISION.md @@ -0,0 +1,74 @@ +# Phase 7 decision – Signing, OTA, recovery and deployment + +## 1. Can the three APKs be updated under `/data/app`? + +Android's updated-system-app mechanism can in principle update Channels, GEL and +TPTvService while retaining their stock `/system` APKs. The device already uses +this mechanism for other packages. None of the three is currently updated. + +## 2–3. What exact signature blocks us? + +All three share UID1000 and the exact Platform signer SHA-256 +`D8:FF:E0:AF:40:4E:AB:8E:45:5A:00:74:D4:17:B0:DA:12:D0:67:26:E5:05:B0:2B:7F:DB:18:31:29:C8:90:68`. +It is also the framework-res signer. The matching private key or OEM signing +service is unavailable; the signer is therefore mandatory and currently +blocking. The separate OTA certificate cannot replace it. + +## 4. tvfw.jar + +No useful isolated deployment exists. It is DEX-bearing, preoptimized and +loaded as a Philips shared library by many clients. Although not on +BOOTCLASSPATH/SYSTEMSERVERCLASSPATH, it requires coherent ART/client/system- +image integration and accepted AVB. + +## 5. AVB boundary + +The device is locked/green with AVB 1.2 and enforcing dm-verity. `system`, +`vendor`, `linux_rootfs`, `3rd` and `factory` have device-proven AVB mappings/ +fstab flags. Exact vbmeta descriptors, keys, chains and rollback indexes are +unread. A content-changed system image is not trusted without OEM AVB signing. + +## 6. Recovery paths actually proven + +Proven: PackageManager fallback for a removable updated-system APK while normal +Android/ADB runs; static A/B architecture, UpdateEngine, Boot-Control HAL and +UpdateVerifier. Not proven: physical recovery key combo, recovery ADB/sideload, +USB emergency mode, manual slot control or end-to-end bad-slot fallback. No +separate recovery partition exists. + +## 7. Bad Channels update + +Likely the TV still boots and the update can be removed with PackageManager, +revealing the untouched stock APK. This is not yet cold-boot tested, and a data +schema migration could make the old APK unusable. Hence it is not a present GO. + +## 8. Bad framework/tvfw patch + +Many Philips TV clients may crash; ADB/UI availability is uncertain. There is +no PM fallback. Recovery depends on A/B rollback or an official full USB +firmware path, neither end-to-end proven. High risk. + +## 9–10. Safest permanent path and live status + +Safest production path: Philips/TPVision integrates, signs and ships the fixes +through its official build/OTA process. Safest first engineering candidate: +OEM-Platform-signed Channels updated-system APK after a lab rollback drill. +**First live patch remains NO-GO.** No installation was performed in Phase 7. + +## 11. What Philips/TPVision must provide + +- matching source/build integration and approved Platform signing; +- preferably an official A/B firmware containing the fixes; +- full `autorun*.upg` recovery firmware for 43PUS7394/12 / `TPM191E_R`; +- exact cold-boot USB recovery/key procedure; +- documented payload/AVB signing and rollback policy; +- or a Platform-signed Channels test APK plus verified uninstall/data rollback. + +## 12. Maintenance fallback + +Use `tools/epg-health-check/health_check.py`. Its default ADB mode reads only +`dumpsys dbinfo` and `df`; offline snapshot mode uses SQLite read-only. It never +repairs automatically. If maintenance is recommended, repeat only the already +validated supervised repair workflow after a fresh backup and explicit +authorization. + diff --git a/patches/RECOVERY-PLAN.md b/patches/RECOVERY-PLAN.md new file mode 100644 index 0000000..e27fc7d --- /dev/null +++ b/patches/RECOVERY-PLAN.md @@ -0,0 +1,52 @@ +# Recovery plan before any future live patch + +## Current gate + +**NO-GO for deployment today.** No patched binary, accepted Philips signer, +verified firmware packer, proven bootloader entry, or independently tested +recovery/USB restore route exists. The commands below are procedural templates, +not instructions executed in Phase 6. + +## Golden component records + +| Component | TV path | SHA-256 | owner:group / mode / label | Runtime impact | +|---|---|---|---|---| +| Channels | `/system/priv-app/channels/channels.apk` | `3a44bc...04d34f` | `root:root 0644 system_file` | EPG/TV provider | +| GEL | `/system/app/gel/gel.apk` | `29a68e...d383d7` | same | GEL actions/WorkManager | +| TPTvService | `/system/app/TPTvService/TPTvService.apk` | `75f492...41ecd` | same | persistent TV/storage service | +| tvfw | `/system/framework/tvfw.jar` | `a00617...8d11ad` | same | registered TV shared library | + +Full APK/JAR and ARM ODEX/VDEX hashes are in `baseline/hashes.sha256`. Local +golden copies are immutable. Before a test, also record package dump, slot, +AVB state, process state, current APK path, data DB hashes/sizes, owners, modes, +SELinux labels and a bootable OEM firmware copy. + +## Rollback by deployment type + +- **Platform-signed `/data/app` update:** use only a previously lab-validated + Package Manager “uninstall system update” procedure for that exact Android + build, then restart the affected process or reboot through the approved test + plan. Do not assume a command spelling until it is confirmed read-only/help + output and tested on a recoverable device. Base `/system` remains intact. +- **Volatile instrumentation/bind mount:** stop the affected process, remove the + test mechanism from a root/engineering environment, restore its mount + namespace, then restart. If that cannot be done, controlled reboot returns to + the verified base only if no persistent boot hook was added. +- **A/B image:** keep the known-good slot untouched. The new slot must have a + boot-success timeout and automatic/manual slot fallback already proven. + Never mark it successful before TV, ADB and storage validation. +- **Full firmware:** use only Philips-signed recovery media and a procedure + demonstrated on the same platform. Preserve the original firmware and all + hashes offline. + +## Crash and bootloop response + +For an app crash with working ADB, collect logs/package state first, then invoke +the prevalidated rollback. For a `tvfw` linkage failure or system_server/boot +loop, ADB may never become available; rollback must therefore be possible from +boot control, recovery or signed USB media without Android userspace. Existing +init/recovery hooks and A/B layout are evidence of capability, not proof that a +publicly accessible restore path accepts our image. + +Do not start any system/shared-library test until cold-boot recovery, power-loss +recovery and return-to-stock hashes have been demonstrated end-to-end. diff --git a/patches/REGRESSION-TEST-PLAN.md b/patches/REGRESSION-TEST-PLAN.md new file mode 100644 index 0000000..cc9546c --- /dev/null +++ b/patches/REGRESSION-TEST-PLAN.md @@ -0,0 +1,51 @@ +# Regression test plan + +## EPG + +- Empty, current, seven-day and 30-day datasets; simulated rows older than + cutoff; time-zone/DST and explicit millisecond-unit tests. +- Verify current/future EPG, Now/Next, guide, channel change, refresh, search, + favorites, reminders, manual/event recordings and series metadata. +- Cleanup twice (idempotence), while DB locked, during acquisition, at low + space, and with injected power loss between batches/commit. +- 2.000-row batching, endtime-index plan, lock duration and auto-vacuum writes. +- Daily measurements for 30 days: DB bytes, pages, rows, oldest/newest; + historical data never exceeds policy plus one batch. + +## GEL + +- 1/100/10.000 service intents and 60-second alarms; reboot during enqueue and + worker execution. +- WorkSpec/dependency counts, maximum chain depth, oldest completion, DB/WAL + size, enqueue-to-run latency and `pruneWork` behavior. +- Idempotent coalescing and guaranteed delivery of classified non-idempotent + events; no backlog after network/service outage. + +## USB free space + +- Public VFAT and private adopted ext4; DMRemote property old/new name, + ready=1/0/missing; service exception. +- Mounted, genuinely full, read-only, removed during query, reinserted with new + path, >2-GiB values. Compare long-byte result to `StatFs`/`df`. + +## Storage monitor + +- Just above/below Android and OEM thresholds; fill/reclaim cycles verify + hysteresis and notification dismissal. +- adopted present/missing/full, but internal `/data` independently full/free. +- eMMC lifetime buckets and PRE_EOL independently represented; no capacity + false positive solely from health data. + +## Selective adopted EPG + +- Cold boot with USB present, absent, slow, corrupt and removed during write; + vold delayed; user unlock; factory reset; OTA; UUID replacement. +- SELinux enforcing throughout; correct owner/label; no path traversal; + SQLite integrity and locking across all fallback transitions. + +## Phase-5 repair acceptance + +Software gates: ADB/network alive, Android volumes NORMAL, Channels/EPGData/ +PlayTV/TVInput running, DB integrity OK, no SQLite exception/crashloop, 358 +channel mirrors and 203 mappings retained. Visual gates are listed in +`diagnostics/phase5/epg-repair.md` and require the owner. diff --git a/patches/SAFE-LIVE-TEST-PLAN.md b/patches/SAFE-LIVE-TEST-PLAN.md new file mode 100644 index 0000000..e547a9a --- /dev/null +++ b/patches/SAFE-LIVE-TEST-PLAN.md @@ -0,0 +1,43 @@ +# Safe future live-test plan + +No deployment is authorized by this document. Begin only after a matching +platform-signed artifact, static validation and the recovery gates in +`RECOVERY-PLAN.md` exist. Test one patch per build. + +## Candidate order + +1. EPG cleanup (clearest root cause and measurable result). +2. USB free-size. +3. GEL bounded outbox. +4. Storage monitor policy. + +The first binary candidate should be a Philips-signed Channels updated-system +APK in `/data/app`, not a `/system` overwrite. Until that exists, the already +validated UID1000 one-time EPG data repair is the only proven live mechanism. + +## Per-patch protocol + +**BEFORE:** preserve golden binaries/data, hashes, current package paths, +signer/version, storage/EPG/GEL/USB metrics, logs, slot/AVB state and user-visible +TV baseline. Prove rollback after a cold boot. + +**DEPLOY:** verify input and output hashes/signature, install through the single +approved path, and make no other change. Never pair changed DEX with golden +ODEX/VDEX manually. + +**RESTART:** stop/start only the minimal affected package when supported; a +shared-library image requires the vendor-defined reboot. Record downtime and +all startup/linkage errors. + +**VALIDATE:** run software gates plus Live TV, picture/audio, zapping, channel +list, favorites, Guide/Now-Next, recordings/timers/reminders, standby/wake, +network/ADB and low-storage UI. Add target metrics: EPG cutoff/plateau; USB vs +`df`; GEL graph/outbox bounds; storage hysteresis/health separation. + +**ROLLBACK:** trigger rollback on signature/linkage failure, crashloop, +boot-time regression, missing TV function, data corruption, wrong units, event +loss, unexpected storage UI or inability to collect evidence. Restore the +known-good artifact/slot, validate hashes and rerun the baseline. + +Minimum release soak is 30 days including power interruption and USB removal; +EPG must cross several daily cleanup cycles. diff --git a/patches/SIGNING-ANALYSIS.md b/patches/SIGNING-ANALYSIS.md new file mode 100644 index 0000000..37cdcbb --- /dev/null +++ b/patches/SIGNING-ANALYSIS.md @@ -0,0 +1,38 @@ +# Platform-signing and shared-UID analysis + +## Proven baseline + +Channels, GEL and TPTvService carry the same RSA-2048 certificate, SHA-256 +`D8:FF:E0:AF:40:4E:AB:8E:45:5A:00:74:D4:17:B0:DA:12:D0:67:26:E5:05:B0:2B:7F:DB:18:31:29:C8:90:68`, +and APK Signature Schemes v1, v2 and v3. Their manifests request the system +shared UID; Channels is privileged and TPTvService is core/persistent. The +public certificate exists in each APK, but the private key does not exist in +the workspace. + +## Package Manager consequences + +An updated system app can normally live under `/data/app` and override its +read-only `/system` base while preserving system-app identity. For these +packages it must have the same package name, compatible version and—decisively— +the exact accepted signer/signing lineage. The shared UID also requires signer +compatibility with the other members. A debug/test key, a certificate with the +same subject text, or copied signature files is rejected or invalid. + +Therefore: + +- a Philips-platform-signed Channels/GEL/TPTvService APK is potentially the + safest first deployment because it need not alter `/system` or AVB; +- a locally rebuilt but differently signed APK is not an installable update; +- changing sharedUserId/package name creates a different app and cannot replace + privileged providers/services or inherit signature permissions; +- no “signature bypass” is proposed. + +`tvfw.jar` is not an APK and has no package update route. Its integrity is +controlled by the verified system image and ART/shared-library integration. + +## What is still unknown + +Philips signer custody/lineage, whether Philips will sign an external patch, +their current build-tools versions, OTA payload keys and firmware container +signing are unknown. They must be supplied by Philips or a documented accepted +engineering/recovery chain; they cannot be inferred from public certificates. diff --git a/patches/STORAGE-REDESIGN.md b/patches/STORAGE-REDESIGN.md new file mode 100644 index 0000000..381a453 --- /dev/null +++ b/patches/STORAGE-REDESIGN.md @@ -0,0 +1,72 @@ +# Recommended storage redesign + +## Decision + +Keep `/data` on internal eMMC. Repair retention/pruning leaks first. Use adopted +USB only for large, reconstructible and non-boot-critical content through +Android's private-volume APIs, always with an internal fallback. Do not make +boot, Package Manager, user unlock or core TV playback depend on USB. + +## Architecture + +1. Internal: settings, credentials, TV channel source, favorites, + recordings/reminders metadata, small EPG index/mapping and a minimal empty + EPG DB. +2. Adopted private ext4: optional EPG schedule payload/cache and downloadable + assets only after an OEM platform integration. Android has no ordinary + public API that allocates one arbitrary SQLite file directly in another + app-private adopted-volume root. `getExternalFilesDirs()` may expose a + secondary/emulated path but is not the desired direct private-ext4 SQLite + contract; PackageManager movement moves the package/data as a unit and is + not selective. Never use `/storage/emulated` FUSE for this database. +3. At boot, Channels opens its internal minimal DB immediately. An OEM helper + creates/types an app-owned directory below the adopted private volume. A + volume callback activates it only after vold reports `MOUNTED` and an + app-owned sentinel/schema/hash check passes. This needs a narrow SELinux + type; it is not available on the stock image. +4. If USB disappears or I/O fails, close adopted SQLite connections, fall back + to internal Now/Next/minimal schedule, notify without crashloop, and rebuild + adopted cache after reinsertion. +5. Split the current monolithic DB at the provider layer: internal + channel/mapping/Now/Next authority, optional external schedule cache. Do not + use cross-volume SQLite `ATTACH` as an atomicity assumption. DB operations + use a private ext4 directory, normal SQLite locking, fsync/atomic rename and + bounded retention. No symlink or global bind mount. + +## Why this order + +The EPG and GEL fixes already recover about 1,84 GiB combined without changing +boot architecture. Official app movement offers little here because the large +listed packages are updated system apps. A whole-`/data` USB design intersects +early init, vold timing, FBE/fscrypt, SELinux, recovery, factory reset, OTA and +USB power; removal could make the TV unbootable. + +## Failure and migration protocol + +- Feature flag default off; preflight performance and free space. +- Copy only reconstructible schedule rows with SQLite backup into a versioned + temp directory, run `integrity_check`, fsync file and directory, then + atomically switch an app-level generation marker; retain internal fallback. +- Never migrate recording/reminder DB or channel/favorite authority blindly. +- Maintain last-known-good schema version and garbage-collect old adopted + generations only after successful reopen. +- USB missing/slow/corrupt is a tested normal state, not a boot failure. +- Removal mid-query/write must close the external pool, discard the incomplete + generation and continue with internal Now/Next. Reinsertion never reuses a + DB until UUID, schema, integrity and generation checks pass. +- Adopted media is device-bound/encrypted and not a portable backup. Factory + reset, reformat, UUID change and OTA migration are explicit test cases. + +## Classification + +Leak fixes: **SUPPORTED APP/FIRMWARE FIX**. Movable ordinary apps: **ANDROID +PLATFORM BEHAVIOR**. Selective EPG private-volume path: **POSSIBLE WITH PATCH**. +Bind mount/symlink: **HIGH-RISK HACK**. Whole `/data`: **NOT PRACTICAL**. + +## Release gates + +Random-write latency and fsync p95 must remain within an OEM-defined TV UI +budget; 1,000 removal/reinsert cycles, standby power cuts, full-volume writes, +corruption, factory reset and OTA must leave Live TV and the internal provider +usable. Without OEM source, SELinux policy and a recoverable image build this +remains architecture documentation, not a deployable patch. diff --git a/patches/VALIDATION.md b/patches/VALIDATION.md new file mode 100644 index 0000000..def0942 --- /dev/null +++ b/patches/VALIDATION.md @@ -0,0 +1,49 @@ +# Phase 6 validation report + +Validation date: 2026-08-26. Scope is offline engineering only; no patched +binary was installed and no TV state was changed during the validation run. + +## Automated result + +`python patches/tests/run_all.py --write-hashes` completed successfully: + +- all five golden inputs matched their pinned SHA-256 hashes; +- EPG synthetic tests passed empty, boundary, current/future, 30-day, + >1-year, multi-batch and idempotent cases; +- the GEL stock DEX verifier matched the exact `GEL_WORK` policy-4 enqueue + instructions; +- bounded-outbox tests passed 10,000 coalescible and 10,000 FIFO events, O(1) + active WorkSpec, caps and crash retry; +- USB tests passed DeviceManager validation, StatFs fallback, >2-TiB, true + zero and unknown states; +- storage policy tests passed low debounce, critical, hysteresis, notification + deduplication, unknown measurement and health/capacity separation. + +Hashes of the central small artifacts are in `artifacts/hashes.sha256`. + +## Historic EPG snapshot result + +On a disposable byte-identical copy of the 1,742,053,376-byte DB, the seven-day +algorithm deleted 3,063,863 of 3,071,250 schedule rows in 1,532 transactions +(maximum 2,000 rows), retained 7,387, and shrank the FULL-auto-vacuum DB to +4,059,136 bytes. `integrity_check=ok`; the oldest retained end time equaled the +cutoff, future data remained, and an immediate second run changed neither bytes +nor hash. Runtime was 141.609 seconds; slowest batch 1,391 ms. See +`epg-stale-cleanup/phase6-full-snapshot-result.json`. + +## Binary static-validation status + +Golden APK/JAR archives and DEX callsites were parseable; manifests, DEX counts, +certificates, v2/v3 signing blocks and preopt inventory were recorded. There is +no patched APK/JAR to re-decompile, compare, align or signature-verify. Those +gates are **NOT RUN / BLOCKED**, not passed: Philips source/build configuration, +platform private key, AVB key and proven recovery route are absent. Emitting a +randomly signed package would not satisfy Package Manager or shared UID. + +## Remaining release gates + +Vendor-source integration, rebuilt-DEX comparison, unchanged manifest/resource +diff, exact signer-lineage verification, ARM preopt/client linkage, recoverable +image or accepted `/data/app` deployment, acquisition-lock contention, +standby/power-loss, 30-day soak and full user-visible TV regression all remain. +The status is **offline patch design validated; live firmware binary NO-GO**. diff --git a/patches/adopted-storage/FAILURE-MATRIX.md b/patches/adopted-storage/FAILURE-MATRIX.md new file mode 100644 index 0000000..7c27357 --- /dev/null +++ b/patches/adopted-storage/FAILURE-MATRIX.md @@ -0,0 +1,19 @@ +# Selective adopted-storage failure matrix + +| Event | Required behavior | Forbidden outcome | +|---|---|---| +| USB absent at boot | Open internal channel/Now/Next DB; schedule cache empty/reacquires | boot/provider crashloop | +| Removal during read | cancel cursor, close external pool, retry against internal minimum | stale FD loop or TV UI ANR | +| Removal during write | rollback/discard incomplete external generation | promote partial DB | +| Slow/failing fsync | trip latency/I/O circuit breaker and fall back internal | block Live TV thread | +| External full | stop cache acquisition, retain internal authority | internal low-space warning caused by USB | +| Corrupt DB/sentinel | quarantine generation logically and rebuild later | attach/open as authoritative | +| Reinsert same UUID | verify schema, integrity and generation before use | blind reopen | +| Different/reformatted UUID | initialize a new cache | trust old pointer | +| Factory reset | forget pointer/key and recreate internal minimum | require removed USB to boot | +| OTA/schema change | migrate a copy or discard/reacquire cache | in-place irreversible migration without fallback | +| Standby/power loss | recover last committed generation | cross-volume atomicity assumption | + +The external payload is reconstructible cache only. Channel numbering, +favorites, recordings, timers, reminders, credentials and the minimal provider +schema remain internal. diff --git a/patches/artifacts/MANIFEST.md b/patches/artifacts/MANIFEST.md new file mode 100644 index 0000000..ead3818 --- /dev/null +++ b/patches/artifacts/MANIFEST.md @@ -0,0 +1,18 @@ +# Phase 6 artifact manifest + +No patched APK, JAR, ODEX, VDEX, system image or firmware container is emitted. +The required Philips platform/AVB signing material, vendor source/build config +and proven recovery path are unavailable. An unsigned or randomly signed APK +would fail the shared-UID update gate; a loose changed JAR would not be a safe +firmware artifact. + +Successfully built and tested artifacts are instead: + +- exact source-level `PATCH.diff` specifications for EPG, GEL and USB; +- storage policy and selective adopted-storage design; +- executable guarded EPG cleanup reference and four offline test harnesses; +- stock-callsite/signing-block verifiers; +- golden hashes and the machine-readable full-snapshot test result. + +Their final hashes are generated in `patches/artifacts/hashes.sha256` by the +central validation harness. Binary output remains **BLOCKED**, not failed. diff --git a/patches/artifacts/hashes.sha256 b/patches/artifacts/hashes.sha256 new file mode 100644 index 0000000..f27d419 --- /dev/null +++ b/patches/artifacts/hashes.sha256 @@ -0,0 +1,7 @@ +6b4e70f384b6296166d2695b745b82957b0ce6a5aa9fda7cea7d2c98739fa559 patches/epg-stale-cleanup/PATCH.diff +1af44bd2d1792569a993f31c2314cb769fae1289dcff84d77f0fddb86104b99f patches/epg-stale-cleanup/tools/epg_retention.py +a1cf5ad08d16962bb15290c6bd5dcf2e9b6f715c0f0eebc9a3d177db0da445f7 patches/gel-workmanager/PATCH.diff +2c0b1e9cde0cc244bc530bb5bfef32a7750871a1d285e450ac0140268ba4fd87 patches/gel-workmanager/tools/verify_stock_callsite.py +be82f55e864d127d5ad8f0eacd2748438d6eefd7cdbb986c4c5480b1d21b41c4 patches/usb-freesize/PATCH.diff +9cf6295eef02b9efcefb2e22c76034bea05b83cbaf0bcfcdc2bc8111b7b7edaf patches/storage-monitor/POLICY.md +c3de6303e22e33bbcda3270b178f166018a3bad929d7eb7821c29d98841f6193 patches/storage/SELECTIVE-ADOPTED-DESIGN.md diff --git a/patches/baseline/BASELINE.md b/patches/baseline/BASELINE.md new file mode 100644 index 0000000..211936e --- /dev/null +++ b/patches/baseline/BASELINE.md @@ -0,0 +1,62 @@ +# Phase 6 golden baseline + +Captured 2026-08-26 for `TPM191E_R.211.000.103.000`. The four local binaries +are immutable reference inputs. No build step may overwrite them; patched +outputs must use a new path and filename. Full hashes are in `hashes.sha256`. + +| Component | TV path | Local reference | Bytes | Package / SDK | DEX | Runtime importance | +|---|---|---|---:|---|---:|---| +| Channels | `/system/priv-app/channels/channels.apk` | `diagnostics/raw/storage/channels.apk` | 14,047,493 | `org.droidtv.channels`, v1/0.0.0.1, min 31, target 30, compile 31 | 4 | privileged UID 1000, `android.uid.system`, `system_app`; core TV/EPG | +| GEL | `/system/app/gel/gel.apk` | `diagnostics/raw/storage/gel.apk` | 398,392 | `org.droidtv.gel`, v58724408/7.2.0.56, min 28, target/compile 33 | 1 | system UID 1000, `android.uid.system`, `system_app`; telemetry/WorkManager | +| TV framework | `/system/framework/tvfw.jar` | `diagnostics/phase4/reference-binaries/tvfw.jar` | 3,275,657 | registered Java shared library `tvfw`, no APK manifest | 1 | broad TV client surface; highest deployment risk | +| TP TV service | `/system/app/TPTvService/TPTvService.apk` | `diagnostics/raw/TPTvService.apk` | 5,471,048 | `org.droidtv.tv.tptvservice`, v31/12, min 31, target 30, compile 31, `coreApp` | 1 | persistent system UID 1000, `android.uid.system`, `system_app`; storage warning service | + +## Signing baseline + +All three APKs carry the same v1 signer certificate and APK Signature Scheme +v2 and v3 blocks (plus verity padding): + +- subject/issuer: `EMAILADDRESS=android@android.com, CN=Android, OU=Android, O=Android, L=Mountain View, ST=California, C=US` +- certificate SHA-256: `D8:FF:E0:AF:40:4E:AB:8E:45:5A:00:74:D4:17:B0:DA:12:D0:67:26:E5:05:B0:2B:7F:DB:18:31:29:C8:90:68` +- certificate SHA-1: `43:80:01:76:AC:53:BF:E9:FD:33:4A:EB:86:1D:40:A1:2A:00:AF:C0` +- algorithm: SHA1withRSA, RSA-2048; validity 2015-08-10 through 2042-12-26. + +The public certificate is present; the private platform signing key is not. +Matching the subject name is not sufficient—Package Manager requires the same +cryptographic signer for the shared UID. `tvfw.jar` has no JAR signature. + +## Compiled artifacts on the stock image + +All APK/JAR and OAT/VDEX files are `root:root`, mode `0644`, SELinux +`u:object_r:system_file:s0`, timestamp 2009-01-01. The device has 32-bit ARM +compiled artifacts: + +| Input | ODEX bytes | VDEX bytes | +|---|---:|---:| +| Channels | 66,212 | 131,484 | +| GEL | 21,156 | 7,292 | +| TPTvService | 49,828 | 105,692 | +| tvfw | 37,540 | 51,572 | + +Their hashes are recorded in `hashes.sha256`. A replacement image must either +ship matching regenerated artifacts or deliberately use a tested image-build +path that causes ART to regenerate them. Mixing a modified DEX with stock +ODEX/VDEX is not an acceptable deployment artifact. + +## Trust and deployment boundary + +The active `/system` is AVB/dm-verity protected and the device is locked/green. +The APKs use the system shared UID; Channels is also privileged and TPTvService +is persistent/core. Therefore a normally installed APK, an ADB overwrite, or a +randomly signed rebuild is not a valid replacement. The viable production +artifact is an OEM/platform-signed APK integrated into a correctly generated +and AVB-signed firmware image. `tvfw.jar` is not in `BOOTCLASSPATH`; it is the +registered `tvfw` shared Java library and still has preoptimization and client +class-loader-context coupling. + +## Evidence boundary + +Local SHA-256, ZIP/DEX layout, manifests and certificates were inspected +offline. TV paths, labels, sizes and compiled-artifact hashes were captured by +read-only `pm path`, `ls`, `stat`, `ps` and `sha256sum`; no TV state was +changed. The EPG snapshot is a test input only and remains untouched. diff --git a/patches/baseline/hashes.sha256 b/patches/baseline/hashes.sha256 new file mode 100644 index 0000000..99721b1 --- /dev/null +++ b/patches/baseline/hashes.sha256 @@ -0,0 +1,15 @@ +3a44bc648624f0cc9cd9feb4cfc3fd33f41323597718e4fccfc25fe35504d34f diagnostics/raw/storage/channels.apk +29a68e6b09700ed4ccdf312c0b99eee0c3f67e329a537fe87ed4341a71d383d7 diagnostics/raw/storage/gel.apk +a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad diagnostics/phase4/reference-binaries/tvfw.jar +75f492ee89906c8c267d685d2b7d0ce02bb09f3a6b4cc8456f8b096b9e441ecd diagnostics/raw/TPTvService.apk +71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba diagnostics/phase4/epg-snapshot/EpgDatabase.db + +# Read-only hashes captured on the TV; files were not pulled or modified. +a440f6914a3d6010e9482cc0be962978ab2db73eb2384fb5dc2f5c1e72a25215 /system/priv-app/channels/oat/arm/channels.odex +be8923c2aa6232ed44dc81b141e24ed9dc92d84ba346cdb9625da338cd86a29b /system/priv-app/channels/oat/arm/channels.vdex +587cd8b7a1a3b6861bd0fb577a0fc0b9e101bf8899c38acae6a83c30d77895d1 /system/app/gel/oat/arm/gel.odex +f3ba405a06d1129b19e197207cde94842703acb39663d251f34156778ae867f9 /system/app/gel/oat/arm/gel.vdex +8dade47e3181a2906bc7485096d5d62b25da7ee7888bd878180bb1e0f949513d /system/app/TPTvService/oat/arm/TPTvService.odex +7a29519398c8a136a1bf7872470e51085e9688af33500f4e707c90fdee63016e /system/app/TPTvService/oat/arm/TPTvService.vdex +a4af432c54153c9325d92f338f93b2b0a6cfa6b618a5d2c31d587e93eceff531 /system/framework/oat/arm/tvfw.odex +abd0f4ce99404199332a779b195c53ca57d2238d0368910b0936cd55a8572509 /system/framework/oat/arm/tvfw.vdex diff --git a/patches/baseline/inspect_apk_signing_block.py b/patches/baseline/inspect_apk_signing_block.py new file mode 100644 index 0000000..6cd7468 --- /dev/null +++ b/patches/baseline/inspect_apk_signing_block.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""List APK Signing Block pair IDs without changing the APK.""" + +import struct +import sys +from pathlib import Path + +NAMES = { + 0x7109871A: "APK Signature Scheme v2", + 0xF05368C0: "APK Signature Scheme v3", + 0x1B93AD61: "APK Signature Scheme v3.1", + 0x6DFF800D: "Source stamp", + 0x42726577: "verity padding", +} + +for arg in sys.argv[1:]: + path = Path(arg) + data = path.read_bytes() + eocd = data.rfind(b"PK\x05\x06") + if eocd < 0: + raise SystemExit(f"{path}: EOCD not found") + central = struct.unpack_from("run()V` +at `code_off=0xe298c`. The Runnable executes under the provider monitor, reads +the Philips TV clock, calculates and logs a cutoff, but never calls a delete, +batch, `applyBatch`, or SQLite mutation. The production integration must move +the potentially long legacy cleanup outside that provider-wide monitor and +serialize it with EPG acquisition writes. + +## Unit contract and retention + +The clock API returns epoch **milliseconds**. Physical `scheduletable.starttime` +and `endtime` values are epoch **seconds**, proven from both the historic and +reacquired databases. Therefore the only supported conversion is: + +```java +long nowMillis = clock.currentTimeMillis(); +long cutoffSeconds = TimeUnit.MILLISECONDS.toSeconds(nowMillis) + - TimeUnit.DAYS.toSeconds(7); +``` + +Seven past days preserve backward-guide behavior while all future rows remain. +The first fix intentionally touches only `scheduletable`; Now/Next are tiny and +FVP expiry semantics require separate product tests. + +## Database operation + +Use `scheduletable(endtime)` for the ordered candidate scan. Each non-exclusive +transaction removes at most 2,000 IDs selected by `endtime, _id`, commits, then +yields briefly. Abort cleanly on interruption or sustained busy/IO failure. +Log the unit, cutoff, batch count, rows, elapsed time, DB size, and error class. +Schedule one daily maintenance invocation with unique/KEEP semantics. + +SQLite journaling makes each batch atomic; an interrupted run merely resumes +at the next oldest row. The predicate is idempotent. The stock database has +`auto_vacuum=FULL`, so continuous small retention also returns tail pages +without a separate `VACUUM`. + +## Deliverables and evidence + +- `PATCH.diff`: source-level production specification. +- `SMALI-PATCH.md`: exact DEX boundary and integration constraints. +- `tools/epg_retention.py`: guarded offline reference implementation. +- `tools/test_epg_retention.py`: synthetic regression harness. +- `TEST-RESULTS.md`: complete old-snapshot result. + +This is not a signed APK. Philips source or a controlled smali/DEX rebuild, +platform signing, ART/preopt handling, and a recoverable image path are still +required for firmware deployment. diff --git a/patches/epg-stale-cleanup/PATCH-PLAN.md b/patches/epg-stale-cleanup/PATCH-PLAN.md new file mode 100644 index 0000000..a45d801 --- /dev/null +++ b/patches/epg-stale-cleanup/PATCH-PLAN.md @@ -0,0 +1,76 @@ +# Patch plan – bounded EPG retention + +## Target + +`/system/priv-app/channels/channels.apk`, DEX class +`org.droidtv.channels.epg.bcepg.EpgProvider$deleteStaleDataRunnable` plus a +small helper in `EpgDatabaseHelper`. Original APK SHA-256: +`3A44BC648624F0CC9CD9FEB4CFC3FD33F41323597718E4FCCFC25FE35504D34F`. + +## Policy + +- Keep the clock API explicitly in **epoch milliseconds**, then convert once to + **epoch seconds** before touching the database. The physical `starttime` and + `endtime` values in both the old and freshly reacquired databases are epoch + seconds. Use `TimeUnit.MILLISECONDS.toSeconds(nowMillis)` and + `TimeUnit.DAYS.toSeconds(7)`; never compare a millisecond cutoff to the DB. +- Retain seven past days for backward-guide/recording metadata compatibility; + retain all future events. Make the value OEM-configurable in 1–30 days. +- Now/Next may be deleted after `endtime < now-2h`; FVP rows must also respect + their expiry semantics. + +## Algorithm + +Ensure `scheduletable(endtime)` remains indexed. Once per day, initially 5–15 +minutes after provider startup and only while no acquisition transaction is +active, delete at most 2.000 rows per transaction: + +```java +long nowMillis = clock.millis(); +long cutoffSeconds = TimeUnit.MILLISECONDS.toSeconds(nowMillis) + - TimeUnit.DAYS.toSeconds(retentionDays); +do { + int n = db.deleteByIdBatch( + "scheduletable", "endtime < ?", cutoffSeconds, "endtime", 2000); + metrics.addDeleted(n); + if (n == 2000) SystemClock.sleep(100); +} while (n == 2000 && powerAndLoadPolicy.permitsMaintenance()); +``` + +`deleteByIdBatch` must run one transaction containing an index-ordered ID +selection with `LIMIT ?` and a delete by those IDs. Do not build a 2.000- +placeholder SQL string; use a temp-ID table or +`DELETE ... WHERE _id IN (SELECT _id ... ORDER BY endtime LIMIT 2000)` with +bound cutoff. Commit each batch so rollback journal/lock duration is bounded. + +The fixed Runnable should call this helper and log cutoff, unit, rows, duration, +DB bytes and remaining old-row count—not the misleading word “Deleting” before +any mutation. Schedule a unique daily WorkManager job with `KEEP`, not a chain. + +Phase 6 deliberately limits the first production change to `scheduletable`. +Now/Next are tiny and FVP expiry semantics are not sufficiently proven for a +shared delete rule. They can be added only after table-specific retention tests. + +## Auto-vacuum / power / locks + +The DB is `auto_vacuum=FULL`; small daily batches prevent the massive page-move +storm seen after years of accumulation. Use normal SQLite transactions and +`busy_timeout`/bounded retry with jitter; never delete while acquisition owns a +write transaction. A power loss can roll back one batch only. Do not `VACUUM` +during Live TV. For a one-time legacy migration, offer the already validated +compact-rebuild workflow with backup and atomic rename. + +## Smali plan + +Replace the Runnable body after cutoff computation with a typed call to the new +helper; do not inline a shell or raw concatenated SQL payload. If adding DEX +methods is impractical, redirect to the already present +`EpgProgramPump.deleteScheduleProgramsByTime(long)` only after correcting its +unit and adding batching; its current unbounded delete is unsuitable for a +3-million-row legacy DB. + +## Acceptance + +See `patches/REGRESSION-TEST-PLAN.md`. Key gate: after a 30-day synthetic soak, +oldest row >= cutoff (within one batch), DB growth plateaus, current/future EPG, +favorites and recording/reminder tables remain correct. diff --git a/patches/epg-stale-cleanup/PATCH.diff b/patches/epg-stale-cleanup/PATCH.diff new file mode 100644 index 0000000..d726b5a --- /dev/null +++ b/patches/epg-stale-cleanup/PATCH.diff @@ -0,0 +1,57 @@ +--- a/src/org/droidtv/channels/epg/bcepg/EpgProvider.java ++++ b/src/org/droidtv/channels/epg/bcepg/EpgProvider.java +@@ +- long staleTime = System.currentTimeMillis() - 1800L; +- if (epgInfo != null && epgInfo.isFreeViewAvailable()) { +- staleTime -= 604800L; +- } +- Log.d(TAG, "Deleting stale data for time " + staleTime); ++ final long nowMillis = System.currentTimeMillis(); ++ final long cutoffSeconds = TimeUnit.MILLISECONDS.toSeconds(nowMillis) ++ - TimeUnit.DAYS.toSeconds(7); ++ maintenanceExecutor.execute(() -> { ++ EpgCleanupResult r = mDbHelper.deleteOldScheduleInBatches( ++ cutoffSeconds, 2000); ++ Log.i(TAG, "EPG cleanup unit=seconds cutoff=" + cutoffSeconds ++ + " deleted=" + r.deleted + " batches=" + r.batches ++ + " durationMs=" + r.durationMs); ++ }); + +--- a/src/org/droidtv/channels/epg/bcepg/EpgDatabaseHelper.java ++++ b/src/org/droidtv/channels/epg/bcepg/EpgDatabaseHelper.java +@@ ++ EpgCleanupResult deleteOldScheduleInBatches(long cutoffSeconds, int batchSize) { ++ if (batchSize < 1 || batchSize > 2000 ++ || cutoffSeconds < 1_000_000_000L ++ || cutoffSeconds > 4_102_444_800L) { ++ throw new IllegalArgumentException("invalid cleanup boundary"); ++ } ++ SQLiteDatabase db = getWritableDatabase(); ++ int total = 0, batches = 0; ++ long started = SystemClock.elapsedRealtime(); ++ for (;;) { ++ int changed; ++ db.beginTransactionNonExclusive(); ++ try { ++ db.execSQL("DELETE FROM scheduletable WHERE _id IN (" ++ + "SELECT _id FROM scheduletable WHERE endtime < ? " ++ + "ORDER BY endtime, _id LIMIT ?)", ++ new Object[]{cutoffSeconds, batchSize}); ++ changed = (int) DatabaseUtils.longForQuery( ++ db, "SELECT changes()", null); ++ db.setTransactionSuccessful(); ++ } finally { ++ db.endTransaction(); ++ } ++ total += changed; ++ batches++; ++ if (changed < batchSize || Thread.currentThread().isInterrupted()) break; ++ SystemClock.sleep(100); ++ } ++ return new EpgCleanupResult(total, batches, ++ SystemClock.elapsedRealtime() - started); ++ } + +# Reconstructed source-level specification. Do not apply blindly to a Philips +# source tree. Acquisition locking and the existing provider monitor must be +# integrated and tested in the vendor implementation. diff --git a/patches/epg-stale-cleanup/REFERENCE-PATCH.diff b/patches/epg-stale-cleanup/REFERENCE-PATCH.diff new file mode 100644 index 0000000..0a014c6 --- /dev/null +++ b/patches/epg-stale-cleanup/REFERENCE-PATCH.diff @@ -0,0 +1,57 @@ +--- a/src/org/droidtv/channels/epg/bcepg/EpgProvider.java ++++ b/src/org/droidtv/channels/epg/bcepg/EpgProvider.java +@@ +- long staleTime = System.currentTimeMillis() - 1800L; +- if (epgInfo != null && epgInfo.isFreeViewAvailable()) { +- staleTime -= 604800L; +- } +- Log.d(TAG, "Deleting stale data for time " + staleTime); ++ // Contract: TV clock is milliseconds; the physical EPG columns are epoch seconds. ++ final long nowMillis = System.currentTimeMillis(); ++ final long cutoffSeconds = TimeUnit.MILLISECONDS.toSeconds(nowMillis) ++ - TimeUnit.DAYS.toSeconds(7); ++ final EpgCleanupResult result = mDbHelper.deleteOldScheduleInBatches( ++ cutoffSeconds, 2000); ++ Log.i(TAG, "EPG cleanup unit=seconds cutoff=" + cutoffSeconds ++ + " deleted=" + result.deleted + " batches=" + result.batches ++ + " durationMs=" + result.durationMs); ++ mEpgProviderHandler.removeCallbacks(mDeleteStaleDataRunnable); ++ mEpgProviderHandler.postDelayed(mDeleteStaleDataRunnable, ++ TimeUnit.DAYS.toMillis(1)); + +--- a/src/org/droidtv/channels/epg/bcepg/EpgDatabaseHelper.java ++++ b/src/org/droidtv/channels/epg/bcepg/EpgDatabaseHelper.java +@@ ++ EpgCleanupResult deleteOldScheduleInBatches(long cutoffSeconds, int batchSize) { ++ if (cutoffSeconds < 1_000_000_000L || cutoffSeconds > 4_102_444_800L) { ++ throw new IllegalArgumentException("cutoff is not plausible epoch seconds"); ++ } ++ final SQLiteDatabase db = getWritableDatabase(); ++ int total = 0; ++ int batches = 0; ++ int n; ++ final long started = SystemClock.elapsedRealtime(); ++ do { ++ db.beginTransactionNonExclusive(); ++ try { ++ db.execSQL("DELETE FROM scheduletable WHERE _id IN (" ++ + "SELECT _id FROM scheduletable WHERE endtime < ? " ++ + "ORDER BY endtime, _id LIMIT ?)", ++ new Object[] { cutoffSeconds, batchSize }); ++ n = (int) DatabaseUtils.longForQuery(db, "SELECT changes()", null); ++ db.setTransactionSuccessful(); ++ } finally { ++ db.endTransaction(); ++ } ++ total += n; ++ batches++; ++ if (n == batchSize) SystemClock.sleep(100); ++ } while (n == batchSize && !Thread.currentThread().isInterrupted()); ++ return new EpgCleanupResult(total, batches, ++ SystemClock.elapsedRealtime() - started); ++ } + +# This is a source-level implementation specification reconstructed from DEX. +# It is not claimed to apply cleanly to an unavailable Philips source tree. +# A production integration must also move the database work outside the +# provider-wide monitor currently held by deleteStaleDataRunnable.run(). diff --git a/patches/epg-stale-cleanup/SMALI-PATCH.md b/patches/epg-stale-cleanup/SMALI-PATCH.md new file mode 100644 index 0000000..1949d52 --- /dev/null +++ b/patches/epg-stale-cleanup/SMALI-PATCH.md @@ -0,0 +1,39 @@ +# Exact DEX patch boundary + +## Stock method + +The target is `channels.apk!/classes4.dex`: + +`Lorg/droidtv/channels/epg/bcepg/EpgProvider$deleteStaleDataRunnable;->run()V` + +Stock code item: `registers=6`, `ins=1`, `outs=4`, `code_off=0xe298c`. +It enters the provider-global monitor, obtains `ITvClockManager`, calls +`System.currentTimeMillis()`, subtracts literal `1800`, conditionally subtracts +`604800`, logs “Deleting”, and returns without a mutation. The instruction +offsets and full disassembly are preserved in +`diagnostics/deep-forensics/philips/epg-runnable-context.txt`. + +## Required implementation, not an unsafe in-place hex edit + +An acceptable smali rebuild must: + +1. calculate `nowMillis` as a `long`; +2. invoke `TimeUnit.MILLISECONDS.toSeconds(nowMillis)` or divide by the explicit + long `1000` with overflow-safe ordering; +3. subtract `TimeUnit.DAYS.toSeconds(7)` (=604800 seconds); +4. reject implausible cutoffs outside 2001-09-09 through 2100-01-01; +5. call a new typed helper that executes ordered 2,000-row transactions; +6. schedule the next daily run and log post-mutation metrics; +7. perform database I/O after leaving the existing provider-global monitor. + +Do **not** redirect directly to the existing +`EpgProgramPump.deleteScheduleProgramsByTime(long)`: its DEX method performs one +unbounded `ContentResolver.delete()` and would create an uncontrolled +multi-million-row transaction. Do not pass the current millisecond value to +that method: the database columns demonstrably contain epoch seconds. + +Adding the helper changes method/string/type tables and code-item sizes, so a +full classes4.dex rebuild is required. The absence of a local smali/apktool +tool and, more importantly, of the Philips platform private key means no +deployable signed APK is emitted in this phase. `REFERENCE-PATCH.diff` and the +executable SQLite harness are the reviewable patch artifacts. diff --git a/patches/epg-stale-cleanup/TEST-RESULTS.md b/patches/epg-stale-cleanup/TEST-RESULTS.md new file mode 100644 index 0000000..3bd2e51 --- /dev/null +++ b/patches/epg-stale-cleanup/TEST-RESULTS.md @@ -0,0 +1,43 @@ +# Phase 6 EPG retention test results + +All mutating tests ran offline. The golden 1,742,053,376-byte snapshot remained +at SHA-256 `71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba`. +The full test used a disposable byte-for-byte copy. + +## Synthetic gates + +`python patches/epg-stale-cleanup/tools/test_epg_retention.py` + +PASS for empty DB, exact cutoff, just-expired row, current and future rows, +30-day and >1-year rows, duplicate timestamps spanning multiple batches, +integrity and a second idempotent run. + +## Golden-snapshot workload + +Fixed test clock: 2026-08-26 12:00:00 +02:00 (`1787738400` seconds). +Seven-day cutoff: `1787133600` seconds. + +| Metric | Before | After | +|---|---:|---:| +| Schedule rows | 3,071,250 | 7,387 | +| Rows older than cutoff | 3,063,863 | 0 | +| DB bytes | 1,742,053,376 | 4,059,136 | +| SQLite pages | 425,306 | 991 | +| Freelist pages | 0 | 0 | + +The implementation committed 1,532 transactions of at most 2,000 rows. Total +runtime was 141.609 s on this PC; the slowest batch was 1,391 ms. SQLite +`integrity_check` returned `ok`. Because the stock DB has +`auto_vacuum=FULL`, the file shrank by 1,737,994,240 bytes without a VACUUM. + +The immediate second run deleted zero rows, took effectively zero seconds and +left both size and SHA-256 unchanged. Detailed machine-readable results are in +`phase6-full-snapshot-result.json`. + +## Interpretation and release gate + +This proves SQL correctness and bounded transactions for this database, not +TV-runtime scheduling or lock safety. A real firmware build still needs an +acquisition-contention test, standby/power-loss test, 30-day soak and the full +Live TV/Guide/recording/reminder regression matrix. The production patch must +not hold the provider-global monitor during the 2–3 minute legacy cleanup. diff --git a/patches/epg-stale-cleanup/phase6-full-snapshot-result.json b/patches/epg-stale-cleanup/phase6-full-snapshot-result.json new file mode 100644 index 0000000..f9cebd0 --- /dev/null +++ b/patches/epg-stale-cleanup/phase6-full-snapshot-result.json @@ -0,0 +1,46 @@ +{ + "input_sha256": "71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba", + "working_copy_only": true, + "now_seconds": 1787738400, + "cutoff_seconds": 1787133600, + "retention_days": 7, + "time_unit": "unix_epoch_seconds", + "before": { + "bytes": 1742053376, + "page_size": 4096, + "page_count": 425306, + "freelist_count": 0, + "auto_vacuum": 1, + "schedule_rows": 3071250, + "eligible": 3063863, + "retained": 7387, + "min_end_seconds": 0, + "max_end_seconds": 1787608800 + }, + "maintenance": { + "batch_size": 2000, + "batches_including_final": 1532, + "deleted": 3063863, + "duration_seconds": 141.609, + "max_batch_ms": 1391.0 + }, + "after": { + "bytes": 4059136, + "page_count": 991, + "freelist_count": 0, + "schedule_rows": 7387, + "eligible": 0, + "retained": 7387, + "min_end_seconds": 1787133600, + "max_end_seconds": 1787608800, + "integrity_check": "ok", + "sha256": "44c2da13ce17df9031bc83474fcf91a2f3c4d75fd68a2acdfd3674913e9e4cd4" + }, + "second_run": { + "deleted": 0, + "duration_seconds": 0.0, + "bytes_unchanged": true, + "sha256_unchanged": true, + "integrity_check": "ok" + } +} diff --git a/patches/epg-stale-cleanup/tools/epg_retention.py b/patches/epg-stale-cleanup/tools/epg_retention.py new file mode 100644 index 0000000..363c7ee --- /dev/null +++ b/patches/epg-stale-cleanup/tools/epg_retention.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Reference implementation and offline verifier for bounded EPG retention. + +The Philips schema stores EPG timestamps as Unix epoch *seconds*. This tool is +read-only unless both --apply and --ack-working-copy are supplied. Mutating +mode is restricted to the sibling work/ directory so the golden snapshot +cannot be opened for writing by accident. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +from pathlib import Path +import sqlite3 +import time + + +ROOT = Path(__file__).resolve().parents[1] +WORK_ROOT = (ROOT / "work").resolve() +GOLDEN_SHA256 = "71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba" + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for block in iter(lambda: f.read(8 * 1024 * 1024), b""): + h.update(block) + return h.hexdigest() + + +def parse_now(value: str | None) -> int: + if value is None: + return int(time.time()) + if value.isdecimal(): + n = int(value) + if n > 10_000_000_000: + raise ValueError("--now must be epoch seconds, not milliseconds") + return n + parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise ValueError("ISO --now requires an explicit timezone") + return int(parsed.timestamp()) + + +def pragma_int(db: sqlite3.Connection, name: str) -> int: + return int(db.execute(f"PRAGMA {name}").fetchone()[0]) + + +def table_exists(db: sqlite3.Connection, table: str) -> bool: + return db.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,) + ).fetchone() is not None + + +def table_stats(db: sqlite3.Connection, table: str, cutoff: int) -> dict: + if not table_exists(db, table): + return {"present": False} + row = db.execute( + f"SELECT count(*), min(endtime), max(endtime), " + f"sum(CASE WHEN endtime < ? THEN 1 ELSE 0 END), " + f"sum(CASE WHEN endtime >= ? THEN 1 ELSE 0 END) FROM {table}", + (cutoff, cutoff), + ).fetchone() + return { + "present": True, + "rows": row[0], + "min_end_seconds": row[1], + "max_end_seconds": row[2], + "eligible": row[3] or 0, + "retained": row[4] or 0, + } + + +def snapshot(db: sqlite3.Connection, path: Path, cutoff: int) -> dict: + return { + "path": str(path), + "bytes": path.stat().st_size, + "page_size": pragma_int(db, "page_size"), + "page_count": pragma_int(db, "page_count"), + "freelist_count": pragma_int(db, "freelist_count"), + "auto_vacuum": pragma_int(db, "auto_vacuum"), + "scheduletable": table_stats(db, "scheduletable", cutoff), + } + + +def delete_batches(db: sqlite3.Connection, cutoff: int, batch_size: int) -> dict: + sql = ( + "DELETE FROM scheduletable WHERE _id IN (" + "SELECT _id FROM scheduletable WHERE endtime < ? " + "ORDER BY endtime, _id LIMIT ?)" + ) + total = batches = 0 + max_batch_ms = 0.0 + started = time.monotonic() + while True: + one = time.monotonic() + db.execute("BEGIN IMMEDIATE") + try: + cur = db.execute(sql, (cutoff, batch_size)) + deleted = cur.rowcount + db.commit() + except BaseException: + db.rollback() + raise + elapsed_ms = (time.monotonic() - one) * 1000 + max_batch_ms = max(max_batch_ms, elapsed_ms) + batches += 1 + total += deleted + if deleted < batch_size: + break + return { + "deleted": total, + "batches_including_final": batches, + "batch_size": batch_size, + "max_batch_ms": round(max_batch_ms, 3), + "duration_seconds": round(time.monotonic() - started, 3), + } + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("database", type=Path) + ap.add_argument("--now", help="epoch seconds or timezone-qualified ISO-8601") + ap.add_argument("--retention-days", type=int, default=7) + ap.add_argument("--batch-size", type=int, default=2000) + ap.add_argument("--apply", action="store_true") + ap.add_argument("--ack-working-copy", action="store_true") + ap.add_argument("--hash", action="store_true", help="include file SHA-256") + args = ap.parse_args() + + path = args.database.resolve() + if not path.is_file(): + ap.error(f"not a file: {path}") + if not 1 <= args.retention_days <= 30: + ap.error("retention must be between 1 and 30 days") + if not 100 <= args.batch_size <= 10_000: + ap.error("batch size must be between 100 and 10000") + + now_seconds = parse_now(args.now) + cutoff = now_seconds - args.retention_days * 86_400 + result = { + "time_unit": "unix_epoch_seconds", + "now_seconds": now_seconds, + "retention_days": args.retention_days, + "cutoff_seconds": cutoff, + "mode": "apply" if args.apply else "read_only", + } + + if args.apply: + if not args.ack_working_copy: + ap.error("--apply requires --ack-working-copy") + if path.parent != WORK_ROOT: + ap.error(f"writes are restricted to working copies directly in {WORK_ROOT}") + db = sqlite3.connect(path, timeout=5.0, isolation_level=None) + else: + uri = f"file:{path.as_posix()}?mode=ro&immutable=1" + db = sqlite3.connect(uri, uri=True) + db.execute("PRAGMA query_only=ON") + + try: + result["before"] = snapshot(db, path, cutoff) + if args.hash: + result["sha256_before"] = sha256(path) + if args.apply: + db.execute("PRAGMA busy_timeout=5000") + result["maintenance"] = delete_batches(db, cutoff, args.batch_size) + result["integrity_check"] = db.execute("PRAGMA integrity_check").fetchone()[0] + result["after"] = snapshot(db, path, cutoff) + if args.hash: + result["sha256_after"] = sha256(path) + print(json.dumps(result, indent=2, sort_keys=True)) + finally: + db.close() + + +if __name__ == "__main__": + main() diff --git a/patches/epg-stale-cleanup/tools/test_epg_retention.py b/patches/epg-stale-cleanup/tools/test_epg_retention.py new file mode 100644 index 0000000..8407619 --- /dev/null +++ b/patches/epg-stale-cleanup/tools/test_epg_retention.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Synthetic regression tests for epg_retention.delete_batches().""" + +from __future__ import annotations + +import sqlite3 +import tempfile +from pathlib import Path + +from epg_retention import delete_batches + + +NOW = 1_800_000_000 +CUTOFF = NOW - 7 * 86_400 + + +def rows(db: sqlite3.Connection) -> list[tuple[int, int]]: + return db.execute("SELECT _id,endtime FROM scheduletable ORDER BY _id").fetchall() + + +def run_case(values: list[int], expected: list[int], batch: int = 100) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "test.db" + db = sqlite3.connect(path, isolation_level=None) + db.execute("PRAGMA auto_vacuum=FULL") + db.execute("CREATE TABLE scheduletable(_id INTEGER PRIMARY KEY, endtime INTEGER)") + db.execute("CREATE INDEX indexschedendtime ON scheduletable(endtime)") + db.executemany("INSERT INTO scheduletable(endtime) VALUES(?)", [(v,) for v in values]) + first = delete_batches(db, CUTOFF, batch) + remaining = [v for _, v in rows(db)] + assert remaining == expected, (remaining, expected) + assert db.execute("PRAGMA integrity_check").fetchone()[0] == "ok" + second = delete_batches(db, CUTOFF, batch) + assert second["deleted"] == 0, second + assert first["deleted"] == len(values) - len(expected), first + db.close() + + +def main() -> None: + # empty; exact cutoff retained; just-before deleted; recent/future retained + run_case([], []) + run_case( + [NOW - 400 * 86_400, NOW - 30 * 86_400, CUTOFF - 1, CUTOFF, NOW - 1, NOW, NOW + 7 * 86_400], + [CUTOFF, NOW - 1, NOW, NOW + 7 * 86_400], + ) + # Multiple transactions and stable ordering across duplicate end times. + old = [NOW - 365 * 86_400] * 251 + current = [NOW - day * 86_400 for day in range(7)] + future = [NOW + day * 86_400 for day in range(1, 31)] + run_case(old + current + future, current + future, batch=100) + print("PASS: empty, boundary, current/future preservation, multi-batch and idempotence") + + +if __name__ == "__main__": + main() diff --git a/patches/gel-workmanager/PATCH-PLAN.md b/patches/gel-workmanager/PATCH-PLAN.md new file mode 100644 index 0000000..0a9a0c9 --- /dev/null +++ b/patches/gel-workmanager/PATCH-PLAN.md @@ -0,0 +1,49 @@ +# Patch plan – bound GEL WorkManager history + +## Target and evidence boundary + +`/system/app/gel/gel.apk`, `GatheringService.onStartCommand()` feeding +`GELWorker` under unique name `GEL_WORK`. SHA-256: +`29A68E6B09700ED4CCDF312C0B99EEE0C3F67E329A537FE87ED4341A71D383D7`. +The exact app callsite loads policy integer 4. The embedded AndroidX enqueue +implementation proves this is the `APPEND_OR_REPLACE` leaf-dependency path. +See `ROOT-CAUSE.md` and the executable verifier. + +## Design + +1. Classify events as idempotent/coalescible or durable. +2. Persist all events in a bounded outbox; UPSERT only verified periodic/state + families and keep durable events in monotonic FIFO order. +3. Enqueue one unique `GEL_DRAIN` worker with `ExistingWorkPolicy.KEEP`, plus a + single periodic KEEP watchdog for crash/race recovery. +4. Acknowledge outbox rows only after dispatch; retry the same WorkSpec when + work remains. Never represent the outbox as a dependency chain. +5. Set a short result-retention period, call `pruneWork()` after idle batches, + and expose counters: WorkSpec count, dependency depth, oldest completion, + DB/WAL bytes and enqueue latency. + +Pseudocode: + +```java +outbox.enqueue(classify(intent), serialize(intent)); +WorkRequest request = oneShot(GELDrainWorker.class); +wm.enqueueUniqueWork("GEL_DRAIN", KEEP, request); +// GELDrainWorker drains bounded persisted events FIFO, then succeeds +``` + +Do not blindly replace every event with `REPLACE`: that could lose non- +idempotent telemetry. The event classification is a release gate. + +## Migration + +At first patched start, if no RUNNING work exists and graph depth/DB size +exceeds a conservative threshold, cancel only obsolete completed GEL work, +prune, checkpoint, and retain any bounded outbox. Never delete the entire app +data automatically. Log a privacy-safe migration result. + +## Acceptance + +10.000 alarms and service intents: dependency depth remains O(1), completed +rows are pruned, DB remains under a vendor-defined cap (suggested <10 MiB), +latest event latency remains seconds rather than minutes, reboot/power loss +does not duplicate non-idempotent delivery. diff --git a/patches/gel-workmanager/PATCH.diff b/patches/gel-workmanager/PATCH.diff new file mode 100644 index 0000000..9ca6038 --- /dev/null +++ b/patches/gel-workmanager/PATCH.diff @@ -0,0 +1,41 @@ +--- a/src/org/droidtv/gel/gathering/GatheringService.java ++++ b/src/org/droidtv/gel/gathering/GatheringService.java +@@ +- OneTimeWorkRequest request = GELWorker.buildWorkRequest(intent); +- workManager.enqueueUniqueWork("GEL_WORK", +- ExistingWorkPolicy.APPEND_OR_REPLACE, request); ++ eventOutbox.enqueue(EventRecord.from(intent), EventClassifier.forIntent(intent)); ++ OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(GELDrainWorker.class) ++ .keepResultsForAtLeast(10, TimeUnit.MINUTES).build(); ++ workManager.enqueueUniqueWork("GEL_DRAIN", ExistingWorkPolicy.KEEP, request); + +--- a/src/org/droidtv/gel/gathering/GELWorker.java ++++ b/src/org/droidtv/gel/gathering/GELDrainWorker.java +@@ +- Intent intent = intentFromWorkData(getInputData()); +- gatheringService.dispatch(intent); +- return Result.success(); ++ long deadline = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(8); ++ while (SystemClock.elapsedRealtime() < deadline) { ++ EventRecord event = eventOutbox.peekOldest(); ++ if (event == null) return Result.success(); ++ gatheringService.dispatch(event.toIntent()); ++ eventOutbox.ack(event.sequence); // acknowledge only after dispatch ++ } ++ return Result.retry(); + +--- /dev/null ++++ b/src/org/droidtv/gel/gathering/EventOutbox.sql +@@ ++CREATE TABLE gel_event_outbox( ++ sequence INTEGER PRIMARY KEY AUTOINCREMENT, ++ action TEXT NOT NULL, ++ payload BLOB NOT NULL, ++ coalesce_key TEXT UNIQUE, ++ created_ms INTEGER NOT NULL, ++ attempts INTEGER NOT NULL DEFAULT 0 ++); + +# Add a unique periodic GEL_DRAIN_WATCHDOG with KEEP. Bound the outbox to 4,096 +# rows / 8 MiB, retain a dropped-event metric, and coalesce only action classes +# proven to be replaceable state/alarm snapshots. diff --git a/patches/gel-workmanager/REFERENCE-PATCH.diff b/patches/gel-workmanager/REFERENCE-PATCH.diff new file mode 100644 index 0000000..562b2ad --- /dev/null +++ b/patches/gel-workmanager/REFERENCE-PATCH.diff @@ -0,0 +1,47 @@ +--- a/src/org/droidtv/gel/gathering/GatheringService.java ++++ b/src/org/droidtv/gel/gathering/GatheringService.java +@@ +- OneTimeWorkRequest request = GELWorker.buildWorkRequest(intent); +- workManager.enqueueUniqueWork("GEL_WORK", +- ExistingWorkPolicy.APPEND_OR_REPLACE, request); ++ eventOutbox.enqueue(EventRecord.from(intent), EventClassifier.forIntent(intent)); ++ OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(GELDrainWorker.class) ++ .keepResultsForAtLeast(10, TimeUnit.MINUTES) ++ .build(); ++ workManager.enqueueUniqueWork("GEL_DRAIN", ExistingWorkPolicy.KEEP, request); + +--- a/src/org/droidtv/gel/gathering/GELWorker.java ++++ b/src/org/droidtv/gel/gathering/GELDrainWorker.java +@@ +- Intent intent = intentFromWorkData(getInputData()); +- gatheringService.dispatch(intent); +- return Result.success(); ++ final long deadline = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(8); ++ while (SystemClock.elapsedRealtime() < deadline) { ++ EventRecord event = eventOutbox.peekOldest(); ++ if (event == null) { ++ SystemClock.sleep(500); ++ if (eventOutbox.peekOldest() == null) return Result.success(); ++ continue; ++ } ++ gatheringService.dispatch(event.toIntent()); ++ eventOutbox.ack(event.sequence); // only after successful dispatch ++ } ++ return Result.retry(); // same WorkSpec; no appended dependency + +--- /dev/null ++++ b/src/org/droidtv/gel/gathering/EventOutbox.sql +@@ ++CREATE TABLE gel_event_outbox( ++ sequence INTEGER PRIMARY KEY AUTOINCREMENT, ++ action TEXT NOT NULL, ++ payload BLOB NOT NULL, ++ coalesce_key TEXT UNIQUE, ++ created_ms INTEGER NOT NULL, ++ attempts INTEGER NOT NULL DEFAULT 0 ++); + +# Add one PeriodicWorkRequest named GEL_DRAIN_WATCHDOG with KEEP (15 minutes) +# for crash/race recovery. Enforce a 4,096-row/8-MiB cap with an explicit +# dropped-event counter. Only repetitive alarm/state snapshots may coalesce; +# command/debug actions and events with delivery semantics may not. diff --git a/patches/gel-workmanager/ROOT-CAUSE.md b/patches/gel-workmanager/ROOT-CAUSE.md new file mode 100644 index 0000000..72c23a0 --- /dev/null +++ b/patches/gel-workmanager/ROOT-CAUSE.md @@ -0,0 +1,50 @@ +# GEL WorkManager root cause and exact patch boundary + +## Confirmed callsite + +APK: `diagnostics/raw/storage/gel.apk`, SHA-256 +`29A68E6B09700ED4CCDF312C0B99EEE0C3F67E329A537FE87ED4341A71D383D7`. +Target: `classes.dex`, +`Lorg/droidtv/gel/gathering/GatheringService;->onStartCommand(Intent,int,int)int`, +code item `code_off=0x50bf0`, `registers=6`, `ins=4`, `outs=5`. + +| DEX PC | Stock instruction | +|---:|---| +| `0x0000` | build `OneTimeWorkRequest` from the incoming Intent | +| `0x000f` | load unique name `GEL_WORK` | +| `0x0011` | load policy integer `4` | +| `0x0012` | construct `WorkContinuationImpl(manager,name,policy,list)` | +| `0x0015` | call `enqueue()` | + +The embedded `EnqueueRunnable.run()` branches policy values 3 and 4 into the +append path. It gathers current leaf WorkSpecs and inserts each as a prerequisite +of the new request. For value 4, failed/cancelled leaves trigger replacement; +otherwise it is the AndroidX `APPEND_OR_REPLACE` behavior. This directly +explains the observed single-name dependency chain, expensive leaf traversal, +delayed enqueue and 229,453,824-byte WorkDatabase. + +## Patch decision + +A one-instruction change from integer 4 to integer 2 would select KEEP and +bound the WorkSpec graph, but it would silently discard every incoming Intent +while another GEL job exists. The worker handles many unrelated actions and +extras, so that binary micro-patch is **not release-safe**. + +The release design is a bounded persistent FIFO/outbox plus one KEEP drain +worker and one periodic KEEP watchdog. Repetitive sampling/state alarms may be +coalesced; non-idempotent actions remain FIFO and are acknowledged only after +dispatch. It preserves the current serial order without representing each +event as a WorkManager dependency. Delivery is at-least-once after a crash, so +event IDs or idempotent receivers are required. + +## Validation + +- `tools/verify_stock_callsite.py` asserts the APK hash and the five exact DEX + instructions above. +- `tools/test_bounded_outbox.py` covers 10,000 coalescible alarms, 10,000 + durable FIFO events, O(1) active WorkSpec, bounded depth, ordering and a crash + between dispatch and acknowledgement. + +This phase emits source/design and executable model artifacts, not a signed +APK. A rebuilt GEL APK still needs the Philips platform private key and an +AVB-integrated system image. diff --git a/patches/gel-workmanager/TEST-PLAN.md b/patches/gel-workmanager/TEST-PLAN.md new file mode 100644 index 0000000..8cdfd10 --- /dev/null +++ b/patches/gel-workmanager/TEST-PLAN.md @@ -0,0 +1,27 @@ +# GEL bounded-outbox test plan + +## Static gate + +Run `tools/verify_stock_callsite.py`. It must match the golden APK hash and +prove `GEL_WORK`, policy integer 4 (`APPEND_OR_REPLACE`), continuation +construction, and enqueue in `GatheringService.onStartCommand`. + +## Offline model gates + +Run `tools/test_bounded_outbox.py` and require: + +- 10,000 coalescible alarms keep bounded queue depth; +- 10,000 non-coalescible commands preserve FIFO order; +- active WorkManager graph remains O(1), never an APPEND chain; +- queue caps and dropped-event telemetry are deterministic; +- crash after dispatch/before acknowledgement produces at-least-once retry; +- reboot watchdog resumes a non-empty outbox. + +## Firmware integration gates + +With a signed recoverable build, test every action family handled by +`GELWorker.doWork`, airplane/network loss, process death at each durability +boundary, standby/reboot, full disk, corrupt outbox, clock jumps, and a 30-day +soak. Compare delivered action IDs and ordering to stock. Verify WorkDatabase +rows plateau and old successful/cancelled specs prune. Do not ship the tempting +single-byte `APPEND_OR_REPLACE` to `KEEP` edit: it can discard distinct Intents. diff --git a/patches/gel-workmanager/tools/test_bounded_outbox.py b/patches/gel-workmanager/tools/test_bounded_outbox.py new file mode 100644 index 0000000..bc72c36 --- /dev/null +++ b/patches/gel-workmanager/tools/test_bounded_outbox.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Executable model tests for the proposed bounded GEL drain architecture.""" + +from __future__ import annotations + +import json +import sqlite3 +import tempfile +from pathlib import Path + +MAX_ROWS = 4096 + + +class Model: + def __init__(self, path: Path): + self.db = sqlite3.connect(path) + self.db.executescript( + "PRAGMA journal_mode=MEMORY; PRAGMA synchronous=OFF;" + "CREATE TABLE outbox(seq INTEGER PRIMARY KEY AUTOINCREMENT," + " action TEXT NOT NULL, payload TEXT NOT NULL, coalesce_key TEXT UNIQUE);" + "CREATE TABLE metrics(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" + "INSERT INTO metrics VALUES('dropped',0);" + ) + self.worker_enqueued = False + self.max_work_specs = 0 + self.max_outbox = 0 + + def enqueue(self, action: str, payload: dict, coalescible: bool) -> None: + encoded = json.dumps(payload, sort_keys=True) + key = action if coalescible else None + if key is not None: + self.db.execute( + "INSERT INTO outbox(action,payload,coalesce_key) VALUES(?,?,?) " + "ON CONFLICT(coalesce_key) DO UPDATE SET payload=excluded.payload", + (action, encoded, key), + ) + else: + count = self.db.execute("SELECT count(*) FROM outbox").fetchone()[0] + if count >= MAX_ROWS: + self.db.execute("UPDATE metrics SET value=value+1 WHERE key='dropped'") + self.db.commit() + return + self.db.execute( + "INSERT INTO outbox(action,payload,coalesce_key) VALUES(?,?,NULL)", + (action, encoded), + ) + self.db.commit() + self.worker_enqueued = True # enqueueUniqueWork(GEL_DRAIN, KEEP) + self.max_work_specs = max(self.max_work_specs, int(self.worker_enqueued)) + self.max_outbox = max(self.max_outbox, + self.db.execute("SELECT count(*) FROM outbox").fetchone()[0]) + + def drain(self, limit: int, crash_after_handle: bool = False) -> list[int]: + handled = [] + selected = self.db.execute( + "SELECT seq FROM outbox ORDER BY seq LIMIT ?", (limit,) + ).fetchall() + for (seq,) in selected: + handled.append(seq) + if crash_after_handle: + return handled # no ack: event remains for at-least-once retry + self.db.execute("DELETE FROM outbox WHERE seq=?", (seq,)) + self.db.commit() + if self.db.execute("SELECT count(*) FROM outbox").fetchone()[0] == 0: + self.worker_enqueued = False + return handled + + def count(self) -> int: + return self.db.execute("SELECT count(*) FROM outbox").fetchone()[0] + + +def main() -> None: + with tempfile.TemporaryDirectory() as tmp: + m = Model(Path(tmp) / "outbox.db") + for i in range(10_000): + m.enqueue("GATHERING_ALARM_INTENT", {"tick": i}, coalescible=True) + assert m.count() == 1 and m.max_work_specs == 1 + payload = json.loads(m.db.execute("SELECT payload FROM outbox").fetchone()[0]) + assert payload == {"tick": 9999} + m.drain(100) + assert m.count() == 0 + + seen = [] + for i in range(10_000): + m.enqueue("DURABLE_EVENT", {"seq": i}, coalescible=False) + if i % 32 == 31: + seen.extend(m.drain(32)) + seen.extend(m.drain(MAX_ROWS)) + assert m.count() == 0 and m.max_outbox <= 32 and m.max_work_specs == 1 + assert seen == sorted(seen) + + m.enqueue("DURABLE_EVENT", {"seq": "crash"}, coalescible=False) + first = m.drain(1, crash_after_handle=True) + assert m.count() == 1 + second = m.drain(1) + assert first == second and m.count() == 0 + m.db.close() + print("PASS: 10k coalescing, 10k FIFO, O(1) WorkSpec, bounded queue, crash retry") + + +if __name__ == "__main__": + main() diff --git a/patches/gel-workmanager/tools/verify_stock_callsite.py b/patches/gel-workmanager/tools/verify_stock_callsite.py new file mode 100644 index 0000000..ca9310d --- /dev/null +++ b/patches/gel-workmanager/tools/verify_stock_callsite.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Assert the exact GEL unique-work callsite in the immutable stock APK.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path +import sys +import zipfile + +ROOT = Path(__file__).resolve().parents[3] +HELPERS = ROOT / "diagnostics" / "deep-forensics" / "philips" +sys.path.insert(0, str(HELPERS)) +from dex_disasm import Dex, fmt, iter_instructions # noqa: E402 + +APK = ROOT / "diagnostics" / "raw" / "storage" / "gel.apk" +EXPECTED = "29a68e6b09700ed4ccdf312c0b99eee0c3f67e329a537fe87ed4341a71d383d7" +METHOD = "Lorg/droidtv/gel/gathering/GatheringService;->onStartCommand(Landroid/content/Intent;II)I" + + +def main() -> None: + assert hashlib.sha256(APK.read_bytes()).hexdigest() == EXPECTED + with zipfile.ZipFile(APK) as zf: + dex = Dex(zf.read("classes.dex"), str(APK) + "!/classes.dex") + encoded = next(m for m in dex.encoded_methods if dex.methods[m.method_idx] == METHOD) + instructions = {pc: fmt(dex, pc, op, units) + for pc, op, units in iter_instructions(dex.data, encoded.code_off)} + expected = { + 0x0000: "invoke-static {v3}, Lorg/droidtv/gel/gathering/GELWorker;->buildWorkRequest(Landroid/content/Intent;)Landroidx/work/OneTimeWorkRequest;", + 0x000F: "const-string v0, 'GEL_WORK'", + 0x0011: "const/4 v1, #4", + 0x0012: "invoke-direct {v5, v4, v0, v1, v3}, Landroidx/work/impl/WorkContinuationImpl;->(Landroidx/work/impl/WorkManagerImpl;Ljava/lang/String;ILjava/util/List;)V", + 0x0015: "invoke-virtual {v5}, Landroidx/work/impl/WorkContinuationImpl;->enqueue()Landroidx/work/Operation;", + } + for pc, text in expected.items(): + assert instructions.get(pc) == text, (hex(pc), instructions.get(pc), text) + print("PASS: stock GEL onStartCommand uses GEL_WORK with policy integer 4") + + +if __name__ == "__main__": + main() diff --git a/patches/phase7/ART-PREOPT-ANALYSIS.md b/patches/phase7/ART-PREOPT-ANALYSIS.md new file mode 100644 index 0000000..6b72a02 --- /dev/null +++ b/patches/phase7/ART-PREOPT-ANALYSIS.md @@ -0,0 +1,48 @@ +# Phase 7I – tvfw.jar ART/preopt analysis + +## Inventory + +| Artifact | Bytes | SHA-256 | +|---|---:|---| +| `/system/framework/tvfw.jar` | 3,275,657 | `a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad` | +| `/system/framework/oat/arm/tvfw.odex` | 37,540 | `a4af432c54153c9325d92f338f93b2b0a6cfa6b618a5d2c31d587e93eceff531` | +| `/system/framework/oat/arm/tvfw.vdex` | 51,572 | `abd0f4ce99404199332a779b195c53ca57d2238d0368910b0936cd55a8572509` | + +No adjacent `.art` file was observed. `tvfw.jar` contains one DEX and is +registered as the Java shared library `tvfw`. + +## Boot criticality + +Live environment inspection shows `tvfw` is in neither `BOOTCLASSPATH` nor +`SYSTEMSERVERCLASSPATH`. It is therefore not itself part of the boot image and +is not a direct system_server boot classpath input. This narrows, but does not +remove, risk: numerous Philips TV applications load it through their +PathClassLoader. Bugreport logs include a shared-library class-loader-context +size mismatch involving `tvstrings`, `tvwidgets`, `tvfw` and `javax.obex`. + +## Can ART regenerate it? + +For a PackageManager-managed `/data/app` APK update, ART normally creates +data-side compiled artifacts. `tvfw.jar` is different: it is system image +shared-library content with stock ARM ODEX/VDEX and client class-loader +contexts. `/data/dalvik-cache/arm` was inaccessible to shell, so a working +fallback for a modified system JAR is not device-proven. + +Possible build choices are: + +1. regenerate matching ARM32 ODEX/VDEX and all affected class-loader contexts + in the OEM build; or +2. deliberately omit stale preopt outputs and validate boot-time/dexopt fallback + in an otherwise correctly signed system image. + +Blindly replacing the JAR while leaving stock ODEX/VDEX is unsafe. A checksum +mismatch may cause rejection/recompilation, while a class-loader-context or API +mismatch can crash many client processes. Neither outcome is a production plan. + +## Conclusion + +`tvfw.jar` is **not realistically deployable as an isolated file**. It needs a +coherent system-image/OEM-build path, compatible clients/preopt, SELinux/file +metadata and AVB. It is less likely than a bootclasspath JAR to stop +system_server directly, but the blast radius across TV apps is high. + diff --git a/patches/phase7/AVB-TOPOLOGY.md b/patches/phase7/AVB-TOPOLOGY.md new file mode 100644 index 0000000..c31c368 --- /dev/null +++ b/patches/phase7/AVB-TOPOLOGY.md @@ -0,0 +1,65 @@ +# Phase 7H – AVB topology + +## Verified boot state + +- `ro.boot.avb_version=1.2`; vbmeta header reports AVB 1.0. +- `ro.boot.vbmeta.device_state=locked` +- `ro.boot.verifiedbootstate=green` +- dm-verity mode: enforcing +- active slot: `_a` +- vbmeta device `179:8` = `/dev/mmcblk0p8` (`vbmeta_a`) +- vbmeta digest: `66eae984496730f2339e5e5531f513645ce32263ac24b95cd80d4f787d701dc6` +- digest algorithm SHA-256; reported vbmeta size 17,280 bytes + +## Proven topology + +```text +locked boot ROM / bootloader trust root (key identity unread) + -> vbmeta_a (p8, active; exact descriptors unread) + -> system_a p15 -> dm-0 -> `/` system-as-root ext4, ro + -> vendor_a p35 -> dm-1 -> `/vendor` ext4, ro + -> linux_rootfs_a p17 -> dm-2 -> SquashFS + -> 3rd_a p19 -> dm-3 -> SquashFS + -> factory_a p39 -> AVB SquashFS when mounted + -> boot/dtbo and other OTA partitions: boot-chain protection likely, + but exact vbmeta descriptors/chains not readable +``` + +The fstab marks `system`, `vendor`, `linux_rootfs`, `3rd` and `factory` with +`avb,slotselect`. The exact active system dm table is a SHA-1 4-KiB hashtree +with 602,165 data blocks, root hash +`3f16afd...`, salt `d468...`, FEC and `restart_on_corruption`. Vendor's dm-verity +mapping is confirmed, but its full descriptor/table was not captured. + +## Partition status + +| Logical role | Active physical partition | AVB evidence | Exact descriptor/key/rollback index | +|---|---|---|---| +| vbmeta | p8 `vbmeta_a` | locked green root metadata | raw content inaccessible | +| system/root | p15 `system_a`, dm-0 | fstab AVB + live dm-verity | partial dm table known | +| vendor | p35 `vendor_a`, dm-1 | fstab AVB + live dm-verity | unknown | +| linux_rootfs | p17, dm-2 | fstab AVB + live mapping | unknown | +| 3rd | p19, dm-3 | fstab AVB + live mapping | unknown | +| factory | p39 (B: p16) | fstab AVB/slotselect | not mounted; unknown | +| boot / dtbo | A/B raw partitions | locked green boot strongly implies authentication | descriptor/chaining unknown | +| odm | p36/p57, not mounted in normal inventory | no AVB fstab proof | unknown | +| product/system_ext | directories within system image | protected as system content | no separate partition | +| recovery/super | absent | n/a | n/a | + +There are no named chained-vbmeta partitions and no dynamic `super` partition. +That is not proof that `vbmeta_a` contains no chain descriptor. Without reading +and parsing raw vbmeta, public-key digest, descriptor algorithms, chained vbmeta +and rollback indexes remain unknown. + +## Consequence for patches + +Changing `/system` content invalidates the hashtree. A bootable image would need +a coherent new filesystem, hashtree/FEC/footer and vbmeta accepted by the locked +boot chain. Disabling verification is forbidden and unnecessary for the +legitimate goal. A correctly platform-signed `/data/app` update leaves AVB +untouched. + +References: Android Verified Boot + and AVB source +. + diff --git a/patches/phase7/COLD-BOOT-RECOVERY-MATRIX.md b/patches/phase7/COLD-BOOT-RECOVERY-MATRIX.md new file mode 100644 index 0000000..d0b913b --- /dev/null +++ b/patches/phase7/COLD-BOOT-RECOVERY-MATRIX.md @@ -0,0 +1,43 @@ +# Phase 7G – Cold-boot recovery matrix + +Ratings are conditional on the failure mode stated. “Likely” is not a tested +cold-boot guarantee. No recovery command was executed. + +| Scenario | Android boot / ADB | Primary rollback | Stock APK fallback | USB/full restore | Rating | +|---|---|---|---|---|---| +| A. bad Channels `/data/app` update | likely boots; package/provider may crash; ADB likely | `cmd package uninstall-system-updates org.droidtv.channels` if ADB works | yes by PM; data-schema compatibility unproven | official package not locally available | **RECOVERABLE**, conditional; cold boot untested | +| B. bad GEL `/data/app` update | likely boots; GEL/candeebug process may loop | uninstall system update | yes; WorkManager DB rollback risk | same | **RECOVERABLE**, conditional | +| C. bad TPTvService `/data/app` update | persistent/core service may loop and degrade boot/UI; ADB uncertain | uninstall update if ADB reaches stable shell | yes mechanically; migrated data unknown | same | **RECOVERABLE WITH RECOVERY / UNKNOWN** until tested | +| D. bad `tvfw.jar` in system image | system_server not directly bootclasspath-dependent, but many TV clients can fail | A/B slot fallback if truly functional, else OEM full firmware | no PM fallback; JAR is system image content | needed if both slot/fallback fail | **HIGH RISK** | +| E. system_server does not boot | UI absent; adbd availability unknown | inactive-slot fallback/boot control | n/a | OEM recovery required if fallback fails | **HIGH RISK / UNKNOWN** | +| F. Android UI does not boot but system/adbd do | ADB may work | remove updated app or revert slot | for APK update yes | possible only with official file/procedure | **RECOVERABLE** if ADB proven in failure | +| G. ADB does not start | no normal-shell rollback | boot-control automatic rollback only | cannot trigger PM rollback | requires known recovery/USB full firmware | **RECOVERABLE WITH USB FIRMWARE**, currently **UNKNOWN** | + +## Safe mode and recovery deletion + +- A model-specific safe-mode entry and behavior are not documented in local + resources; do not rely on generic Android TV instructions. +- Direct deletion of `/data/app` in recovery is not documented or approved. + The proper normal-boot path is PackageManager's `uninstall-system-updates`. +- A factory reset would remove data-side updates but also destroys user data; + it is a last resort, not a validated rollback plan. + +## A/B fallback strength + +Static A/B partitions, Boot-Control HAL, UpdateEngine and `update_verifier` +strongly support automatic rollback if the new slot never becomes successful. +However `bootctl` is unavailable and current bootable/successful/retry counters +could not be read. An end-to-end failed-slot test was neither authorized nor +performed. Treat A/B recovery as **architecturally likely**, not proven for a +manually modified image. + +## Deployment gate + +Before the first Channels live update: + +1. OEM/platform-signed candidate. +2. Equivalent-device install, reboot, crash and uninstall-update drill. +3. Proved data rollback or restorable backup. +4. Official full firmware and exact 43PUS7394/12 USB recovery procedure. +5. Stable ADB access after intentional candidate failure in the lab. + diff --git a/patches/phase7/DEPLOYMENT-MATRIX.md b/patches/phase7/DEPLOYMENT-MATRIX.md new file mode 100644 index 0000000..aa7d1ca --- /dev/null +++ b/patches/phase7/DEPLOYMENT-MATRIX.md @@ -0,0 +1,25 @@ +# Phase 7M – Final deployment matrix + +| Path | Targets | Root | Signer / AVB | Cold-boot risk | Rollback / persistence | OTA compatibility | Rating | +|---|---|---:|---|---|---|---|---| +| 1. Philips-signed updated-system APK | Channels, GEL, TPTvService separately | no | exact Platform signer; no AVB change | low–medium for Channels/GEL, high for persistent TPTvService; must be lab-tested | persistent `/data/app`; PM fallback to stock `/system` APK; data rollback unproven | OEM OTA may replace/remove/supersede; test | **BEST** for first Channels candidate when gates pass | +| 2. Official vendor firmware with patch | all APKs, tvfw, policy | no user root | OEM Platform, payload and AVB signing | lowest achievable with OEM QA/A/B recovery | vendor-supported full/incremental rollback | native | **BEST PRODUCTION** | +| 3. Temporary runtime instrumentation | behavior prototype only | instrumentation/root authority likely | no durable signer/AVB change | process crash; normally reboot clears | temporary and fragile | not relevant | **TEST ONLY** | +| 4. Root + bind mount | APK/JAR prototype, not coherent release | yes every boot | cannot satisfy shared UID; underlying AVB intact but ART/labels remain | high; boot-service ordering and client crash risk | nonstandard mount removal/reboot | conflicts with OTA assumptions | **HIGH RISK / NO-GO** | +| 5. Modified system image | all system targets | build/flash authority | Platform signer plus accepted AVB/vbmeta required | very high without proven A/B/USB recovery | A/B only if accepted and tested | must regenerate payload/meta | **HIGH RISK / NO-GO today** | +| 6. Full modified firmware | all components/partitions | OEM process | complete Platform/payload/AVB/container keys | high for independent build, low only under OEM QA | full A/B/vendor recovery possible if supplied | becomes the OTA | **GOOD only as official OEM build; otherwise NO-GO** | + +## First live candidate + +BUG-001 as a Platform-signed Channels updated-system APK remains technically the +best first candidate because `/system` stays intact and PackageManager has a +documented fallback. It is **not yet authorized or safe to install**. Missing: + +- OEM Platform signature/lineage; +- real data migration and uninstall-update rollback test; +- cold boot/ADB failure drill on equivalent hardware; +- official full firmware and model-specific USB recovery procedure. + +Until all four exist, the production decision remains **NO-GO** and the manual +read-only EPG health check is the operational fallback. + diff --git a/patches/phase7/OTA-COMPONENT-MAP.md b/patches/phase7/OTA-COMPONENT-MAP.md new file mode 100644 index 0000000..db89916 --- /dev/null +++ b/patches/phase7/OTA-COMPONENT-MAP.md @@ -0,0 +1,66 @@ +# Phase 7D – Philips/TPVision OTA component map + +## Proven update path + +```text +USB `/upgrades/autorun*.upg` + -> org.droidtv.settings AutoRunUpgrade / SilentUpgradeService (UID1000) + -> product/version filter (`TPM191E_R`, current SW version) + -> ZIP parse + ├─ payload.bin + ├─ payload_properties.txt + └─ META-INF/updateinfo/upg.xml + -> android.os.UpdateEngine.applyPayload(URL, offset, size, properties) + -> root update_engine daemon + -> inactive A/B partitions + -> boot-control / update_verifier after reboot +``` + +This graph is reconstructed from the installed Settings DEX, init/fstab files, +live services and bugreport logs. No update was initiated. + +## Components + +| Component | Evidence / path | Role | Confidence | +|---|---|---|---| +| `org.droidtv.settings` | `/system/priv-app/settings/settings.apk` | Software-update UI and OEM orchestration | confirmed | +| `SilentUpgradeService` | Settings manifest/DEX/logs | watches USB/staging; registers mounted-volume receiver | confirmed | +| `AutoRunUpgrade` | Settings DEX `code_off=0x3b6ba0` | scans `/upgrades`, pattern `[aA]utorun.*.upg`, product/version checks | confirmed | +| `UpdateParser.parse(File)` | Settings DEX `code_off=0x3c01ec` | opens `.upg` with `ZipFile`; locates payload and properties; computes payload offset/size | confirmed | +| `UpdateUtils.installUpdate` | Settings DEX `code_off=0x3c71e4` | binds Android `UpdateEngine`, calls `applyPayload` | confirmed | +| `UpdateUtils.getUpgXml` | Settings DEX `code_off=0x3c2e88` | extracts `META-INF/updateinfo/upg.xml` to staging | confirmed | +| `android.os.UpdateEngineService` | live service list | Binder API for payload update | confirmed | +| `/system/bin/update_engine` | init/process evidence | root A/B payload writer/verifier | confirmed | +| `update_verifier` | init/fstab inventory | verifies post-boot slot and marks successful through boot control | confirmed architecture | +| `android.hardware.boot@1.0` | vendor init/HAL inventory | A/B slot state/control | confirmed service definition; status query failed | +| `/data/ota_package` | `/dev/mmcblk0p32`, label `upgrade`, ~1,352 MiB | update staging/history (`last_install`, `upg.xml`, payload metadata) | confirmed; shell contents inaccessible | +| `/data/vendor/user_setting/rw/upgrade` | DEX strings / prior private-state report | OEM upgrade state/history | confirmed path reference | +| `org.droidtv.oad` | `/system/priv-app/oad/oad.apk` | broadcast/OAD TV firmware feed integration, writes `autorun.upg` staging | confirmed; not the primary USB parser | +| `com.easy.oad` | `/system/app/btota/btota.apk` | Bluetooth/remote-device OTA | confirmed distinct role | +| `org.droidtv.usbbreakin` | `/system/app/USBService/USBService.apk` | USB service/trigger surface | confirmed installed; exact firmware-writer role not proven | +| `uncrypt`, BCB services | `/system/etc/init/uncrypt.rc` | setup/clear bootloader-control-block paths used by Android recovery flows | confirmed definitions; not invoked | +| RecoverySystem/system_update services | live service list | Android framework recovery/update coordination | confirmed present | + +## Metadata and state names found + +The installed update code references `/data/ota_package/build.prop`, +`installstatus`, `last_install`, version/history files, +`persist.odm.tpv.sys.upg_filename`, `persist.odm.tpv.sys.upg_source`, +`reboot-ab-update` and native slot APIs. Logs show the Settings service starting +and observing USB mounts; they do not show a staged current update. + +## What was not proven + +- No official `.upg` file is present locally, so outer-container signature, + exact payload metadata and Philips private key identities remain unknown. +- No call to `RecoverySystem.verifyPackage` was found on the current Settings + A/B path. This does not prove the outer ZIP is unsigned; it proves only that + the inspected path hands the inner payload/properties to UpdateEngine. +- Factory/emergency USB modes, key combinations and recovery ADB were not found + in local resources. +- `upgrade_loader`, `upgrade.pkg`, Android non-A/B `update.zip` and sideload are + not established as valid paths for this build. + +Android's A/B OTA model writes the inactive slot and uses boot-control rollback: +. + diff --git a/patches/phase7/PHILIPS-UPDATE-FORMAT.md b/patches/phase7/PHILIPS-UPDATE-FORMAT.md new file mode 100644 index 0000000..efa0b62 --- /dev/null +++ b/patches/phase7/PHILIPS-UPDATE-FORMAT.md @@ -0,0 +1,72 @@ +# Phase 7E – Philips update format + +## Local artifact search + +No official Philips/TPVision firmware package, `payload.bin`, system image or +`.upg` was found in the workspace. The only ZIPs are diagnostic/Codex bundles +and a bugreport attachment. Therefore no vendor package was reverse-engineered +or reconstructed, and no third-party image was used. + +## Format accepted by the installed Settings path + +The current `TPM191E_R` Settings DEX proves a **ZIP-compatible `.upg` outer +container** with at least: + +```text +autorun*.upg (ZIP opened with java.util.zip.ZipFile) +├─ payload.bin (A/B OTA payload) +├─ payload_properties.txt (hash/size/signature properties for UpdateEngine) +└─ META-INF/updateinfo/upg.xml (TPVision product/version metadata) +``` + +`UpdateParser.parse(File)` locates the payload entry and calculates its URL, +byte offset and compressed size. `UpdateUtils.installUpdate` passes those plus +the property lines to `UpdateEngine.applyPayload`. This is consistent with an +Android A/B payload embedded in a vendor wrapper. + +`upg.xml` parsing references these tags: + +- `productid` +- `softwareversion` +- `buildid`, `buildtype` +- `dateofthebuild`, `timeofthebuild` +- `tagofsource`, `typeofupg`, `creator`, `description` +- `sourceswver` + +The USB scanner filters the filename with `[aA]utorun.*.upg` and compares +product/version data against `ro.odm.tpv.product.id=TPM191E_R` and +`ro.odm.tpvision.product.swver`. + +## Verification boundary + +Confirmed: + +- UpdateEngine receives `payload_properties.txt` and the binary payload. +- Locked/green verified boot protects booting the resulting slot. +- Device-side OTA and APK signing use different certificates. + +Unknown until an official package is available: + +- outer ZIP comment/JAR signature/custom signature or checksum; +- payload manifest contents, partition list and payload signing certificate; +- whether full and incremental packages differ in additional entries; +- precise anti-rollback/source-version enforcement; +- AVB public keys/descriptors shipped inside the package; +- vendor container padding/alignment and any post-payload footer. + +Absence of `RecoverySystem.verifyPackage` in the inspected Settings code must +not be interpreted as permission to omit signatures. UpdateEngine performs its +own payload verification and the final images must pass AVB. + +## Official file required for completion + +Obtain directly from Philips/TPVision an unmodified full USB recovery/update +file named `autorun*.upg` for **43PUS7394/12, platform `TPM191E_R`**, preferably +the exact current `TPM191E_R.211.000.103.000` or a newer explicitly compatible +release. A full recovery image is preferable to an incremental OTA because it +allows offline structure comparison and independent restoration. Record the +vendor URL and SHA-256 before analysis. + +No reconstructed container should be offered to the television without the OEM +payload/AVB signatures and a proven recovery path. + diff --git a/patches/phase7/RECOVERY-INVENTORY.md b/patches/phase7/RECOVERY-INVENTORY.md new file mode 100644 index 0000000..34523b8 --- /dev/null +++ b/patches/phase7/RECOVERY-INVENTORY.md @@ -0,0 +1,50 @@ +# Phase 7F – Recovery inventory + +## Summary + +This build has A/B update infrastructure and Android recovery coordination, but +the exact user-accessible cold-boot recovery method is **not proven**. There is +no separate recovery partition in the complete 63-partition inventory. + +## Device-specific evidence + +| Item | Finding | Classification | +|---|---|---| +| Separate `recovery` partition | absent from full p1–p63 layout; fstab recovery line commented | disproved | +| A/B layout | static A/B, slot A active, `slotselect`; no `super` | confirmed | +| Recovery-in-boot possibility | standard A/B design can place recovery ramdisk in boot | plausible, content not inspected | +| `recovery` executable | not present under `/system/bin` in normal boot | confirmed absence at that path | +| `fastbootd` / `bootctl` | not present under `/system/bin` | confirmed absence at that path | +| Recovery framework service | `recovery` and `system_update` services present | confirmed | +| BCB helpers | `uncrypt`, `setup-bcb`, `clear-bcb` definitions in `uncrypt.rc` | confirmed definitions | +| Recovery persist/refresh | AOSP init scripts and helpers present | confirmed | +| OTA cert store | `/system/etc/security/otacerts.zip` | confirmed | +| Vendor recovery hook | `vendor_flash_recovery.rc` references `/vendor/bin/install-recovery.sh` | definition confirmed; binary absent in mounted vendor | +| Factory image | A/B `factory` SquashFS, AVB/slotselect, not mounted normally | confirmed partition/path; contents unknown | +| Factory service | `/factory/bin/factory_service`, oneshot, `vold` domain, `/tmp/factory` | init definition only; runtime/entry unknown | +| USB `.upg` updater | Settings scans `/upgrades/autorun*.upg` | confirmed normal-Android USB OTA path | +| Recovery sideload | no local evidence | unknown | +| ADB in recovery | no local evidence | unknown | +| Physical key combination | no local resource found | unknown | +| Emergency upgrade | no confirmed trigger/protocol | unknown | +| Public Fastboot/unlock | no evidence; device locked/green | unknown/unavailable in normal boot | +| UART | `console=ttyMT0,115200n1`; Android console is UID shell/domain shell; `forbid_uart0=1` | configuration confirmed, pins/access/root unknown | + +## What recovery can likely write + +The A/B OTA partition list advertises `boot,system,vbmeta,vendor,dtbo, +linux_rootfs,factory,3rd,tzbp,pq,aq,adsp,tpv_misc,uboot,uboot_env,ubootB, +uboot_envB,dolby,r20,r21,3dlut`. This proves intended OTA coverage, not that a +user-accessible recovery shell can freely write them. The trusted UpdateEngine +path writes the inactive slot according to a signed payload. + +## Practical conclusion + +- If normal Android and ADB boot, a bad updated-system APK may be removable via + PackageManager's system-update rollback. +- If Android/ADB do not boot, neither a key-based recovery UI nor USB emergency + restore has yet been device-proven. Do not perform a live system/framework + patch on that assumption. +- An OEM full `autorun*.upg` plus documented model-specific key/USB recovery + procedure is required before high-risk deployment. + diff --git a/patches/phase7/SIGNER-MATRIX.md b/patches/phase7/SIGNER-MATRIX.md new file mode 100644 index 0000000..9f55083 --- /dev/null +++ b/patches/phase7/SIGNER-MATRIX.md @@ -0,0 +1,76 @@ +# Phase 7A – Signing inventory + +Stand: 2026-08-26, Gerät `TPM191E_R.211.000.103.000`. Die APK-Hashes +wurden lokal berechnet und gegen die read-only Live-Pfade geprüft. Package- +Metadaten stammen aus `dumpsys package`; Zertifikate aus den APK-Signaturdaten. + +## Ergebnis + +Die drei Patchziele Channels, GEL und TPTvService sowie Androids +`framework-res.apk`, EUM, Settings, OAD, USBService und SettingsProxy sind mit +demselben kryptografischen **Platform Signer** signiert. Der PackageManager +meldet für sie dieselbe Signer-ID `[476d2253]` und Signing-Version 3. Das +OTA-/Recovery-Zertifikat in `/system/etc/security/otacerts.zip` ist ein zweites, +verschiedenes Zertifikat. Beide privaten Schlüssel fehlen. + +## APK-Matrix + +`Platform` bezeichnet den unten vollständig ausgewiesenen Zertifikat-Fingerprint. +Alle geprüften APKs tragen JAR/v1-Signaturen sowie APK-Signing-Block v2 und v3. +Eine v4-`.idsig`-Sidecar-Datei wurde an keinem Systempfad beobachtet; v4 ist +daher **nicht belegt** (nicht gleichbedeutend mit einer eingebetteten v4- +Signatur). `Priv.` bedeutet Installation unter `priv-app`. + +| Package / Rolle | Aktiver APK-Pfad | APK SHA-256 | Signer | v1/v2/v3/v4 | UID / sharedUser | Priv. | Relevante Vertrauensrechte | +|---|---|---|---|---|---|---:|---| +| `android` | `/system/framework/framework-res.apk` | `d50a2ad1caa5c6e95ee1540d798150f40246f20c90d3a0d9371c58b541a27508` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | n/a | definiert Plattform-/Signaturgrenze | +| `org.droidtv.channels` | `/system/priv-app/channels/channels.apk` | `3a44bc648624f0cc9cd9feb4cfc3fd33f41323597718e4fccfc25fe35504d34f` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | ja | EPG/TV-Provider, Shared-UID-Union, privileged | +| `org.droidtv.gel` | `/system/app/gel/gel.apk` | `29a68e6b09700ed4ccdf312c0b99eee0c3f67e329a537fe87ed4341a71d383d7` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | nein | Shared-UID-Union, interne Philips-Schnittstellen | +| `org.droidtv.tv.tptvservice` | `/system/app/TPTvService/TPTvService.apk` | `75f492ee89906c8c267d685d2b7d0ce02bb09f3a6b4cc8456f8b096b9e441ecd` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | nein | persistent/coreApp, Shared-UID-Union | +| `org.droidtv.eum` | `/system/priv-app/eum/eum.apk` | `3b27806ac60b946c47ccac1be7edad46938cae10c43c3f2e1f3ef4f9d2d30b98` | Platform | ja/ja/ja/nicht belegt | 10119 / kein Shared UID | ja | privileged/signature Plattformrechte | +| `org.droidtv.settings` | `/system/priv-app/settings/settings.apk` | `bf63a78bae8259032144598927c2fe8af19005662de2cc0053db2a1f7918a348` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | ja | SilentUpgrade/TVUpdate, persistent, privileged | +| `org.droidtv.oad` | `/system/priv-app/oad/oad.apk` | `3d4ba64ce6d3e4b6c8c65ff5411b4058aeb239c95ac15dd806e15c1d991842f9` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | ja | OAD/TV-Upgrade-Integration | +| `com.easy.oad` | `/system/app/btota/btota.apk` | `8fcd51ba44e1f3188c06d01615dde274c652f8caacfd14c3ae7db16a266e4101` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | nein | Bluetooth-/Remote-OTA, nicht Hauptfirmware | +| `org.droidtv.usbbreakin` | `/system/app/USBService/USBService.apk` | `a34edfa8345ba4b7513ef648a7c0ffaf40945359e7bb951fbbef3c98d6c612bb` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | nein | USB-Service, persistent | +| `org.droidtv.common.accesshiddenapi` | `/system/app/settingsproxyservice/settingsproxyservice.apk` | `e0e966babadd77f6fc8d71af05cd58c74e5a7a16a4ad6abd664b5625c0508b7e` | Platform | ja/ja/ja/nicht belegt | 1000 / `android.uid.system` | nein | Hidden-API-/Settings-Proxy | + +## Platform-Signer-Zertifikat + +- Subject/Issuer (self-signed): `EMAILADDRESS=android@android.com, CN=Android, OU=Android, O=Android, L=Mountain View, ST=California, C=US` +- Serial: `9e71a5645c4fc22f` +- SHA-256: `D8:FF:E0:AF:40:4E:AB:8E:45:5A:00:74:D4:17:B0:DA:12:D0:67:26:E5:05:B0:2B:7F:DB:18:31:29:C8:90:68` +- SHA-1: `43:80:01:76:AC:53:BF:E9:FD:33:4A:EB:86:1D:40:A1:2A:00:AF:C0` +- Schlüssel/Signatur: RSA-2048, SHA1withRSA, X.509 v3 +- Gültigkeit: 2015-08-10 05:01:32 CEST bis 2042-12-26 04:01:32 CET + +Der generische Subject-Text `Android` ist kein Identitätsbeweis. Entscheidend +ist der vollständige kryptografische Fingerprint beziehungsweise eine vom +PackageManager akzeptierte Signer-Lineage. Ein gleichnamiges neues Zertifikat +ist inkompatibel. + +## OTA-/Recovery-Zertifikat + +`/system/etc/security/otacerts.zip` enthält `releasekey.x509.pem` mit SHA-256 +`E0:18:AB:2F:96:83:8E:B4:B7:B9:1A:D1:12:0B:C6:74:90:F7:E1:01:0F:D4:0C:1A:E5:1E:C4:F8:B1:2A:A0:F8`. +Es ist **nicht** das APK-Platform-Zertifikat. Details stehen in +`certificates.txt`. Das Zertifikat allein erlaubt weder APK- noch OTA-Signing; +der jeweilige private Schlüssel ist erforderlich und wurde nicht gesucht. + +## tvfw.jar + +`/system/framework/tvfw.jar` (SHA-256 +`a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad`) +ist kein APK und besitzt keine JAR-Signatur. Seine Vertrauensgrenze entsteht +durch die AVB-geschützte Systempartition, SELinux/Dateimetadaten, den +registrierten Java-Shared-Library-Pfad und ART-Preopt. Die oben gelisteten +Platform-signed Philips-Clients laden die Library; ein unsigniertes JAR ist +deshalb nicht frei austauschbar. + +## Grenzen + +- Kein privater Schlüssel wurde gesucht, extrahiert oder rekonstruiert. +- Eine PM-Signer-ID ist geräteintern; die X.509-SHA-256 ist der portable Beleg. +- Nicht jede einzelne Permission aller 58 UID1000-Pakete wird hier einem Paket + zugerechnet: Shared-UID-Prozesse wirken über die gemeinsame UID- und + Signaturgrenze. Die vollständige Beziehung steht in `TRUST-GRAPH.md`. + diff --git a/patches/phase7/SIGNING-OPTIONS.md b/patches/phase7/SIGNING-OPTIONS.md new file mode 100644 index 0000000..b7e9676 --- /dev/null +++ b/patches/phase7/SIGNING-OPTIONS.md @@ -0,0 +1,35 @@ +# Phase 7K – Signed-update reality + +## Direct answer + +No legitimate local method currently exists to produce an APK, OTA payload or +AVB image that this locked television will accept. Public certificates were +found; the private keys and OEM signing service were not. Public X.509 data +cannot generate signatures. + +| Option | What it would provide | Feasibility | Result | +|---|---|---|---| +| A. Official Philips/TPVision build and signing process | coherent source build, Platform-signed APK, possibly OTA/AVB integration | technically ideal; no access yet | **BEST / REQUEST** | +| B. Manufacturer ships its own patched update | OEM controls source, tests, Platform/OTA/AVB keys and support recovery | realistic vendor remediation path | **BEST PRODUCTION** | +| C. Manufacturer signs a supplied patch/artifact | could enable `/data/app` APK if OEM accepts code review and exact build inputs | possible organizationally; no service known | **GOOD**, requires OEM | +| D. Official firmware already includes fix | avoids third-party modification entirely | no newer verified package inspected | **GOOD**, verify release | +| E. Local signing | arbitrary new key is rejected by shared UID/PackageManager/AVB | cryptographically incompatible | **NO-GO** | + +## Keys are separate + +- APK Platform signer: SHA-256 `D8:FF:E0:AF:...:90:68`. +- OTA/recovery certificate: SHA-256 `E0:18:AB:2F:...:A0:F8`. +- AVB root/public-key identity: unread/unknown. +- Payload signing key: unknown until an official `.upg` is analyzed. + +One key does not substitute for another. Even OEM-signed APKs do not authorize +a modified system image, and an OTA certificate does not satisfy the APK +shared-UID check. + +## Ethical/security boundary + +No leaked keys, private-key extraction, forged certificate, PackageManager or +recovery check patch, AVB disablement or signature-verification bypass is part +of this plan. The only acceptable signing paths terminate in Philips/TPVision +or another explicitly authorized owner of the device trust keys. + diff --git a/patches/phase7/SYSTEMIMAGE-BUILD-ANALYSIS.md b/patches/phase7/SYSTEMIMAGE-BUILD-ANALYSIS.md new file mode 100644 index 0000000..d02ae8f --- /dev/null +++ b/patches/phase7/SYSTEMIMAGE-BUILD-ANALYSIS.md @@ -0,0 +1,44 @@ +# Phase 7J – Modified system image feasibility + +## REPACK POSSIBLE + +The system partitions are fixed-size (~2,390 MiB) ext4 images in a static A/B, +system-as-root layout. With a complete source/target-files package, a build +pipeline could theoretically: + +1. stage the full root/system tree without mutating the baseline; +2. integrate APK/JAR changes and compatible resources/manifests; +3. regenerate ARM32 dexpreopt ODEX/VDEX and class-loader contexts; +4. apply exact UID/GID/mode/capability metadata (`fs_config`); +5. apply complete SELinux labels/xattrs and policy-compatible file contexts; +6. create deterministic fixed-size ext4 images (sparse conversion only as a + transport representation); +7. regenerate dm-verity hashtree, FEC and AVB footer/descriptors; +8. build/sign vbmeta and the Android A/B payload/container; +9. validate inactive-slot install, boot, mark-successful and rollback. + +This proves only that the format is technically buildable. + +## BOOTABLE/TRUSTED – current status + +**NO-GO.** Missing inputs include: + +- exact Philips build/target-files tree and proprietary integrations; +- private Platform signer or an OEM signing service for the APK targets; +- AVB signing keys/root-of-trust compatibility and rollback indexes; +- official OTA payload/container signing process; +- authoritative SELinux contexts, fs_config and dexpreopt configuration; +- exact firmware package and full recovery procedure; +- a lab device or proven A/B failure recovery drill. + +The device is locked and green. A well-formed ext4 image is not accepted merely +because it mounts offline. Reusing stock vbmeta after a content change fails +verity; signing with an arbitrary AVB key fails the locked root of trust. + +## Why root is insufficient + +Runtime root could mount or write files only by stepping outside the production +trust model. It cannot create the OEM Platform/AVB signatures, make stale ART +outputs coherent or guarantee cold-boot recovery. Persistent root/remount/ +verification bypasses are outside authorization and are not deployment paths. + diff --git a/patches/phase7/TRUST-GRAPH.md b/patches/phase7/TRUST-GRAPH.md new file mode 100644 index 0000000..4ebc3da --- /dev/null +++ b/patches/phase7/TRUST-GRAPH.md @@ -0,0 +1,86 @@ +# Phase 7B – Shared-UID and signature trust graph + +## Core graph + +```text +Philips/Android Platform Signer D8:FF:E0:AF:...:90:68 + ├─ framework-res.apk (`android`) + ├─ Channels ───────────────┐ + ├─ GEL ────────────────────┤ + ├─ TPTvService ────────────┤ + ├─ Settings/OAD/USBService ┤ + └─ 58 packages total ──────┴─> sharedUser `android.uid.system` + └─ Linux UID 1000 / `system_app` + ├─ signature/platform permissions + ├─ privileged Binder services + ├─ Settings/Package/USB/user control + └─ Philips TV providers/services + +Separate trust domains: + OTA release cert E0:18:AB:...:A0:F8 -> recovery/OTA certificate store + AVB/vbmeta key -> public key not extracted; identity unknown +``` + +## Exact UID1000 membership + +The live PackageManager lists 58 packages in `android.uid.system`: + +`com.mediatek.tunerservice`, `org.droidtv.alhuewizard`, +`org.droidtv.tv.packageinfo`, `com.android.networkstack.inprocess`, +`org.droidtv.lkb`, `org.droidtv.epgdata`, `org.droidtv.factory`, +`org.droidtv.homeintentresolver`, `com.mediatek.backgrounddetection`, +`com.android.tv.settings`, `com.mediatek.tv.agent`, +`org.droidtv.euinstallersat`, `com.mediatek.network`, +`org.droidtv.nettvbrowser`, `android`, `org.droidtv.channels`, +`com.android.location.fused`, `org.droidtv.sunrise`, +`org.droidtv.tv.frameworklogger`, `com.android.providers.settings`, +`com.android.inputdevices`, `org.droidtv.GlobalKey`, `org.droidtv.ci`, +`org.droidtv.networkwizard`, `com.mediatek.tvinput`, `org.droidtv.xtv`, +`org.droidtv.sam`, `org.droidtv.oad`, `org.droidtv.teletext`, +`org.droidtv.nettvapps`, `org.droidtv.tv.tptvservice`, +`com.android.localtransport`, `org.droidtv.devicesetup`, +`com.phorus.playfi.softap`, `org.droidtv.contentexplorer`, +`com.android.wallpaperbackup`, `org.droidtv.tv.inactivityservice`, +`org.droidtv.icp`, `org.droidtv.settings`, `org.droidtv.tvsystemui`, +`org.droidtv.exoplayerservice`, `org.droidtv.gel`, +`org.droidtv.euinstallertc`, `com.android.dynsystem`, +`com.android.keychain`, `org.droidtv.recommendation`, `org.droidtv.csm`, +`org.droidtv.dukservice`, `org.droidtv.playtv`, +`org.droidtv.common.accesshiddenapi`, `com.easy.oad`, +`org.droidtv.candeebug`, `com.android.providers.tv`, +`com.android.networkstack.tethering.inprocess`, +`org.droidtv.usbbreakin`, `org.droidtv.tvpowermanagerservice`. + +## Permission and dependency edges + +The shared UID's PackageManager grant union includes especially sensitive +platform permissions such as `WRITE_SECURE_SETTINGS`, `INSTALL_PACKAGES`, +`DELETE_PACKAGES`, `REBOOT`, `MASTER_CLEAR`, `MANAGE_USERS`, +`INTERACT_ACROSS_USERS_FULL`, `MANAGE_USB`, `FORCE_STOP_PACKAGES`, `DUMP` and +`TV_INPUT_HARDWARE`, plus Philips TV/EPG channel/provider permissions. Some are +also gated by privileged allowlists or component permissions; shared UID alone +must not be read as an unconditional API guarantee. + +| Package | Shared boundary | Main dependency edges relevant to patching | +|---|---|---| +| Channels | UID1000 + Platform signer + privileged system app | EPG provider clients, channel/TV input data, Philips TV framework/library | +| GEL | UID1000 + Platform signer | WorkManager DB, Philips internal services, platform/signature APIs | +| TPTvService | UID1000 + Platform signer + persistent/core | boot-started TV service, storage monitor, Philips Binder/framework clients | +| Settings | UID1000 + Platform signer + privileged/persistent | UpdateEngine binding, secure/global settings, USB update receiver | +| OAD | UID1000 + Platform signer + privileged | OAD service and upgrade staging integration | +| EUM | Platform signer + privileged, separate UID | privileged package/update management; not protected by shared UID membership | + +## Consequence of a signer change + +Android requires applications sharing a UID to be signed compatibly. A newly +built APK with a different certificate cannot legitimately join the existing +`android.uid.system` group and cannot update the installed package. Removing +the shared UID or changing package identity would create a different app, +losing UID1000-owned data, access to signature permissions and provider/Binder +relationships. Patching PackageManager checks would be a trust bypass and is +outside scope. + +Android's official app-signing documentation describes the same-certificate +requirement for shared UIDs and signature permissions: +. + diff --git a/patches/phase7/UPDATED-SYSTEM-APP-ANALYSIS.md b/patches/phase7/UPDATED-SYSTEM-APP-ANALYSIS.md new file mode 100644 index 0000000..55c8baf --- /dev/null +++ b/patches/phase7/UPDATED-SYSTEM-APP-ANALYSIS.md @@ -0,0 +1,86 @@ +# Phase 7C – Updated-system-app feasibility + +## Device facts + +| Package | Base path | Version | Current state | `/data/app` update technically possible? | +|---|---|---|---|---| +| `org.droidtv.channels` | `/system/priv-app/channels/channels.apk` | 1 / `0.0.0.1` | base system app, not `UPDATED_SYSTEM_APP` | yes in principle, with accepted Platform signer and compatible higher version | +| `org.droidtv.gel` | `/system/app/gel/gel.apk` | 58724408 / `7.2.0.56` | base system app, not updated | yes in principle, same constraints | +| `org.droidtv.tv.tptvservice` | `/system/app/TPTvService/TPTvService.apk` | 31 / `12` | base system app, persistent/core, not updated | Android mechanism yes; operational risk materially higher | + +The device already has other updated system applications under `/data/app`, so +the Android mechanism is enabled. None of the three targets currently uses it. + +## Hard acceptance conditions + +1. Same package name. +2. A PackageManager-compatible signing identity/lineage. Here that means the + Philips/Android Platform signer SHA-256 `D8:FF:E0:AF:...:90:68`. +3. `android.uid.system` compatibility; it cannot be dropped or replaced without + breaking UID/data/permission semantics. +4. Accepted version/update policy (normally a higher versionCode; downgrade is + not a production plan). +5. Compatible manifest, SDK, native ABI and provider/service contracts. +6. Explicit data-schema migration and rollback testing. + +The public certificate is available, the private signing key or an OEM signing +service is not. Therefore no accepted update can currently be produced. + +## Expected lifecycle for a correctly signed Channels candidate + +This is a documentation-only sequence. No command below was executed. + +### BEFORE + +- Record base/current package paths, versions, signer, `/data` free space and + EPG database schema/backup. +- Prove a cold-boot recovery route and test on an equivalent lab device. +- Verify the candidate's signer, manifest, version and hashes offline. + +### INSTALL + +- PackageManager validates the candidate and places the accepted update under + `/data/app`; `/system/priv-app/channels/channels.apk` remains unchanged. +- No AVB/system-image modification is required for this path. +- A nonmatching signer/shared UID must be rejected; bypassing this check is not + an option. + +### PROCESS RESTART + +- PackageManager stops/restarts the package as part of replacement. +- The `/data/app` APK becomes active and ART compiles/caches artifacts in the + data-side runtime locations. Stock `/system/.../oat/arm` files remain the + immutable base artifacts. +- Providers and clients must tolerate the restart and schema migration. + +### VALIDATE + +- Confirm active path/version/signer, process stability, channel guide, + Now/Next, favorites, timers/reminders/recordings and EPG retention. +- Reboot/standby/cold-boot tests remain mandatory before production approval. + +### UNINSTALL UPDATE / FALLBACK + +- Live `cmd package help` documents `uninstall-system-updates []` as: + “Removes updates to the given system application and falls back to its + /system version.” This is the intended rollback mechanism, not an APK delete. +- The stock `/system` APK becomes active again because it was never overwritten. +- Data preservation and compatibility are **not guaranteed by this observation**. + A newer update may have migrated DB/preferences beyond the old base's ability + to read. Rollback must be tested with real data and an independent backup. +- Factory reset would also remove `/data/app` updates but is destructive and is + not a routine rollback. + +The live help text is the strongest device-specific fallback evidence. AOSP's +PackageManager shell implementation is available at +. + +## Risk ranking + +- **Channels:** best first candidate if signing and recovery conditions are met; + not persistent/core, clear measured benefit. Current status: **NO-GO**. +- **GEL:** mechanism feasible but WorkManager schema/migration and telemetry + semantics need source-level integration. Current status: **NO-GO**. +- **TPTvService:** technically updateable but persistent/core boot-start role + increases crashloop and recovery risk. Current status: **NO-GO**. + diff --git a/patches/phase7/certificates.txt b/patches/phase7/certificates.txt new file mode 100644 index 0000000..202f1ba --- /dev/null +++ b/patches/phase7/certificates.txt @@ -0,0 +1,46 @@ +PHASE 7 CERTIFICATE INVENTORY +Captured/verified: 2026-08-26 +Device build: TPM191E_R.211.000.103.000 + +CERTIFICATE 1: APK PLATFORM SIGNER +Used by: framework-res.apk, Channels, GEL, TPTvService, EUM, Settings, + OAD, btota, USBService, SettingsProxy (and many other OEM apps) +PackageManager signer ID: [476d2253] +PackageManager signing version: 3 +Subject: EMAILADDRESS=android@android.com, CN=Android, OU=Android, + O=Android, L=Mountain View, ST=California, C=US +Issuer: EMAILADDRESS=android@android.com, CN=Android, OU=Android, + O=Android, L=Mountain View, ST=California, C=US +Serial: 9e71a5645c4fc22f +SHA-256: D8:FF:E0:AF:40:4E:AB:8E:45:5A:00:74:D4:17:B0:DA:12:D0:67:26:E5:05:B0:2B:7F:DB:18:31:29:C8:90:68 +SHA-1: 43:80:01:76:AC:53:BF:E9:FD:33:4A:EB:86:1D:40:A1:2A:00:AF:C0 +Public key: RSA 2048 bit +Certificate signature: SHA1withRSA +Validity: 2015-08-10 05:01:32 CEST to 2042-12-26 04:01:32 CET +Schemes observed: v1/JAR + APK Signing Block v2 + v3 +v4: no adjacent .idsig observed; not proven +Private key: NOT PRESENT / NOT SEARCHED + +CERTIFICATE 2: OTA/RECOVERY RELEASE CERTIFICATE +Source: /system/etc/security/otacerts.zip!/releasekey.x509.pem +otacerts.zip SHA-256: cc26a1d15440b2990ccc465c3d6c3c800225fc88a23083724753ee1f3de5ec3c +PEM file SHA-256: E20C85B3CE3B2E8B757C4C261C1F3595C6F7914332744F84C709494D9609E64A +Subject: EMAILADDRESS=android@android.com, CN=Android, OU=Android, + O=Android, L=Mountain View, ST=California, C=US +Issuer: EMAILADDRESS=android@android.com, CN=Android, OU=Android, + O=Android, L=Mountain View, ST=California, C=US +Serial: dd7b52f76828c966 +SHA-256: E0:18:AB:2F:96:83:8E:B4:B7:B9:1A:D1:12:0B:C6:74:90:F7:E1:01:0F:D4:0C:1A:E5:1E:C4:F8:B1:2A:A0:F8 +SHA-1: 98:B1:DF:C5:2D:99:28:B2:2A:51:93:AB:10:86:7A:37:85:A4:1B:C0 +Public key: RSA 2048 bit +Certificate signature: SHA1withRSA +Validity: 2015-08-10 05:00:59 CEST to 2042-12-26 04:00:59 CET +Private key: NOT PRESENT / NOT SEARCHED + +CONCLUSION +The two certificates are cryptographically different despite the same textual +distinguished name. The OTA public certificate cannot sign PackageManager APK +updates. Possession of the APK public certificate cannot create accepted APKs. +No AVB public-key descriptor was readable, so neither certificate is asserted +to be the AVB key. + diff --git a/patches/storage-monitor/DESIGN.md b/patches/storage-monitor/DESIGN.md new file mode 100644 index 0000000..822cb75 --- /dev/null +++ b/patches/storage-monitor/DESIGN.md @@ -0,0 +1,40 @@ +# Storage monitor redesign + +## Stock behavior proven from DEX + +`TPTvService.apk!/classes.dex`, +`TVStorageMonitorService$2.run()`, `code_off=0x2ad380`, polls `/data` using +legacy `getBlockSize/getAvailableBlocks/getBlockCount`. Static constants are +exactly 524,288,000 bytes and 1,073,741,824 bytes. It also reads +`persist.odm.tpv.sys.emmc_lifetime`; 80–90%, 90–100% and exceeded return true. + +Capacity and health are then conflated: + +- below 500,000 KiB-ish bytes: critical cleanup + UI; +- below 1 GiB while health is not critical: low-space UI; +- at or above 1 GiB while health is critical: `MEMORY_CRITICAL` cleanup anyway. + +The same service contains a global cache sweep over all installed packages and +a separate 150 MiB `/data/vendor/user_setting` cleanup branch. Polling normally +runs every 30 seconds; a 300-second assignment is immediately overwritten by +30 seconds at DEX PCs `0x02de`–`0x02e6`. + +## Replacement policy + +Use `StatFs.getAvailableBytes()` and Android `StorageManager` low/full +thresholds for the actual `/data` filesystem. Enter LOW after two low samples; +enter CRITICAL immediately at the full threshold. Clear only after three +samples above `max(low+256 MiB, low*1.25)`. Notify/broadcast on state transition, +not every poll, and rate-limit repeated user UI. Poll every ten minutes in OK, +one minute in LOW/CRITICAL, plus platform storage broadcasts. + +eMMC lifetime and PRE_EOL are a separate health state with separate wording, +telemetry and escalation. A wear bucket must never claim that filesystem space +is low. Unknown health data is not “healthy” or “critical”. + +Do not automatically clear every application's cache. Preserve the +`user_setting` check only as a separately named OEM-data policy, with its own +threshold, metrics and user impact; it must not drive the internal-storage UI. + +`tools/test_storage_state.py` exercises entry debounce, critical transition, +hysteresis, deduplication, unknown measurements and health/capacity separation. diff --git a/patches/storage-monitor/PATCH-PLAN.md b/patches/storage-monitor/PATCH-PLAN.md new file mode 100644 index 0000000..133fc11 --- /dev/null +++ b/patches/storage-monitor/PATCH-PLAN.md @@ -0,0 +1,30 @@ +# Storage-monitor patch plan + +## Exact stock target + +`/system/app/TPTvService/TPTvService.apk`, SHA-256 +`75F492EE89906C8C267D685D2B7D0CE02BB09F3A6B4CC8456F8B096B9E441ECD`, +`classes.dex`, `TVStorageMonitorService$2.run()`, `code_off=0x2ad380`. +Stock constants are exactly 524,288,000 and 1,073,741,824 bytes. The code uses +legacy StatFs block multiplication, reads `persist.odm.tpv.sys.emmc_lifetime`, +and mixes capacity and wear into the same cleanup/warning path. Its intended +300-second delay is overwritten with 30 seconds at PCs `0x02de`–`0x02e6`. + +## Implementation + +Replace the Runnable body with a small sampler feeding the state machine in +`POLICY.md`. Use long byte APIs. Persist only the last notified state/timestamp, +not a fabricated capacity. Emit structured metrics showing the source and +unit. Route capacity transitions and health transitions to different message +IDs and actions. Trigger owner-specific cleanup through bounded, idempotent +jobs; remove the global package-cache sweep from the automatic path. + +## Verification + +Run the offline policy harness, then build/re-sign TPTvService and re-decompile +the result. On a recoverable test image simulate values around both thresholds, +measurement failure, rapid fill/reclaim, EPG/GEL cleanup success and failure, +health-only warnings, adopted USB removal, reboot and a 30-day soak. Verify UI +deduplication and that Android's own storage state agrees. This APK is +platform-signed, shared-UID and persistent/core; no live replacement is +authorized or safe without the OEM signer and recovery path. diff --git a/patches/storage-monitor/POLICY.md b/patches/storage-monitor/POLICY.md new file mode 100644 index 0000000..adb5941 --- /dev/null +++ b/patches/storage-monitor/POLICY.md @@ -0,0 +1,40 @@ +# Storage-monitor policy + +## Separate states + +Capacity and flash health are independent signals: + +1. `CAPACITY_OK`, `CAPACITY_LOW`, `CAPACITY_CRITICAL`, or `CAPACITY_UNKNOWN` + comes from the filesystem containing `/data`. +2. `HEALTH_NORMAL`, `HEALTH_WARNING`, `HEALTH_URGENT`, or `HEALTH_UNKNOWN` + comes from eMMC lifetime/PRE_EOL evidence. + +An eMMC wear bucket must never produce the text “internal storage almost +full”. Health gets separate maintenance wording. Unknown is never silently +mapped to healthy or critical. + +## Capacity thresholds + +Read `StorageManager`/platform low and full thresholds for `/data`, plus +`StatFs.getAvailableBytes()`. Enter CRITICAL immediately at the platform full +threshold. Enter LOW after two consecutive samples below the platform low +threshold. Clear only after three samples above +`max(lowThreshold + 256 MiB, lowThreshold * 1.25)`. This combines Android's +device-specific absolute threshold with hysteresis; do not retain the stock +unconditional 1-GiB warning. + +Report total, available, reclaimable, recent growth rate and adopted-volume +state separately. Reclaimable bytes inform the cleanup offer but are not +pretended to be already free. + +## Actions and timing + +- OK: poll every 10 minutes and react to platform storage broadcasts. +- LOW/CRITICAL: poll every minute until stable recovery. +- Show UI only on state transition and apply a user-visible cooldown. +- First run bounded owner-specific cleanup (EPG retention, GEL pruning), then + user-approved cache cleanup. Never sweep all package caches automatically. +- Keep `/data/vendor/user_setting` as a separately named OEM policy with its + own metrics and user-impact review. + +The state-machine harness is `tools/test_storage_state.py`. diff --git a/patches/storage-monitor/REFERENCE-PATCH.diff b/patches/storage-monitor/REFERENCE-PATCH.diff new file mode 100644 index 0000000..32be935 --- /dev/null +++ b/patches/storage-monitor/REFERENCE-PATCH.diff @@ -0,0 +1,30 @@ +--- a/src/org/droidtv/tv/tptvservice/tvstoragemonitor/TVStorageMonitorService.java ++++ b/src/org/droidtv/tv/tptvservice/tvstoragemonitor/TVStorageMonitorService.java +@@ +- long blockSize = statFs.getBlockSize(); +- long availableBlocks = statFs.getAvailableBlocks(); +- long free = blockSize * availableBlocks; +- boolean emmcCritical = getEmmcLifetime(); +- if (free < under_500MB) { ... MEMORY_CRITICAL ... } +- else if (free < under_1GB && !emmcCritical) { ... low-storage UI ... } +- else if (free >= under_1GB && emmcCritical) { ... MEMORY_CRITICAL ... } ++ final File data = Environment.getDataDirectory(); ++ final StatFs fs = new StatFs(data.getAbsolutePath()); ++ final long freeBytes = fs.getAvailableBytes(); ++ final StorageManager sm = getSystemService(StorageManager.class); ++ final long lowBytes = sm.getStorageLowBytes(data); ++ final long fullBytes = sm.getStorageFullBytes(data); ++ final long clearBytes = Math.max(lowBytes + 256L * 1024 * 1024, ++ lowBytes * 5L / 4L); ++ CapacityState next = capacityState.update(freeBytes, lowBytes, fullBytes, ++ clearBytes, 2, 3); ++ if (next.changed()) publishCapacityTransition(next, freeBytes, lowBytes); ++ // Health is a separate diagnostic channel. It must never synthesize a ++ // low-storage notification or CLEANUP_APPS broadcast. ++ EmmcHealth health = readEmmcHealthIfAvailable(); ++ publishHealthTransitionOnly(health); +@@ +- for (PackageInfo pkg : pm.getInstalledPackages(0)) +- pm.deleteApplicationCacheFiles(pkg.packageName, null); ++ // Removed: global asynchronous cache sweeping is not a monitor action. ++ // Offer an explicit user/OEM cleanup workflow with attribution instead. diff --git a/patches/storage-monitor/tools/test_storage_state.py b/patches/storage-monitor/tools/test_storage_state.py new file mode 100644 index 0000000..b1489b9 --- /dev/null +++ b/patches/storage-monitor/tools/test_storage_state.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Pure state-machine tests for the proposed storage monitor hysteresis.""" + +from dataclasses import dataclass + +MIB = 1024 * 1024 + + +@dataclass +class Monitor: + state: str = "OK" + low_streak: int = 0 + good_streak: int = 0 + notifications: int = 0 + + def sample(self, free: int | None, low: int, full: int) -> str: + if free is None: + return self.state + clear = max(low + 256 * MIB, low * 5 // 4) + old = self.state + if free <= full: + self.state, self.low_streak, self.good_streak = "CRITICAL", 0, 0 + elif free <= low: + self.low_streak += 1 + self.good_streak = 0 + if self.low_streak >= 2: + self.state = "LOW" + elif free >= clear: + self.good_streak += 1 + self.low_streak = 0 + if self.good_streak >= 3: + self.state = "OK" + else: + self.low_streak = self.good_streak = 0 + if self.state != old: + self.notifications += 1 + return self.state + + +def main(): + low, full = 800 * MIB, 100 * MIB + m = Monitor() + assert m.sample(700 * MIB, low, full) == "OK" + assert m.sample(700 * MIB, low, full) == "LOW" + assert m.notifications == 1 + for _ in range(10): + assert m.sample(750 * MIB, low, full) == "LOW" + assert m.notifications == 1 + assert m.sample(90 * MIB, low, full) == "CRITICAL" + assert m.notifications == 2 + clear = max(low + 256 * MIB, low * 5 // 4) + assert m.sample(clear, low, full) == "CRITICAL" + assert m.sample(clear, low, full) == "CRITICAL" + assert m.sample(clear, low, full) == "OK" + assert m.notifications == 3 + assert m.sample(None, low, full) == "OK" + # eMMC health is intentionally absent from the capacity input/state. + health_warning = True + assert health_warning and m.state == "OK" + print("PASS: entry debounce, critical, hysteresis, dedupe, unknown, health separation") + + +if __name__ == "__main__": + main() diff --git a/patches/storage/SELECTIVE-ADOPTED-DESIGN.md b/patches/storage/SELECTIVE-ADOPTED-DESIGN.md new file mode 100644 index 0000000..ecf328b --- /dev/null +++ b/patches/storage/SELECTIVE-ADOPTED-DESIGN.md @@ -0,0 +1,55 @@ +# Selective adopted-storage design + +## Candidate matrix + +| Candidate | Observed size | Owner / label | Opener and boot timing | Reconstructible | USB failure / fallback | Decision | +|---|---:|---|---|---|---|---| +| EPG `EpgDatabase.db` schedule payload | 1,742,053,376 B before repair; ~806,912 B after reacquisition | `system:system`, `system_app_data_file` | Channels UID1000 provider; needed for Guide, not Android boot | schedule yes; channel/mapping/favorite/recording relations not assumed so | Live TV must retain an internal minimal DB; reacquire schedule | Architecture C now; optional A only with OEM integration | +| GEL WorkManager DB | 229,453,824 B pathological snapshot; tiny after repair | `system:system`, `system_app_data_file` | GEL/WorkManager, system-app startup | derived work history partly; undelivered actions are not disposable | internal bounded outbox required | Fix queue; do not move | +| Downloads/media cache | not measured as a dominant `/data` cause | package-specific | consumer-specific, normally post-boot | only explicitly cached/downloadable objects | missing-volume UI and internal metadata | Eligible case-by-case after inventory | +| Updated-system APK code/data | package-dependent | Package Manager types | package/runtime dependent | generally no | Android package move is whole-package, not selective | Not a DB-path solution | +| Accounts, Settings, PM, boot/FBE metadata | not relevant to recovered leak | core platform types | early/boot-critical | no | no safe USB fallback | Excluded | + +## Architecture A — configurable EPG path + +Channels keeps authoritative channel, mapping, favorites, Now/Next and +recording/reminder links internally. After vold reports the expected adopted +UUID mounted, an OEM helper opens a narrowly typed private-ext4 schedule-cache +directory. It validates UUID, schema, generation and integrity before switching +the provider's schedule backend. Removal closes that pool and immediately +returns to the internal minimal provider. This needs Philips source, a narrow +SELinux type, explicit volume callbacks and OTA/schema migration tests. It must +not use emulated/FUSE storage, symlinks or a boot bind mount. + +## Architecture B — internal live DB plus archive + +Keep current guide data internal and place optional old/archive payloads on +adopted storage. This reduces neither the repaired seven-day working set nor +the need for retention, complicates queries and cross-volume consistency, and +has little demonstrated product value. Never rely on cross-volume SQLite +`ATTACH` for atomic transactions. Classification: possible but unjustified. + +## Architecture C — retention only + +Keep EPG internal and deploy BUG-001's bounded cleanup. Phase 5 reduced the DB +from 1.62 GiB to about 0.8 MiB while the Guide, Live TV, favorites and timers +remained functional; the seven-day offline test retained 7,387 rows in a +4.06-MiB database. This is the preferred architecture because it removes the +actual leak without USB power, mount-order, locking, recovery or OTA coupling. + +## Migration and failure gates + +If A is ever built, default the feature off. Copy only reconstructible schedule +rows into a versioned temporary generation, check integrity, fsync file and +directory, then atomically switch an app-level marker. Retain the internal +fallback. A missing, slow, full, removed, corrupt, reformatted or UUID-changed +USB device is a normal tested state. Factory reset, OTA, standby power loss, +1,000 remove/reinsert cycles and fsync latency must never impair Android boot +or Live TV. Adopted media is encrypted/device-bound and is not a backup. + +## Decision + +Select C for this television. Reconsider A only if a future measured, +reconstructible payload remains large after retention and Philips can ship the +required source, SELinux and recoverable firmware changes. Reject moving whole +`/data`. diff --git a/patches/tests/run_all.py b/patches/tests/run_all.py new file mode 100644 index 0000000..b8c9ba5 --- /dev/null +++ b/patches/tests/run_all.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Run all Phase 6 offline gates from any working directory.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +TESTS = [ + ROOT / "patches/epg-stale-cleanup/tools/test_epg_retention.py", + ROOT / "patches/gel-workmanager/tools/verify_stock_callsite.py", + ROOT / "patches/gel-workmanager/tools/test_bounded_outbox.py", + ROOT / "patches/usb-freesize/tools/test_usb_size_policy.py", + ROOT / "patches/storage-monitor/tools/test_storage_state.py", +] +GOLDEN = { + "diagnostics/raw/storage/channels.apk": "3a44bc648624f0cc9cd9feb4cfc3fd33f41323597718e4fccfc25fe35504d34f", + "diagnostics/raw/storage/gel.apk": "29a68e6b09700ed4ccdf312c0b99eee0c3f67e329a537fe87ed4341a71d383d7", + "diagnostics/phase4/reference-binaries/tvfw.jar": "a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad", + "diagnostics/raw/TPTvService.apk": "75f492ee89906c8c267d685d2b7d0ce02bb09f3a6b4cc8456f8b096b9e441ecd", + "diagnostics/phase4/epg-snapshot/EpgDatabase.db": "71403743206d5082aae56c4a947f3429f5c3d54b908c4abe2eb0ca66e06694ba", +} +ARTIFACTS = [ + "patches/epg-stale-cleanup/PATCH.diff", + "patches/epg-stale-cleanup/tools/epg_retention.py", + "patches/gel-workmanager/PATCH.diff", + "patches/gel-workmanager/tools/verify_stock_callsite.py", + "patches/usb-freesize/PATCH.diff", + "patches/storage-monitor/POLICY.md", + "patches/storage/SELECTIVE-ADOPTED-DESIGN.md", +] + + +def digest(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def main() -> int: + omitted_baselines: list[str] = [] + for rel, expected in GOLDEN.items(): + path = ROOT / rel + if not path.exists(): + omitted_baselines.append(rel) + print(f"SKIP unpublished golden binary: {rel}") + continue + got = digest(path) + if got != expected: + raise SystemExit(f"FAIL golden hash: {rel}: {got}") + print(f"PASS golden hash: {rel}") + + for test in TESTS: + if test.name == "verify_stock_callsite.py" and not ( + ROOT / "diagnostics/raw/storage/gel.apk" + ).exists(): + print("SKIP stock GEL callsite test: unpublished APK is required") + continue + print(f"RUN {test.relative_to(ROOT)}", flush=True) + subprocess.run([sys.executable, str(test)], cwd=test.parent, check=True) + + lines = [f"{digest(ROOT / rel)} {rel}" for rel in ARTIFACTS] + if "--write-hashes" in sys.argv: + out = ROOT / "patches/artifacts/hashes.sha256" + out.write_text("\n".join(lines) + "\n", encoding="ascii") + print(f"WROTE {out.relative_to(ROOT)}") + else: + print("\n".join(lines)) + if omitted_baselines: + print( + "NOTE: publication omits private/redistributable baselines; " + "their expected hashes remain documented" + ) + print("PASS: all available Phase 6 offline gates") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/patches/usb-freesize/DEX-BOUNDARY.md b/patches/usb-freesize/DEX-BOUNDARY.md new file mode 100644 index 0000000..1f91d5e --- /dev/null +++ b/patches/usb-freesize/DEX-BOUNDARY.md @@ -0,0 +1,22 @@ +# USB size DEX boundary + +Stock `tvfw.jar!/classes.dex`, class +`Lorg/droidtv/tv/os/storage/UsbVolumeListerImpl;`, private +`getVolumes(String)`, code item `code_off=0x17b5e0`, registers 15. + +At PC `0x0133` the method reads only +`sys.mtk.dmremoteservice.ready`. If it is not exactly `"1"`, execution jumps +past PC `0x0157` without ever assigning `UsbVolume.mFreeDiskSize`; the +constructor default zero is then published and logged. This is the confirmed +zero-size mechanism. + +The MediaTek producer itself uses `StatFs`, divides bytes by 1024 and stores +KiB in `MountPoint.mTotalSize/mFreeSize`. `UsbVolumeListerImpl` divides those +fields by another 1024, proving that the public `mRawDiskSize/mFreeDiskSize` +values on this path are MiB. The fallback must therefore divide StatFs bytes by +1,048,576, not store raw bytes. + +Adding guarded StatFs calls and exception handling grows the code item and +requires a rebuilt framework DEX. `tvfw.jar` is the registered `tvfw` Java +shared library (not BOOTCLASSPATH) and is coupled to stock `tvfw.odex`/VDEX and +client class-loader contexts; no live or standalone JAR replacement is produced. diff --git a/patches/usb-freesize/IMPLEMENTATION.md b/patches/usb-freesize/IMPLEMENTATION.md new file mode 100644 index 0000000..9bbb858 --- /dev/null +++ b/patches/usb-freesize/IMPLEMENTATION.md @@ -0,0 +1,23 @@ +# USB free-size implementation + +The exact stock target is `tvfw.jar!/classes.dex`, +`UsbVolumeListerImpl.getVolumes(String)`, `code_off=0x17b5e0`. At DEX PC +`0x0133` it reads only `sys.mtk.dmremoteservice.ready`; when the value is not +`1`, the size assignment is skipped and constructor-default zero is published. + +MediaTek `DMServer$4` derives `MountPoint` sizes as StatFs bytes divided by +1,024, so those fields are KiB. `UsbVolumeListerImpl` divides by 1,024 again, +making its public field contract MiB. The fallback must therefore use +`StatFs.getAvailableBytes()/1,048,576`, with `long` arithmetic. + +Accept DeviceManager values only when the service is ready, total is positive, +and `0 <= free <= total`. Otherwise query the actual mounted `VolumeInfo` path. +A successful query may legitimately return zero; a missing path, removal race, +permission error, or invalid range must remain UNKNOWN/omitted—not numeric zero. +The same decision applies to public, private/adopted and emulated volumes. + +`PATCH.diff` is the source-level specification; `SMALI-PATCH.md` records the +DEX integration boundary. `tools/test_usb_size_policy.py` validates both +sources, property states, >2-TiB arithmetic, true zero and unknown. Deployment +still requires a rebuilt `tvfw` shared-library artifact plus client/preopt and +AVB testing. diff --git a/patches/usb-freesize/PATCH-PLAN.md b/patches/usb-freesize/PATCH-PLAN.md new file mode 100644 index 0000000..f26b44c --- /dev/null +++ b/patches/usb-freesize/PATCH-PLAN.md @@ -0,0 +1,45 @@ +# Patch plan – reliable USB free-space reporting + +## Target + +`/system/framework/tvfw.jar`, `UsbVolumeListerImpl.getVolumes(String)`, SHA-256 +`a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad`. + +## Java pseudodiff + +```diff +- long freeMiB = -1; +- if (SystemProperties.get("sys.mtk.dmremoteservice.ready").equals("1")) +- free = deviceManager.getMountPoint(path).mFreeSize; ++ long freeMiB = -1; ++ boolean ready = SystemProperties.getBoolean( ++ "vendor.mtk.dmremoteservice.ready", ++ SystemProperties.getBoolean("sys.mtk.dmremoteservice.ready", false)); ++ if (ready) freeMiB = safeDmFreeKiB(path) / 1024L; ++ if (freeMiB < 0 || freeMiB > totalMiB) { ++ try { freeMiB = new StatFs(path).getAvailableBytes() / (1024L * 1024L); } ++ catch (RuntimeException e) { freeMiB = UNKNOWN_MIB; } ++ } +``` + +The existing `UsbVolume` field contract is MiB on the MediaTek-ready path, so +the fallback must retain MiB rather than mixing raw bytes. Resolve the path +from the current `VolumeInfo`: public uses its mounted path; +private/adopted uses `/mnt/expand/`. Only return a numeric zero when a +successful filesystem query actually reports zero; otherwise expose unknown or +omit the volume. Use `long` end-to-end. + +## Compatibility / smali + +Supporting both old and vendor-prefixed properties avoids regressions across +MediaTek branches, but the correctness guarantee comes from `StatFs`, not the +property spelling. A smali patch adds guarded `StatFs.`, +`getAvailableBytes`, range validation and exception handling. Framework-JAR +replacement requires shared-library/client-compatible DEX, correct oat +handling and AVB/system-image resigning; do not hot-patch the live locked device. + +## Tests + +Public/adopted, ready true/false/missing, removed between enumeration/query, +read-only volume, >2-TiB arithmetic, zero genuinely full, reinsertion and path +change. Compare against `df` within filesystem allocation tolerance. diff --git a/patches/usb-freesize/PATCH.diff b/patches/usb-freesize/PATCH.diff new file mode 100644 index 0000000..8ef5774 --- /dev/null +++ b/patches/usb-freesize/PATCH.diff @@ -0,0 +1,38 @@ +--- a/src/org/droidtv/tv/os/storage/UsbVolumeListerImpl.java ++++ b/src/org/droidtv/tv/os/storage/UsbVolumeListerImpl.java +@@ +- if ("1".equals(SystemProperties.get("sys.mtk.dmremoteservice.ready"))) { +- MountPoint mp = DeviceManager.getInstance().getMountPoint(path); +- if (mp != null) { +- volume.mRawDiskSize = mp.mTotalSize / 1024; +- volume.mFreeDiskSize = mp.mFreeSize / 1024; +- } +- } ++ boolean known = false; ++ if ("1".equals(SystemProperties.get("sys.mtk.dmremoteservice.ready"))) { ++ MountPoint mp = DeviceManager.getInstance().getMountPoint(path); ++ if (mp != null) { ++ long totalMiB = mp.mTotalSize / 1024L; ++ long freeMiB = mp.mFreeSize / 1024L; ++ if (totalMiB > 0 && freeMiB >= 0 && freeMiB <= totalMiB) { ++ volume.mRawDiskSize = totalMiB; ++ volume.mFreeDiskSize = freeMiB; ++ known = true; ++ } ++ } ++ } ++ if (!known) { ++ try { ++ StatFs fs = new StatFs(path); ++ long totalMiB = fs.getTotalBytes() / 1_048_576L; ++ long freeMiB = fs.getAvailableBytes() / 1_048_576L; ++ if (totalMiB <= 0 || freeMiB < 0 || freeMiB > totalMiB) ++ throw new IllegalStateException("invalid StatFs size"); ++ volume.mRawDiskSize = totalMiB; ++ volume.mFreeDiskSize = freeMiB; ++ known = true; ++ } catch (RuntimeException e) { ++ Log.w(TAG, "volume size unknown: " + volume.mUUID, e); ++ } ++ } ++ if (!known) continue; diff --git a/patches/usb-freesize/REFERENCE-PATCH.diff b/patches/usb-freesize/REFERENCE-PATCH.diff new file mode 100644 index 0000000..9eeeacd --- /dev/null +++ b/patches/usb-freesize/REFERENCE-PATCH.diff @@ -0,0 +1,39 @@ +--- a/src/org/droidtv/tv/os/storage/UsbVolumeListerImpl.java ++++ b/src/org/droidtv/tv/os/storage/UsbVolumeListerImpl.java +@@ +- if ("1".equals(SystemProperties.get("sys.mtk.dmremoteservice.ready"))) { +- MountPoint mp = DeviceManager.getInstance().getMountPoint(path); +- if (mp != null) { +- volume.mRawDiskSize = mp.mTotalSize / 1024; +- volume.mFreeDiskSize = mp.mFreeSize / 1024; +- } +- } ++ boolean sizeKnown = false; ++ if ("1".equals(SystemProperties.get("sys.mtk.dmremoteservice.ready"))) { ++ MountPoint mp = DeviceManager.getInstance().getMountPoint(path); ++ if (mp != null) { ++ long totalMiB = mp.mTotalSize / 1024; // MountPoint is KiB ++ long freeMiB = mp.mFreeSize / 1024; ++ if (totalMiB > 0 && freeMiB >= 0 && freeMiB <= totalMiB) { ++ volume.mRawDiskSize = totalMiB; ++ volume.mFreeDiskSize = freeMiB; ++ sizeKnown = true; ++ } ++ } ++ } ++ if (!sizeKnown) { ++ try { ++ StatFs fs = new StatFs(path); ++ long totalMiB = fs.getTotalBytes() / (1024L * 1024L); ++ long freeMiB = fs.getAvailableBytes() / (1024L * 1024L); ++ if (totalMiB <= 0 || freeMiB < 0 || freeMiB > totalMiB) { ++ throw new IllegalStateException("invalid StatFs size"); ++ } ++ volume.mRawDiskSize = totalMiB; ++ volume.mFreeDiskSize = freeMiB; ++ sizeKnown = true; ++ } catch (RuntimeException e) { ++ Log.w(TAG, "size unavailable for mounted volume " + volume.mUUID, e); ++ } ++ } ++ if (!sizeKnown) continue; // never publish an unknown size as numeric zero diff --git a/patches/usb-freesize/SMALI-PATCH.md b/patches/usb-freesize/SMALI-PATCH.md new file mode 100644 index 0000000..c3958be --- /dev/null +++ b/patches/usb-freesize/SMALI-PATCH.md @@ -0,0 +1,18 @@ +# USB DEX/smali integration boundary + +Target the golden `tvfw.jar` SHA-256 +`A0061795D87CF76C9B739E32022AB3D8C29F47ED01DDC1EEE189DD815A8D11AD`, +`classes.dex`, method `UsbVolumeListerImpl.getVolumes(String)` at +`code_off=0x17b5e0` (`registers=15`). The stock property branch starts at PC +`0x0133`. + +The rebuilt method must add a guarded `android/os/StatFs` construction, +`getTotalBytes`, `getAvailableBytes`, 64-bit division by `0x100000`, range +checks and a `RuntimeException` handler. Extend register count and try ranges +through an assembler; do not hand-overwrite code bytes or offsets. Preserve +the output-unit contract (MiB), all non-size fields, enumeration order and +volume filtering. Re-disassemble the output and verify branch/handler targets. + +This JAR is a registered `tvfw` Java shared library, not bootclasspath. Test +client class-loader contexts and regenerate/omit stock ODEX/VDEX through a +controlled image build; never pair modified DEX with the golden preopt files. diff --git a/patches/usb-freesize/tools/test_usb_size_policy.py b/patches/usb-freesize/tools/test_usb_size_policy.py new file mode 100644 index 0000000..46f537d --- /dev/null +++ b/patches/usb-freesize/tools/test_usb_size_policy.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Unit tests for the tvfw USB size fallback and unit contract.""" + +MIB = 1024 * 1024 + + +def sizes_mib(dm_ready, dm_total_kib, dm_free_kib, stat_total_bytes, stat_free_bytes): + if dm_ready and dm_total_kib is not None and dm_free_kib is not None: + total = dm_total_kib // 1024 + free = dm_free_kib // 1024 + if total > 0 and 0 <= free <= total: + return total, free, "dm" + if stat_total_bytes is None or stat_free_bytes is None: + return None + total = stat_total_bytes // MIB + free = stat_free_bytes // MIB + if total <= 0 or not 0 <= free <= total: + return None + return total, free, "statfs" + + +def main(): + gib = 1024 * MIB + assert sizes_mib(False, None, None, 64 * gib, 48 * gib) == (65536, 49152, "statfs") + assert sizes_mib(True, 64 * 1024 * 1024, 48 * 1024 * 1024, None, None) == (65536, 49152, "dm") + assert sizes_mib(True, 0, 0, 64 * gib, 48 * gib) == (65536, 49152, "statfs") + assert sizes_mib(True, 64 * 1024 * 1024, 70 * 1024 * 1024, 64 * gib, 48 * gib) == (65536, 49152, "statfs") + assert sizes_mib(False, None, None, 3 * 1024 * gib, 2 * 1024 * gib) == (3145728, 2097152, "statfs") + assert sizes_mib(False, None, None, 64 * gib, 0) == (65536, 0, "statfs") + assert sizes_mib(False, None, None, None, None) is None + print("PASS: property states, DM validation, StatFs fallback, >2TiB, real zero, unknown") + + +if __name__ == "__main__": + main() diff --git a/recovery/BASELINE-HASHES.md b/recovery/BASELINE-HASHES.md new file mode 100644 index 0000000..6dcf763 --- /dev/null +++ b/recovery/BASELINE-HASHES.md @@ -0,0 +1,29 @@ +# Golden recovery hashes + +Captured 2026-08-26 from the matching live build. Compare SHA-256 exactly; +filenames and X.509 subjects alone are insufficient. + +| Component | Stock path | SHA-256 | +|---|---|---| +| Channels | `/system/priv-app/channels/channels.apk` | `3a44bc648624f0cc9cd9feb4cfc3fd33f41323597718e4fccfc25fe35504d34f` | +| GEL | `/system/app/gel/gel.apk` | `29a68e6b09700ed4ccdf312c0b99eee0c3f67e329a537fe87ed4341a71d383d7` | +| TPTvService | `/system/app/TPTvService/TPTvService.apk` | `75f492ee89906c8c267d685d2b7d0ce02bb09f3a6b4cc8456f8b096b9e441ecd` | +| tvfw.jar | `/system/framework/tvfw.jar` | `a0061795d87cf76c9b739e32022ab3d8c29f47ed01ddc1eee189dd815a8d11ad` | +| framework-res | `/system/framework/framework-res.apk` | `d50a2ad1caa5c6e95ee1540d798150f40246f20c90d3a0d9371c58b541a27508` | + +Preopt: + +| Path | SHA-256 | +|---|---| +| `/system/priv-app/channels/oat/arm/channels.odex` | `a440f6914a3d6010e9482cc0be962978ab2db73eb2384fb5dc2f5c1e72a25215` | +| `/system/priv-app/channels/oat/arm/channels.vdex` | `be8923c2aa6232ed44dc81b141e24ed9dc92d84ba346cdb9625da338cd86a29b` | +| `/system/app/gel/oat/arm/gel.odex` | `587cd8b7a1a3b6861bd0fb577a0fc0b9e101bf8899c38acae6a83c30d77895d1` | +| `/system/app/gel/oat/arm/gel.vdex` | `f3ba405a06d1129b19e197207cde94842703acb39663d251f34156778ae867f9` | +| `/system/app/TPTvService/oat/arm/TPTvService.odex` | `8dade47e3181a2906bc7485096d5d62b25da7ee7888bd878180bb1e0f949513d` | +| `/system/app/TPTvService/oat/arm/TPTvService.vdex` | `7a29519398c8a136a1bf7872470e51085e9688af33500f4e707c90fdee63016e` | +| `/system/framework/oat/arm/tvfw.odex` | `a4af432c54153c9325d92f338f93b2b0a6cfa6b618a5d2c31d587e93eceff531` | +| `/system/framework/oat/arm/tvfw.vdex` | `abd0f4ce99404199332a779b195c53ca57d2238d0368910b0936cd55a8572509` | + +Platform certificate SHA-256: +`D8:FF:E0:AF:40:4E:AB:8E:45:5A:00:74:D4:17:B0:DA:12:D0:67:26:E5:05:B0:2B:7F:DB:18:31:29:C8:90:68`. + diff --git a/recovery/FULL-FIRMWARE-RECOVERY.md b/recovery/FULL-FIRMWARE-RECOVERY.md new file mode 100644 index 0000000..264ba23 --- /dev/null +++ b/recovery/FULL-FIRMWARE-RECOVERY.md @@ -0,0 +1,35 @@ +# Full firmware recovery plan + +## Acquisition + +Request from Philips/TPVision: + +1. full, not merely incremental, USB firmware for `43PUS7394/12` and + `TPM191E_R`; +2. current or explicitly upgrade/downgrade-compatible release; +3. SHA-256 and signed publication/release note; +4. model-specific cold-boot/emergency installation instructions; +5. clarification whether user settings/data are preserved; +6. boot-slot recovery and failure/power-loss behavior. + +## Offline validation before use + +- hash/provenance and exact filename; +- ZIP/UPG structure, `upg.xml` product ID and source/target versions; +- payload partition list and properties; +- no modification, repacking or third-party wrapper; +- sufficient, stable power and known expected duration. + +## Execution gate + +A full firmware action writes many boot-critical partitions and may erase data. +It requires a new explicit authorization and a final preflight. Do not start it +merely because Android UI is slow; first distinguish app-update rollback from a +true boot/system-image failure. + +## Post-recovery verification + +Check build/fingerprint, active slot, locked/green AVB, hashes, `/data` capacity, +TV setup, tuner/channels, Guide, favorites, recordings/reminders, network, +HDMI/CEC, sound, standby/wake and whether the original bug is present. + diff --git a/recovery/README.md b/recovery/README.md new file mode 100644 index 0000000..a7ce189 --- /dev/null +++ b/recovery/README.md @@ -0,0 +1,34 @@ +# Philips 43PUS7394/12 recovery packet + +Scope: `TPM191E_R.211.000.103.000`. These runbooks are preparation only; no +recovery, uninstall, reboot, flash or file write was performed while creating +them. + +## Emergency order + +1. **Do not power-cycle repeatedly.** Record screen state, LEDs and whether ADB + answers. +2. If ADB works, collect read-only package/path/version/log evidence first. +3. Identify whether the failed artifact is a `/data/app` update or a system- + image change. +4. For an updated-system APK, use only the package-specific guide after explicit + authorization. PackageManager fallback is preferred over file deletion. +5. For `tvfw.jar`, verified-system or no-ADB failures, stop. Use only an + official Philips full firmware and a proven model-specific procedure. + +## Files + +- `BASELINE-HASHES.md` – known-good identities. +- `RESTORE-CHANNELS.md`, `RESTORE-GEL.md`, `RESTORE-TPTVSERVICE.md` – + PackageManager fallback plans. +- `RESTORE-TVFW.md` – why no standalone restore is approved. +- `USB-RECOVERY.md` – current evidence and prerequisites. +- `FULL-FIRMWARE-RECOVERY.md` – OEM recovery acquisition/checklist. + +## Stop conditions + +Stop and reassess if the serial/model/build do not match, ADB targets another +device, the base APK hash differs, the official firmware provenance/hash is +unknown, battery/power is unstable, or any instruction requires AVB bypass, +unlock, arbitrary block writes or third-party images. + diff --git a/recovery/RESTORE-CHANNELS.md b/recovery/RESTORE-CHANNELS.md new file mode 100644 index 0000000..516bd86 --- /dev/null +++ b/recovery/RESTORE-CHANNELS.md @@ -0,0 +1,34 @@ +# Restore Channels updated-system app + +Use only if a newly installed `/data/app` update of `org.droidtv.channels` +causes failures and normal ADB is available. This is a future state-changing +rollback and requires explicit approval at execution time. + +## Read-only triage + +```powershell +.\platform-tools\adb.exe -s :5555 shell pm path org.droidtv.channels +.\platform-tools\adb.exe -s :5555 shell dumpsys package org.droidtv.channels +.\platform-tools\adb.exe -s :5555 shell dumpsys activity processes org.droidtv.channels +``` + +Proceed only if the active code path is under `/data/app` and the untouched base +path/hash matches `BASELINE-HASHES.md`. + +## Authorized rollback concept + +PackageManager documents: + +```text +cmd package uninstall-system-updates org.droidtv.channels +``` + +This removes the update and falls back to the `/system` APK. It is **not** to be +run from this document automatically. After explicit approval, record complete +output, then verify active path/version/signer, provider stability and EPG data. + +## Data warning + +The base APK reappears, but an update-created schema or preferences may be +incompatible. Never clear app data as a reflex. Preserve the pre-update DB and +validate channels, Guide/Now/Next, favorites, recordings, reminders and timers. diff --git a/recovery/RESTORE-GEL.md b/recovery/RESTORE-GEL.md new file mode 100644 index 0000000..c276545 --- /dev/null +++ b/recovery/RESTORE-GEL.md @@ -0,0 +1,20 @@ +# Restore GEL updated-system app + +Applicable only to a `/data/app` update of `org.droidtv.gel` while ADB works. +Read-only triage: + +```powershell +.\platform-tools\adb.exe -s :5555 shell pm path org.droidtv.gel +.\platform-tools\adb.exe -s :5555 shell dumpsys package org.droidtv.gel +.\platform-tools\adb.exe -s :5555 shell dumpsys dbinfo org.droidtv.gel -v +``` + +Future rollback after explicit approval: + +```text +cmd package uninstall-system-updates org.droidtv.gel +``` + +Then verify fallback to `/system/app/gel/gel.apk`, exact stock signer/hash, +process stability, WorkManager DB compatibility and normal device telemetry/ +consent behavior. Do not `pm clear`; it is a separate destructive action. diff --git a/recovery/RESTORE-TPTVSERVICE.md b/recovery/RESTORE-TPTVSERVICE.md new file mode 100644 index 0000000..2edf399 --- /dev/null +++ b/recovery/RESTORE-TPTVSERVICE.md @@ -0,0 +1,23 @@ +# Restore TPTvService updated-system app + +`org.droidtv.tv.tptvservice` is persistent/core and UID1000. A faulty update can +create a more serious restart loop than Channels/GEL. + +If ADB remains stable, first collect: + +```powershell +.\platform-tools\adb.exe -s :5555 shell pm path org.droidtv.tv.tptvservice +.\platform-tools\adb.exe -s :5555 shell dumpsys package org.droidtv.tv.tptvservice +.\platform-tools\adb.exe -s :5555 shell dumpsys activity processes org.droidtv.tv.tptvservice +``` + +The future PackageManager rollback command would be: + +```text +cmd package uninstall-system-updates org.droidtv.tv.tptvservice +``` + +Run only with explicit approval. Verify fallback hash, boot/service stability, +storage monitor behavior, TV power/standby, tuner/playback and no repeated +crashes. If ADB is unstable or absent, do not improvise file deletion; switch to +the OEM full-firmware recovery plan. diff --git a/recovery/RESTORE-TVFW.md b/recovery/RESTORE-TVFW.md new file mode 100644 index 0000000..adc6ca1 --- /dev/null +++ b/recovery/RESTORE-TVFW.md @@ -0,0 +1,22 @@ +# Restore tvfw.jar + +There is no PackageManager updated-system rollback for +`/system/framework/tvfw.jar`. It is part of the AVB-protected system image with +matching ODEX/VDEX and many Philips client class-loader contexts. + +## If only an experimental volatile mount was used + +Do not assume reboot is sufficient unless the exact experiment documented its +mount lifecycle. Collect mount/process evidence if ADB works and follow that +experiment's explicit teardown after approval. No such deployment was made in +Phase 7. + +## If a system image was changed + +Do not copy the baseline JAR over a mounted system, delete ODEX/VDEX, remount rw, +disable verity or write blocks. Restore the entire coherent, OEM-signed slot via +a tested A/B or official full-firmware procedure. Verify all hashes and AVB +green state after recovery. + +Current classification: **HIGH RISK / no standalone approved restore path**. + diff --git a/recovery/USB-RECOVERY.md b/recovery/USB-RECOVERY.md new file mode 100644 index 0000000..5ffc78e --- /dev/null +++ b/recovery/USB-RECOVERY.md @@ -0,0 +1,26 @@ +# USB recovery checklist + +## What is proven + +Normal Android's Settings updater scans a USB filesystem for +`/upgrades/autorun*.upg`, validates product/version metadata and hands an A/B +payload to UpdateEngine. This is an update path, not yet a proven dead-boot +recovery path. + +## What is missing + +- official full `autorun*.upg` for 43PUS7394/12 / `TPM191E_R`; +- Philips SHA-256/provenance and release notes; +- exact USB filesystem/layout requirements; +- physical button/key sequence and whether AC insertion is required; +- proof it operates when Android/ADB cannot boot; +- display/LED progress and safe timeout; +- downgrade/rollback restrictions. + +## Do not proceed until + +Philips/TPVision documentation and firmware are obtained from an official +source, hashes are recorded, model/platform compatibility is explicit and an +equivalent-device rehearsal succeeds. Do not use forum mirrors or similarly +named firmware from another TPM platform. No USB recovery was executed. + diff --git a/tools/epg-health-check/EXAMPLE-OUTPUT.txt b/tools/epg-health-check/EXAMPLE-OUTPUT.txt new file mode 100644 index 0000000..6766f5e --- /dev/null +++ b/tools/epg-health-check/EXAMPLE-OUTPUT.txt @@ -0,0 +1,24 @@ +Example A - live read-only check + +Philips TPM191E EPG health check (READ-ONLY) +Source: adb-dumpsys-dbinfo +EPG DB: 8,724,480 bytes (8.32 MiB) +/data free: 2975.8 MiB (39% used) +Decision: OK +- all available metrics are below empirical thresholds +No repair was attempted. + +Example B - historical defective offline snapshot + +Philips TPM191E EPG health check (READ-ONLY) +Source: offline-sqlite-snapshot +EPG DB: 1,742,053,376 bytes (1661.35 MiB) +Schedule rows: 3,071,250 +Oldest schedule age: >365 days +Rows older than 30 days: >3,000,000 +SQLite quick_check: ok +Decision: MAINTENANCE_RECOMMENDED +- DB is at least 256 MiB +- schedule rows >= 250,000 +- oldest entry >= 30 days and DB >= 64 MiB +No repair was attempted. diff --git a/tools/epg-health-check/README.md b/tools/epg-health-check/README.md new file mode 100644 index 0000000..440f4bb --- /dev/null +++ b/tools/epg-health-check/README.md @@ -0,0 +1,63 @@ +# EPG health check (read-only) + +This utility monitors the failure signature that previously grew +`EpgDatabase.db` to 1.62 GiB. It has no repair, delete, package-management or +device-write function. + +## Live ADB mode + +From the workspace root: + +```powershell +python .\tools\epg-health-check\health_check.py --serial :5555 +``` + +It executes exactly two allowlisted read-only commands: + +```text +adb -s :5555 shell dumpsys dbinfo org.droidtv.channels -v +adb -s :5555 shell df -k /data +``` + +This mode sees DB file size and `/data` capacity, not private row contents. + +## Offline snapshot mode + +Use a **consistent, already existing** copy of `EpgDatabase.db`: + +```powershell +python .\tools\epg-health-check\health_check.py --snapshot C:\path\EpgDatabase.db +``` + +SQLite is opened with `mode=ro&immutable=1` and `PRAGMA query_only=ON`. The tool +reads schedule count, oldest/newest end time, freelist/page metadata and +`quick_check(1)`. Do not point immutable mode at a live, changing DB or a copy +that is missing a required WAL; first obtain a transaction-consistent snapshot +through an authorized maintenance procedure. + +## Empirical thresholds + +These are conservative operational thresholds for this TV, not Android or +Philips specifications: + +| Metric | OK | WARN | MAINTENANCE_RECOMMENDED | +|---|---:|---:|---:| +| DB size | <64 MiB | 64–255 MiB | >=256 MiB | +| schedule rows (snapshot) | <100,000 | 100,000–249,999 | >=250,000 | +| oldest schedule entry | <14 days | >=14 days | >=30 days **and** DB >=64 MiB | + +Healthy post-repair references were about 0.8–8.3 MiB; the defective snapshot +was 1.62 GiB with 3.07 million expired rows. The generous thresholds avoid +reacting to ordinary guide variation. + +Exit codes: `0=OK`, `1=WARN`, `2=MAINTENANCE_RECOMMENDED`, `3=tool/error`. +`--json` provides machine-readable output but still performs no writes. + +## What to do on a warning + +1. Capture an offline, consistent snapshot and rerun snapshot mode. +2. Confirm `/data` pressure and that stale EPG rows—not another app—are the + cause. +3. If maintenance is recommended, stop and obtain explicit authorization. +4. Reuse the documented, supervised Phase-5 repair with a fresh backup and + rollback artifact. This tool will never start it automatically. diff --git a/tools/epg-health-check/health_check.py b/tools/epg-health-check/health_check.py new file mode 100644 index 0000000..0f78d34 --- /dev/null +++ b/tools/epg-health-check/health_check.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Read-only Philips TPM191E EPG health check. + +ADB mode runs only `dumpsys dbinfo` and `df`. Snapshot mode opens an existing +SQLite file with mode=ro&immutable=1. It contains no repair code. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sqlite3 +import subprocess +import sys +import time +from pathlib import Path +from typing import Any + +MIB = 1024 * 1024 +OK_MAX_BYTES = 64 * MIB +WARN_MAX_BYTES = 256 * MIB +WARN_ROWS = 100_000 +MAINTENANCE_ROWS = 250_000 +WARN_OLDEST_DAYS = 14 +MAINTENANCE_OLDEST_DAYS = 30 + + +def run_readonly_adb(adb: Path, serial: str, args: list[str]) -> str: + allowed = { + ("shell", "dumpsys", "dbinfo", "org.droidtv.channels", "-v"), + ("shell", "df", "-k", "/data"), + } + if tuple(args) not in allowed: + raise RuntimeError(f"internal safety check rejected ADB command: {args!r}") + completed = subprocess.run( + [str(adb), "-s", serial, *args], + check=False, + capture_output=True, + text=True, + timeout=30, + ) + output = completed.stdout + completed.stderr + if completed.returncode != 0 or "device offline" in output.lower(): + raise RuntimeError(output.strip() or f"adb exited {completed.returncode}") + return output + + +def parse_dbinfo(text: str) -> dict[str, Any]: + match = re.search(r"^\s*EpgDatabase\.db\s+(\d+)b\b", text, re.MULTILINE) + if not match: + raise RuntimeError("EpgDatabase.db size not found in dumpsys dbinfo") + return {"db_bytes": int(match.group(1)), "source": "adb-dumpsys-dbinfo"} + + +def parse_df(text: str) -> dict[str, Any]: + for line in reversed(text.splitlines()): + fields = line.split() + # Android's `df /data` may report the process-visible bind mount as + # `/data/user/0`; it is backed by the same userdata filesystem. + if len(fields) >= 6 and fields[-1].startswith("/data"): + try: + total_kib, used_kib, free_kib = map(int, fields[1:4]) + return { + "data_total_kib": total_kib, + "data_used_kib": used_kib, + "data_free_kib": free_kib, + "data_use_percent": fields[4], + } + except ValueError: + pass + return {"df_note": "Could not parse /data line"} + + +def snapshot_metrics(path: Path, now_epoch: int) -> dict[str, Any]: + resolved = path.resolve(strict=True) + uri = resolved.as_uri() + "?mode=ro&immutable=1" + connection = sqlite3.connect(uri, uri=True, timeout=5) + try: + connection.execute("PRAGMA query_only=ON") + table = connection.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='scheduletable'" + ).fetchone() + if table is None: + raise RuntimeError("snapshot has no scheduletable") + # Philips data has historically used epoch seconds; the CASE also + # handles accidental millisecond values without changing the database. + epoch = "CASE WHEN ABS(endtime) >= 100000000000 THEN endtime / 1000 ELSE endtime END" + count, oldest, newest, older_30 = connection.execute( + f"SELECT COUNT(*), MIN({epoch}), MAX({epoch}), " + f"SUM(CASE WHEN {epoch} < ? THEN 1 ELSE 0 END) FROM scheduletable", + (now_epoch - MAINTENANCE_OLDEST_DAYS * 86400,), + ).fetchone() + page_size = int(connection.execute("PRAGMA page_size").fetchone()[0]) + page_count = int(connection.execute("PRAGMA page_count").fetchone()[0]) + freelist = int(connection.execute("PRAGMA freelist_count").fetchone()[0]) + integrity = connection.execute("PRAGMA quick_check(1)").fetchone()[0] + finally: + connection.close() + oldest_days = None if oldest is None else max(0.0, (now_epoch - int(oldest)) / 86400) + return { + "source": "offline-sqlite-snapshot", + "snapshot": str(resolved), + "db_bytes": resolved.stat().st_size, + "schedule_rows": int(count), + "oldest_end_epoch": None if oldest is None else int(oldest), + "newest_end_epoch": None if newest is None else int(newest), + "oldest_age_days": oldest_days, + "rows_older_than_30_days": int(older_30 or 0), + "page_size": page_size, + "page_count": page_count, + "freelist_count": freelist, + "quick_check": integrity, + } + + +def assess(metrics: dict[str, Any]) -> tuple[str, list[str]]: + size = int(metrics["db_bytes"]) + reasons: list[str] = [] + level = "OK" + + if size >= WARN_MAX_BYTES: + level = "MAINTENANCE_RECOMMENDED" + reasons.append(f"DB is at least {WARN_MAX_BYTES // MIB} MiB") + elif size >= OK_MAX_BYTES: + level = "WARN" + reasons.append(f"DB is at least {OK_MAX_BYTES // MIB} MiB") + + if "schedule_rows" in metrics: + rows = int(metrics["schedule_rows"]) + age = metrics.get("oldest_age_days") + if rows >= MAINTENANCE_ROWS: + level = "MAINTENANCE_RECOMMENDED" + reasons.append(f"schedule rows >= {MAINTENANCE_ROWS:,}") + elif rows >= WARN_ROWS and level == "OK": + level = "WARN" + reasons.append(f"schedule rows >= {WARN_ROWS:,}") + if age is not None and age >= MAINTENANCE_OLDEST_DAYS and size >= OK_MAX_BYTES: + level = "MAINTENANCE_RECOMMENDED" + reasons.append( + f"oldest entry >= {MAINTENANCE_OLDEST_DAYS} days and DB >= 64 MiB" + ) + elif age is not None and age >= WARN_OLDEST_DAYS and level == "OK": + level = "WARN" + reasons.append(f"oldest entry >= {WARN_OLDEST_DAYS} days") + if metrics.get("quick_check") != "ok": + level = "WARN" + reasons.append(f"SQLite quick_check={metrics.get('quick_check')!r}") + + if not reasons: + reasons.append("all available metrics are below empirical thresholds") + if metrics["source"] == "adb-dumpsys-dbinfo" and level != "OK": + reasons.append("use an offline snapshot to confirm row count and age") + return level, reasons + + +def print_human(metrics: dict[str, Any], level: str, reasons: list[str]) -> None: + print("Philips TPM191E EPG health check (READ-ONLY)") + print(f"Source: {metrics['source']}") + print(f"EPG DB: {metrics['db_bytes']:,} bytes ({metrics['db_bytes'] / MIB:.2f} MiB)") + if "schedule_rows" in metrics: + age = metrics.get("oldest_age_days") + age_text = "n/a" if age is None else f"{age:.1f} days" + print(f"Schedule rows: {metrics['schedule_rows']:,}") + print(f"Oldest schedule age: {age_text}") + print(f"Rows older than 30 days: {metrics['rows_older_than_30_days']:,}") + print(f"SQLite quick_check: {metrics['quick_check']}") + if "data_free_kib" in metrics: + print( + f"/data free: {metrics['data_free_kib'] / 1024:.1f} MiB " + f"({metrics['data_use_percent']} used)" + ) + print(f"Decision: {level}") + for reason in reasons: + print(f"- {reason}") + print("No repair was attempted.") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--snapshot", type=Path, help="consistent offline EpgDatabase.db copy") + parser.add_argument("--adb", type=Path, default=Path("platform-tools/adb.exe")) + parser.add_argument("--serial", default=":5555", help="ADB target, for example 192.0.2.10:5555") + parser.add_argument("--json", action="store_true") + parser.add_argument("--now", type=int, help="UTC epoch seconds for reproducible snapshot tests") + args = parser.parse_args() + + try: + now_epoch = args.now or int(time.time()) + if args.snapshot: + metrics = snapshot_metrics(args.snapshot, now_epoch) + else: + if args.serial == ":5555": + raise RuntimeError("pass the TV address with --serial :5555") + adb = args.adb.resolve(strict=True) + metrics = parse_dbinfo( + run_readonly_adb( + adb, + args.serial, + ["shell", "dumpsys", "dbinfo", "org.droidtv.channels", "-v"], + ) + ) + metrics.update( + parse_df(run_readonly_adb(adb, args.serial, ["shell", "df", "-k", "/data"])) + ) + level, reasons = assess(metrics) + except (OSError, RuntimeError, sqlite3.Error, subprocess.TimeoutExpired) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 3 + + if args.json: + print(json.dumps({"decision": level, "reasons": reasons, **metrics}, indent=2)) + else: + print_human(metrics, level, reasons) + return {"OK": 0, "WARN": 1, "MAINTENANCE_RECOMMENDED": 2}[level] + + +if __name__ == "__main__": + raise SystemExit(main())