Dokumentiere Philips-TV-Speicherdiagnose und Patch-Engineering

This commit is contained in:
Codex
2026-08-26 19:56:27 +02:00
commit a9e3c2f461
102 changed files with 7597 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@@ -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

31
PUBLICATION-MANIFEST.md Normal file
View File

@@ -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.

50
README.md Normal file
View File

@@ -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 <TV_IP>: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`

View File

@@ -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 <Property-, Boot-, AVB-, SoC- und Mountmuster> <lokaler Bugreport>
Get-Content <lokaler Bugreport> -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 `<TV_IP>:5555`.

View File

@@ -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

View File

@@ -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.

View File

@@ -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("<h", struct.pack("<H", v & 0xFFFF))[0]
def s32(v: int) -> int:
return struct.unpack("<i", struct.pack("<I", v & 0xFFFFFFFF))[0]
def ab(unit: int) -> 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()

View File

@@ -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.<init>(android.content.Context)` lädt das Literal `EpgDatabase.db`, setzt die CursorFactory auf `null` und den Versionsparameter auf `6`, bevor `SQLiteOpenHelper.<init>(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 = <bekannter Wert>`, 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

192
diagnostics/diagnosis.md Normal file
View File

@@ -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.

View File

@@ -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.

320
diagnostics/epg-analysis.md Normal file
View File

@@ -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 <Grenzwert>` 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`

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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/<uuid>/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.

View File

@@ -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`.

View File

@@ -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**.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 `<TV_IP>:5555`:
```text
adb devices -l
adb shell pm path <package>
adb shell dumpsys package <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 <targeted system/vendor paths>
adb shell stat <targeted APK/JAR/OAT/VDEX/update paths>
adb shell sha256sum <targeted stock artifacts>
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`.

View File

@@ -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`

View File

@@ -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.

View File

@@ -0,0 +1,149 @@
# Phase 1 – System-, Security-, Kernel- und SoC-Inventar
Stand: 2026-08-25. Ziel: Philips 43PUS7394/12 unter
`<TV_IP>: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 | `<TV_IP>: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.

View File

@@ -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`.

View File

@@ -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`.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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/`.

View File

@@ -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/`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

52
patches/RECOVERY-PLAN.md Normal file
View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

49
patches/VALIDATION.md Normal file
View File

@@ -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**.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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("<I", data, eocd + 16)[0]
size, magic = struct.unpack_from("<Q16s", data, central - 24)
if magic != b"APK Sig Block 42":
print(f"{path}: no APK Signing Block")
continue
start = central - size - 8
assert struct.unpack_from("<Q", data, start)[0] == size
pos, end, ids = start + 8, central - 24, []
while pos < end:
length = struct.unpack_from("<Q", data, pos)[0]
pair_id = struct.unpack_from("<I", data, pos + 8)[0]
ids.append((pair_id, NAMES.get(pair_id, "unknown")))
pos += 8 + length
print(f"{path}: " + ", ".join(f"0x{i:08x} {n}" for i, n in ids))

View File

@@ -0,0 +1,52 @@
# EPG stale-cleanup implementation
## Exact target and lifecycle
The stock target is `channels.apk!/classes4.dex`,
`Lorg/droidtv/channels/epg/bcepg/EpgProvider$deleteStaleDataRunnable;->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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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().

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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()

View File

@@ -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;-><init>(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()

View File

@@ -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.

View File

@@ -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
<https://source.android.com/docs/security/features/verifiedboot> and AVB source
<https://android.googlesource.com/platform/external/avb/>.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:
<https://source.android.com/docs/core/ota/ab>.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:
<https://source.android.com/docs/security/features/apksigning>.

View File

@@ -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 [<PACKAGE>]` 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
<https://android.googlesource.com/platform/frameworks/base/+/698d5999954b3bc7483dedecc2d94a65d69bbd42/services/core/java/com/android/server/pm/PackageManagerShellCommand.java>.
## 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**.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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()

View File

@@ -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`.

84
patches/tests/run_all.py Normal file
View File

@@ -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())

View File

@@ -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.

View File

@@ -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.

View File

@@ -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/<fsUuid>`. 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.<init>`,
`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.

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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()

View File

@@ -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`.

View File

@@ -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.

34
recovery/README.md Normal file
View File

@@ -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.

View File

@@ -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 <TV_IP>:5555 shell pm path org.droidtv.channels
.\platform-tools\adb.exe -s <TV_IP>:5555 shell dumpsys package org.droidtv.channels
.\platform-tools\adb.exe -s <TV_IP>: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.

20
recovery/RESTORE-GEL.md Normal file
View File

@@ -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 <TV_IP>:5555 shell pm path org.droidtv.gel
.\platform-tools\adb.exe -s <TV_IP>:5555 shell dumpsys package org.droidtv.gel
.\platform-tools\adb.exe -s <TV_IP>: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.

View File

@@ -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 <TV_IP>:5555 shell pm path org.droidtv.tv.tptvservice
.\platform-tools\adb.exe -s <TV_IP>:5555 shell dumpsys package org.droidtv.tv.tptvservice
.\platform-tools\adb.exe -s <TV_IP>: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.

22
recovery/RESTORE-TVFW.md Normal file
View File

@@ -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**.

26
recovery/USB-RECOVERY.md Normal file
View File

@@ -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.

View File

@@ -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.

Some files were not shown because too many files have changed in this diff Show More