home intel cve-2026-0032-mem-protect-oob-write-lpe
CVE Analysis 2026-03-02 · 9 min read

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.

#out-of-bounds-write#memory-corruption#privilege-escalation#logic-error#local-exploitation
Technical mode — for security professionals
▶ Attack flow — CVE-2026-0032 · Memory Corruption
ATTACKERRemote / unauthMEMORY CORRUPTIOCVE-2026-0032Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

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:


[  412.883901] kvm [1234]: pkvm: illegal host stage2 mapping request
               addr=0xffffe000 size=0x22000 end=0x00000000
[  412.883945] kvm [1234]: hyp_vmemmap corruption detected: page 0x8800_0000
               expected owner=PKVM_ID_GUEST got owner=PKVM_ID_HOST
[  412.884012] Kernel panic - not syncing: hyp integrity check failed

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.

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 →