Subject: libfsimage/iso9660: harden Rock Ridge SUSP parsing against malformed lengths
From: Syed Abdul Khaliq <[email-masked]>

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 <[email-masked]>
Reviewed-by: Jan Beulich <[email-masked]>

--- 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))