Android AOSP Case Study: 53 Information Debt Points in System Services

I audited three files from Android AOSP system services. 36,003 lines of Java. Three files from a codebase that ships on 3 billion devices.

I found 53 instances of information debt — places where error handling silently degrades the security model. 22 of 53 have no effective error handling at all.

This is the real-world validation of the Error Principle and the unified vulnerability framework.

The Scope

Three core files from frameworks/base/services/core/java/com/android/server/ (android15-release):

File Lines Service
ActivityManagerService.java 21,173 Process lifecycle, activity launch, user management
NotificationManagerService.java 13,041 Notification posting, listener management, URI permissions
UriGrantsManagerService.java 1,763 URI permission grant/revoke, content URI enforcement

The Patterns Found

Five recurrent patterns emerged. Here they are with the raw numbers:

Pattern Count Avg D_e Max D_e Description
A — Broad exception handler 22 0.74 0.95 catch(Exception e) {} — catches everything, enforces nothing
B — Silent return on failure 13 0.79 0.85 return false on exception — same as “permission denied”
C — TOCTOU (permission after action) 2 0.68 0.70 Check happens after resource is already consumed
D — Decorative permission check 6 0.65 0.75 Result of check is never used for enforcement
E — Inconsistent state 10 0.75 0.95 Partial completion after error — state split between “enforced” and “not”

22 of 53 findings have D_e > 0.8 — Maximum Debt. These are not theoretical concerns. These are code paths where error handling actively degrades security.

The Worst Finding: UGMS BadParcelableException

This finding has D_e = 0.95 — the highest in the entire study. Here’s the code pattern:

// UriGrantsManagerService.java:751-769
try {
    final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class);
    if (uri != null) {
        // enforce URI permission check
    }
} catch (BadParcelableException e) {
    Slog.w(TAG, "Failed to unparcel an URI in EXTRA_STREAM, skipping"
            + " requireContentUriPermissionFromCaller: " + e);
}
// Intent proceeds WITHOUT URI permission verification

A malformed parcelable in an intent’s EXTRA_STREAM causes the permission check to be silently skipped. The intent is delivered to the target activity without URI permission verification. An attacker who can trigger BadParcelableException bypasses the foundational URI permission model of Android.

The handler scores:

  • α (detection) = 0.0 — catches BadParcelableException specifically (narrow), but does nothing with it
  • β (handling) = 0.0 — zero remediation
  • γ (audit) = 0.05 — logs a warning, no metric push
  • δ (state) = 0.0 — system continues in inconsistent state (permission check assumed, but not performed)

D_e = 1 – 0.05 = 0.95.

The Attackability Surface

53 findings is a lot. How many can an attacker actually trigger?

Controllability Count Examples
Directly triggerable ~5–8 BadParcelableException via crafted Intent, MediaStyle without MEDIA_CONTENT_CONTROL
Triggerable with preconditions ~10–15 CompanionDeviceService exception, URI grant failure
Not directly triggerable ~30–35 IPC failures, I/O errors, broadcast failures

Even the “not directly triggerable” category matters. In a chain, an attacker doesn’t need direct control over every trigger. They just need to control enough triggers to walk through the debt lattice.

The Debt Lattice in Action

Here’s how information debt stacks across services:

UGMS: BadParcelableException → permission check skipped (D_e=0.95)
  ↓ Intent forwarded without verification
NMS: URI grant SecurityException caught → grant not created (D_e=0.85)
  ↓ Notification posted referencing URIs without grants
UGMS: System URI grant returns -1 → caller proceeds (D_e=0.85)
  ↓ Grant state ambiguous
NMS: Companion SecurityException swallowed → privilege downgrade (D_e=0.85)
  ↓ Listener treated as unprivileged

Each hop transfers debt from one service to the next. The attacker controls exception triggers at each boundary. The system’s security model assumes each service enforces permissions independently — but the debt lattice shows that error handling failures cascade.

The Secure IPC Pattern

All 53 findings could be eliminated by three architectural rules:

Rule 1: No broad catches. Every catch block must specify exactly which exceptions it handles. catch(Exception e) is banned in system_server IPC paths.

Rule 2: Default to DENY. Any exception in a permission check must re-throw as SecurityException. Never return false or null on failure — that’s a silent bypass.

Rule 3: Audit by default. Every catch block must push a metric. Slog.wtf for unexpected exceptions, StatsLog for expected ones.

These three rules would eliminate the entire 53-finding catalog at the architectural level. No per-file fixes. No security reviews. Just a style guide for error handling.

The Bigger Picture

If the error handling failure rate observed in this study (~1.5 instances per 1,000 lines) holds across the entire Android framework (estimated 500K+ lines of Java in system services alone), the platform contains hundreds to thousands of similar information debt points.

And Android is not special. Every system where security-critical code crosses trust boundaries — iOS, Linux kernel, web browsers, cloud infrastructure — is susceptible to the same patterns. The methodology is language-independent. The patterns are identical.

Up next: Practical Guide: How to Measure Error Debt in Your Codebase →


Part of the Geometric Vulnerability Analysis series

← Previous: Grand Unification | Research Overview →

Leave a Comment