Mikey Strauss

A Zero-Day in Android’s On-Device AI: Any App Can Open a Door to Gemini Nano

August 25, 2026

SECURITY RESEARCH · OATS SOLUTIONS

Any app can grant itself one ordinary-looking permission and open a live door to Gemini Nano — no prompt, no consent screen.

By Mikey Strauss, Dani Levenfeld & Tal Dim — Oats Solutions

TL;DR — Google’s on-device AI model, Gemini Nano, runs inside an app called AICore — present on Pixel phones and other Android devices Google’s shipped it to — behind a door that’s meant to open only for Google’s own apps. The lock doesn’t actually check who’s knocking: any app can declare one ordinary-looking permission and Android hands it over automatically at install, with no prompt and no consent screen. We built a brand-new, unsigned test app that did exactly that: it reached the real service and got back a live connection — a foothold, not a working prompt or an actual answer from the model, but a door standing open all the same.

We found this reading AICore’s manifest and its compiled code, then proved it end to end on a real device — then used Runzee, Oats Solutions’ dynamic-analysis platform for Android, to independently confirm this pipeline crossing the kernel boundary, twice, from two different angles. More on how below; first, the bug itself.

→ See what Runzee can do

The gate that isn’t there

Android is supposed to gate on-device AI behind OnDeviceIntelligenceManager — a platform API where the OS decides who talks to Gemini Nano, not the app itself. AICore backs that with two Binder services, both locked down correctly with a signature-level permission only Google’s own apps can hold. Then there’s a third one: AiCoreMultiUserService, gated by a permission AICore wrote for itself.

real code manifest declaration:

<permission android:name="com.google.android.apps.aicore.service.BIND_SERVICE"/>

No protection level set. That silently defaults to normal — the same tier apps get for asking to check network state. Android grants it automatically at install, with no dialog and no signature match required. The same manifest correctly marks its other two permissions signature — someone remembered to lock two out of three doors.

The code doesn’t help either. The method every caller actually reaches builds the caller’s real UID and signing certificate into an object — then hands back a live connection to the AI service without ever looking at what it just built. Four lines below it, a sibling method does check the caller and rejects a mismatch. The check exists. It’s just never called.

Proving it, not just reading it

Reading a manifest tells you what’s possible. Only a real device tells you what actually happens. So we built the smallest possible test app — one permission, no other relationship to Google, unsigned — and pointed it at the real service.

live POC negative control:

BIND_SERVICE_CALLED[negctrl] intent=Intent { cmp=com.google.android.aicore/....AiCoreMultiUserService_DOES_NOT_EXIST } result=false
RESULT[negctrl]: bindService() returned false -- component did not resolve

A sanity check first: pointing at a component that doesn’t exist correctly fails, which tells us the harness itself is honest before we trust a positive result. Then the real one:

BIND_SERVICE_CALLED[test] intent=Intent { cmp=com.google.android.aicore/com.google.android.apps.aicore.service.multiuser.AiCoreMultiUserService } result=true
ON_SERVICE_CONNECTED[test] binder_null=false
RESULT[test]: BIND SUCCEEDED, live IBinder obtained
TRANSACT_CALLED[test] code=2 (get(), one-way) transacted=true
CALLBACK_SUCCESS[test]: get() delivered a live IAiCoreService binder, binder_null=false

A brand-new app, holding one permission it granted to itself, walked up to Google’s real on-device AI service and got handed a live connection — confirmed not just by our own harness, but by Android’s own bookkeeping of who’s connected to what:

$ adb shell dumpsys activity services com.google.android.aicore
ServiceRecord{... com.google.android.aicore/....AiCoreMultiUserService}
    recentCallingPackage=com.runzee.hooktest
    recentCallingUid=10319

Here’s the part a test app’s own logs can’t prove by themselves: an independent witness that watches the kernel, not our app’s word for it. Runzee’s Binder collector saw this pipeline light up twice, from two different angles — once ambiently, catching repeated traffic on the callback interface this same call delivers its result through, during ordinary background activity with no test running at all; and once deliberately, re-running our harness under live capture (a fresh install of the same test app — new install, new UID, same harness), which caught the get() call itself, direct:

interface: com.google.android.apps.aicore.aidl.IAiCoreServiceProvider
code:      2 (get(), one-way)
uid:       10329          -- our test app's real, kernel-verified UID
comm:      aicore-diag    -- our harness's own named worker thread
success:   true

Not just the app’s own say-so: both the UID and the thread name from this deliberate run line up exactly with our own app’s log for that same call — an independent, kernel-level witness. No UI capture of this session exists yet; what’s shown above is the raw data the collector recorded.

Before we ran anything, that same service already had live connections open from Gboard and Google’s own Private Compute Services — real production traffic, running through the exact door we just walked through.

We also checked for the one thing that could have quietly neutralized this anyway — a deeper OS-level restriction blocking outside callers from this specific service. There isn’t one, on every device and every layer we looked at.

The fix, and why this matters

The fix is two lines, and both matter: set protectionLevel=“signature” on the permission — the same level already correctly applied two doors over — which keeps outside apps out entirely; and wire up the caller check that already exists in the same file, unused, which is what stops a caller that has some legitimate reason to talk to AICore from reaching this one method it shouldn’t.

On-device AI is quickly becoming the thing other features quietly depend on — Gboard and Google’s own Private Compute Services already talk to this exact service today. Access to it isn’t just a data question — it’s a “can you make the model run for you” question, bypassing every consent screen and quota built to control exactly that. Keep tracing what that connection leads to, and a few steps later it reaches a real inference-session interface — the same kind of object an app would use to actually send the AI a prompt. We stopped there: reaching it was the point, proving we could use it wasn’t — though whether that interface enforces some check of its own, further in, is still an open question we don’t have an answer to. Either way, the door standing open at all is the finding.

Reported to Google through the Mobile Vulnerability Reward Program, following standard coordinated disclosure.

Found using Runzee, Oats Solutions’ dynamic-analysis platform for Android — built to show you what a device is actually doing at the Binder, JNI, filesystem, and network layers, independent of anything the app under test claims about itself.

Originally published on Medium.