An integer overflow vulnerability exists in Apple's EXR image decoder within the ImageIO framework, where crafted EXR images with large width and height values cause the buffer size calculation to wrap around to zero, resulting in a heap buffer overflow when the decoder allocates a tiny 16-byte buffer and attempts to copy large amounts of pixel data into it, potentially enabling memory corruption exploits on iOS and macOS devices.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
This image crashes your iPhone (iOS 26 Vulnerability)Added:
This image is made up of carefully crafted data that is capable of triggering a memory corruption vulnerability on iOS and Mac OS devices.
In its most innocent form, it can crash various system apps and background processes. But at its most dangerous, it could be used as an entry point to deliver an exploit payload to a victim's iPhone. Simply viewing the image on your iOS device is enough for it to trigger an integer overflow bug in one of Apple's system frameworks and then trick the image decoder into writing huge amounts of pixel data into adjacent memory, corrupting important pointers and data structures. Let's take a look at how I managed to generate this image and exactly how it is capable of causing memory corruption. The image is in the EXR format, which according to the official spec is a format used to accurately and efficiently represent high dynamic range seen linear image data. But that's not really important.
What is important, however, is how Apple decodes these images. Image files come in all kinds of formats and types, and in order for your phone to be able to render them properly, it needs to have decoders built into the system capable of understanding each of the formats.
These decoders can of course be prone to bugs and sometimes even critical vulnerabilities as we've seen previously in iPhone exploits such as blast pass used in the Pegasus spyware. For the majority of image formats, Apple provides a decoder inside their image IO framework. This is a big chunk of shared code that system processes and normal iPhone apps can use in order to render images. Obviously, if you're developing an app, you don't want to manually implement your own paraser for every possible image format. you instead call into Apple's pre-ompiled image IO framework and make use of the decoders provided there. Image IO provides a decoder for images in the EXR format.
And it is within a specific function in this decoder that an integer overflow bug occurs during the calculation for the size of some buffer. But let's quickly rewind a bit and actually understand how we got to this point.
This bug was not originally discovered by me. Although as of the time of recording this video, I think I'm the first to publicly discuss its details.
The bug was actually found by another security researcher and was patched in Apple's recent iOS and Mac OS 26.5 update. On the security notes page, we can see that iOS 26.5 fixed three separate vulnerabilities within the image IO framework. When I saw this, I decided to do some patch diff to figure out what had changed between the two versions. Patch diffiffing is something we've looked at previously on this channel and in its simplest form it refers to the process of comparing two versions of the same codebase and deriving information from it. Usually related to security issues by observing what the newly patched code is checking for that the older code missed. It's a very common technique used among the security community and it's a really good exercise to practice vulnerability research as it shows you real life examples of what to look out for when hunting for bugs. So I decided to patch diff the image IO library by comparing the 26.5 version to the prior 26.4.2 version. And this is how I uncovered the details of this specific EXR bug using IDA Pro as my disassembler and decompiler of choice. I was able to identify which functions had been modified between the two versions and analyze the exact lines of code that were changed. This is where I found the function EXR read plugin decode block Apple EXR. This function is part of the C++ class used for decoding EXR images.
And an important check was added in version 26.5. Look at this snippet of code here. The decoder is trying to calculate the size needed for a buffer which it will allocate using Malo. To calculate the required size, it performs this multiplication. That is the number of channels multiplied by the width and height of the images data window. This of course makes sense if we infer that this is some buffer to store the image data. For example, you can see how you'd need to multiply the data windows width and height together in order to get the total pixel number and then multiply this by the amount of channels per pixel. Now, here's the issue. The newer patched version of the code adds a check around this multiplication. The is mole okay that we see here is an integer overflow check that ensures that the result of this calculation has not wrapped around due to the values being too large. If you're unfamiliar with integer overflows, let's take a quick look at how that would work. Integers are stored as binary values and in this case 32-bit binary values. 32 bits means that the largest value an integer could possibly be is 4.29 billion or so or in hexadeimal this is ox fff fff. This is the value when all 32 bits are set to one. Now what happens if we have the maximum 32-bit integer value and we try to add one more. For example, if we look at it in decimal, that 4.29 billion number becomes 4.29 billion and just one more ending in a six instead of a five.
But what does that actually look like in binary? As you can see, the binary representation for this number is actually a 1 followed by 32 zero bits.
And the one bit that was actually set is now outside of the 32-bit range. So if a piece of code is working specifically with 32-bit unsigned integers, when we add one to the maximum 32-bit value, the variable being used doesn't actually have a way to store that extra bit because it is outside of the 32-bit size of the variable used. And since all of the other bits have been set to zero, we actually end up with a value of just zero. That upper bit gets completely lost and our program is none the wiser.
This is known as an integer overflow or a wraparound. By adding beyond the maximum value for a given integer, we have essentially wrapped back around to the lowest possible value. So if we look back at the patch diff between the two versions, we can see the 26.5 code has added a new check that will detect this type of integer overflow bug on the multiplication that takes place here.
And if it does overflow, it will bail out of the decoding operation and return an error. From this we can gather that on the prior version if an integer overflow occurred during the multiplication then it went completely unnoticed and presumably led to unwanted side effects. So at this point you might be thinking yeah I understand how an integer overflow works but it still seems pretty uninteresting. How could something like a single bit going over the maximum size cause any real issues?
It is of course all about the context of the overflow and more specifically what the code does next. As we discussed, the multiplication in question here is used to calculate the size to use in a buffer allocation. The buffer will be allocated using Malo and Malo's first argument is the size in bytes that the buffer should be. So if we had an image of 10x 10 pixels and four channels, what should happen is that the program would multiply these values together and the result of 400 will be used in the call to malo to allocate a buffer of 400 bytes. The program will then do what it needs to do in terms of copying the image data into this buffer and continuing the decoding process. But what if we had an image with absurdly large width and height values? What if the width and height values have been set in such a way that when multiplying them together, we reach a value that falls outside of the 32-bit integer limit and the result of that calculation wraps around to zero due to the integer overflow. This is exactly the problem.
the decoder will still proceed to call malo and it will pass this incorrectly calculated size of zero to the call. Now I would have actually assumed that calling maloc with a size of zero would just return a null pointer or in other words would fail and not give you a buffer. But actually this is not the case at least on iOS and Mac OS. Calling Malo with zero still allocates the memory for you and it returns a pointer to it. it just defaults to using the smallest available allocation size which on iOS and Mac OS is 16 bytes. So at this point the issue might be more obvious to you if we have an image file with enormous width and height values due to the faulty calculation the decoder will allocate a mere 16 bytes for the buffer. This is hugely problematic because as soon as the decoder tries to copy the image data into the buffer, it will almost immediately begin writing out of bounds, corrupting adjacent memory in the process because the buffer is simply not big enough for the data that it was intended to hold. This is known as a heap buffer overflow vulnerability. Up until now, this has all been theory based on reading the code between the two iOS versions. But how can we actually generate a real image file capable of triggering this integer wraparound and subsequent heap overflow?
First, we'll need to set up a small program that will actually call into image IO and more specifically the vulnerable function whenever I supply an image file. I built a simple harness program based on the code used in this blog post about image IO fuzzing and I'll leave the link down below.
Secondly, we of course need a real EXR image file to work with. I started by googling for EXR sample images and I downloaded a couple. I ran these through my harness program and I used LLDB to set a breakpoint on the vulnerable function to confirm that I was triggering the correct code path. Here I ran into a small issue. While I was hitting the vulnerable function, I was never actually reaching the buggy calculation code because that block was actually guarded by this decode to SDR flag check. I did some digging and I tried to use a bunch of other EXR image samples to see if they would hit it. But eventually I discovered that this was actually an option that you configure in the dictionary that I was passing into the original decoding function. So I had to amend the harness program to include this decode request key and a value of CG image source decode to SDR to tell image IO to follow this specific block.
With that, I reran the harness under LLDB and this time my breakpoint was hit successfully. So, we have a way to reach the vulnerable calculation code. Now, it just becomes a challenge of generating an EXR image with a width and height that result in this overflow. Upon minimal investigation into the EXR format and specifically the header fields that it uses, I discovered that the width and height values for the data window were defined as constants right here within the file. So, my initial approach was to just modify a legitimate EXR file inside a hex editor to change the width and height to something very large. I did this and I tried to run it through the harness again. However, as you can see from this error right here, image IO had actually bailed out early as it considered the EXR file to be corrupted. So, it seemed that changing the width and height values alone wasn't going to work. I probably needed to generate my own EXR from scratch somehow so that the pixel data was still in line with the defined width and height. I asked Claude to write a short Python script to do this and this actually worked right out of the box. It built a very simple script that manually constructs the header fields of an EXR file and then appends the appropriate pixel data based on the width and height of the image. So with this, I could configure the values used for the width and height and the script would generate a non-corrupt output image that I could run through the decoder. In the end, this is the proof of concept image that I managed to generate. It has a width of hex 4,000 and a height of hex 10,000 and a channel count of four. When multiplying the three of these values together, we see that we of course get the integer overflow that we intended to have. And the result is actually hex 1 followed by 8 zeros, which when interpreted as a 32-bit value is just zero. So let's run this through the harness and see what happens. We see we hit the break point again. And now if we single step through, we can observe the buggy calculation happening in real time. We see these mul instructions used to multiply values. And we see the output value has wrapped around to zero just as we'd hoped. We continue on a bit and we see that the call to malo is being made with a size argument of zero.
And once malo has returned, we can see that it's given us a pointer to a small allocation of just 16 bytes which we can verify by calling maloc size. Now if I let the program continue normally, we see that we almost immediately hit crash with an invalid access violation due to the memory corruption caused. We've confirmed that the generated image file satisfies the exact conditions required to cause memory corruption. Anywhere on the system that tries to decode this image should be vulnerable to the exact same issues. This includes opening it in the previews app or the iOS photos app which as you can see causes those applications to crash. But it could also include more critical system processes.
Anything that calls into the vulnerable image IO decoder is vulnerable to this issue. So, what about exploitability?
What could you really do with this bug?
Memory corruption is, of course, much more serious than just making your friend's apps crash. Many similar style bugs within Apple's image and video processing code have been known to have been used in advanced exploit technology against iPhone users. So while to a novice crashing a decoder might seem nothing more than a small inconvenience, if a dedicated attacker is able to craft an image that not only triggers the heap overflow but also carefully arranges object in memory in such a way that the corruption to the memory would result in important data being manipulated. They could leverage such a bug to actually take control over the code execution inside a given app. This could then be used to actually kickstart a full remote exploit chain on the victim's iPhone, just as we've seen in past attacks, as I mentioned, the Pegasus Blastpass exploit, which I covered in depth in another video. Check that out if you're interested in how a seemingly dumb bug like this can be used to gain full remote code execution on iOS given enough dedication and creativity. The full written analysis of this EXR vulnerability is available on my blog at zygosc.com.
You can also find the sample image file and the generation scripts on the GitHub repo. Let me know what you thought of this video down below in the comments.
Thanks for watching and I'll see you in the next one.
Related Videos
Agentforce NOW AMA: Build with React and Salesforce Multi-Framework
SalesforceDevs
490 views•2026-05-28
How agent o11y differs from traditional o11y — Phil Hetzel, Braintrust
aiDotEngineer
450 views•2026-05-28
WEB TECHNOLOGIES UNIT-2 | Degree 4th sem BCOM Computers web technologies unit-2 full explanation💯✅
LearnwithSahera
1K views•2026-05-29
More tests are always better? How to use AI to identify tests that bring little value
Alliance4Qualification
335 views•2026-05-29
Search Algorithms Explained in 60 Seconds! 🤖💨
samarthtuliofficial
218 views•2026-06-01
People of Game of Thrones using JavaScript DOM
AltCampus
296 views•2026-05-30
Introduction to Problem Solving Part - 1 | Lecture 1 | Intermediate DSA
ascensionix
107 views•2026-05-29
🚀 BCS613C Compiler Design | Module 1 to 5 Schema Evaluation 🔥 | VTU 6th Sem 💯 #VTU #bcs613c #exam
Pranavaa-y4y
104 views•2026-06-02











