EVChargePilot reads the car and does not write to it, with one stated exception. Its manifest declares the car permissions needed to read speed, energy, exterior environment, climate and vendor-extension data, plus network and location for route planning (CP-043). It has no overlay, no installer, no boot receiver and no vehicle-setting write path.
Trip history, evidence captures and crash reports stay in app-private storage. Telemetry never leaves the device. What leaves it for a route is origin, destination and road profile, and — only when a charging stop is needed — a window of road around that stop. Never a trip, a charge level, an odometer, a speed, a climate state or an identifier.
Unstable builds ask GitHub's rolling unstable pre-release at start whether a newer APK exists,
download it, and say where it landed. They do not install it, and they cannot: this app declares
no REQUEST_INSTALL_PACKAGES, runs under no system UID and calls no pm install. Installing the
downloaded file is the driver's own tap in the head unit's package installer. Stable builds
contain none of this code — no release URL, no socket to GitHub, nothing a preference could
switch on.
Every control fails closed:
httpsand an exact-match host allowlist, on the initial URL and on every redirect hop. ALocation:header is a remote instruction; anhttps→httpdowngrade or a hop togithub.com.attacker.netis a refusal, not a download.- A size ceiling on the declared and on the transferred length. A lying
Content-Lengthand a chunked body are the same attack — filling a car's storage — and both are cut. - Same signing certificate as the running app, or the file is deleted. The download lands in app-private cache and only reaches shared storage after that proof. An unreadable archive is the same answer as a mismatched one: refuse.
- The version comes from a remote asset name, so it never reaches a path unsanitised.
- One storage permission, unstable only. The verified APK is written to the head unit's
Downloadfolder so the driver can find it, which on API 28 costsWRITE_EXTERNAL_STORAGE. It is declared inapp/src/unstable/AndroidManifest.xmland nowhere else, capped atmaxSdkVersion=28(from API 29 scoped storage refuses that write whatever is granted), and requested at runtime from the dashboard — a permission that is never asked for is a permission the app does not have. The grant is re-read at the moment of writing, never remembered. A refusal is a supported state: the APK goes to this app's ownDownloaddirectory on the same volume instead, and the dialog names whichever path was used. One published APK is kept. Nothing else in this app reads or writes shared storage; the USB diagnostic export still adds no permission of its own.
ChargeStopActivity can send an ACTION_VIEW intent with a geo: URI, which moves whichever
navigation app the driver uses to a destination this app planned. It is the only thing here that
asks a vehicle system to act rather than to answer, and it exists because the alternative was a
driver retyping a route — the step that gets skipped, and that silently invalidates every figure
on the screen when it is done wrong.
Its bounds, all of which are testable:
- Parked, for the tap. The parked gate is re-read at the tap, not trusted from the render that drew the button. A navigation screen changing under someone at 110 km/h is the hazard.
- One tap, one plan — but a plan with a charging stop is two handovers. Nothing starts a
handover except a press. What a press may start is the plan the driver read and accepted: this
head unit has not been seen to drive a stop and a destination from one command, so
NavLegsholds the accepted plan's remaining leg in memory and sends it when the car's own guidance flag reports the previous leg reached. No new destination can enter that chain, a chain is replaced rather than raced by the next press, it is never written to disk, and a process restart drops it. Nothing hands over on a drift, on a timer of its own, or on anything the driver did not already accept. - The chained leg is NOT speed-gated, and that is a known gap. It fires on arrival, where the
car is stopping or stopped, but nothing reads
PERF_VEHICLE_SPEEDbefore it sends — unlike the tap. Closing it means deciding what a handover refused at 3 km/h rolling into a charger should do, which is a product decision and not a patch; until it is made, this is the one place in this app where a vehicle-facing command can leave without a speed reading behind it. - One method.
IMapNotificationListenercarriesstopNav,goHomeandsetFastestRoute. None of them is called anywhere in this tree, and registering on that interface — which would mean impersonating a navigation provider — is not done. - It cannot lie about what it did. A destination handed over without the route it belongs to is labelled on screen as exactly that: the car picks its own road, and the plan's figures stop describing the drive if it picks a different one.
- No coordinates in any log or probe. The validation artifact records that a handoff was tapped, whether anything accepted it, and the package name of the map installed on the car. Never where to.
- The fallback carries nothing. When no app answers the URI — which is this head unit —
MapAppsstarts the vendor's navigation package withACTION_MAIN, the intent its own launcher sends. It contains no destination and no data of any kind; the map opens where it already was, and the coordinates are printed on this app's screen for the driver to type.
CP-058 freezes the chosen plan on that same tap so the drive can be compared against it later. It is stored in app-private preferences and describes no particular journey: a leg distance, the charge and odometer at departure, two rates, a reserve, and the route's sections as distance-and-duration pairs. No destination, no coordinate, no place name, no road name — none of it is needed by the arithmetic, and all of it would be an itinerary sitting on disk. The file expires twelve hours after it is written, is cleared by the driver from the dashboard while parked, and is never exported: no diagnostic bundle, USB export or log line reads it.
Watching it costs no network. The comparison is a division over the charge gauge and the odometer, both of which are already being read, and the speed that would restore the plan comes from the sections stored above and a model fitted from trips already on disk.
Do not open a public issue for a vulnerability that could affect a vehicle. Use GitHub's private vulnerability reporting for the repository and include the firmware generation, application version, reproduction steps and relevant redacted logs.
Security CI blocks undeclared permissions and leaked secrets. Release APKs are minified and must be signed through CI or local secret properties. Emulator success never replaces an on-vehicle stability check.