Commit a136d006b for clamav.net

commit a136d006b2ed388bcf1bff367690099a64d014da
Author: Val S. <valsnyde@cisco.com>
Date:   Mon Apr 27 16:38:04 2026 -0400

    HFS+: Validate compressed attribute record bounds (#1708)

    The HFS+ compressed-file attribute parser validated the attribute name
    length as a UTF-16 character count, but later used that same field as a
    byte offset by multiplying it by two. A crafted attribute record could
    therefore place the inline attribute record header near the end of the
    node and trigger an out-of-bounds read when ClamAV copied the record
    header or payload.

    Fix this by converting the attribute name length to a checked byte count
    before using it in offset calculations. Validate that the inline
    attribute record header fits in the node before reading it, and verify
    that the claimed attribute payload also fits before copying it.

    Credit: Sebastián Alba Vives

    CLAM-2969

diff --git a/libclamav/hfsplus.c b/libclamav/hfsplus.c
index 8a0be063e..57f147310 100644
--- a/libclamav/hfsplus.c
+++ b/libclamav/hfsplus.c
@@ -610,14 +610,30 @@ static cl_error_t hfsplus_check_attribute(cli_ctx *ctx, hfsPlusVolumeHeader *vol
                 goto done;
             }

-            if (recordStart + sizeof(hfsPlusAttributeKey) + attrKey.nameLength >= topOfOffsets) {
+            uint32_t nameBytes;
+            uint32_t attrRecordStart;
+            uint32_t attrDataStart;
+            uint32_t bytesRemaining;
+
+            nameBytes       = (uint32_t)attrKey.nameLength * 2;
+            attrRecordStart = recordStart + sizeof(hfsPlusAttributeKey) + nameBytes;
+
+            if (attrRecordStart >= topOfOffsets) {
                 cli_dbgmsg("hfsplus_check_attribute: Attribute name is longer than expected: %u\n", attrKey.nameLength);
                 status = CL_EFORMAT;
                 goto done;
             }

-            if (attrKey.cnid == expectedCnid && attrKey.nameLength * 2 == nameLen && memcmp(&nodeBuf[recordStart + 14], name, nameLen) == 0) {
-                memcpy(&attrRec, &(nodeBuf[recordStart + sizeof(hfsPlusAttributeKey) + attrKey.nameLength * 2]), sizeof(attrRec));
+            bytesRemaining = topOfOffsets - attrRecordStart;
+            if (bytesRemaining < sizeof(attrRec)) {
+                cli_dbgmsg("hfsplus_check_attribute: Not enough data for an attribute record at location %x for %u!\n",
+                           nextStart, recordNum);
+                status = CL_EFORMAT;
+                goto done;
+            }
+
+            if (attrKey.cnid == expectedCnid && nameBytes == nameLen && memcmp(&nodeBuf[recordStart + 14], name, nameLen) == 0) {
+                memcpy(&attrRec, &(nodeBuf[attrRecordStart]), sizeof(attrRec));
                 attrRec.recordType    = be32_to_host(attrRec.recordType);
                 attrRec.attributeSize = be32_to_host(attrRec.attributeSize);

@@ -631,7 +647,15 @@ static cl_error_t hfsplus_check_attribute(cli_ctx *ctx, hfsPlusVolumeHeader *vol
                     goto done;
                 }

-                memcpy(record, &(nodeBuf[recordStart + sizeof(hfsPlusAttributeKey) + attrKey.nameLength * 2 + sizeof(attrRec)]), attrRec.attributeSize);
+                attrDataStart = attrRecordStart + sizeof(attrRec);
+                if (attrDataStart > topOfOffsets || topOfOffsets - attrDataStart < attrRec.attributeSize) {
+                    cli_dbgmsg("hfsplus_check_attribute: Attribute data overruns node at location %x for %u!\n",
+                               nextStart, recordNum);
+                    status = CL_EFORMAT;
+                    goto done;
+                }
+
+                memcpy(record, &(nodeBuf[attrDataStart]), attrRec.attributeSize);
                 *recordSize = attrRec.attributeSize;

                 if (found) {