CVE-2026-86950: The Great Glyph Grift
An in-the-wild iOS bug with a possible WhatsApp zero-click path.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
An in-the-wild iOS bug with a possible WhatsApp zero-click path.
Dion Blazakis, Josh Maine, and Anna Groza
Apple recently released iOS 26.7.1 to fix a bug in CoreGraphics (CVE-2026-86950). The security advisory stated that the vulnerability was reported by Meta Product Security and "may have been exploited in an extremely sophisticated attack against specific targeted individuals." For those who are unfamiliar with Apple’s English dialect, it means that the bug was actually exploited in the wild. As usual, Apple provides no meaningful technical information for our curious minds. Well, the research team at Calif has an unusual affinity for zero-click attacks (check out WeWorm) so we decided to investigate the bug and share the details with everyone.
Through patch analysis, we identified a unit conversion error introduced by a compiler optimization. A malicious PDF containing a specially crafted font can trigger the bug during rendering. When CoreGraphics estimates how much memory it needs to draw a letter, the bug could break the arithmetic leading to an undersized allocation. It then draws using positions outside that memory, causing an out-of-bounds write. We provide a simple PoC that triggers the bug on both macOS and iOS.
We started from the mechanically generated ipsw-diffs report:
Based on this output, every changed function in CoreGraphics was prefixed with aa_.... These functions are known, to our friendly neighborhood agent at least, to be part of the CoreGraphics anti-aliased path rasterizer. An instruction-level binary diff between the CoreGraphics frameworks on 26.7 and 26.7.1 shows that the same code pattern has been patched over 20 times throughout these 8 functions. Looking closely at the changes, this release fixes a small inline function used throughout the CoreGraphics path scan-conversion code. Specifically, the patched function is responsible for converting a floating point coordinate value into a fixed point coordinate value.
While this path rasterization code is used throughout CoreGraphics, the Apple advisory suggests the vulnerability has been triggered by remote attackers and exercised when processing a “maliciously crafted file.” Given the credit for this advisory (Meta Product Security), we next looked at recent changes to WhatsApp to provide some clues about which file formats might have been abused.
We grabbed two recent WhatsApp versions (26.37.73 and 26.38.74) and began reviewing the changes. WhatsApp includes some heuristic checks to determine how risky an attachment might be via the WAAttachmentChecker class. Meta’s Kaleidoscope framework is used to parse and score different formats, with a focus on safety under the assumption they are not yet trusted, before the heavier platform-specific processing occurs. Kaleidoscope is developed in Rust with the goal of providing a safe cross-platform file format evaluation system. Changes to Kaleidoscope PDF processing code immediately jumped out to us.
The latest WhatsApp code added an optional strict PDF analysis and scoring feature. When analyzing an attachment, a new feature flag (checked using the ks_pdf_strict_validation_enabled function) enables deeper PDF validation. To support the strict mode, Kaleidoscope now includes support to parse PDF object streams including embedded FontFile object streams. The parsing and validation logic either passes the PDF as benign or produces a set of new font-based defect tags:
If any of these tags are returned by the new parser, a high risk score is returned to the WAAttachmentChecker. The attachment checker will treat risky files with more caution, preventing automatic parsing.
Given these changes, we decided to dig deeper into how the Apple system uses the CoreGraphics path rasterization via PDF parsing (and, more specifically, during font glyph rendering).
Imagine drawing a big “d” on the screen. The shape of the letter is called a glyph, and its outline can be described by a set of vector paths made up of lines and curves. To render a glyph to the screen, the scalable vector paths must undergo rasterization: deciding which pixels on the screen to fill. This is also called scanline conversion, named after the horizontal CRT scan path. If you don’t want your text to look like an Atari game score screen, your rendering engine must support anti-aliasing, which smooths jagged edges by partially coloring pixels along the outline. Anti-aliased rasterization uses a sub-pixel representation to estimate the alpha (or transparency) value of a rendered pixel to effectively blend partial pixels along the border of a shape with the background colors. The larger the percentage of a pixel the path and fill covers, the less transparent the pixel. The CoreGraphics code uses the same underlying code and algorithms to render a font glyph as it would any other path in the rendering engine.
The stages of an anti-aliasing rasterization pipeline when rendering the “d” glyph
When rendering a glyph to a bitmap, the rasterizer will walk across each scanline (horizontal line of pixels), collect the edges of each path that cross that scanline, and then accumulate the alpha value at each pixel based on how each edge crosses the pixel (including which side of the edge is the filled side). Notice that the rasterizer, to be able to support anti-aliasing, must keep track of the path at a resolution higher than the pixel resolution. It is during this process, computing the alpha mask, that those aa_* CoreGraphics functions are used.
While the path and coordinates are stored as floating point values, the CoreGraphics anti-aliased rasterization code uses a fixed point format that represents each pixel as a 4096 x 4096 sub-pixel box. Converting a floating point coordinate value to a fixed point integer is almost as simple as multiplying by 4096 (with a 0.5 addend to ensure it rounds to the nearest subpixel):
static inline int32_t aa_double_to_fixed(double px) { double v = 0.5 + 4096.0 * px; return (int32_t)v; }
The 26.7.1 patch modified this inline function to ensure the fixed point value does not overflow the integer when cast:
static inline int32_t aa_double_to_fixed(double px) { double v = 0.5 + 4096.0 * px; if (isnan(v)) return 0; if (v < -1073737729.0) return -1073737729; // -0x3FFFF001 if (v > 1073737729.0) return 1073737729; // 0x3FFFF001 return (int32_t)v; }
Looking closer at the usage of this conversion function among the patched outer functions, the device coordinates may be bound prior to conversion. For example, see the pseudo code for aa_moveto showing the flags-based device coordinate clamping prior to conversion:
void aa_moveto(aa_ctx *ctx, float x, float y) { double dx = x, dy = y; if (ctx->factor != 0.0) { dx *= ctx->factor; dy *= ctx->factor; }
if (ctx->flags & 0x30000) { (dx, dy) = clamp(dx, dy, ±1e15); outcode = cohen_sutherland_outcode(dx, dy, ctx->clip_rect); (dx, dy) = clamp(dx, dy, ctx->clip_rect); } // else: dx, dy are whatever the caller passed
ctx->cur_x = aa_double_to_fixed(dx); ctx->cur_y = aa_double_to_fixed(dy); }
To reach the patched vulnerable code, we have to ensure the functions are reached with the context flags masked by 0x30000 clear. Chasing the initialization of the context flags, the rasterizer context is created with the aa_create function and this function is only called in two locations:
The first, ripr_Acquire, creates the context with the flags set and initializes a device coordinate clipping rectangle via a call to aa_clipping. The second, CGGlyphBitmapCreateWithPathAndDilation, creates a context with flags clear and never sets up a device coordinate clipping rectangle. In other words, to reach the vulnerable code, we must trigger the rendering of our glyph via CGGlyphBitmapCreateWithPathAndDilation.
After some work with our friendly agent, we found a path between the CoreGraphics font rendering entrypoint and the patched rasterization functions (under macOS for ease of testing). For the harness, we can use CGContextShowGlyphsAtPositions to render a glyph from a loaded font file:
#include <CoreGraphics/CoreGraphics.h>
int main(int argc, char **argv) { CGDataProviderRef dp = CGDataProviderCreateWithFilename(argv[1]); CGFontRef font = CGFontCreateWithDataProvider(dp); CGGlyph glyph = CGFontGetGlyphWithGlyphName(font, CFSTR("A")); CGContextRef ctx = CGBitmapContextCreate(NULL, 64, 64, 8, 64, CGColorSpaceCreateDeviceGray(), kCGImageAlphaNone); CGContextSetShouldSmoothFonts(ctx, false); CGContextSetFont(ctx, font); CGContextSetFontSize(ctx, 1024); CGContextShowGlyphsAtPositions(ctx, &glyph, &CGPointZero, 1); return 0; }
Compiling and running this with a breakpoint on aa_cache_render and then grabbing a backtrace shows:
$ lldb ./render_font ./trigger.ttf... (lldb) c Process 85311 resuming Process 85311 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x00000001920b55f0 CoreGraphics
aa_cache_render CoreGraphicsaa_cache_render: -> 0x1920b55f0 <+0>: pacibsp 0x1920b55f4 <+4>: stp d9, d8, [sp, #-0x70]! 0x1920b55f8 <+8>: stp x28, x27, [sp, #0x10] 0x1920b55fc <+12>: stp x26, x25, [sp, #0x20] Target 0: (render_font) stopped. (lldb) bt - thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
- frame #0: 0x00000001920b55f0 CoreGraphics
aa_cache_render frame #1: 0x00000001920b13c0 CoreGraphicsCGGlyphBitmapCreateWithPathAndDilation + 1560 frame #2: 0x00000001920aef24 CoreGraphicsCGGlyphBuilderLockBitmaps + 1352 frame #3: 0x00000001920ae8f0 CoreGraphicsrender_glyphs + 296 frame #4: 0x00000001920ae098 CoreGraphicsdraw_glyph_bitmaps + 880 frame #5: 0x00000001920adc58 CoreGraphicsripc_DrawGlyphs + 1536 frame #6: 0x00000001920ad414 CoreGraphicsCGContextDelegateDrawGlyphs + 344 frame #7: 0x00000001924abeb4 CoreGraphicsdraw_glyphs.18922 + 540 frame #8: 0x000000010000061c render_fontmain(argc=<unavailable>, argv=<unavailable>) at render_font.c:15:5 [opt] frame #9: 0x000000018af13da4 dyldstart + 6992
So far, we’ve proven that we can reach the patched code using CoreGraphics font rendering. But how can we turn this fixed-point coordinate wrapping into memory corruption? It wasn’t so simple.
As discussed earlier, one step of the full glyph rendering process includes rendering the glyph path components into an alpha mask bitmap. This process is done via the aa_cache_render call. The glyph’s CGPath is constructed inside CGGlyphBitmapCreateWithPathAndDilation prior to the aa_cache_render call. The rendering call walks the list of edges for each scanline storing an alpha value for each column. After all edges for a single scanline are processed, the alpha mask of the row is used to render the bitmap row.
Computing the alpha mask for a single scanline of the “d” glyph
The temporary “coverage buffer” is used to track the current scanline’s alpha values across all row intersecting edges. This buffer is allocated early in aa_cache_render and reused for each row. The buffer is sized based on the CGPath’s bounding box:
width_px = (bbox_max_x − bbox_min_x) / 4096; coverage = (width_px > 1015) ? malloc(4*(width_px+16)) : alloca(4*(width_px+16));
Interestingly, the coverage buffer can be located on the stack or heap depending on the width of the bounding box in pixels. The bounding box size tracking can be corrupted due to the vulnerable fixed point conversions leading to an out of bounds. While the corruption occurs in aa_cache_render, the root cause is in the conversion from path operations to edges and the tracking of the path bounding box.
A scalable font glyph can be represented as a set of path operations such as MoveTo, LineTo, QuadTo, …, Close. CoreGraphics represents these portions of a path as CGPathElements of CGPathElementType. To render the glyph, the CGGlyphBitmapCreateWithPathAndDilation function creates a rasterization context, iterates over each path element, and calls the anti-aliased rasterizer helper function corresp
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。