-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The container says size zero. The parser keeps reading.\n\n`poc.jxl` is 149 bytes. It is a standard ISOBMFF wrapper containing a JPEG XL codestream. The first 12 bytes are the JXL signature box. The next 20 bytes are the `ftypjxl` box. Then comes a `jxlc` codestream box whose 32-bit size field is `00 00 00 00`. In ISOBMFF that is not a zero-length box. It is the spec's reserved value for \"this box extends to the end of the file.\"\n\nThe codestream that follows is a valid, short JPEG XL codestream. Then, inside the region the outer `jxlc` box already claimed for itself, the file embeds a second `jxl ` and `jxlc` sequence with identical codestream parameters.\n\n```\nBox: JXL signature (12 bytes)\nBox: ftyp jxl (20 bytes)\nBox: jxlc size=0 (extends to EOF)\n body of jxlc:\n [valid codestream bytes]\n [second jxl / jxlc box markers]\n [second codestream bytes]\n```\n\nThe container layer and the codestream parser are operating on different layers, and the bug lives in the gap. The outer box says \"every byte to EOF is mine.\" The parser inside its body walks the bytes looking for codestream markers and treats the second `jxlc` it finds as a new stream to initialize. The pipeline object built for the first stream is reused; the second stream's setup releases the first's `std::vector` backing buffer. The wrapper object retains pointers into that buffer. The wrapper object's destructor has not run yet.\n\nThis is the shape we name `duplicate-chunk-reinit`: a parser that treats a second occurrence of a stream-defining marker as a signal to reset per-stream state in place, releasing the first cycle's allocations as a side effect, while an outer wrapper that owns the lifecycle keeps the original pointers and reaches them at teardown.\n\n## The destructor walks the buffer the decoder already freed.\n\nThe vulnerable structure is a `std::vector` at offset `+0x1330` (decimal 4912) inside the pipeline object. When the caller eventually invokes `JxlDecoderDestroy`, the destructor chain runs:\n\n```\nJxlDecoderDestroy\n └── sub_23547A8F0 // swap and destroy pipeline\n └── sub_23547B140 // vector destructor entry\n └── sub_235460544 // iterate backing buffer backwards\n └── sub_235451C2C // per-Stage destructor, per element\n```\n\n`sub_235451C2C` is the per-element teardown. With the live vector it walks a real `Stage` object and runs its cleanups. With the freed vector it walks whatever now occupies the freed allocation. Under Guard Malloc that is `0xaaaaaaaaaaaaaaaa`, the scribble pattern, and the destructor segfaults indirecting through it. That is the crash signature in the PoC's expected output.\n\nUnder an attacker controlling what occupies the freed allocation, the per-element destructor performs three observable operations against attacker-supplied pointers loaded from that buffer:\n\n```c\nv1 = *(element_ptr + 24); // read from freed vector buffer\natomic_fetch_add(&ref, -*(v1 - 24)); // write at (v1 - 24)\nfree(*(v1 - 32)); // read at (v1 - 32), then free that pointer\n```\n\nBy controlling `element_ptr+24`, the attacker steers `v1`. By controlling what sits at `v1 - 24` and `v1 - 32`, the attacker steers both an atomic write to an arbitrary 8-byte slot and a `free` of an arbitrary pointer. That is a write-anywhere primitive, a read-anywhere primitive (the value loaded from `v1 - 32` is the next iteration's seed), and an arbitrary-free primitive, all from one allocation in a fixed-size bucket roughly 6144 bytes wide.\n\nThe PoC ships two ways to land the bucket. `interpose_free.c` hooks `free` directly and overwrites the freed bytes with crafted contents the moment they are released. That is the lab-clean way to prove the primitive exists. The real-world way is heap massage: pick another caller in the same parse whose input lands the same ~6144 bucket, fill it with the eight-byte pointer pairs the destructor expects, and let `JxlDecoderDestroy` walk the assembly. AppleJPEGXL has plenty of attacker-influenced allocations in its decode path. The PoC author chose the interposer because the interposer is faster to write, not because the heap-massage path is hard.\n\nWhat looks at first read like a duplicate-free quirk is a write-anywhere, read-anywhere, free-anywhere primitive inside a sandboxed but JIT-bearing process.\n\n## ImageIO is the reach.\n\nThe PoC's entry point is `CGImageSourceCreateWithData` followed by `CGImageSourceCreateImageAtIndex`. This is the ImageIO API surface every macOS process that displays an image goes through. The list is not short.\n\nSafari's WebContent calls ImageIO for every `` tag on every page. JPEG XL has been a default-on content type in Safari since version 17, which shipped in 2023. A web page with `` triggers the bug zero-click inside WebContent.\n\nMail calls ImageIO when rendering inline images in HTML email. An email with an inline JXL image triggers the bug the moment the message is displayed in the preview pane. The user has not clicked on the attachment; the user has clicked on the message.\n\nMessages renders inline image previews automatically when a JXL attachment arrives in a conversation that is already open. AirDrop renders a preview thumbnail on the recipient's screen before the recipient accepts or declines the transfer. Spotlight calls ImageIO to extract thumbnails during file-system indexing, which means a JXL file dropped into any indexed location is decoded by the OS before any user interaction. Finder calls ImageIO for the thumbnails in icon view. Quick Look calls ImageIO when the user presses Space on an image in Finder. Preview calls it on every open.\n\nA 149-byte JXL file delivered to any of those surfaces decodes through `AppleJPEGXL.framework`. The disclosed PoC stops at the crash; the PoC does not need to do more. Anything that decides what bytes occupy the bucket reachable from the ~6144-byte freed allocation gets register control on the per-Stage destructor's first dereference.\n\n## The comm page hosts the bounty's targets. The PoC reads them.\n\nThe two odd globals in `interpose_free.c` are not invented. `_COMM_PAGE_ASB_TARGET_VALUE` lives at `_COMM_PAGE64_BASE_ADDRESS + 0x320`. `_COMM_PAGE_ASB_TARGET_ADDRESS` lives at `+0x328`. The commpage is the read-only region the kernel maps into every userspace process at a fixed address. Apple ships these two slots specifically for Apple Security Bounty submissions. The values change between boots.\n\nA researcher demonstrating that a primitive yields register control fills the freed buffer with the `TARGET_VALUE` and submits the crash log, in which the corrupted register state shows the value across `x0`, `x1`, `x16`. A researcher demonstrating arbitrary read and write fills the freed buffer with `TARGET_ADDRESS` and submits a log showing the destructor's writes and reads land at exactly that address.\n\n```c\n__attribute__((constructor))\nstatic void init_interpose(void) {\n g_asb_target_value = *(volatile uint64_t *)ASB_TARGET_VALUE_ADDR;\n g_asb_target_address = *(volatile uint64_t *)ASB_TARGET_ADDRESS_ADDR;\n ...\n}\n```\n\nThis is Apple's own validation protocol. Apple built the institutional machinery to evaluate primitive submissions rigorously. They shipped the kernel hooks that let researchers prove register control and arbitrary memory access against per-boot, per-machine target values. They published the protocol on the same page as the bounty schedule. The PoC's `interpose_free.c` reads both slots, fills the freed AppleJPEGXL buffers with the target address in `arbrw` mode, and lets the destructor's reads and writes hit Apple's own commpage flag for the bounty reviewer to confirm.\n\nThe protocol exists because Apple wants verifiable submissions. The PoC submits a verifiable submission.\n\n## The bounty schedule names six figures for the category demonstrated here.\n\nApple's published bounty schedule names \"WebContent sandbox escape\" via \"navigating to a malicious webpage in Safari\" at up to three hundred thousand dollars. The network-attack-with-user-interaction tier going through kernel arbitrary code execution tops at one million dollars. The same page that publishes those numbers is the page that publishes the commpage target protocol the researcher's submission used.\n\nWhat the PoC demonstrates is not a sandbox escape. It is the building block of a sandbox escape: register control on a writeable instruction stream inside WebContent, an arbitrary write to any address the attacker chooses, an arbitrary `free` of any address the attacker chooses, and an arbitrary read of any address the attacker chooses, all from one `` tag. The researcher's own writeup identifies candidate non-PAC `br x3` gadgets in WebKit but does not develop the chain to code execution.\n\nThe disclosure timeline:\n\n| Date | Event |\n|--------------|------------------------------------------------------------------------|\n| 2026-03-05 | PoC delivered to Apple Product Security. |\n| 2026-05-12 | CVE-2026-28956 assigned. The patch had already shipped silently. |\n| 2026-05-12 | Researcher integrates the commpage target protocol; submits artifact. |\n| 2026-05-15 | Apple Security Bounty awards one thousand dollars. |\n\nThe CVE was assigned the same day the researcher submitted the commpage-validated artifact. Between March and May, Apple held the report, patched the bug, and did not assign a CVE. The CVE arrived after the researcher proved the primitive against Apple's own validation protocol. This is a familiar Apple shape: silent patch first, public number assigned only when the reporter demonstrates the severity bar Apple has already privately accepted.\n\nJPEG XL is one of three image codecs Apple has added to ImageIO in the last four years; HEIF, AVIF, and JPEG XL all expose the same shape of surface to the same set of decoder-reaching surfaces. Every multi-stream container format the framework absorbs is another duplicate-chunk-reinit waiting on a parser whose state machine treats inner markers as instructions to reset. The risk profile of the broad default-on adoption is the structural reason a 149-byte file reaches Mail, Messages, AirDrop, Spotlight, Finder, Quick Look, Preview, and Safari. The codec's adoption was a product decision. The substrate that decision created is what produced this CVE.\n\nThe bounty schedule does not enumerate a \"demonstrated primitive without a developed chain\" line item. That is the discretion the four figures spent. We are not required to guess which line of the schedule the four figures came from.\n\nPoC: [impost0r/CVE-2026-28956](https://github.com/impost0r/CVE-2026-28956). Writeup: [ret2p.lt/2025/05/17/AppleJPEGXL-CVE.html](https://ret2p.lt/2025/05/17/AppleJPEGXL-CVE.html).","closing_line":"Apple's bounty schedule names six figures for the category demonstrated here. The receipt says four.","hook_md":"The PoC for CVE-2026-28956 is a 149-byte JPEG XL image. ImageIO decodes it through the same code path Safari's WebContent uses for `` tags, so the trigger is one tag on any page the target visits. The crash is a use-after-free in `JxlDecoderDestroy`: the destructor walks a `std::vector` backing buffer the decoder freed two callbacks earlier, and a DYLD interposer that scribbles the freed buffer with the Apple Security Bounty target address turns the same primitive into register control plus arbitrary read and write. Apple paid one thousand dollars.","post_id":324,"slug":"applejpegxl-cve-2026-28956-one-thousand-dollars","title":"CVE-2026-28956: One Thousand Dollars For Register Control From ``","type":"initial","unreadable_sentence":"Apple paid one thousand dollars."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCavkTAwAKCRDeZjl4jgkQ Jhq2AP4udoF+7xL2PCEBcNKElnGDqRKydvoCdVG/Kp2xISYg+gEA7YM8dy6I1jhF eU0IEE2eEew/sMO+7i9JO1KxKjiTpAY= =+IhY -----END PGP SIGNATURE-----