Mikey Strauss

A Zero-Day in Google’s Confidential Computing: Your Phone Can Be Fooled Into Trusting a Fake…

August 18, 2026 googlecybersecurityzero-daycryptographyandroid

Security research · Oats Solutions

A Zero-Day in Google’s Confidential Computing: Your Phone Can Be Fooled Into Trusting a Fake Enclave

A forged proof passes Google’s enclave attestation — the check binding it to the real program is silently skipped.

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

TL;DR: Before your phone hands any of your data to Google’s confidential cloud AI system, it’s supposed to get cryptographic, hardware-backed proof that the program receiving it is the exact, publicly-audited one it claims to be — not something an attacker swapped in. We found a coding bug that quietly skips the one check that actually confirms that claim. An attacker on your network, or whoever operates a malicious version of that cloud server, can just claim to be the trusted program — and get waved through. This isn’t a theoretical reading of a diff: we ran the real, unfixed code, handed it forged proof, and it accepted it. The same stale, unfixed code still ships today in two separate Google apps, weeks after Google itself quietly fixed the bug upstream.

We found this reading Google’s own open-source verification code, then built a real, working proof against the exact vulnerable version — not just a theoretical read of a diff. We also used Runzee, Oats Solutions’ dynamic-analysis platform for Android, to confirm this code is live, actively-running infrastructure on real devices, not a dead corner of Google Play services. More on how below; first, the bug itself.

→ See what Runzee can do

The promise: prove it, don’t just claim it

Google’s Confidential Federated Computations system lets your phone send some of your on-device data to a cloud server for processing — but only if that server can prove, cryptographically, that it’s running inside a sealed, tamper-proof hardware enclave, and that it’s running the exact program Google has publicly audited. Not “trust us.” Proof, signed by the hardware itself.

That signed proof is called attestation evidence, and it’s supposed to cover two separate facts, both backed by the same hardware signature:

1. "This really is a sealed, tamper-proof enclave" (real hardware, not emulated)
2. "This exact program — digest 9f2a...— is what's running inside it"

A piece of code on your phone — Google’s own open-source Project Oak verifier — is supposed to check both facts against the hardware’s signature before your phone trusts the connection with your data:

The bug: the most important fact never gets checked

Rust has a built-in way to pair up two lists item by item. If one list is longer than the other, it doesn’t complain — it just quietly stops at the shorter length and throws away whatever’s left over in the longer one. No error, no warning.

The verifier uses exactly this to pair up the enclave’s certificate chain against its event log. For the evidence shape Google’s own restricted-kernel enclaves actually produce, there’s 1 certificate layer but 2 event-log entries. Pairing a list of 1 against a list of 2 leaves the second entry — the one carrying fact #2, the actual program’s digest — completely unpaired. Silently dropped. Never checked against the hardware signature at all:

That second fact — which program is actually running — is the entire point of attestation. Skipping it doesn’t weaken the check. It removes it. Google’s own upstream fix commits close exactly this: one adds a proper length check so mismatched evidence shapes get rejected instead of silently truncated; the other adds the missing cryptographic check on that second event. Upstream’s own regression test names the exact scenario this restores: “A MITM replaces it with a forged ApplicationLayerData containing an attacker-chosen digest.” That’s Google’s own engineers describing the property being restored — not our inference from a diff’s shape.

None of this rests on our reading, though — here’s the actual code, and the public fixes. The whole bug is one zip() in Google’s own open-source verifier, in the validate_that_event_log_is_captured_in_dice_layers() routine (oak_attestation_verification/src/verifier.rs):

dice_layers.iter().zip(event_log.encoded_events.iter()).try_for_each(...)
// 1 DICE layer, 2 event-log entries. zip() stops at the shorter sequence,
// so encoded_events[1] -- ApplicationLayerData, the actual program's digest --
// is left unpaired, and never checked against the signed chain.
// `zip` below stops at the shorter of the two sequences, so without this check
ensure!(
    dice_layers.len() == event_log.encoded_events.len(),
    "event log has {} events but the DICE chain has {} layers",
    event_log.encoded_events.len(),
    dice_layers.len()
);

Proving it, not just reading it

Reading a diff tells you what changed. It doesn’t tell you whether the old code actually misbehaves when you run it. So we built the real upstream verifier from source, at the exact commit both vulnerable Google binaries below are compiled from, and handed it forged evidence — the same technique Google’s own regression test uses, just pointed at the version before the fix:

test forged_rk_application_event_is_silently_accepted_prefix_bug_poc ... ok
test result: ok. 147 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

verify() returned success on forged evidence. As a negative control, we ran the identical forged evidence against the fixed, post-patch build — correctly rejected. Same technique, same forged input, only the code version changed. That rules out anything unusual about our test harness: the difference is entirely the stale code, not the method.

We also checked whether the caller’s own plain-value comparison — the fallback the pre-fix code relies on instead of a real signature check — might accidentally still catch a forgery. It doesn’t: for any real, published program, its expected digest is public information. An attacker doesn’t need to break any cryptography. They just need to say the already-known-good number.

This isn’t a lab curiosity — it’s live in production, twice

Two separate Google binaries each compile in their own private copy of this same vulnerable verifier — and neither copy has caught up to Google’s own fix:

The BrellaDynamite copy isn’t just present, either — it’s wired into a live dispatch table and scheduled to run by real background jobs inside three separate real consumer apps: Gboard, Contacts, and Android System Intelligence.

What Runzee showed us

We wanted to watch this code actually get exercised on a real device, not just confirm it was present. Attaching kernel-level eBPF probes to the real host processes, across an extended observation window on two physical devices, we caught the actual background job that carries this verifier firing under its real, documented trigger conditions — charging, network connectivity, and device idle, all at once. This wasn’t a quick, on-demand capture — forcing the job to fire early didn’t reliably work (Android’s own job re-scheduling raced our attempts), so we started a long passive capture and let it run for hours until real background conditions triggered it naturally:

from runzee import RunzeeClient
client = RunzeeClient(base_url="http://localhost:9999")
session = client.sessions.create(
    package="com.google.android.as.oss",
    collectors=["libs"],
    spawn=False,   # attach to the already-running system process
)
# Left running for hours — the job only fires under real
# charging + network + idle conditions, not on demand.
job fired: InAppJobService, host=com.google.android.as.oss
job fired: InAppJobService, host=com.google.android.inputmethod.latin  (Gboard)
job fired: InAppJobService, host=com.google.android.contacts
worker thread observed: real class-resolution work inside the host process

That confirms this is live, scheduled, actively-executing infrastructure — not a dormant copy nobody runs. What we haven’t yet captured directly is the verifier’s own function completing a run on-device. Runzee’s own observation told us exactly why, instead of leaving it a shrug: every firing we captured was cancelled 13 to 131 milliseconds after the host process unfroze — before the vulnerable library could even finish loading, let alone reach the verification step itself. The cancellation decision is actually made while the process is still frozen; it only becomes visible the instant it unfreezes, because Android structurally can’t deliver that callback while a process is frozen. This is standard, unmodified JobScheduler behavior, not something specific to our test devices — this job type requires all three trigger conditions to hold simultaneously, and stops the instant any one lapses.

Whether a typical phone, left charging overnight without a research harness attached, would clear that same narrow window more often and actually reach a completed verify() call is an open question — continuous instrumentation traffic on a lab device plausibly disturbs its idle state more than ordinary use would. That’s a plausible, honest explanation for why we haven’t caught a completed run yet. It isn’t evidence the code never runs.

Is “a network attacker” actually realistic here?

The threat model above names two attackers: whoever operates a compromised cloud host, and separately, a plain network attacker sitting between your phone and Google’s servers. That second one only holds up if the connection carrying this evidence isn’t pinned to a specific certificate — so we checked, instead of assuming it either way.

It isn’t pinned. We decompiled BrellaDynamite’s full app code and manifest and found no certificate pinning anywhere in the network client that carries this exchange — no pinning API calls, no custom trust manager, no network security config requiring a specific certificate. The connection uses ordinary TLS, validated only against the phone’s standard trusted-certificate list. That’s the same trust model your browser uses for any ordinary website — which means the standard tools a network-positioned attacker already has (a rogue certificate authority, a compromised corporate proxy, a hostile network) apply here too. This isn’t a narrower “only a rogue cloud operator” bug. The network-attacker framing holds up.

What’s next

The one thing we haven’t directly captured is the verifier’s own function completing a live run on an ordinary, non-instrumented device. A future pass would target the exported native symbol directly, rather than watching the job scheduler around it:

@probe.user(lib="libBrella.so", method="OakRustAttestationVerifier::Verify")
def verify_call(args, save):
    save(result=args.ret)

This is a planned next step, not something we’ve run yet — everything above it in this post is real, already-observed evidence.

The fix

Update the vendored verifier snapshot to include both upstream fix commits — in both places it’s separately compiled in. Both fixes are already public, reviewed, and merged in Google’s own repository; this should be a routine dependency update, not novel engineering. Fixing only one of the two copies would leave the other silently vulnerable.

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.