/

CVE-2026-102703

Xenbleed: The Eleven-Year-Old Shortcut


CVE-2026-102703

CVSS 9.4

Hemanshu Baviskar

CONTENTS

0% READ

In January 2015, a patch made guest networking on Xen up to 25 percent faster. It also taught the hypervisor to skip a permission check, but only when a guest asked in one specific order.

That door stayed open for 4,296 days. Kernel code in any guest on Xen 4.6 or later could use it to read and write dom0, the privileged VM that runs the host.

The CodeAnt AI Security Research Team independently found this flaw, which we call Xenbleed. We built a working guest-to-dom0 escape on XenServer 9 and reported it to Xen, which shipped the fix on November 3, 2026, as XSA-523. Below is the full chain, and why the same bug is probably in your code too.

How CodeAnt AI found it, and what happened next

Hemanshu Baviskar, a security researcher at CodeAnt AI, independently found the flaw in Xen's grant-copy code and turned it into a working exploit chain on XenServer 9.0.0 running Xen 4.21.2-3, a current release. He proved every step before we wrote a word: a guest reads dom0 memory, writes dom0 memory, then runs instructions inside dom0's kernel.

On October 13, the CodeAnt AI Security Research Team sent the full package to the Xen Project security team and to Cloud Software Group, which makes XenServer: root cause, step-by-step reproduction, proof-of-concept source, demonstrated impact and a proposed fix. Our report hit a spam filter on the way in. Xen's security team dug it out anyway and replied the next day: the issue was already known to them, a fix was being prepared, and they offered us independent discovery credit. We accepted, offered to help test the fix, and kept the details private until users could patch.


Date (UTC)

What happened

Oct 13, 2026

We report a guest-to-dom0 escape with dom0 kernel code execution to the Xen Project security team and Cloud Software Group, with root cause, reproduction steps, PoC source and a proposed fix.

Oct 13, 2026

Cloud Software Group’s security response team confirms receipt and starts its analysis.

Oct 14, 2026

Xen’s security team confirms the issue is known, says a fix is in progress and offers independent discovery credit.

Oct 14, 2026

We confirm credit as the CodeAnt AI Security Research Team and offer to test the fix.

Nov 3, 2026

Xen publishes XSA-523 after a two-week embargo. Both patches carry a Reported-by line for CodeAnt AI Security Research.

Nov 4, 2026

We publish this write-up and our CVSS assessment for both CVEs: 9.3 Critical (v3.1) and 9.4 Critical (v4.0).


TL;DR

  • Bug: Xen reused security-sensitive state across grant-copy batch entries without fully validating how that state had been authorized.

  • Access: The attacker starts with kernel-level control inside a Xen guest.

  • Impact: Unauthorized read/write of other domains, including dom0. On XenServer 9, we demonstrated controlled instruction execution in the dom0 Linux kernel.

  • Status: Xen 4.6 onward is affected. Xen published fixes in XSA-523 on November 3, 2026.

A guest with kernel-level control could turn Xen’s normal memory-copy interface into read and write access to XenServer’s management domain, dom0. The authorization check worked for an isolated request but could be skipped after Xen reused state from an earlier entry in the same batch.

The flaw dates to a grant-copy performance change committed on January 29, 2015. It remained in Xen for more than eleven years. We independently found the same flaw in 2026, reproduced the boundary crossing on XenServer 9, and used the memory primitive on an exact kernel image to execute a short, reversible instruction sequence in the dom0 Linux kernel.

Xen in one diagram

Xen is a type-1 hypervisor. It runs directly on the hardware and divides the machine into isolated virtual machines called domains. A conventional Xen host starts a privileged control domain called dom0 and can run one or more ordinary workload domains called domU.

Dom0 is a Linux VM with extra authority. On XenServer it runs management services, creates and destroys guests, and provides much of their storage and network I/O. A domU does not sit “inside” dom0; both run beside each other on Xen. The privilege difference is what makes dom0 memory a meaningful boundary.

Guests ask Xen to perform privileged operations through hypercalls. Grant tables are the memory-sharing system behind many Xen I/O paths: a domain creates a small permission record, called a grant, that tells Xen which peer may access one of its pages and whether the peer may read or write it.


Xen architecture showing an attacker-controlled domU sending a GNTTABOP_copy batch through vulnerable Xen grant-table code to access a dom0-owned page

The demonstrated boundary crossing is domU → Xen grant-copy code → dom0 memory. The proof reached the dom0 Linux kernel, not Xen’s own code pages.

One copy, two address modes

GNTTABOP_copy asks Xen to copy bytes between a source page and a destination page. The caller may send many operations in one hypercall. Each source and destination carries a domain ID, a page offset, a value, and a flag that decides how Xen interprets that value.

A foreign domain is any domain other than the caller. A guest frame number identifies a page in the named domain’s address space; this post calls the flag-clear form direct-frame mode.

Mode

Meaning

Authorization rule

*_gref flag set

The value is a grant reference created by the page owner.

Required when accessing a foreign domain.

*_gref flag clear

The value is a guest frame number resolved in the named domain.

Allowed only for DOMID_SELF, meaning the caller’s own memory.

The public ABI says that foreign-domain operands must use grant references. The vulnerable implementation contained the same rule in a comment and a guard:

xen/common/grant_table.c:2926

static int gnttab_copy_lock_domain(domid_t domid, bool is_gref,
                                   struct gnttab_copy_buf *buf)
{
    /* Only DOMID_SELF may reference via frame. */
    if ( domid != DOMID_SELF && !is_gref )
        return GNTST_permission_denied;

    buf->domain = rcu_lock_domain_by_any_id(domid

static int gnttab_copy_lock_domain(domid_t domid, bool is_gref,
                                   struct gnttab_copy_buf *buf)
{
    /* Only DOMID_SELF may reference via frame. */
    if ( domid != DOMID_SELF && !is_gref )
        return GNTST_permission_denied;

    buf->domain = rcu_lock_domain_by_any_id(domid

static int gnttab_copy_lock_domain(domid_t domid, bool is_gref,
                                   struct gnttab_copy_buf *buf)
{
    /* Only DOMID_SELF may reference via frame. */
    if ( domid != DOMID_SELF && !is_gref )
        return GNTST_permission_denied;

    buf->domain = rcu_lock_domain_by_any_id(domid

Vulnerable source at commit e4da1829. The rule is correct; its position is not.

Batches often touch adjacent ranges in the same page, so Xen caches locked domains and page mappings between entries. That optimization avoids repeating the expensive setup for every small copy. The cache becomes security-sensitive because it holds objects that were reached through an authorization decision.

The cache remembered too little

XSA-523 covers two defects introduced into this cache. Both came from the same design mistake: Xen decided that cached state was reusable without checking every fact that made the state valid.

CVE

Incomplete identity

Consequence

CVE-2026-102703

A cached page buffer was compared by its numeric value without first requiring the old and new address modes to match.

A grant reference could be mistaken for a frame number.

CVE-2026-102704

The foreign direct-frame check ran only when Xen locked a domain, while later entries could reuse that lock.

A same-domain-ID entry could switch from grant mode to direct-frame mode and skip the check.

Our reproduced chain exercises CVE-2026-102704. The domain-cache decision compared the source and destination domain IDs, but did not compare the grant-reference flags:

xen/common/grant_table.c:3127

if ( !src->domain || op->source.domid != src->ptr.domid ||
     !dest->domain || op->dest.domid != dest->ptr.domid )
{
    gnttab_copy_release_buf(src);
    gnttab_copy_release_buf(dest);
    gnttab_copy_unlock_domains(src, dest);
    rc = gnttab_copy_lock_domains(op, src, dest

if ( !src->domain || op->source.domid != src->ptr.domid ||
     !dest->domain || op->dest.domid != dest->ptr.domid )
{
    gnttab_copy_release_buf(src);
    gnttab_copy_release_buf(dest);
    gnttab_copy_unlock_domains(src, dest);
    rc = gnttab_copy_lock_domains(op, src, dest

if ( !src->domain || op->source.domid != src->ptr.domid ||
     !dest->domain || op->dest.domid != dest->ptr.domid )
{
    gnttab_copy_release_buf(src);
    gnttab_copy_release_buf(dest);
    gnttab_copy_unlock_domains(src, dest);
    rc = gnttab_copy_lock_domains(op, src, dest

The vulnerable cache key contained the two domain IDs, but not the bits that selected grant-reference or direct-frame mode. View the source.

An entry that failed grant lookup released its page buffers but kept the locked domains for the next entry. When the next operation kept the same domain IDs, Xen skipped gnttab_copy_lock_domain(), including its DOMID_SELF check. The direct-frame branch then resolved the selected frame inside the cached foreign domain.

Xen already enforced the correct rule when the cache was empty. The batch cache created another route to the operation without repeating that check.

CVE-2026-102703 affected a second layer of the cache. gnttab_copy_buf_valid() needed to require b->have_grant == has_gref before accepting a cached numeric match. The official patch adds that mode check. Our invalid-grant primer chain exercises CVE-2026-102704; we did not independently exercise CVE-2026-102703.

Two entries, one skipped check

The read primitive fits in one two-entry GNTTABOP_copy batch. The attacker deliberately makes the first grant lookup fail after Xen caches the domains. That discards the page state while preserving the domain cache. The second entry keeps the domain IDs and changes the source operand from a grant reference to a direct frame.

  1. Entry 0: Name dom0 as a grant-reference source, use the invalid reference ~0U, and copy toward a scratch page in DOMID_SELF.

  2. Cached state: Xen locks dom0 and the caller’s domain. Grant lookup fails with GNTST_bad_gntref (-3). Page state is released; the domain locks remain cached.

  3. Entry 1: Keep both domain IDs, clear the source grant flag, and put a selected dom0 guest frame number in the same operand field.

  4. Result: The domain cache matches, so the lock-time permission check does not run. Xen resolves the frame in dom0 and copies its bytes into the guest. Status: 0.

The negative control makes the bypass visible. A standalone foreign direct-frame copy starts with an empty cache and returns GNTST_permission_denied (-8). In our proof, the same direct-frame operation succeeded when it followed the invalid-grant primer in one batch.

Reversing source and destination turns the read into a write. Hypercall preemption can discard the cache between entries, so the targeted probes retry. No timing race is required; our fresh XenServer run succeeded on the first batch.

From dom0 memory to dom0 instructions

A marker page with no grant

We first tested the primitive against a private marker page allocated by a fixture in dom0. The fixture created no grant and disclosed neither its address nor its frame number. The guest scanned candidate dom0 frames, found XEN_COPY_SECRET_A, and changed it to XEN_COPY_SECRET_B. Dom0 independently reported the new value.

Test

Observed evidence

Result

Standalone read

status=-8

DENIED

Two-entry read

first_status=-3 second_status=0; buffer contained XEN_COPY_SECRET_A

READ

Two-entry write

first_status=-3 second_status=0; dom0 reported XEN_COPY_SECRET_B

WRITE

Guest types

A PVH guest and a XenServer-managed HVM guest, covering two Xen x86 guest modes, reached the same hypercall path

REPRODUCED

A reversible kernel-control proof

For code execution, we constrained the proof to the exact XenServer dom0 kernel image we had hashed and ran Xen with one pCPU. SMP reliability was not measured.

From the guest, the read primitive located the page containing xen_evtchn_do_upcall(), dom0’s Xen event-channel interrupt handler, using a 28-byte signature, an expected five-byte entry sequence, and a 13-byte NOP region.

The guest wrote a 13-byte stub into that NOP region. The stub saved registers, called the kernel’s existing dump_stack(), restored registers, and returned. A five-byte patch redirected the event-handler entry to the stub. We sent a XenStore event, an ordinary host/guest notification, observed the injected call in dom0’s kernel trace, and restored both modified regions with read-back verification.

Closing notes

Xenbleed began with a grant-copy performance optimization committed in January 2015. Xen already had the correct rule: a guest may use direct frame numbers only for its own memory. The optimization created a cached path where that rule was not checked again. The flaw remained in Xen for more than eleven years.

The XSA-523 patches repair both incomplete cache checks. Xen lists no known mitigation, so operators running untrusted or tenant-controlled guests should deploy a hypervisor or downstream update that explicitly includes both fixes.

Frequently Asked
Questions
What is Xenbleed?
Which systems are affected?
What does an attacker need?
What can an attacker do with it?
What causes the bug?
How do I fix it?
Find out what's already
exploitable in your codebase.