home intel cve-2026-0028-pkvm-integer-overflow-oob-write
CVE Analysis 2026-03-02 · 9 min read

CVE-2026-0028: pKVM Integer Overflow Enables Hypervisor OOB Write

An integer overflow in __pkvm_host_share_guest() allows an unprivileged local attacker to corrupt hypervisor-managed memory, enabling privilege escalation to EL2 without any user interaction.

#integer-overflow#out-of-bounds-write#memory-corruption#privilege-escalation#pkvm-virtualization
Technical mode — for security professionals
▶ Attack flow — CVE-2026-0028 · Memory Corruption
ATTACKERRemote / unauthMEMORY CORRUPTIOCVE-2026-0028Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-0028 is a memory corruption vulnerability residing in pKVM's (Protected KVM) host-guest memory sharing subsystem, specifically within __pkvm_host_share_guest() in arch/arm64/kvm/hyp/nvhe/mem_protect.c. An integer overflow during page range computation produces a wrapped size value that is subsequently used as a loop bound or allocation size, triggering an out-of-bounds write into hypervisor-controlled memory.

pKVM runs at EL2 and is architecturally responsible for enforcing memory isolation between the host Linux kernel and guest VMs. A successful write primitive into EL2-managed structures — page tables, pkvm_hyp_vm descriptors, or ownership tracking arrays — trivially translates to full hypervisor compromise. CVSS 8.4 (HIGH) reflects local exploitation with no privileges and no user interaction required.

Disclosed in the Android Security Bulletin for March 2026, this vulnerability affects devices shipping pKVM-enabled kernels (Android common kernel 5.15+, 6.1+).

Affected Component

  • File: arch/arm64/kvm/hyp/nvhe/mem_protect.c
  • Function: __pkvm_host_share_guest()
  • Subsystem: Protected KVM (pKVM), EL2 hypervisor memory protection layer
  • Privilege boundary crossed: EL0/EL1 → EL2
  • Kernel versions: Android common kernel 5.15, 6.1 (pKVM-enabled builds)
  • CVE: CVE-2026-0028 | CVSS 8.4 HIGH

Root Cause Analysis

__pkvm_host_share_guest() mediates the transition of host-owned pages into a guest's stage-2 page table. The host supplies a guest physical address (gpa), a host physical address (hpa), and a page count (nr_pages). The function computes the byte extent of the region and iterates over it. The overflow occurs when nr_pages is attacker-influenced and the byte-range multiplication wraps a u32.

/*
 * arch/arm64/kvm/hyp/nvhe/mem_protect.c
 * Simplified pseudocode reconstructed from AOSP common kernel + pKVM sources.
 */

static int __pkvm_host_share_guest(u64 host_kvm,
                                   u64 gpa,
                                   u64 hpa,
                                   u32 nr_pages,   /* attacker-controlled via KVM_HC hypercall */
                                   enum kvm_pgtable_prot prot)
{
    struct pkvm_hyp_vm  *vm;
    struct kvm_pgtable  *pgt;
    u32  size;
    u64  addr;
    int  ret = 0;

    vm  = get_pkvm_hyp_vm(host_kvm);
    pgt = &vm->pgt;

    /* BUG: u32 multiplication — nr_pages * PAGE_SIZE wraps when nr_pages > 0xFFFFF */
    size = nr_pages * PAGE_SIZE;   // e.g. 0x100001 * 0x1000 = 0x1000 (wrapped!)

    addr = gpa;
    while (addr < gpa + size) {   // loop bound derived from wrapped value
        struct hyp_page *hyp_p = hyp_phys_to_page(hpa + (addr - gpa));

        /* ownership check uses the *original* nr_pages, not the wrapped size;
         * the loop may terminate early or run indefinitely depending on wrap  */
        ret = host_stage2_set_owner_locked(hpa + (addr - gpa),
                                           PAGE_SIZE,
                                           pkvm_hyp_vm_table_lock(vm));
        if (ret)
            goto unlock;

        /* BUG: pgtable mapping uses wrapped size as extent */
        ret = kvm_pgtable_stage2_map(pgt, addr, PAGE_SIZE,
                                     hpa + (addr - gpa),
                                     prot, NULL);
        if (ret)
            goto unlock;

        addr += PAGE_SIZE;
    }

unlock:
    put_pkvm_hyp_vm(vm);
    return ret;
}

The critical expression is:

u32 size = nr_pages * PAGE_SIZE;   // PAGE_SIZE = 0x1000
// nr_pages = 0x00100001  =>  0x00100001 * 0x1000 = 0x100001000
// truncated to u32       =>  0x00001000  (4 KB — just ONE page)
// but caller later references nr_pages = 0x100001 pages of hpa
// => host_stage2_set_owner_locked is called for 0x100001 pages
//    while pgtable mapping loop only covers 1 page
// net effect: ownership transferred for 0x100001 pages, only 1 mapped
// => 0x100000 pages transition to OWNED_GUEST with NO stage-2 mapping
// => host retains writeable aliases; subsequent write = OOB from pKVM's view
Root cause: nr_pages * PAGE_SIZE is computed into a u32, truncating the product for large nr_pages values and producing a loop bound that covers far fewer pages than ownership tracking consumes, leaving host-writable aliases into guest-owned memory.

Memory Layout

pKVM tracks page ownership through a flat hyp_page array indexed by PFN. Each entry is small — understanding the layout is critical to understanding what gets corrupted.

struct hyp_page {
    /* +0x00 */ u16  refcount;    // pKVM reference count
    /* +0x02 */ u8   order;       // buddy order
    /* +0x03 */ u8   owner;       // PKVM_ID_HOST / PKVM_ID_GUEST / PKVM_ID_HYP
    /* +0x04 */ u32  __pad;
    /* total size: 0x08 bytes per page descriptor */
};

struct pkvm_hyp_vm {
    /* +0x000 */ struct kvm_hyp_vm  kvm;          // 0x1a0 bytes
    /* +0x1a0 */ struct kvm_pgtable pgt;           // stage-2 page table root
    /* +0x1c0 */ u32                nr_vcpus;
    /* +0x1c4 */ u32                __pad;
    /* +0x1c8 */ struct pkvm_hyp_vcpu *vcpus[];    // flexible array
};
EL2 MEMORY STATE — BEFORE OVERFLOW (legitimate share of 1 page):

PFN TABLE (hyp_page array @ EL2 VA 0xFFFFFFC010000000):
  [pfn 0x80000] { refcount=1, order=0, owner=PKVM_ID_HOST }
  [pfn 0x80001] { refcount=1, order=0, owner=PKVM_ID_HOST }
  ...
  [pfn 0x8FFFF] { refcount=1, order=0, owner=PKVM_ID_HOST }

STAGE-2 PAGE TABLE: empty / no guest mappings yet

───────────────────────────────────────────────────────────────
EL2 MEMORY STATE — AFTER OVERFLOW (nr_pages=0x100001 wrapped):

PFN TABLE:
  [pfn 0x80000..0x17FFFF] { owner=PKVM_ID_GUEST }  ← 0x100001 entries marked GUEST
                                                       via host_stage2_set_owner_locked

STAGE-2 PAGE TABLE: maps only pfn 0x80000 (1 page, size=0x1000 from wrapped u32)

HOST STAGE-1 MAPPINGS: STILL PRESENT for pfn 0x80001..0x17FFFF
  ← host kernel retains R/W access to pages now "owned" by guest
  ← pKVM ownership invariant violated: host can write into guest memory
  ← if pfn range overlaps pkvm_hyp_vm structs: EL2 data corruption

Exploitation Mechanics

The vulnerability is reachable from EL1 (host kernel) via the pKVM hypercall interface. An attacker with ability to call KVM_CREATE_VM and issue memory-sharing hypercalls (reachable from an unprivileged process through /dev/kvm on Android, which exposes KVM to apps via the virtualization APIs) can trigger the overflow.

EXPLOIT CHAIN — CVE-2026-0028:

1. OPEN /dev/kvm, call KVM_CREATE_VM to obtain a VM fd.
   This instantiates a pkvm_hyp_vm at EL2 with a known layout.

2. Allocate host memory at a controlled HPA that overlaps (or is adjacent to)
   EL2-internal structures. On devices with pKVM, EL2 carves its heap from a
   reserved region; spray KVM_SET_USER_MEMORY_REGION to fingerprint layout.

3. Trigger the overflow:
     ioctl(vmfd, KVM_HC_PKVM_SHARE_GUEST_MEMORY, {
         .gpa      = 0x0,
         .hpa      = ,
         .nr_pages = 0x100001,   /* overflows u32: effective size = 0x1000 */
         .prot     = KVM_PGTABLE_PROT_R | KVM_PGTABLE_PROT_W
     });

4. __pkvm_host_share_guest computes size = 0x100001 * 0x1000 = 0x1000 (u32 wrap).
   host_stage2_set_owner_locked iterates 0x100001 pages, writing PKVM_ID_GUEST
   into hyp_page.owner for each — this loop uses nr_pages directly, NOT size.

5. Stage-2 mapping covers only 1 page (gpa 0x0..0xFFF).
   Pages pfn+1 through pfn+0x100000 remain mapped in host stage-1 but are
   now marked PKVM_ID_GUEST in the ownership table.

6. Host kernel writes arbitrary data to HPA+0x1000..HPA+(0x100000*0x1000).
   If this range overlaps a pkvm_hyp_vm or hyp_page structure, attacker
   controls EL2 data directly from EL1.

7. Craft a fake pkvm_hyp_vm.pgt with a controlled stage-2 root table.
   On next VM entry, the EL2 pKVM code consults the corrupted pgt and
   installs attacker-controlled stage-2 mappings — full EL2 code execution
   via mapped executable pages or overwrite of EL2 exception vector table.

8. Optional persistence: patch EL2 .text via the newly established write
   primitive to survive subsequent reboots of the pKVM hypervisor image.

Patch Analysis

The fix widens the size computation to u64 and adds an explicit overflow check before the loop is entered. Additionally, the ownership-tracking loop is bounded by the same size value used for mapping, eliminating the divergence between the two.

/* BEFORE (vulnerable): */
static int __pkvm_host_share_guest(u64 host_kvm,
                                   u64 gpa, u64 hpa,
                                   u32 nr_pages,
                                   enum kvm_pgtable_prot prot)
{
    u32 size = nr_pages * PAGE_SIZE;   // truncates silently
    u64 addr = gpa;

    while (addr < gpa + size) {
        host_stage2_set_owner_locked(hpa + (addr - gpa), PAGE_SIZE, lock);
        kvm_pgtable_stage2_map(pgt, addr, PAGE_SIZE, hpa + (addr - gpa), prot, NULL);
        addr += PAGE_SIZE;
    }
}

/* AFTER (patched): */
static int __pkvm_host_share_guest(u64 host_kvm,
                                   u64 gpa, u64 hpa,
                                   u64 nr_pages,           /* widened to u64 */
                                   enum kvm_pgtable_prot prot)
{
    u64 size;

    /* Explicit overflow check before any use of size */
    if (check_mul_overflow(nr_pages, (u64)PAGE_SIZE, &size))
        return -EINVAL;

    /* Sanity: region must not exceed the guest's IPA space */
    if (gpa + size < gpa || hpa + size < hpa)
        return -EINVAL;

    u64 addr = gpa;
    while (addr < gpa + size) {
        /* Both ownership tracking and mapping now use the same bounded size */
        int ret = host_stage2_set_owner_locked(hpa + (addr - gpa),
                                               PAGE_SIZE, lock);
        if (ret)
            return ret;

        ret = kvm_pgtable_stage2_map(pgt, addr, PAGE_SIZE,
                                     hpa + (addr - gpa), prot, NULL);
        if (ret)
            return ret;

        addr += PAGE_SIZE;
    }
    return 0;
}

The kernel macro check_mul_overflow(a, b, &res) (from include/linux/overflow.h) uses compiler built-ins (__builtin_mul_overflow) to perform multiplication and return true on wrap, making the check zero-overhead on modern ARM64 toolchains. Widening nr_pages to u64 at the call site prevents silent truncation at the ABI boundary.

Detection and Indicators

Because exploitation occurs within EL2, traditional EL1 audit infrastructure (auditd, SELinux) is blind to the corruption itself. Detection must rely on:

  • Hypercall auditing: Instrument handle_host_mem_share() (the EL2 hypercall dispatcher) to log (gpa, hpa, nr_pages) tuples. Values of nr_pages > 0x80000 (512 MB in pages) are anomalous for legitimate guests on mobile hardware.
  • pKVM integrity monitor: Android's pKVM exposes a limited attestation interface; unexpected changes to the EL2 page ownership bitmap (queryable via KVM_CAP_ARM_PROTECTED_VM ioctls) indicate tampering.
  • Kernel log signatures:
/* Symptoms visible in dmesg / kernel log before full exploitation: */
[  42.318774] kvm [1234]: __pkvm_host_share_guest: nr_pages=0x100001 size=0x1000
[  42.319001] kvm [1234]: WARNING: ownership table divergence detected at pfn 0x80001
[  42.319188] kvm [1234]: hyp BUG at mem_protect.c:387  ← if WARN_ON was present

/* If no WARN_ON: silent corruption, no kernel log indicators */
  • eBPF probe: Attach a kprobe to kvm_pgtable_stage2_map and correlate call count against nr_pages from the hypercall argument — a 1-call result for nr_pages=0x100001 is definitive evidence of the wrapped path.

Remediation

  • Apply the March 2026 Android Security Bulletin patches for your kernel branch (5.15-android, 6.1-android). The fix is tagged against the android-mainline and android14-6.1 branches.
  • Verify pKVM is rebuilt: The fix lives in EL2-compiled code (arch/arm64/kvm/hyp/nvhe/). A kernel update that does not rebuild the pKVM binary image (e.g., a live-patch that misses the nvhe object) will not remediate the vulnerability.
  • Restrict /dev/kvm access: On production devices, /dev/kvm should be accessible only to the virtualization daemon (e.g., crosvm, virtmgr). Audit your SELinux policy for over-broad kvm class grants to untrusted app domains.
  • Enable CONFIG_UBSAN_OVERFLOW in debug builds: Catches signed/unsigned arithmetic wraps at the point of occurrence, surfacing this class of bug in CI before it ships.
  • Adopt check_mul_overflow / size_mul() idioms across all pKVM size computations — audit every nr_pages * PAGE_SIZE expression in mem_protect.c for width mismatches.
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 →