
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
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.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.
Upgrade. Move to ImageMagick 7.1.2-33 or 6.9.13-58, which contain the fix.
Defense in depth for operators. Set
<policy domain="profile" rights="none" pattern="xmp"/>inpolicy.xmlto reject XMP profiles outright, and monitor worker RSS — a steady climb after image uploads indicates a leak.
References
- CVE-2026-116154 — NVD
- GHSA-8ghq-p369-g395 — ImageMagick advisory
- CWE-401: Missing Release of Memory after Effective Lifetime
- CWE-400: Uncontrolled Resource Consumption
TIMELINES
- Sep 27, 2026 — Vulnerability discovered during source-level audit of
xml-tree.cand confirmed with ASan and valgrind on ImageMagick 7.1.2-33 (commit7656865) - 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