Abstract. CVE-2026-65395 is Apple’s ImageIO out-of-bounds write: “processing a maliciously crafted image may result in memory corruption,” fixed “with improved bounds checking.”1 On the iOS 26.4 (23E246) ImageIO, _JPEGFile::copyXMPProperties reassembles Adobe Extended XMP chunks into a heap buffer sized by the attacker’s TotalXMPLength. Before each copy it checks XmpOffset + chunkLength <= TotalXMPLength with a 32-bit ADD W, then forms the destination as buffer + (uint64_t)XmpOffset. An offset of 0xfffffff0 with a 0x20-byte chunk wraps the sum to 0x10, passes the check, and copies 32 attacker bytes to buffer + 0xfffffff0, almost 4 GiB past a 256-byte allocation. iOS 26.7 (23H24) does the add and the compare in 64 bits. Three JPEGs that differ only in that 32-bit field were replayed through the private file-copy entry on the iOS 26.4 VM: the in-bounds and rejected controls returned 3/3; the wrapping input faulted 10/10 inside the call. The same three inputs return normally on macOS 27.0, whose ImageIO has the 64-bit check. Scope: this is a component-level memory fault with a matching binary fix. The faulting PC was never captured because the device refused to store crash reports, no system app was shown to call this entry when “just opening” an image, and there is no code-execution claim.

1. Introduction

Apple credits Mateusz Jurczyk of Google Project Zero and Varik Matevosyan.123 The advisory says nothing about which ImageIO parser is involved. ImageIO in the 26.7 update grew several new size checks, so this note had to rank candidates and test them, not pick the first diff that looked like a bound.

The note is laid out as follows: where Apple lists the CVE (§2); the binaries (§3); the Extended XMP format and the vulnerable copy, with ARM64 from both sides (§4–5); how a file reaches it (§6); the paired-input replay on the vulnerable VM and the crash-report gap (§7); the fixed host (§8); two other JPEG crashes found by the same campaign (§9); candidates that were ruled out (§10); impact (§11); open items and artifacts (§12–13).

Evidence labels used throughout.

  • Apple-confirmed — advisory text and which Apple security pages list the CVE.
  • Patch-observed — ARM64 read on an exact, hashed ImageIO, with the build named on each side.
  • Lab-observed — run on a named VM or host whose build and loaded ImageIO UUID were recorded.
  • Hypothesis — inference not yet tested; never used in a conclusion.

2. Where Apple lists it

Advisory (14 Sep 2026) CVE-2026-65395
iOS 27 and iPadOS 27 present, ImageIO1
iOS 26.7 and iPadOS 26.7 present, ImageIO2
macOS Golden Gate 27 present, ImageIO3
macOS Tahoe 26.7, Sequoia 15.8 present
visionOS 27, tvOS 27 present
watchOS 27 absent
Item Advisory text
Impact Processing a maliciously crafted image may result in memory corruption
Description An out-of-bounds write issue was addressed with improved bounds checking

Apple does not name a last vulnerable build and does not claim code execution for this entry.

3. Binaries

All files are arm64e ImageIO extracted from a dyld shared cache. The vulnerable side is the ImageIO actually loaded in the lab Fuzzer VM (pulled over SSH from the guest’s cache), not a file assumed to match.

Role Build Cache UUID ImageIO UUID ImageIO SHA-256 Source ver.
Vulnerable, lab VM iOS 26.4 / 23E246 5C0E73C3-4103-3A14-99F4-D0269D830F55 A7A1EDD0-F349-3E7E-8713-5AAA3995A5FB d03edda238273cc38b0fc9e7bf1d8ee6fd1c11f6cadbd635cc2dc8ba8612331a 2784.4.10
Fixed, iPhone17,3 OTA iOS 26.7 / 23H24 5625EC35-B6D6-3C10-91B0-5B4C20FB77B7 5108CBD3-6DF2-30CA-A2FB-8B9277209C80 a1e992db6ed219a8e7f7b40d3c9df5ac0d4b19eb8dff8169b4fd056092d84122 2784.6.7
Fixed, lab Mac macOS 27.0 / 26A428 EA2C265E-297C-39C2-8646-7D8A2DFF648A 9F466BFF-CD36-33EB-9240-A9DC03202193 extract 65e76b7c3d0177c6beefd4aae9b0e7855c1f3bc4692088188b4de8c8bd595225 —
Search aid iOS 27 beta 24A5390f → 24A5408d — CCD25E8D-… → DA08C738-… — —

The 23H24 OTA was fetched from Apple’s asset server (AEA SHA-256 9ca0ffdd66390ba91630b276e4133c3ca6506bee189e40c3e35602ce113f4cea). The iOS 27 beta pair is where the change was first spotted. The 26.4/26.7 pair is the one the conclusions rest on. Each UUID got its own IDA 9.4 database; nothing below mixes addresses across builds.

4. Extended XMP in one paragraph

A JPEG APP1 segment holds at most 64 KiB, so XMP packets larger than that are split. The standard XMP packet stays in a normal APP1, and the remainder goes into one or more Extended XMP APP1 segments that the reader stitches back together:4

FF E1                         APP1 marker
LL LL                         segment length, big-endian, includes these 2 bytes
"http://ns.adobe.com/xmp/extension/\0"   35-byte signature
GUID                          32 ASCII hex chars, same in every chunk of one packet
TotalXMPLength  (u32 BE)      size of the full reassembled packet
XmpOffset       (u32 BE)      where this chunk goes in that packet
chunk[LL - 2 - 35 - 32 - 8]   payload

Every length and offset in that header comes from the file. A correct reader has to allocate TotalXMPLength once and check, per chunk, that XmpOffset + chunkLength does not exceed it, in arithmetic that cannot wrap.

ImageIO parses these segments in _APP1ExtendedXMP::processData and stores the GUID, the total, the offset, and the chunk as CFData/integers on the marker object. The reassembly happens later, in _JPEGFile::copyXMPProperties.

5. Root cause: a 32-bit check guarding a 64-bit address

5.1 Vulnerable side, 23E246

_JPEGFile::copyXMPProperties is 0x1865b2fa0..0x1865b3284 (image base 0x18637b000). Hex-Rays output, trimmed to the Extended XMP branch:

v10            = _APP1XMP::getXmpData(v7);              // chunk CFData
GUID           = _APP1ExtendedXMP::getGUID(v7);
v12            = CFDataGetBytePtr(GUID);                // before NULL check (§9.1)
TotalXMPLength = _APP1ExtendedXMP::getTotalXMPLength(v7);
XmpOffset      = _APP1ExtendedXMP::getXmpOffset(v7);    // int, 32-bit
v16            = CFDataGetLength(v10);                  // chunkLength
if (GUID != 0 && CFDataGetLength(GUID) == 32) {
    if (!have_buffer) {                                 // first chunk wins
        guid_saved = *(GUID bytes);
        saved_total = TotalXMPLength;
        buf = malloc_type_calloc(1, TotalXMPLength, 0x100004077774924);
    }
    if (guid matches && TotalXMPLength == saved_total
        && XmpOffset + v16 <= saved_total)   // 32-bit add + compare
    {
        _TIFFmemcpy_0(&buf[XmpOffset], CFDataGetBytePtr(v10), v16);
        total_copied += v16;
    }
}

The ARM64 at the check and the copy:

0x1865b30cc  MOV   W1, W28                      ; size = TotalXMPLength
0x1865b30d0  MOV   W0, #1
0x1865b30d4  MOV   X2, #0x100004077774924       ; malloc type id
0x1865b30e4  BL    malloc_type_calloc
0x1865b30ec  STR   X0, [SP,#buf]
...
0x1865b312c  LDR   W9, [SP,#saved_total]
0x1865b3130  CMP   W28, W9                      ; total == first chunk's
0x1865b3134  B.NE  skip
0x1865b3138  ADD   W8, W25, W27                 ; W25 offset + W27 len, wraps
0x1865b313c  CMP   W8, W9
0x1865b3140  B.HI  skip
0x1865b3144  LDR   X8, [SP,#buf]
0x1865b3148  ADD   X21, X8, W25, UXTW           ; dest = buf + (u64)offset
0x1865b314c  MOV   X0, X26
0x1865b3150  BL    CFDataGetBytePtr             ; src = chunk bytes
0x1865b3154  MOV   X1, X0
0x1865b3158  MOV   W2, W27                      ; len = chunkLength
...
0x1865b3160  BL    __TIFFmemcpy_0

The whole bug is the pair ADD W8, W25, W27 and ADD X21, X8, W25, UXTW. The bound is evaluated modulo 2^32; the address is not.

5.2 What the trigger does, in numbers

Field Value Note
TotalXMPLength 0x00000100 calloc(1, 0x100): one 256-byte block
XmpOffset 0xfffffff0
chunkLength 0x20
W8 = XmpOffset + chunkLength (32-bit) 0x00000010 wraps
check 0x10 <= 0x100 passes
destination buf + 0xfffffff0 4 GiB − 16 bytes past a 256-byte block
bytes written 32, attacker-chosen the chunk payload

The offset and length are each attacker-chosen within their 32-bit fields, so the destination is anywhere in [buf, buf + 4 GiB) and the write is up to ~64 KiB per chunk (one APP1 segment). Several Extended XMP segments with the same GUID and total each get their own copy, so one file can carry several writes. What sits 4 GiB after a small calloc block depends on the malloc layout; in the runs below it was usually unmapped or protected, which is why the observable is a fault rather than silent corruption (§7).

5.3 Fixed side, 23H24

Same function, 0x1865fb33c..0x1865fb650 (image base 0x1863c3000):

0x1865fb4cc  LDR   W8, [SP,#saved_total]
0x1865fb4d0  CMP   W26, W8
0x1865fb4d4  B.NE  skip
0x1865fb4d8  MOV   W21, W21                     ; zero-extend chunkLength into X21
0x1865fb4dc  ADD   X8, X21, W28, UXTW           ; 64-bit: chunkLength + XmpOffset
0x1865fb4e0  CMP   X8, W26, UXTW                ; 64-bit compare against total
0x1865fb4e4  B.HI  skip
0x1865fb4e8  MOV   W25, W28
...
0x1865fb504  BL    __TIFFmemcpy_0

0xfffffff0 + 0x20 = 0x1_0000_0010 > 0x100, so the wrapped chunk is skipped before the destination is formed. 23H24 also moves the GUID/XMP-data NULL checks ahead of CFDataGetBytePtr, which is the fix for the separate NULL dereference in §9.1. The iOS 27 beta pair shows the identical change (ADD W8,W25,W27 at 0x187aa7bac in 24A5390f; ADD X8,X21,W28,UXTW at 0x187b1dc4c in 24A5408d).

6. How bytes reach it

copyXMPProperties is not on the decode path. IDALib finds two callers on 23E246:

Caller Site
CGImageCopyFileWithParameters 0x1865b6a84 (after _JPEGWriter::processInput at 0x1865b6690)
IIOMetadataUpdater::createModifiedJPEGWithDateTime 0x186615da0

Both are rewrite paths: ImageIO copies an existing JPEG into a new file while changing metadata, and carries the existing XMP across by reassembling it. CGImageCopyFileWithParameters is a private, exported ImageIO function; the harness resolves it with dlsym and asks it to set a keyword, which forces the XMP rewrite:

copy_file = dlsym(RTLD_DEFAULT, "CGImageCopyFileWithParameters");
keywords_key = *(CFStringRef *)dlsym(RTLD_DEFAULT, "kCGImageCopyFileSetKeywords");
params = @{ keywords_key : @[ @"cve65395" ] };  // CF in the real harness

CFDataRef input = CFDataCreate(NULL, data, size);        // the JPEG under test
copy_file(input, CFSTR("/private/var/tmp/…/out.jpg"), params);

The hot path is only CFDataCreate → CGImageCopyFileWithParameters → output check. No CGImageSource decode or render runs first, so a fault cannot come from an unrelated parser.

Not shown: a system caller. A bind-symbol search of the 23G90 dyld cache found no importer of CGImageCopyFileWithParameters in Photos, Quick Look, or the import stack. createModifiedJPEGWithDateTime looks like what a “change date and time” edit would use (Hypothesis), but no consumer was traced to it. dlsym and non-cache executables could still reach either function, so “not found” is not “absent.”

7. Replay on the vulnerable VM (23E246)

7.1 Inputs

All three files are the same 5391-byte JPEG with one Extended XMP APP1 inserted right after SOI by build_extended_xmp_jpeg.py; each is 5502 bytes. GUID, total, chunk payload, and every other byte are equal. Only the 32-bit XmpOffset differs.

Input total offset chunk 32-bit sum SHA-256
exact-fit 0x100 0xe0 0x20 0x100 77d07fbe143b3c4dcc75044c2ad7868ebba8e49293211d1b084dd8293ba7e69b
guard-reject 0x100 0xf0 0x20 0x110 b354b232cd95b14457909c1ca87e33ebf3cce016923960ef2f03a37bf95c0879
offset-wrap 0x100 0xfffffff0 0x20 0x10 b34c86a212cf8e27e6bd4c0e09abd2b5ffda296cae2cac510d3a1096965e78d7

cmp -l guard-reject.jpg trigger.jpg shows three differing bytes (78–80, 1-based); byte 81 is 0xf0 in both. Any behavioral difference between those two files is caused by the high 24 bits of the offset, which only matter to the 32-bit add.

7.2 Environment

Item Value
VM vphone-based Fuzzer VM, iPhone17,3, no SpringBoard
OS / build iOS 26.4 / 23E246, guest id 504edb8ca1d1dd05eacbe1ce9daa2082
Loaded ImageIO UUID A7A1EDD0-…, SHA-256 d03edda2… (§3)
Harness imageio_xmp_harness.c, SHA-256 2e46ecf4488104edab6c25420b4993183d01f1fbfaef5ab19aa0b833d3dd1eed; signed build 6ca47c7c42b215fc2c9c9a00b0178edb48e66c08434611ddf3d906c43db2659e
Run / revision 1876977018355856 / 89, native forkserver, 1 worker
Coverage module CGImageCopyFileWithParameters@ImageIO

Replays use a fresh loader process each time. The harness prints COPY_ENTER before the private call and COPY_RETURN after it.

7.3 Result

Input Fresh runs Outcome
exact-fit 3 3× exit 0, COPY_RETURN output_created=1
guard-reject 3 3× exit 0, COPY_RETURN output_created=1
offset-wrap 10 10× killed inside the call: 7× SIGBUS, 3× SIGSEGV; every run has COPY_ENTER and none has COPY_RETURN

That is the expected pattern for §5: the in-bounds copy and the copy the 32-bit check rejects both complete; the one whose wrapped sum passes the check dies inside the copy. The SIGBUS/SIGSEGV split is consistent with a destination 4 GiB out that lands on a protected or unmapped region depending on layout.

7.4 Where the crash reports went

The ten corpses were not lost to tooling. The device’s raw unified log (/private/var/db/diagnostics, 625 MB, pulled by rsync) has, for each of the ten PIDs (16307, 16310–16312, 16314, 16317, 16319, 16325, 16329, 16330):

kernel          Corpse allowed …
ReportCrash     Parsing corpse data for pid …
ReportCrash     Formulating fatal 309 report for …
osanalyticshelper  … not saved because the limit of 25 logs has been reached

So ReportCrash built a fatal report every time and the publisher dropped it at a 25-report quota that earlier campaign crashes had already used. Two of the PIDs (16307, 16314) also logged libsystem_malloc: memory corruption of free block. There is still no .ips for these runs, so no fault PC, no faulting thread, and no ImageIO frame. That is the main gap in this note.

7.5 What the history taught

Earlier attempts are worth recording because each one looked like a result and was not:

  • revision 44–45: the same trigger in a harness that also decoded and rendered the image faulted 5 of 6 times, then returned normally 3 of 3 after one rebuild. The mixed outcome was heap-layout noise from the extra work, not a fix.
  • revisions 50–53: a standalone malloc_type_calloc(0x100) + memcpy(base + 0xfffffff0, …, 0x20) probe faulted 2 of 3. The fuzzer’s coverage map stayed at size 1 for control and trigger alike; the cause was the NO_FORKSRV execution mode, not the target. Coverage only became real (initial map 3887, 7.90% at stop) with the native forkserver in revision 89.
  • AFL artifacts: revision 89 saved 39 crash files. 23 still parse as Extended XMP and none satisfies the wrap predicate offset + chunk > 0xffffffff && sum32 <= total. They are not counted as reproductions.

8. Fixed host: macOS 27.0 / 26A428

The lab Mac’s ImageIO (cache image, UUID 9F466BFF-…) has the 64-bit form:

0x18e13d284  MOV   W21, W21
0x18e13d288  ADD   X8, X21, W28, UXTW
0x18e13d28c  CMP   X8, W26, UXTW
0x18e13d290  B.HI  reject
0x18e13d294  MOV   W8, W28
0x18e13d298  LDR   X9, [SP,#0x10]
0x18e13d29c  ADD   X25, X9, X8                  ; dest formed after check
...
0x18e13d2b4  BL    copy

The same three files, run five times each in fresh processes through the same private entry with DYLD_PRINT_LIBRARIES=1 confirming the ImageIO UUID: 15/15 exit 0 with source=1 copy_entry=1 copy_invoked=1 output_created=1 properties=1. This shows the trigger file is well-formed enough to go through the rewrite and that the fixed check stops it. It is not yet the same harness on iOS 26.7; that is open (§12).

9. Two other JPEG crashes from the same entry

The directed campaign (revision 91, 73 artifacts) produced two stable fingerprints with matching .ips. Neither is the out-of-bounds write Apple describes, and neither is used as evidence for this CVE. Both are recorded because they are fixed in the same 23H24 ImageIO.

9.1 Truncated Extended XMP marker: NULL dereference

One byte changed from the exact-fit control (offset 5: segment length 0x006d → 0x002a), so the GUID never gets parsed. Three fresh replays, three .ips with the same stack:

EXC_BAD_ACCESS / SIGSEGV / KERN_INVALID_ADDRESS at 0x0
CoreFoundation  __CF_IS_OBJC
CoreFoundation  CFDataGetBytePtr+36
ImageIO         _JPEGFile::copyXMPProperties+224
ImageIO         CGImageCopyFileWithParameters+1904

+224 is the return address of CFDataGetBytePtr(GUID) at +0xdc, which on 23E246 runs before the GUID != 0 check (§5.1). 23H24 moves the check first. Denial of service only.

9.2 Exif IFD: source-side out-of-bounds read in _TAG::writeToBuffer

AFL corrupted the ExifIFDPointer entry of IFD0 (count 1 → 4, value 0x1a → 0x7e040100). A minimized input that changes only those five bytes (a84399832c57db87a9748b9202946f3244f8adf0cc22df0192fd6f25338797ac) faults 3/3:

ImageIO  _TAG::writeToBuffer+352          ; 0x1865bc4c0: LDR W9, [X1], #4
ImageIO  _TAGList::writeToBuffer+136
ImageIO  _APP1::writeToStream+1196
ImageIO  _JPEGFile::writeToStream+424
ImageIO  _JPEGWriter::writePropertiesAndData+124
ImageIO  _JPEGWriter::writeToPath
ImageIO  CGImageCopyFileWithParameters+3260

On 23E246 the source pointer is tiffBase + tiffStart + tagOffset with no upper bound. 23H24 adds one:

0x186604880  ADD   X11, X9, X10
0x186604884  ADD   X12, X8, X11
0x186604888  LDR   X11, [X21,#0x10]             ; jpegDataSize
0x18660488c  CMP   X12, X11
0x186604890  B.LS  valid_source                 ; else "out-of-bounds source"

This is a clean crash-plus-fix pair, but it is a read, and Apple’s text says write.

10. Candidates ruled out

Each of these is a real 26.7/iOS 27 ImageIO change that also widens a size computation. Each was tested and failed reachability.

KTX / BC texture writer. BCWritePlugin::writeOne on 24A5390f and 23G90 sizes its output from a truncated block count:

0x187a4ea60  MUL   X8, X8, X0                   ; rowStride = blocksX * blockBytes
0x187a4ea64  MUL   W9, W28, W26                 ; blocksX * blocksY, 32-bit
0x187a4ea68  MUL   X26, X9, X0                  ; allocSize = truncated * blockBytes
0x187a4ea7c  BL    calloc

24A5408d uses UMULL X8, W28, W8 and checks UMULH X9, X8, X0 before the final multiply. A 2564 × 26801668 image is 641 × 6700417 = 2^32 + 1 blocks, so the old code would allocate one block and then write rows of 641. The writer is only reached when a caller creates a KTX destination and asks for the BC encoder. JPEG, PNG, and a Leica M8 DNG were converted to KTX/BC on the VM through public API, but every way of getting a huge image to it failed first: the DNG’s 320×240 IFD0 preview is what gets encoded, editing the RAW SubIFD or using 1,675,105 repeated strips is rejected before CGImage creation, CIRAWFilter clamps a huge DefaultCropSize, and HEIC ispe edits create a large logical image that never reaches the encoder. A viewer-equivalent decode-and-draw harness never creates a KTX destination at all.

IOSurface allocation guard. 24A5408d adds UMULH multiply checks and rowBytes/allocSize <= INT32_MAX caps in IIO_CreateSurfaceWithFormat and the planar helpers. But CopyIOSurfaceProc asks the read plugin canHandleRemoteIOSurface() first, and in all 28 bundled read-plugin vtables on both the old and fixed ImageIO that slot is the base implementation that returns 0. The remote-decoder branch is taken and IIO_CreateSurfaceWithFormat is never called; a JPEG provider control confirmed it.

JPEG marker-length over-read on the current Mac. Found during this audit on 26A428, it has no CVE mapping and is tracked as a separate lab record. It is not evidence for this CVE.

11. Impact

  • What the bug gives: a write of up to one APP1 payload of attacker bytes at an attacker-chosen offset in [0, 4 GiB) from a small, attacker-sized calloc block, repeatable per Extended XMP segment, in whatever process rewrites the JPEG’s metadata.
  • What limits it: the offset is relative to an unknown heap address; there is no read-back; and the destination is far from the allocation unless the offset is small, in which case the 32-bit check does not wrap. Turning this into control flow would need heap layout knowledge the bug does not supply.
  • Who calls it: not established. The evidence covers the private copy entry only.

Apple’s wording (“memory corruption”) matches what was observed. Nothing here supports a stronger claim.

12. Open items

  1. A target-side marker on 0x1865b3138 / __TIFFmemcpy_0 with the live offset, length, total, and destination, and a stored .ips (fresh loader identity or external crash trace) with the fault PC and ImageIO frame.
  2. The same harness, three inputs, and markers on 23H24, showing the trigger hits the 64-bit reject and returns.
  3. A system consumer of CGImageCopyFileWithParameters or createModifiedJPEGWithDateTime, found dynamically rather than by static binds.

13. Record and artifacts

Item Result Label
Fix ADD W/CMP W → ADD X/CMP X in copyXMPProperties, 23E246 → 23H24 Patch-observed
Vulnerable replay exact-fit 3/3 return, guard-reject 3/3 return, offset-wrap 10/10 fault in call Lab-observed
Crash reports 10 type-309 reports built, all dropped at a 25-log quota; no PC Lab-observed (gap)
Fixed host macOS 26A428: 64-bit check; 15/15 return Patch- + Lab-observed
Side findings NULL deref (§9.1), _TAG::writeToBuffer OOB read (§9.2), both fixed in 23H24 Lab- + Patch-observed
KTX/BC, IOSurface real patches, not reachable with any input tried Patch-observed, ruled out
Viewer trigger, code execution not shown —
Artifact SHA-256
harness/imageio_xmp_harness.c 2e46ecf4488104edab6c25420b4993183d01f1fbfaef5ab19aa0b833d3dd1eed
signed harness (deployed) 6ca47c7c42b215fc2c9c9a00b0178edb48e66c08434611ddf3d906c43db2659e
macOS smoke binary 36971c24a9a4def777c423a0254a8033b505737f47d7f8d2a3d78e8beab50203
boundary-exact-fit.jpg 77d07fbe143b3c4dcc75044c2ad7868ebba8e49293211d1b084dd8293ba7e69b
guard-reject.jpg b354b232cd95b14457909c1ca87e33ebf3cce016923960ef2f03a37bf95c0879
trigger-offset-wrap.jpg b34c86a212cf8e27e6bd4c0e09abd2b5ffda296cae2cac510d3a1096965e78d7
tag-oob-read-minimal.jpg a84399832c57db87a9748b9202946f3244f8adf0cc22df0192fd6f25338797ac
NULL-deref .ips ×3 24162d3b…6e0e, c22f38c1…f83b, 5d600067…d220
_TAG OOB-read .ips ×3 f77d0a90…fa59, bc8fc3db…6dc, e0e275f9…d14e

[1] Apple, “About the security content of iOS 27 and iPadOS 27,” 14 September 2026. https://support.apple.com/en-us/149034

[2] Apple, “About the security content of iOS 26.7 and iPadOS 26.7,” 14 September 2026. https://support.apple.com/en-us/149041

[3] Apple, “About the security content of macOS Golden Gate 27,” 14 September 2026. The same ImageIO entry is on the Tahoe 26.7, Sequoia 15.8, visionOS 27, and tvOS 27 pages. https://support.apple.com/en-us/149035

[4] Adobe, “XMP Specification Part 3: Storage in Files,” section on JPEG and extended XMP. https://github.com/adobe/XMP-Toolkit-SDK/blob/main/docs/XMPSpecificationPart3.pdf

End of noteBack to the archive