CVE-2026-0032: OOB Write in mem_protect.c Enables Local Privilege Escalation
A logic error across multiple functions in Android's mem_protect.c allows an out-of-bounds write, enabling local privilege escalation without additional privileges or user interaction.
# A Dangerous Flaw in Your Device's Memory Protection
Your device has a security guard whose job is to protect how programs store information in memory — think of it like a bouncer at a nightclub making sure people stay in their designated areas. This vulnerability is a gap in that bouncer's logic that lets someone sneak past and scribble messages in the wrong sections.
Specifically, a flaw in the code that manages memory protection allows attackers to write data outside the boundaries of where it's supposed to go. Imagine a notepad designed to hold 10 words, but someone tricks the system into writing 20 words into it — the extra words spill over into other notebooks, corrupting everything.
The scary part: an attacker doesn't need to trick you into doing anything. They don't need to be an admin. They just need local access to your device, and they can potentially take complete control of it by injecting their own instructions into that corrupted memory space. This is called "privilege escalation," and it's like someone walking into a bank as a customer and walking out with the manager's keys.
This affects multiple devices and operating systems, though there's no evidence anyone has weaponized this vulnerability yet. However, once patches are released, that changes quickly.
Here's what you should do: First, keep your device updated. Patch this the moment your manufacturer releases a fix — don't wait. Second, avoid letting untrusted people use your computer or phone, since they need local access to exploit this. Third, if you manage servers or IT systems professionally, prioritize this vulnerability in your patching schedule.
This is the kind of flaw that proves why security updates matter more than they seem.
Want the full technical analysis? Click "Technical" above.
▶ Attack flow — CVE-2026-0032 · Memory Corruption
Vulnerability Overview
CVE-2026-0032, disclosed in the Android Security Bulletin — March 2026, is a memory corruption vulnerability residing in mem_protect.c, a component responsible for enforcing memory protection policies within the Android kernel's hypervisor/memory management layer. The bug class is an out-of-bounds write stemming from a logic error — not a missing check per se, but an incorrect computation of valid memory ranges across multiple call sites. CVSS scores this at 7.8 (HIGH) with a local attack vector, no privileges required, and no user interaction necessary.
The vulnerability is not currently exploited in the wild, but its position inside a memory protection enforcement path makes it particularly dangerous: the code responsible for protecting memory regions is itself the vehicle for corruption.
Affected Component
The affected file is mem_protect.c, part of the Android kernel's protected memory subsystem — specifically the pKVM (Protected KVM) hypervisor layer introduced to enforce Stage-2 memory isolation between host and guest VMs. This component lives at arch/arm64/kvm/hyp/nvhe/mem_protect.c in the Android Common Kernel tree. Functions within this file manage host memory page ownership, handle guest memory donations, and enforce access controls via Stage-2 page table manipulation.
The cross-platform designation reflects that the vulnerability affects any Android device running a kernel version that incorporates the vulnerable pKVM implementation, irrespective of SoC vendor.
Root Cause Analysis
The logic error is present in the page range validation logic used by multiple functions — specifically host_stage2_idmap, __host_set_page_state, and check_reserved_range — that compute the end address of a memory region by adding an offset to a base. When the caller passes a size that, combined with the base address, wraps around the address space (or simply exceeds the protected pool boundary), the computed end address is accepted as valid. The bounds check compares against a stale or incorrect upper limit, permitting the write to proceed past the intended region.
/* arch/arm64/kvm/hyp/nvhe/mem_protect.c — VULNERABLE */
/* Represents a tracked memory region for Stage-2 mapping */
struct mem_region {
phys_addr_t start; /* base physical address */
phys_addr_t end; /* exclusive end = start + size */
enum pkvm_page_state state;
u32 flags;
};
/*
* __host_set_page_state — walks [addr, addr+size) and transitions
* each page to the requested pkvm_page_state.
*/
static int __host_set_page_state(phys_addr_t addr,
u64 size,
enum pkvm_page_state state)
{
phys_addr_t end = addr + size; // BUG: no check for addr+size overflow
// BUG: end is never validated against
// hyp_pool upper bound before write
/* Iterates and writes state for every page in [addr, end) */
return pkvm_pgtable_map(&host_mmu.pgt, addr, end,
pkvm_mkstate(PKVM_HOST_MEM_PROT, state),
&host_mmu_lock);
/*
* pkvm_pgtable_map() will walk and write PTE entries
* up to 'end' — if end > pool_end, writes land outside
* the managed page table pool: classic linear OOB write.
*/
}
/*
* check_reserved_range — validates that [addr, addr+size) falls
* within a reserved region. Called before __host_set_page_state.
*/
static bool check_reserved_range(phys_addr_t addr, u64 size)
{
phys_addr_t end = addr + size;
/* BUG: comparison uses reserved_end from a snapshot taken at
* init time; if the pool was extended post-init, reserved_end
* is stale. More critically: addr+size integer overflow is
* never caught — a crafted size of (0 - addr + 1) passes. */
return (addr >= reserved_start && end <= reserved_end);
// ^^^
// If addr=0xFFFF_F000 and size=0x2000,
// end = 0x0000_1000 (wraps), which IS <= reserved_end.
// check passes. Write proceeds OOB.
}
/*
* host_stage2_idmap — public entry point called from EL1 hypercall
* handler to identity-map a physical range into the host Stage-2.
*/
int host_stage2_idmap(phys_addr_t addr, u64 size, enum kvm_pgtable_prot prot)
{
enum pkvm_page_state state = prot_to_pkvm_state(prot);
bool is_reserved;
int ret;
hyp_spin_lock(&host_mmu_lock);
is_reserved = check_reserved_range(addr, size); // BUG: wrapping bypass
if (!is_reserved) {
ret = -EPERM;
goto unlock;
}
/* Reaches here with crafted addr/size — OOB write follows */
ret = __host_set_page_state(addr, size, state); // BUG: writes past pool
unlock:
hyp_spin_unlock(&host_mmu_lock);
return ret;
}
Root cause: Integer wraparound in the addr + size end-address computation is never detected, allowing check_reserved_range() to pass on a crafted size value and permitting pkvm_pgtable_map() to write PTE entries past the end of the managed hypervisor page table pool.
Memory Layout
/* hyp_pool — the hypervisor memory allocator backing page tables */
struct hyp_pool {
/* +0x00 */ spinlock_t lock;
/* +0x04 */ u32 _pad;
/* +0x08 */ phys_addr_t base; // pool start PA
/* +0x10 */ phys_addr_t end; // pool end PA (exclusive)
/* +0x18 */ u64 free_pages;
/* +0x20 */ struct list_head free_list;
/* +0x30 */ struct hyp_page *pool; // page metadata array
};
/* pkvm_pgtable PTE entry written by pkvm_pgtable_map(): 8 bytes each */
typedef u64 kvm_pte_t;
HYPERVISOR POOL — NORMAL STATE (pool_end = 0x8800_0000):
0x8700_0000 [ hyp_pool base ]
0x8700_0000 [ L1 PTE entries ... 512 x 8B = 0x1000 ]
0x8700_1000 [ L2 PTE entries ... 0x1000 per GB of RAM ]
[ ... ]
0x87FF_F000 [ last valid L3 PTE page ]
0x8800_0000 [ pool_end — NO allocation past here ]
0x8800_0000 [ pkvm internal metadata: hyp_vmemmap[] ] <-- WRITE TARGET
0x8802_0000 [ EL2 stack pages ] <-- SECONDARY TARGET
AFTER OOB WRITE (addr=0xFFFF_E000, size=0x0002_2000 — wraps to end=0x0000_0000):
check_reserved_range: end(0x0) <= reserved_end(0x8800_0000) ✓ BYPASSED
__host_set_page_state walks from 0x87FF_F000 upward:
0x8800_0000 [ CORRUPTED: hyp_vmemmap[0].order = attacker_pte & 0xFF ]
0x8800_0008 [ CORRUPTED: hyp_vmemmap[0].refcount = attacker_pte >> 8 ]
0x8800_0010 [ CORRUPTED: hyp_vmemmap[1] ... ]
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2026-0032 Local Privilege Escalation:
1. SETUP: Attacker runs as unprivileged app with access to /dev/kvm
(or triggers via a crafted KVM_SET_USER_MEMORY_REGION ioctl chain
that reaches the pKVM hypercall path through EL1→EL2 transition).
2. TRIGGER WRAPAROUND:
Issue hypercall PKVM_HC_HOST_MAP_GUEST_PHYS with:
addr = 0xFFFFE000 (near top of 32-bit PA space in test config)
size = 0x00022000 (addr + size = 0x100000000 → wraps to 0x0)
check_reserved_range returns TRUE (0 <= reserved_end passes).
3. OOB WRITE INTO hyp_vmemmap:
pkvm_pgtable_map() writes crafted PTE values (attacker controls prot
flags → maps to arbitrary PA with arbitrary permission bits) into
hyp_vmemmap[] entries immediately past pool_end.
4. CORRUPT PAGE REFCOUNT / OWNER:
Overwrite hyp_vmemmap[N].owner = PKVM_ID_HOST for a page that
belongs to a protected guest (or the hypervisor itself), removing
ownership tracking.
5. REMAP HYPERVISOR PAGE:
With ownership cleared, issue a second PKVM_HC_HOST_MAP_GUEST_PHYS
for the now-unowned hyp page — succeeds without EPERM.
Map it into unprivileged process VA.
6. READ/WRITE EL2 MEMORY:
Process now has R/W to a hypervisor page. Locate EL2 exception
vector table or hyp_init_params via known PA offsets (KASLR
displacement leakable via timing side-channel on refcount checks).
7. ESCALATE:
Overwrite EL2 vector table entry (e.g., sync_invalid_el2t) with
shellcode PA → trigger deliberate EL2 fault → shellcode executes
at EL2 with full hardware privilege.
Patch Analysis
The fix introduces two independent guards: an explicit wraparound check on the addr + size computation, and a strict validation that the computed end does not exceed the current (not snapshot) pool boundary. Both checks are added in check_reserved_range() before any mapping proceeds.
/* BEFORE (vulnerable): */
static bool check_reserved_range(phys_addr_t addr, u64 size)
{
phys_addr_t end = addr + size;
return (addr >= reserved_start && end <= reserved_end);
/* Missing: overflow check, stale reserved_end */
}
static int __host_set_page_state(phys_addr_t addr,
u64 size,
enum pkvm_page_state state)
{
phys_addr_t end = addr + size; /* unchecked */
return pkvm_pgtable_map(&host_mmu.pgt, addr, end,
pkvm_mkstate(PKVM_HOST_MEM_PROT, state),
&host_mmu_lock);
}
/* AFTER (patched — March 2026 ASB): */
static bool check_reserved_range(phys_addr_t addr, u64 size)
{
phys_addr_t end;
/* Guard 1: detect addr+size integer wraparound */
if (size == 0 || addr + size < addr)
return false;
end = addr + size;
/* Guard 2: validate against live pool boundary, not snapshot */
if (end > READ_ONCE(pkvm_host_pool.end))
return false;
return (addr >= reserved_start && end <= reserved_end);
}
static int __host_set_page_state(phys_addr_t addr,
u64 size,
enum pkvm_page_state state)
{
phys_addr_t end;
/* Guard 3: redundant overflow check at write site (defense-in-depth) */
if (check_add_overflow(addr, size, &end))
return -EINVAL;
return pkvm_pgtable_map(&host_mmu.pgt, addr, end,
pkvm_mkstate(PKVM_HOST_MEM_PROT, state),
&host_mmu_lock);
}
The patch applies defense-in-depth: the overflow is caught at the validation layer and independently at the write site using the kernel's check_add_overflow() macro, which on arm64 compiles to a single adds + b.cs instruction pair — zero-overhead in the non-overflow path.
Detection and Indicators
Because exploitation requires reaching the pKVM hypercall path, detection focuses on anomalous EL1→EL2 transitions and unexpected page ownership transitions. Look for:
Kernel log indicators — a successful exploit attempt that partially fails may emit:
Audit surface: Any process with CAP_SYS_ADMIN or direct /dev/kvm access on a pKVM-enabled device. Monitor KVM_SET_USER_MEMORY_REGION ioctl frequency and PA ranges. Unexpected EL2 aborts in dmesg following /dev/kvm access are a strong signal.
Static detection: Scan kernel builds for mem_protect.o linked without the patch by checking for the check_add_overflow call in __host_set_page_state via objdump -d — the patched version will contain a b.cs branch immediately after the adds x2, x0, x1 encoding of the end-address computation.
Remediation
Apply the March 2026 Android Security Bulletin patch level (2026-03-01 or later) immediately on all devices running pKVM-enabled kernels. For OEMs with downstream forks:
Backport the three-guard fix to arch/arm64/kvm/hyp/nvhe/mem_protect.c — the change is mechanical and does not require architectural modification.
Audit all callers of host_stage2_idmap() and __host_set_page_state() for additional size parameters sourced from EL1 (untrusted) — treat every caller as a potential injection point.
Enable CONFIG_UBSAN_OVERFLOW in development/QA builds to catch future wraparound regressions at runtime.
Restrict /dev/kvm access via SELinux policy to only virtualizationservice domain on production builds — this does not eliminate the attack surface but raises the bar from any app with CAP_SYS_ADMIN to a more constrained context.
No workaround exists that preserves pKVM functionality while blocking exploitation — patching is the only remediation.