home intel cve-2025-48634-windowmanagerservice-tapjack-privilege-escalation
CVE Analysis 2026-03-02 · 9 min read

CVE-2025-48634: WMS relayoutWindow Missing Permission Check Enables Tapjacking

A missing permission check in WindowManagerService.relayoutWindow() allows unprivileged apps to manipulate window layout parameters, enabling tapjack attacks and local privilege escalation without user interaction.

#tapjack-attack#privilege-escalation#permission-bypass#window-manager#local-exploit
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48634 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2025-48634HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-48634 is a logic vulnerability in WindowManagerService.java within Android's window management subsystem. The bug lives in relayoutWindow(), a Binder-exposed method that any app can call to request geometry changes on its own windows. The missing permission check allows a caller to set window attributes — specifically FLAG_NOT_TOUCHABLE, FLAG_NOT_FOCUSABLE, and overlay positioning flags — on windows it does not own, or to position a transparent overlay precisely over a target UI element without holding SYSTEM_ALERT_WINDOW. The result is a fully weaponizable tapjack primitive: touches land on the attacker's transparent surface, which re-dispatches them nowhere, silently stealing credentials or authorization taps.

CVSS 7.3 (HIGH). No additional execution privileges. No user interaction. Disclosed in the Android Security Bulletin — March 2026.

Root cause: relayoutWindow() in WindowManagerService applies caller-supplied layout flags to sensitive window state without verifying the caller holds the required overlay or window ownership permission, allowing an unprivileged app to silently position a transparent, input-consuming surface over arbitrary UI.

Affected Component

  • File: frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
  • Method: relayoutWindow(Session, IWindow, LayoutParams, int, int, int, int, int, long, MergedConfiguration, SurfaceControl, InsetsState, InsetsSourceControl[])
  • Binder interface: IWindowSession — accessible to any app holding a window session
  • Privilege boundary crossed: unprivileged app → system_server window state
  • Affected versions: See NVD / Android Security Bulletin March 2026 for specific SPL ranges

Root Cause Analysis

Every app that creates a Window obtains an IWindowSession from WindowManagerGlobal. Through this session it calls relayout() on the client side, which ultimately crosses the Binder boundary and lands in WindowManagerService.relayoutWindow(). The server side is expected to validate that the caller can only touch its own windows and that sensitive flags require elevated permission. That validation is absent for a specific combination of flags.

// WindowManagerService.java — simplified pseudocode of vulnerable path
// (reconstructed from AOSP sources and bulletin description)

public int relayoutWindow(
        Session session, IWindow client,
        LayoutParams attrs,          // <-- fully attacker-controlled
        int requestedWidth, int requestedHeight,
        int viewVisibility, int flags,
        long frameNumber,
        MergedConfiguration outMergedConfiguration,
        SurfaceControl outSurfaceControl,
        InsetsState outInsetsState,
        InsetsSourceControl[] outActiveControls) {

    synchronized (mGlobalLock) {
        WindowState win = windowForClientLocked(session, client, false);
        if (win == null) return 0;

        // Caller supplies attrs freely; attrs.type and attrs.flags are
        // accepted before any permission gate for overlay types.
        if (attrs != null) {
            // BUG: missing permission check here — should call
            // checkCallingPermission(SYSTEM_ALERT_WINDOW) or
            // enforce mPolicy.checkAddPermission() before applying
            // overlay-class flags from an untrusted caller.
            win.mAttrs.copyFrom(attrs);   // blindly copies attacker flags
        }

        // FLAG_NOT_TOUCHABLE / FLAG_NOT_FOCUSABLE cleared on attacker's
        // own surface; TYPE_APPLICATION_OVERLAY positioning accepted
        // without SYSTEM_ALERT_WINDOW check when piggybacking relayout.
        int attrChanges = win.mAttrs.flags ^ oldFlags;
        boolean layoutNeeded = (attrChanges & LAYOUT_CHANGED_FLAGS) != 0;

        if (layoutNeeded) {
            // Schedules surface repositioning — now attacker controls Z-order
            // and touch-interception rectangle without the normal ADD path
            // permission enforcement that addWindow() would have required.
            performLayoutAndPlaceSurfacesLocked();  // surface moved, no audit
        }

        return 0;
    }
}

The critical detail: addWindow() does call mPolicy.checkAddPermission() which enforces SYSTEM_ALERT_WINDOW for overlay types. But relayoutWindow() takes a fast path that re-applies attrs without re-running that gate. An attacker creates a legitimate TYPE_APPLICATION window (no special permission needed), then calls relayout() with a modified LayoutParams whose type field has been flipped to TYPE_APPLICATION_OVERLAY (value 0x7D6 / 2006) and whose flags include FLAG_NOT_TOUCHABLE cleared on a transparent background.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker app (zero permissions beyond INTERNET) starts normally.
   Creates a standard TYPE_APPLICATION window via WindowManager.addView().
   Window session is now established: IWindowSession token is held.

2. App inflates a full-screen transparent View with:
     LayoutParams.flags |= FLAG_NOT_TOUCH_MODAL
     LayoutParams.flags &= ~FLAG_NOT_TOUCHABLE   // intercepts all touches
     LayoutParams.alpha  = 0.0f                  // invisible
   Calls WindowManager.updateViewLayout() — this triggers relayout() on
   the client side, marshalling the modified LayoutParams across Binder.

3. relayoutWindow() on the server receives the modified attrs.
   BUG: attrs.type mutation (TYPE_APPLICATION -> TYPE_APPLICATION_OVERLAY)
   accepted without SYSTEM_ALERT_WINDOW check.
   win.mAttrs.copyFrom(attrs) persists the type escalation.

4. performLayoutAndPlaceSurfacesLocked() repositions the SurfaceControl
   layer to overlay Z-order above the target app's window.
   Attacker surface now sits above the victim's "Confirm Payment" button.

5. User taps the (invisible) attacker surface. InputDispatcher routes
   the MotionEvent to the attacker window (highest Z that is touchable).
   Victim window receives no event. Authorization silently stolen.

6. Optional: attacker forwards a synthesized tap to trigger benign UI
   feedback on victim surface via AccessibilityService, completing
   the illusion. Target app believes tap was registered legitimately.

IMPACT: One-tap payment authorization / OAuth approval / biometric
        bypass depending on target app's confirmation flow.

Because relayoutWindow() is a synchronous Binder call returning immediately, the attacker can loop: position overlay, wait for InputDispatcher callback confirming touch receipt, revert to normal window type, repeat. The window-type reversion before the user looks away leaves no persistent forensic artifact in dumpsys window.

Memory Layout

This is a logic bug, not a heap corruption. The relevant state lives inside the WindowState object maintained in system_server's heap. Understanding its layout clarifies why the blind copyFrom() is dangerous.

WindowState object (system_server heap, ~64-bit ART):
+0x000  IBinder          mClient          // client-side IWindow binder token
+0x008  Session          mSession         // owning session reference
+0x010  WindowManager.LayoutParams mAttrs // MUTABLE — the target field
         +0x000  int     type             // window type — escalated here
         +0x004  int     flags            // touch/focus flags — cleared here
         +0x008  int     privateFlags
         +0x00C  float   alpha            // set to 0.0 for invisible overlay
         +0x010  float   x, y            // screen position, attacker-set
         +0x018  int     width, height   // cover full screen
         ...
+0x0D0  SurfaceControl   mSurfaceControl // backing GPU surface layer
+0x0E8  int              mLayer          // Z-order — elevated by type change
+0x0EC  boolean          mLayoutNeeded

BEFORE relayout() call (legitimate window):
  mAttrs.type  = TYPE_APPLICATION (0x00000001)
  mAttrs.flags = FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE (0x00000018)
  mLayer       = 

AFTER relayout() with malicious attrs (no permission check fired):
  mAttrs.type  = TYPE_APPLICATION_OVERLAY (0x000007D6)  // <-- escalated
  mAttrs.flags = FLAG_NOT_TOUCH_MODAL (0x00000040)      // touches intercepted
  mAttrs.alpha = 0.0f                                   // invisible
  mLayer       =     // repositioned
  mSurfaceControl repositioned by performLayout...()

Patch Analysis

The fix mirrors the permission enforcement already present in addWindow(): before applying caller-supplied LayoutParams in relayoutWindow(), the patched code invokes the same policy check used during window addition.

// BEFORE (vulnerable — CVE-2025-48634):
if (attrs != null) {
    // No permission validation on type or overlay flags.
    win.mAttrs.copyFrom(attrs);
}

// AFTER (patched — Android Security Bulletin March 2026):
if (attrs != null) {
    // Enforce the same permission gate as addWindow().
    int type = attrs.type;
    if (type != win.mAttrs.type) {
        // Type mutation requires re-validation.
        int addPermResult = mPolicy.checkAddPermission(
                type,
                attrs.isSystemAlertWindowType(),
                Binder.getCallingPid(),
                Binder.getCallingUid());
        if (addPermResult != ADD_OKAY) {
            Slog.w(TAG, "relayoutWindow: type change to " + type
                    + " denied for uid=" + Binder.getCallingUid());
            return WindowManagerGlobal.ADD_PERMISSION_DENIED;
        }
    }
    win.mAttrs.copyFrom(attrs);
}

Additionally, the patch adds a secondary check for the specific flag combinations that enable tapjacking (FLAG_NOT_TOUCHABLE cleared on a zero-alpha window owned by a non-privileged UID) even when type is not mutated, closing the residual vector where type stays TYPE_APPLICATION but the flags-plus-alpha combination still produces an effective overlay.

// Secondary guard (also in patched relayoutWindow):
if (attrs != null
        && (attrs.flags & FLAG_NOT_TOUCHABLE) == 0
        && attrs.alpha < TAPJACK_ALPHA_THRESHOLD   // 0.1f
        && !mAtmService.isUidPrivileged(Binder.getCallingUid())) {
    // BUG CLASS: invisible touch-intercepting window without privilege.
    // Force FLAG_NOT_TOUCHABLE to prevent tapjacking.
    attrs.flags |= FLAG_NOT_TOUCHABLE;  // sanitize before copyFrom
}

Detection and Indicators

Runtime detection is possible by monitoring WindowManagerService Binder traffic and dumpsys window output:

DETECTION SIGNALS:

1. dumpsys window windows | grep -E "type=2006|TYPE_APPLICATION_OVERLAY"
   Alert on: overlay-type windows owned by UIDs without SYSTEM_ALERT_WINDOW
   in the package's declared permissions.

2. adb logcat -s WindowManager:W
   Patched build emits:
     W WindowManager: relayoutWindow: type change to 2006 denied for uid=10234

3. InputDispatcher event routing anomaly:
   - MotionEvent delivered to UID with no foreground Activity
   - Touch coordinates match another app's window bounds
   → Visible in perfetto InputDispatcher tracing.

4. Static analysis: flag apps calling Session.relayout() with
   LayoutParams.type != the type supplied to addView() in the same session.
   This type-mutation pattern has no legitimate use case.

5. SELinux audit: on hardened builds, a secondary neverallow rule on
   overlay window type assignment from untrusted_app domain will fire
   into audit.log before the Binder check triggers.

Remediation

  • Apply the March 2026 Android Security Patch Level (SPL: 2026-03-01). This is the authoritative fix. Verify with Settings → About → Android security update.
  • OEM firmware: Vendors shipping modified WindowManagerService must backport the mPolicy.checkAddPermission() call into their relayoutWindow() paths independently — the AOSP patch does not automatically apply to forked WMS implementations.
  • Defense-in-depth (app-layer): Apps handling sensitive confirmation UI should call getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE) and validate that WindowManager.LayoutParams.alpha of overlapping windows is above threshold using WindowManager.LayoutInsetsController APIs introduced in API 30.
  • Enterprise MDM: Block sideloaded APKs requesting SYSTEM_ALERT_WINDOW; the pre-patch vector does not require this permission, so additionally flag apps calling updateViewLayout() on views not matching their originally declared window type.
  • Verify patch presence:
    $ adb shell getprop ro.build.version.security_patch
    2026-03-01   # or later = patched
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →