Xen Security Advisory 497 v2 (CVE-2026-42494,CVE-2026-42495,CVE-2026-62423,CVE-2026-62424,CVE-2026-62425) - buffer overruns in libfsimage iso9660 handling

Xen.org security team posted 1 patch 4 weeks, 1 day ago
Failed in applying to current master (apply log)
Xen Security Advisory 497 v2 (CVE-2026-42494,CVE-2026-42495,CVE-2026-62423,CVE-2026-62424,CVE-2026-62425) - buffer overruns in libfsimage iso9660 handling
Posted by Xen.org security team 4 weeks, 1 day ago
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

 Xen Security Advisory CVE-2026-42494,CVE-2026-42495,CVE-2026-62423,CVE-2026-62424,CVE-2026-62425 / XSA-497
                                                  version 2

            buffer overruns in libfsimage iso9660 handling

UPDATES IN VERSION 2
====================

Public release.

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

The directory and Rock Ridge / SUSP walk in libfsimage's iso9660 driver
derives several lengths directly from attacker-controlled on-disk fields
without validating them:

 * The directory loop itself assumes a good record length.  This is
   CVE-2026-42494.

 * The calculation of the System Use area may underflow.  This is
   CVE-2026-42495.

 * The Rock Ridge extension loop assumes a good (inner) record length.
   This is CVE-2026-62423.

 * The Rock Ridge NM record processing assumes a good entry length.
   This is CVE-2026-62424.

 * The Rock Ridge CE record processing assumes a good size and offset.
   This is CVE-2026-62425.

IMPACT
======

A guest using pygrub can escalate its privilege to that of the domain
construction tools (i.e., normally, to control of the host).

If the mechanism introduced by XSA-443 (see the mitigation section below)
is in use, then the guest can only escalate to this limited context.

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

All Xen versions from at least 3.2 onwards are affected.  Older versions
have not been inspected.

MITIGATION
==========

XSA-443 added a mechanism to run pygrub de-privileged.  Using this mode
will mitigate the vulnerability.

Ensuring that guests do not use the pygrub bootloader will avoid this
vulnerability.

For cases where the PV guest is known to be 64bit, and uses grub2 as a
bootloader, pvgrub is a suitable alternative to pygrub.

Running only HVM or PVH guests will avoid the vulnerability.

CREDITS
=======

This issue was discovered by Syed Abdul Khaliq of BugQore.

RESOLUTION
==========

Applying the attached patch resolves this issue.

Note that patches for released versions are generally prepared to
apply to the stable branches, and may not apply cleanly to the most
recent release tarball.  Downstreams are encouraged to update to the
tip of the stable branch before applying these patches.

xsa497.patch           xen-unstable - Xen 4.17.x

$ sha256sum xsa497*
65925ac5a322b9eacc8538d6486e3e37ddcb83fe6f02f61c18216ccd2770f964  xsa497.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/4UyVfoK9kFAmpomq0MHHBncEB4ZW4u
b3JnAAoJEIP+FMlX6CvZyesH/0dz6MuK3DsILiWvYuGNuX//RcdpBlQtLmyQR382
vpoicCuEAJLJlZFZsPddIxjTxte77nbFs2Ih51br95J//ou6sX7q6V9wDGt4EHDK
pF4ECI8SCkHDszogIDGDFiSdi7V7VknGpLCjFtINaAcaRLMHx+MMcdMV80IZyoXw
8p2XY7xvaXyo90I1FiecN7yU0H49sYJswLvihJU55hDuBZ25HLe2K59UGs2uaVm2
J0QKIBi3k20kZ5p9diny+cjU81fVu8LwGPKjvK3KUoyNnFsNbsO5owrAq2w/FSbE
1K54Lj6FXk0w08c4S0kz4zLXKLhSeCl7hFvqUpmQAXaN7ew=
=0lw8
-----END PGP SIGNATURE-----
From: Syed Abdul Khaliq <abdul@bugqore.com>
Subject: libfsimage/iso9660: harden Rock Ridge SUSP parsing against malformed lengths

The directory and Rock Ridge / SUSP walk in iso9660_dir() derives several
lengths directly from attacker-controlled on-disk fields without validating
them.  libfsimage is used by pygrub, which parses the filesystem of an
untrusted guest disk image from dom0, so these are reachable across a trust
boundary.

Five related problems are addressed:

  * The directory record loop advances by

        idr = (char *)idr + idr->length.l

    and only stops on length.l == 0.  A record whose length is smaller than
    the fixed part of the on-disk layout cannot hold its own mandatory
    fields, yet the body still reads name_len/extent/size and computes the
    System Use area length from it.  Require length to cover at least the
    fixed record (sizeof(*idr) - sizeof(idr->name)) before entering the body.

    This is CVE-2026-42494.

  * The System Use area length is computed before the inner loop as

        rr_len = idr->length.l - idr->name_len.l
                 - sizeof(struct iso_directory_record) + sizeof(idr->name);

    in unsigned arithmetic.  If length.l is smaller than name_len.l plus the
    fixed record size, rr_len underflows to a huge value and the whole SUSP
    walk runs off the directory buffer.  Guard the subtraction and treat such
    records as having no System Use area.

    This is CVE-2026-42495.

  * Inside the loop, each entry is consumed with

        rr_len -= rr_ptr.rr->len;
        rr_ptr.ptr += rr_ptr.rr->len;

    with no lower or upper bound on the entry's own len byte.  A len of 0
    spins forever; a len greater than the remaining rr_len underflows it and
    walks past the buffer.  Validate 4 <= len <= rr_len at the top of the
    loop and stop on violation: a structurally broken entry stream cannot be
    advanced reliably, so continuing is not meaningful.

    This is CVE-2026-62423.

  * The NM handler subtracted the 5-byte SUSP/NM header from len without a
    lower-bound check, underflowing name_len (the original report).  The
    generic check above only guarantees len >= 4; NM has an extra flags byte,
    so keep an NM-specific len >= 5 check.

    This is CVE-2026-62424.

  * The CE continuation resets rr_ptr/rr_len from ce.offset and ce.size, both
    image-controlled, into the fixed single-sector RRCONT_BUF with no bounds
    check.  Reject a window that does not fit in the buffer.

    This is CVE-2026-62425.

This is XSA-497.

Signed-off-by: Syed Abdul Khaliq <abdul@bugqore.com>
Reviewed-by: Jan Beulich <jbeulich@suse.com>

--- a/tools/libfsimage/iso9660/fsys_iso9660.c
+++ b/tools/libfsimage/iso9660/fsys_iso9660.c
@@ -180,7 +180,15 @@ iso9660_dir (fsi_file_t *ffi, char *dirn
 	  extent++;
 
 	  idr = (struct iso_directory_record *)DIRREC;
-	  for (; idr->length.l > 0;
+	  /*
+	   *  length is taken verbatim from the (untrusted) image.  A record
+	   *  shorter than the fixed part of the on-disk layout cannot hold its
+	   *  own mandatory fields (name_len, extent, size, ...), which the loop
+	   *  body reads below; stop the walk rather than dereference past it.
+	   */
+	  for (; idr->length.l >= sizeof(*idr) - sizeof(idr->name)
+		 && idr->length.l
+		    >= sizeof(*idr) - sizeof(idr->name) + idr->name_len.l;
 	       idr = (struct iso_directory_record *)((char *)idr + idr->length.l) )
 	    {
 	      const char *name = (const char *)idr->name;
@@ -201,21 +209,39 @@ iso9660_dir (fsi_file_t *ffi, char *dirn
 		}
 
 	      /*
-	       *  Parse Rock-Ridge extension
+	       *  Parse Rock-Ridge extension.
+	       *
+	       *  length and name_len are taken verbatim from the (untrusted)
+	       *  image.  Reject a record whose name would already overrun the
+	       *  fixed on-disk layout, so that the System Use area length does
+	       *  not underflow to a huge value below.
 	       */
-	      rr_len = (idr->length.l - idr->name_len.l
-			- sizeof(struct iso_directory_record)
-			+ sizeof(idr->name));
+	      if (idr->length.l < idr->name_len.l
+		  + sizeof(struct iso_directory_record) - sizeof(idr->name))
+		rr_len = 0;
+	      else
+		rr_len = (idr->length.l - idr->name_len.l
+			  - sizeof(struct iso_directory_record)
+			  + sizeof(idr->name));
 	      rr_ptr.ptr = ((char *)idr + idr->name_len.l
 			    + sizeof(struct iso_directory_record)
 			    - sizeof(idr->name));
-	      if (rr_ptr.i & 1)
+	      if ((rr_ptr.i & 1) && rr_len)
 		rr_ptr.i++, rr_len--;
 	      ce_ptr = NULL;
 	      rr_flag = RR_FLAG_NM | RR_FLAG_PX /*| RR_FLAG_SL*/;
 
 	      while (rr_len >= 4)
 		{
+		  /*
+		   * A SUSP entry is at least 4 bytes (signature, length,
+		   * version) and must fit in the remaining System Use area.
+		   * A shorter or overlong len is unparseable: stop, rather
+		   * than spin forever (len == 0) or underflow rr_len in the
+		   * advance below (len > rr_len).
+		   */
+		  if (rr_ptr.rr->len < 4 || rr_ptr.rr->len > rr_len)
+		    break;
 		  if (rr_ptr.rr->version != 1)
 		    {
 #ifndef STAGE1_5
@@ -236,9 +262,17 @@ iso9660_dir (fsi_file_t *ffi, char *dirn
 			    rr_flag &= rr_ptr.rr->u.rr.flags.l;
 			  break;
 			case RRMAGIC('N', 'M'):
-			  name = (const char *)rr_ptr.rr->u.nm.name;
-			  name_len = rr_ptr.rr->len - (4+sizeof(struct NM));
-			  rr_flag &= ~RR_FLAG_NM;
+			  /*
+			   * The generic check above only guarantees len >= 4;
+			   * NM additionally has a flags byte, so len must be at
+			   * least 5 for name_len not to underflow.
+			   */
+			  if (rr_ptr.rr->len >= (4+sizeof(struct NM)))
+			    {
+			      name = (const char *)rr_ptr.rr->u.nm.name;
+			      name_len = rr_ptr.rr->len - (4+sizeof(struct NM));
+			      rr_flag &= ~RR_FLAG_NM;
+			    }
 			  break;
 			case RRMAGIC('P', 'X'):
 			  if (rr_ptr.rr->len >= (4+sizeof(struct PX)))
@@ -339,6 +373,15 @@ iso9660_dir (fsi_file_t *ffi, char *dirn
 			  memcpy(NAME_BUF, name, name_len);
 			  name = (const char *)NAME_BUF;
 			}
+		      /*
+		       * offset and size are image-controlled; the loaded
+		       * continuation lives in a single-sector buffer.  Bail
+		       * out if the referenced window does not fit inside it.
+		       */
+		      if (ce_ptr->u.ce.offset.l >= ISO_SECTOR_SIZE
+			  || ce_ptr->u.ce.size.l
+			     > ISO_SECTOR_SIZE - ce_ptr->u.ce.offset.l)
+			break;
 		      rr_ptr.ptr = (char *)RRCONT_BUF + ce_ptr->u.ce.offset.l;
 		      rr_len = ce_ptr->u.ce.size.l;
 		      if (!iso9660_devread(ffi, ce_ptr->u.ce.extent.l, 0, ISO_SECTOR_SIZE, (char *)RRCONT_BUF))