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.
Imagine your phone's screen like a kitchen with multiple windows (the glass kind). Normally, there's a strict bouncer controlling which windows can be opened and who can rearrange them. This vulnerability is like that bouncer falling asleep on the job.
A sneaky app can now slip past the security checks and rearrange those windows without permission. More specifically, it can cover up legitimate apps with fake ones—think of a fake login screen placed on top of your banking app. When you try to enter your password, you're actually typing into the attacker's fake window instead.
This is called a "tapjack" attack, and it's particularly dangerous because it requires no special permissions and doesn't need you to do anything unusual. A malicious app could simply sit quietly on your phone, waiting for you to open your banking app, then instantly cover it with a convincing fake.
The vulnerability affects phones and devices using affected Android versions, particularly older or less-updated devices that haven't received the security patch yet.
What should you actually do? First, keep your phone updated—install security patches as soon as they arrive, usually the first Monday of the month. Second, only download apps from official app stores like Google Play, which at least screens for obvious malicious apps. Third, be suspicious of permission requests: if a calculator app asks for camera access, that's a red flag.
Right now, security researchers haven't confirmed this vulnerability is being actively exploited in the wild, but attackers are certainly taking note.
Want the full technical analysis? Click "Technical" above.
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.
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