Xen Security Advisory 513 v3 (CVE-2026-79605,CVE-2026-79606) - Out-of-bounds accesses in Tapdisk

Xen.org security team posted 1 patch 2 weeks, 2 days ago
Xen Security Advisory 513 v3 (CVE-2026-79605,CVE-2026-79606) - Out-of-bounds accesses in Tapdisk
Posted by Xen.org security team 2 weeks, 2 days ago
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

     Xen Security Advisory CVE-2026-79605,CVE-2026-79606 / XSA-513
                               version 3

                   Out-of-bounds accesses in Tapdisk

UPDATES IN VERSION 3
====================

Public release.

ISSUE DESCRIPTION
=================

Tapdisk is a userspace xen-blkback implementation used by the XAPI
toolstack.  Several bounds checks have been found to be incorrect.

 * There is no upper bounds check for blkif->last_sect.  Passing a value
   larger than 7 will result in a read or write beyond the mapped grant.

   This is CVE-2026-79605.

 * The gcopy_segs[] object has incorrect bounds checks on it.  Passing
   nr_segments between 12 and 32 will corrupt adjacent memory.

   This is CVE-2026-79606.

IMPACT
======

A malicious guest can obtain code execution within the tapdisk process
running in dom0.  Tapdisk normally runs as root.

VULNERABLE SYSTEMS
==================

All versions of tapdisk are vulnerable.

MITIGATION
==========

There are no mitigations.

CREDITS
=======

Found by Jihwan Yoon of NAVER Cloud, and reported via XenServer.

RESOLUTION
==========

Applying the appropriate attached patchs resolves this issue.

xsa513-?.patch           blktap master

$ sha256sum xsa513*
41e5f1929a7acbe83820ee0b359f9558120222d32442ea4fb5a6eee0bf937bf1  xsa513-1.patch
a5af5a73d2ede5124735213e4974f1aeff7f8118dfbacd52fbb03168fe96c6af  xsa513-2.patch
$

DEPLOYMENT DURING EMBARGO
=========================

Deployment of the patches and/or mitigations described above (or
others which are substantially similar) is permitted during the
embargo, even on public-facing systems with untrusted guest users and
administrators.

But: Distribution of updated software is prohibited (except to other
members of the predisclosure list).

Predisclosure list members who wish to deploy significantly different
patches and/or mitigations, please contact the Xen Project Security
Team.


(Note: this during-embargo deployment notice is retained in
post-embargo publicly released Xen Project advisories, even though it
is then no longer applicable.  This is to enable the community to have
oversight of the Xen Project Security Team's decisionmaking.)

For more information about permissible uses of embargoed information,
consult the Xen Project community's agreed Security Policy:
  http://www.xenproject.org/security-policy.html
-----BEGIN PGP SIGNATURE-----

iQFABAEBCAAqFiEEI+MiLBRfRHX6gGCng/4UyVfoK9kFAmqf98kMHHBncEB4ZW4u
b3JnAAoJEIP+FMlX6CvZiuMIAIaNhPYKG/UeGc1JV70GEcyqS4d6NNlNBY0qtuGl
qVQ8LRVBReqRk0aS0hNDI7txFRsZ18ENteBKG/JaXD4mj5rylpnKdl7y8suTFrGi
QuFk1EyYBrud5gtpwW8sq4GKQLf5hoAIIUDGX4qmEC+blRuHTagUIHNwehrkRB+d
38UxmWQR2ppgBSsCJlclMJKSm1nWo04Qx/Nm3Aoc0og0hv+/UkdkDShTGrP/nutb
/r4yywz6LaqdCl7Te1ULx6sRVl1MIxtDqcvsRajqKc9nV56RAgrj3moIJRXRusW9
YqZxuUw8HJkhpisOlj6A/N16f+G7bxZwb3FEZBKuNGvM6lQ=
=oXUQ
-----END PGP SIGNATURE-----
From: Mark Syms <mark.syms@citrix.com>
Subject: Validate guest blkif request segment bounds

first_sect/last_sect in a blkif request segment are guest-controlled 8-bit
values, but each segment addresses at most a single page (8 sectors).
tapdisk_xenblkif_parse_request() only checked last_sect >= first_sect, so a
segment with last_sect > 7 yielded an oversized transfer length. That drives
out-of-bounds pointer arithmetic against the per-request buffer and overflows
the uint16_t gntdev grant-copy length in guest_copy2() (e.g. 68KB truncates to
4KB, so stale buffer contents are transferred).

Reject any segment whose sectors fall outside the page, replacing the
long-standing TODO at the vectorisation loop.

This is CVE-2026-79605, part of XSA-513.

Signed-off-by: Mark Syms <mark.syms@citrix.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-by: Tim Smith <tim.smith@citrix.com>

diff --git a/drivers/td-req.c b/drivers/td-req.c
index 2a3fb05f4ad0..69a947f570ec 100644
--- a/drivers/td-req.c
+++ b/drivers/td-req.c
@@ -644,8 +644,16 @@ tapdisk_xenblkif_parse_request(struct td_xenblkif * const blkif,
         /*
          * Note that first and last may be equal, which means only one sector
          * must be transferred.
+         *
+         * first_sect/last_sect are guest-controlled 8-bit values, but each
+         * segment addresses at most a single page. Reject any segment whose
+         * sectors fall outside the page: an out-of-range last_sect would
+         * produce an oversized transfer length (which also overflows the
+         * uint16_t gntdev grant-copy length) and drive out-of-bounds accesses
+         * to the per-request buffer.
          */
-        if (seg->last_sect < seg->first_sect) {
+        if (seg->last_sect < seg->first_sect ||
+            seg->last_sect >= (PAGE_SIZE >> SECTOR_SHIFT)) {
             RING_ERR(blkif, "req %lu: invalid sectors %d-%d\n",
                     req->msg.id, seg->first_sect, seg->last_sect);
             err = EINVAL;
@@ -670,7 +678,7 @@ tapdisk_xenblkif_parse_request(struct td_xenblkif * const blkif,
         struct blkif_request_segment *seg = &req->msg.seg[i];
         size_t size;
 
-        /* TODO check that first_sect/last_sect are within page */
+        /* first_sect/last_sect are already validated, above */
 
         next = page + (seg->first_sect << SECTOR_SHIFT);
         size = seg->last_sect - seg->first_sect + 1;
From: Mark Syms <mark.syms@citrix.com>
Subject: Bound nr_segments by seg[] capacity and right-size buffer

Two constants were in play for the number of segments in a request:

 - BLKIF_MAX_SEGMENTS_PER_REQUEST (11), the Xen ABI limit and the size of the
   ring descriptor's blkif_request.seg[] array; and
 - BLKIF_MAX_BUFFER_SEGMENTS_PER_REQUEST (32), a blktap-local value introduced
   when multi-page rings were enabled (dd6095c).

Multi-page rings enlarge the ring (more request slots), not the number of
segments per request, so sizing the per-request structures by 32 was both
unnecessary and unsafe:

 - tapdisk_xenblkif_make_vbd_request() validated nr_segments against 32, but
   nr_segments is a guest-controlled uint8_t and msg.seg[] only holds 11
   entries. A request with nr_segments in (11, 32] passed validation and then
   drove out-of-bounds reads of msg.seg[] in tapdisk_xenblkif_parse_request()
   and guest_copy2(), whose stale bytes were used as gref/first_sect/last_sect.
 - The per-request buffer and the iov[]/gref[] arrays were over-allocated to 32
   pages/entries where only 11 are reachable (gcopy_segs[] was already 11).

Bound nr_segments by BLKIF_MAX_SEGMENTS_PER_REQUEST and size the per-request
buffer (TD_REQ_BUFFER_SIZE) and the iov[]/gref[] arrays by the same constant,
so the validation bound, the segment arrays, and the ring descriptor's seg[]
all agree. block-lcache.c inherits the corrected buffer size via
TD_REQ_BUFFER_SIZE. Also derive the bufcache munmap size from TD_REQ_BUFFER_SIZE
so the map and unmap sizes share one definition.

The legacy blktap2 kernel-mmap macros in blktaplib.h (MMAP_PAGES / MMAP_VADDR)
are a separate, kernel-shared layout and are intentionally left untouched.

This is CVE-2026-79606, part of XSA-513.

Signed-off-by: Mark Syms <mark.syms@citrix.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-by: Tim Smith <tim.smith@citrix.com>

diff --git a/drivers/td-req.c b/drivers/td-req.c
index 5b9b33f15c5d..f4679af11c39 100644
--- a/drivers/td-req.c
+++ b/drivers/td-req.c
@@ -121,7 +121,7 @@ td_xenblkif_bufcache_free(struct td_xenblkif * const blkif)
 
     while (blkif->n_reqs_bufcache_free > TD_REQS_BUFCACHE_MIN){
         munmap(blkif->reqs_bufcache[--blkif->n_reqs_bufcache_free],
-               (size_t)BLKIF_MAX_BUFFER_SEGMENTS_PER_REQUEST << PAGE_SHIFT);
+               (size_t)TD_REQ_BUFFER_SIZE);
     }
 }
 
@@ -786,11 +786,14 @@ tapdisk_xenblkif_make_vbd_request(struct td_xenblkif * const blkif,
     gettimeofday(&tapreq->ts, NULL);
 
     /*
-     * Check that the number of segments is sane.
+     * Check that the number of segments is sane. nr_segments is guest-
+     * controlled; the blkif protocol permits at most
+     * BLKIF_MAX_SEGMENTS_PER_REQUEST segments per request, which is how the
+     * ring descriptor and our per-request buffers are sized.
      */
     if (unlikely((tapreq->msg.nr_segments == 0 &&
                 tapreq->msg.operation != BLKIF_OP_WRITE_BARRIER) ||
-            tapreq->msg.nr_segments > BLKIF_MAX_BUFFER_SEGMENTS_PER_REQUEST)) {
+            tapreq->msg.nr_segments > BLKIF_MAX_SEGMENTS_PER_REQUEST)) {
         RING_ERR(blkif, "req %lu: bad number of segments in request (%d)\n",
                 tapreq->msg.id, tapreq->msg.nr_segments);
         err = EINVAL;
diff --git a/drivers/td-req.h b/drivers/td-req.h
index dad40f294628..7279a935a6b5 100644
--- a/drivers/td-req.h
+++ b/drivers/td-req.h
@@ -38,7 +38,14 @@
 #include <xen/gntdev.h>
 #include "td-blkif.h"
 
-#define TD_REQ_BUFFER_SIZE (BLKIF_MAX_BUFFER_SEGMENTS_PER_REQUEST << PAGE_SHIFT)
+/*
+ * A ring request descriptor (blkif_request_t) carries at most
+ * BLKIF_MAX_SEGMENTS_PER_REQUEST segments, each mapping a single page, so the
+ * per-request data buffer and the vectorised segment arrays below only ever
+ * need that many entries. (This backend does not implement BLKIF_OP_INDIRECT,
+ * which is the only mechanism that would raise the per-request segment count.)
+ */
+#define TD_REQ_BUFFER_SIZE (BLKIF_MAX_SEGMENTS_PER_REQUEST << PAGE_SHIFT)
 
 /**
  * Representation of the intermediate request used to retrieve a request from
@@ -80,9 +87,9 @@ struct td_xenblkif_req {
     /**
      * The scatter/gather list td_vbd_request_t.iov points to.
      */
-    struct td_iovec iov[BLKIF_MAX_BUFFER_SEGMENTS_PER_REQUEST];
+    struct td_iovec iov[BLKIF_MAX_SEGMENTS_PER_REQUEST];
 
-    grant_ref_t gref[BLKIF_MAX_BUFFER_SEGMENTS_PER_REQUEST];
+    grant_ref_t gref[BLKIF_MAX_SEGMENTS_PER_REQUEST];
     int prot;
 
 	struct gntdev_grant_copy_segment