From 0d7c9a6056707746ae64b3dec47b81519c3e55c6 Mon Sep 17 00:00:00 2001 From: "Jorge Moya A." Date: Thu, 5 Nov 2026 01:03:24 -0300 Subject: [PATCH] Fix HEIF EXIF extraction with variable-width iloc entries --- ext/exif/exif.c | 127 +++++++++++++++--- ext/exif/tests/heic_variable_iloc.heic | Bin 0 -> 2477 bytes ext/exif/tests/heic_variable_iloc.phpt | 14 ++ .../heic_variable_iloc_extent_count.heic | Bin 0 -> 1294 bytes 4 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 ext/exif/tests/heic_variable_iloc.heic create mode 100644 ext/exif/tests/heic_variable_iloc.phpt create mode 100644 ext/exif/tests/heic_variable_iloc_extent_count.heic diff --git a/ext/exif/exif.c b/ext/exif/exif.c index da0245caae86..941efac19c0f 100644 --- a/ext/exif/exif.c +++ b/ext/exif/exif.c @@ -4377,11 +4377,106 @@ static int exif_isobmff_parse_box(unsigned char *buf, isobmff_box_type *box) return 16; } + +/* Read an unsigned integer of up to 8 bytes from a bounded buffer. */ +static bool exif_isobmff_read_uint(const unsigned char **cursor, const unsigned char *end, + uint8_t width, uint64_t *value) +{ + if (width > 8 || *cursor > end || (size_t)(end - *cursor) < width) { + return false; + } + + uint64_t result = 0; + for (unsigned int i = 0; i < width; i++) { + result = (result << 8) | (*cursor)[i]; + } + *cursor += width; + *value = result; + return true; +} + +/* iloc item records contain field widths declared by the box itself. */ +static void exif_isobmff_parse_iloc(const unsigned char *start, uint64_t box_size, + int header_size, int exif_id, isobmff_item_pos_type *pos) +{ + if (exif_id < 0 || box_size < (uint64_t)(header_size + 8)) { + return; + } + + const unsigned char *p = start + header_size; + const unsigned char *end = start + (size_t)box_size; + uint64_t count, ignored; + uint8_t version = p[0]; + if (version > 2) { + return; + } + + p += 4; /* FullBox version and flags. */ + uint8_t offset_size = p[0] >> 4; + uint8_t length_size = p[0] & 15; + uint8_t base_size = p[1] >> 4; + uint8_t index_size = version ? p[1] & 15 : 0; + p += 2; + + if (offset_size > 8 || length_size > 8 || base_size > 8 || index_size > 8 + || !exif_isobmff_read_uint(&p, end, version == 2 ? 4 : 2, &count)) { + return; + } + + for (uint64_t i = 0; i < count; i++) { + uint64_t item_id, method = 0, reference_index, base, extents; + if (!exif_isobmff_read_uint(&p, end, version == 2 ? 4 : 2, &item_id)) { + return; + } + if (version != 0) { + if (!exif_isobmff_read_uint(&p, end, 2, &method)) { + return; + } + method &= 15; + } + if (!exif_isobmff_read_uint(&p, end, 2, &reference_index) + || !exif_isobmff_read_uint(&p, end, base_size, &base) + || !exif_isobmff_read_uint(&p, end, 2, &extents)) { + return; + } + + /* EXIF can be addressed by a single extent in the current reader. */ + bool supported = item_id == (uint64_t)exif_id && method == 0 + && reference_index == 0 && extents == 1; + uint64_t found_offset = 0, found_length = 0; + for (uint64_t j = 0; j < extents; j++) { + uint64_t extent_offset, extent_length; + if (version != 0 && index_size != 0 + && !exif_isobmff_read_uint(&p, end, index_size, &ignored)) { + return; + } + if (!exif_isobmff_read_uint(&p, end, offset_size, &extent_offset) + || !exif_isobmff_read_uint(&p, end, length_size, &extent_length)) { + return; + } + if (supported && base <= UINT64_MAX - extent_offset) { + found_offset = base + extent_offset; + found_length = extent_length; + } else { + supported = false; + } + } + if (supported && found_offset <= UINT32_MAX && found_length <= UINT32_MAX) { + pos->offset = (uint32_t)found_offset; + pos->size = (uint32_t)found_length; + return; + } + } +} + static void exif_isobmff_parse_meta(unsigned char *data, unsigned char *end, isobmff_item_pos_type *pos) { isobmff_box_type box, item; unsigned char *p; int header_size, exif_id = -1, version, item_count, i; + const unsigned char *iloc_start = NULL; + uint64_t iloc_size = 0; + int iloc_header_size = 0; size_t remain; #define CHECK(n) do { \ @@ -4398,8 +4493,8 @@ static void exif_isobmff_parse_meta(unsigned char *data, unsigned char *end, iso unsigned char *box_offset = data + 4; while (box_offset < end - 16) { header_size = exif_isobmff_parse_box(box_offset, &box); - if (box.size < header_size) { - return; + if (box.size < header_size || box.size > (uint64_t)(end - box_offset)) { + break; } p = box_offset; remain = end - p; @@ -4416,7 +4511,9 @@ static void exif_isobmff_parse_meta(unsigned char *data, unsigned char *end, iso } for (i = 0; i < item_count && p < end - 20; i++) { header_size = exif_isobmff_parse_box(p, &item); - if (item.size < header_size) { + if (item.size < (uint64_t)(header_size + 12) || + (uint64_t)(p - box_offset) > box.size || + item.size > box.size - (uint64_t)(p - box_offset)) { return; } CHECK(header_size + 12); @@ -4431,23 +4528,11 @@ static void exif_isobmff_parse_meta(unsigned char *data, unsigned char *end, iso } } else if (box.type == FOURCC("iloc")) { - ADVANCE(header_size + 6); - version = p[-6]; - if (version < 2) { - ADVANCE(2); - item_count = php_ifd_get16u(p - 2, 1); - } else { - ADVANCE(4); - item_count = php_ifd_get32u(p - 4, 1); + if (iloc_start == NULL) { + iloc_start = box_offset; + iloc_size = box.size; + iloc_header_size = header_size; } - for (i = 0; i < item_count && p < end - 16; i++, p += 16) { - if (php_ifd_get16u(p, 1) == exif_id) { - pos->offset = php_ifd_get32u(p + 8, 1); - pos->size = php_ifd_get32u(p + 12, 1); - break; - } - } - break; } if (end - 16 - box_offset <= box.size) { @@ -4456,6 +4541,10 @@ static void exif_isobmff_parse_meta(unsigned char *data, unsigned char *end, iso box_offset += box.size; } + if (iloc_start != NULL && exif_id >= 0) { + exif_isobmff_parse_iloc(iloc_start, iloc_size, iloc_header_size, exif_id, pos); + } + #undef ADVANCE #undef CHECK } diff --git a/ext/exif/tests/heic_variable_iloc.heic b/ext/exif/tests/heic_variable_iloc.heic new file mode 100644 index 0000000000000000000000000000000000000000..82dbe4987385a7fa53c5793705fd5c043be1c89a GIT binary patch literal 2477 zcmYjP2{=@38$M^oGGvQnjHMxE{fc3v3E5SQpX{_4Gh-Y}rm+l)(kE1eXe`%(`xd!h6Q3ZlcN7Dx+WZBPJULez-g{5KjwAz|xHY%Bm203tFfWdi{~bO)?$7#smd zU|Ioh69_s14}uMe%EJzQJq^wYjG`JKf!`vCq}WD7>*+X%Mc=3c04XXhoVq>{nJLAD zB0`b6^~tLclf{JjAjG7G!59U1wFc3MlmTx5P@M&St^WK80MckbxB_bdN`L|Zl7L1s z(8yA4LRnDYppF%|UiyC#E%;~h+(J|0d+BM(5$A5Iss0o%jdF2F*Fb(#9C)R zeS#C}waA}ru_93&@YJS;GW`STF2P)YOmWMvB9RL7ZWJ+&~l`lnjv{hOEf&v})q z^%bgfM&HU_P_24my!ejiHp`k@-GV6|@*pYFw0v-FUxr&WC4(I{=O1c_+fi-Thuc<^ z@X={}uKDSbvftF&qY}DJHAamdzwaab=P1h@Ebnqnk@)VR&N-=`yx3}cXK2Mw*PD8S zZs_IZ!=48(oKUu-$g}QivxKj|#xaLGa*t$+=4RJkaU~o(Y_1kizgQ+4HgA!^yZK9_ z!O`Y2(UzC5F#q{}$}_RIRkc#mr>#uvP50mNFCo!5^?1UJ&Y48q2*&qghYADngMW7{z1?CgOqrG%|cem}9D`xVxrPjY>Cnk z-3W}l+CD4p@t$(Yz1m}HJP3akebaJe^z)Cm&`8$piywO;HE`ml)?$pQ1;d3HlgJWH zDPN=X)Y;j%vE6_qEx3J(-E$QP#5Qk>^EXT{O53V4{I>Oq-ws^2^1Ykddse>Lji_>> zTALJB|6E)?uUO4bJLE`z5?kzcqrt%GJ!|PXc+ksxm_oD0qC9jwUwgStZ+5VJRJJ*e zhSslZH%g*AnPv;jO3K~AypPKA^Oi?VyIt>^PWec5@@0(gX-uHl=;#gJRHE=k4~hnh`?CouLhF^9D2Fc~8fK(v@}F7%{~~zW!I6 zdL|7CCgves+w+I=ikQUS)FXuziv0~7Rm$=H+_B2~p_k0|>d|xEKkZD{+(wrk**j!wfue9lVik()ybITAKiwA?dR?5OuE#+o$$G~zkOVq zbz&^}@4iP+)dj0GZ_!5l7llJFn%0ssw$Fk-=TAFx7S3e!1rxpNassSd zjI;{|2VIPaj;@w6DkHwBd;v+9r#Gk;rEXgmR~w2uBt<2Y|1$lH7XKfmFI%PRooaF# zFA>k&EiAL3bxHI`e(sO*vrdiG>)@Qj*%QhxZdZ~?yM!|!RSLc==BEyp-{d=nqA&Jhg?s*?)fn#>WjmFW725)_1aU=cA-1 zoTh5oN^9ame+rOTmG5BZE}C&5 zIij|e4bJS8Q>Ef^ON-Y8Mdv`yvcnkrQ_J^J@b+?7sq?(rvw5p`0oN)j^XtN5`#Tu&)QzZxeV^Dir0V4rmO4y>NQH~j(T}ctM>S`!Y(7}jO=|gzH7mo zu*R+;*f!$f;sV{dRmFudOSbqyQdXCm zN9jw61+c>$yLcqs<<0c8GrtTJFA3V3bu2c|VD)9VMp)3y!^}Tkzh@=cWvxk!N&iS$ z;seJj-I#>5>o!HtCm^Ar0vTd4@q%d^Lxc;NObmN9pQ#sdZ=a1#j!CZFlooq(=S8S4>+#)vZmGC;qxV~UzxpAQLWl6r-k)2BBU5|<4D=f6i=4bUQ zQ4)5e*9-!S@3$55go$Bd4?5EtE0X@ksN*dP=z%*N%PK7iV&uS&`nc=&vhw3}4@0v3(QLm=5nkVvjdoo*mL&TJl zXZc;PY~$Izs0e@h^{i-$$~>bOFZItISGR;j$xdO3+IQY=rFmmsMS*rCU|#doqWkyIOQ(!R@VVlM`3`I@4eE{|^vH-Io9W literal 0 HcmV?d00001 diff --git a/ext/exif/tests/heic_variable_iloc.phpt b/ext/exif/tests/heic_variable_iloc.phpt new file mode 100644 index 000000000000..6a780134a726 --- /dev/null +++ b/ext/exif/tests/heic_variable_iloc.phpt @@ -0,0 +1,14 @@ +--TEST-- +HEIC EXIF metadata with variable-width iloc entries +--EXTENSIONS-- +exif +--FILE-- + +--EXPECT-- +heic_variable_iloc.heic: ZopiTech +heic_variable_iloc_extent_count.heic: ZopiTech Lab diff --git a/ext/exif/tests/heic_variable_iloc_extent_count.heic b/ext/exif/tests/heic_variable_iloc_extent_count.heic new file mode 100644 index 0000000000000000000000000000000000000000..d11d54939e007357759ae2ce2e64ad01c32433d7 GIT binary patch literal 1294 zcmbVLNlX)A6#oC2f2K24q*K~b35K?`BH*Dd6|pgqMbHuxBcLL;wzPJPl*KC4#6)p7 z(cr!$CW;$+a>MN;?s_m0Q2{UR+rfytUh4a&qb7!f`r7xu?=Am(@0)26LgsKR-V_PR zK?Fi=gB*4Nlp8`ZKaGK8%W^cVAPlDwoDf4@4MOE@vXZgM3mcUdQ;QL%36S-G6d|jV zcDR#lY^e9g5F#s+t$e;bBtRi3CQ5^1@UV)VfKxGpQoshFyAE(F;3mL&%qsPCs2k!2 zu*<=b*CzW!!FQ}n;y%D0$ib8j4I@vtBB5``FtAtOR z<=5Dn4ek}2_Y2vjCwh!kci!0x0xj*L)|74@o#7apGcGsJGof%|(Iju#)bfgH(|wiI zHM8f;t({jFtPh3fN8|;IVvCoww#ApOT(x@5+I8zUY}vYP`;MKvcJJvtaPZLKBV9+2 zoji5A`^?#M=Pz8keC6u3>o;!R>b=`{@BV}Sfx*X5o<4j2;^nK?Z-(A~`1tAbm#^Qx zQ(hQ;T0do2UJ~$P2w{na^1@6jB`y)p>f-e!vo!wZG@E;cU?|