ANT-2026-3FWTCMVC · wireshark/wireshark
heap-buffer-overflow medium
CVE-2026-15170 GHSA-hfrj-29vh-pcf2
Severity Claude high · Security research firm high · Maintainer medium
Discovered by Claude Mythos Preview
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.
ANT-2026-3FWTCMVC: Z39.50 MARC directory count floor/ceil mismatch overflow
In the Z39.50 dissector's dissect_marc_record(), directory_entry_count is computed with integer floor division and used to size a wmem_alloc0 array, but the subsequent while loop iterates ceil() times over the same region. When the attacker-controlled leader field data_offset makes (data_offset-1-24) not a multiple of the fixed directory_entry_len (12), the loop runs one extra iteration and writes a 12-byte marc_directory_entry past the end of the heap allocation. The sanity check on data_offset only emits an expert-info item and does not abort parsing. A count==0 case (e.g. data_offset=30) additionally yields a NULL-pointer write because wmem_alloc0(...,0) returns NULL. Both are reachable unauthenticated via a single Z39.50 response on default TCP/210 or by opening a malicious pcap.
Target
Project: wireshark/wireshark
Location: epan/dissectors/packet-z3950.c:12862
Discovery: static analysis — not yet dynamically reproduced
Technical Details
Lines 12819-12822 allocate floor((data_offset-1-24)/12) entries, but the while loop at line 12830 (while (offset < (data_offset - 1))) runs ceil((data_offset-1-24)/12) times, writing marc_directory[dir_index].tag/length/starting_character at lines 12904-12906 with no bounds check. data_offset is parsed from 5 attacker-controlled ASCII digits in the MARC leader (line 12748, range 0-99999) and the range check at lines 12756-12762 is non-aborting, so e.g. data_offset=38 allocates 1 entry but performs 2 iterations, writing 12 attacker-derived bytes into the adjacent wmem block_fast chunk header.
Reproduction
- Construct a Z39.50 response carrying an EXTERNAL with OID 1.2.840.10003.5.10 so the MARC dissector is invoked
- In the MARC leader, set data_offset (5 ASCII digits) such that (data_offset - 1 - 24) % 12 != 0, e.g. data_offset=38
- Deliver the packet over TCP/210 to a host being captured, or embed it in a pcap the victim opens
- dissect_marc_record() allocates floor(N/12) entries but loops ceil(N/12) times, writing one extra 12-byte entry past the heap buffer
- Optionally set data_offset so the count is 0 (e.g. 30) to trigger a NULL-pointer write via wmem_alloc0(...,0)==NULL
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Ensure the number of iterated directory entries equals the number allocated: reject (abort dissection of) MARC records whose directory region length (data_offset-1-MARC_LEADER_LENGTH) is not an exact multiple of directory_entry_len, and/or bound the loop by directory_entry_count rather than by offset.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-3FWTCMVC.
Reference: ANT-2026-3FWTCMVC
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Ada Logics.
- Verdict
- true positive
- Severity
- high
The change that resolved this finding.
diff --git a/epan/dissectors/asn1/z3950/packet-z3950-template.c b/epan/dissectors/asn1/z3950/packet-z3950-template.c
index c6a11d3fd13..13160c5ebe4 100644
--- a/epan/dissectors/asn1/z3950/packet-z3950-template.c
+++ b/epan/dissectors/asn1/z3950/packet-z3950-template.c
@@ -1266,6 +1266,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
proto_tree *marc_tree, *leader_tree,
*directory_tree,
*fields_tree;
+ wmem_array_t *marc_directory_array;
marc_directory_entry *marc_directory;
unsigned len = tvb_reported_length(tvb);
const char *marc_value_str;
@@ -1438,10 +1439,8 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
directory_entry_len = 3 + length_of_field_size
+ starting_character_position_size;
- directory_entry_count = ((data_offset - 1) - MARC_LEADER_LENGTH) / directory_entry_len;
- marc_directory = (marc_directory_entry *)wmem_alloc0(pinfo->pool,
- directory_entry_count * sizeof(marc_directory_entry));
+ marc_directory_array = wmem_array_new(pinfo->pool, sizeof(marc_directory_entry));
directory_item = proto_tree_add_item(marc_tree, hf_marc_directory,
tvb, offset, data_offset - offset, ENC_NA);
@@ -1450,9 +1449,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
dir_index = 0;
/* Minus one for the terminator character */
while (offset < (data_offset - 1)) {
- uint32_t tag_value = 0,
- length_value = 0,
- starting_char_value = 0;
+ marc_directory_entry new_entry = {0};
proto_item *length_item;
proto_item *directory_entry_item;
proto_tree *directory_entry_tree;
@@ -1468,7 +1465,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
offset += 3;
if (marc_value_str) {
if (isdigit_string(marc_value_str)) {
- tag_value = (unsigned)strtoul(marc_value_str, NULL, 10);
+ new_entry.tag = (unsigned)strtoul(marc_value_str, NULL, 10);
}
else {
expert_add_info_format(pinfo, item,
@@ -1485,7 +1482,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
offset += length_of_field_size;
if (marc_value_str) {
if (isdigit_string(marc_value_str)) {
- length_value = (unsigned)strtoul(marc_value_str, NULL, 10);
+ new_entry.length = (unsigned)strtoul(marc_value_str, NULL, 10);
}
else {
expert_add_info_format(pinfo, length_item,
@@ -1501,7 +1498,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
offset += starting_character_position_size;
if (marc_value_str) {
if (isdigit_string(marc_value_str)) {
- starting_char_value = (unsigned)strtoul(marc_value_str, NULL, 10);
+ new_entry.starting_character = (unsigned)strtoul(marc_value_str, NULL, 10);
}
else {
expert_add_info_format(pinfo, item,
@@ -1511,23 +1508,24 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
}
}
- if (starting_char_value >= (record_length - data_offset)) {
+ if (new_entry.starting_character >= (record_length - data_offset)) {
expert_add_info_format(pinfo, item,
&ei_marc_invalid_value,
"MARC directory entry %d starting char value %d is outside record size %d",
- dir_index, starting_char_value, (record_length - data_offset));
+ dir_index, new_entry.starting_character, (record_length - data_offset));
}
- if ((starting_char_value + length_value) >= (record_length - data_offset)) {
+ if ((new_entry.starting_character + new_entry.length) >= (record_length - data_offset)) {
expert_add_info_format(pinfo, length_item,
&ei_marc_invalid_value,
"MARC directory entry %d length value %d goes outside record size %d",
- dir_index, length_value, (record_length - data_offset));
+ dir_index, new_entry.length, (record_length - data_offset));
}
- marc_directory[dir_index].tag = tag_value;
- marc_directory[dir_index].length = length_value;
- marc_directory[dir_index].starting_character = starting_char_value;
+ wmem_array_append_one(marc_directory_array, new_entry);
dir_index++;
}
+ directory_entry_count = wmem_array_get_count(marc_directory_array);
+ marc_directory = (marc_directory_entry *)wmem_array_finalize(marc_directory_array);
+
proto_tree_add_item(directory_tree, hf_marc_directory_terminator,
tvb, offset, 1, ENC_ASCII);
offset += 1;
diff --git a/epan/dissectors/packet-z3950.c b/epan/dissectors/packet-z3950.c
index 5c56b20dca4..0c3158ecca1 100644
--- a/epan/dissectors/packet-z3950.c
+++ b/epan/dissectors/packet-z3950.c
@@ -12638,6 +12638,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
proto_tree *marc_tree, *leader_tree,
*directory_tree,
*fields_tree;
+ wmem_array_t *marc_directory_array;
marc_directory_entry *marc_directory;
unsigned len = tvb_reported_length(tvb);
const char *marc_value_str;
@@ -12810,10 +12811,8 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
directory_entry_len = 3 + length_of_field_size
+ starting_character_position_size;
- directory_entry_count = ((data_offset - 1) - MARC_LEADER_LENGTH) / directory_entry_len;
- marc_directory = (marc_directory_entry *)wmem_alloc0(pinfo->pool,
- directory_entry_count * sizeof(marc_directory_entry));
+ marc_directory_array = wmem_array_new(pinfo->pool, sizeof(marc_directory_entry));
directory_item = proto_tree_add_item(marc_tree, hf_marc_directory,
tvb, offset, data_offset - offset, ENC_NA);
@@ -12822,9 +12821,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
dir_index = 0;
/* Minus one for the terminator character */
while (offset < (data_offset - 1)) {
- uint32_t tag_value = 0,
- length_value = 0,
- starting_char_value = 0;
+ marc_directory_entry new_entry = {0};
proto_item *length_item;
proto_item *directory_entry_item;
proto_tree *directory_entry_tree;
@@ -12840,7 +12837,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
offset += 3;
if (marc_value_str) {
if (isdigit_string(marc_value_str)) {
- tag_value = (unsigned)strtoul(marc_value_str, NULL, 10);
+ new_entry.tag = (unsigned)strtoul(marc_value_str, NULL, 10);
}
else {
expert_add_info_format(pinfo, item,
@@ -12857,7 +12854,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
offset += length_of_field_size;
if (marc_value_str) {
if (isdigit_string(marc_value_str)) {
- length_value = (unsigned)strtoul(marc_value_str, NULL, 10);
+ new_entry.length = (unsigned)strtoul(marc_value_str, NULL, 10);
}
else {
expert_add_info_format(pinfo, length_item,
@@ -12873,7 +12870,7 @@ dissect_marc_record(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *
offset += starting_character_position_size;
if (marc_value_str) {
if (isdigit_string(marc_value_str)) {
- starting_char_value = (unsigned)strtoul(marc_value_str, NULL, 10);
+ new_entry.starting_character = (unsigned)strtoul(marc_value_str, NULL, 10);
}
else {
expert_add_info_form
… (truncated)https://github.com/wireshark/wireshark/commit/c9bd49828f4e9cd8c04f90aacb11e2238016bc31
Recorded dates, in order.
- 2026-04-02 Discovered or logged
- 2026-07-06 Sent to maintainer
- 2026-07-08 Patch released
- 2026-08-12 Maintainer acknowledged
- 2026-09-28 Publicly revealed
SHA-3-512 hash:
d412d9b4df5da6160175e4ae967ca471c65afc20dddacda0b5e94dc1890ad9df074ee9882366b2675d462711323835d53ecd2ba92232ec6c59c3e2e2c62e4187
Committed 2026-07-22 07:34 UTC
Revealed 2026-09-28 20:01 UTC
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-3FWTCMVC",
"bug_class": "Heap Buffer Overflow / Integer Rounding",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-04-16T14:11:14+00:00",
"description": "In the Z39.50 dissector's dissect_marc_record(), directory_entry_count is computed with integer floor division and used to size a wmem_alloc0 array, but the subsequent while loop iterates ceil() times over the same region. When the attacker-controlled leader field data_offset makes (data_offset-1-24) not a multiple of the fixed directory_entry_len (12), the loop runs one extra iteration and writes a 12-byte marc_directory_entry past the end of the heap allocation. The sanity check on data_offset only emits an expert-info item and does not abort parsing. A count==0 case (e.g. data_offset=30) additionally yields a NULL-pointer write because wmem_alloc0(...,0) returns NULL. Both are reachable unauthenticated via a single Z39.50 response on default TCP/210 or by opening a malicious pcap.",
"discovered_at": "2026-04-02T00:00:00+00:00",
"location": "epan/dissectors/packet-z3950.c:12862",
"poc_sha256": null,
"preimage_version": 1,
"project": "wireshark/wireshark",
"reproduction": [
"1. Construct a Z39.50 response carrying an EXTERNAL with OID 1.2.840.10003.5.10 so the MARC dissector is invoked",
"2. In the MARC leader, set data_offset (5 ASCII digits) such that (data_offset - 1 - 24) % 12 != 0, e.g. data_offset=38",
"3. Deliver the packet over TCP/210 to a host being captured, or embed it in a pcap the victim opens",
"4. dissect_marc_record() allocates floor(N/12) entries but loops ceil(N/12) times, writing one extra 12-byte entry past the heap buffer",
"5. Optionally set data_offset so the count is 0 (e.g. 30) to trigger a NULL-pointer write via wmem_alloc0(...,0)==NULL"
],
"technical_details": "Lines 12819-12822 allocate floor((data_offset-1-24)/12) entries, but the while loop at line 12830 (`while (offset < (data_offset - 1))`) runs ceil((data_offset-1-24)/12) times, writing marc_directory[dir_index].tag/length/starting_character at lines 12904-12906 with no bounds check. data_offset is parsed from 5 attacker-controlled ASCII digits in the MARC leader (line 12748, range 0-99999) and the range check at lines 12756-12762 is non-aborting, so e.g. data_offset=38 allocates 1 entry but performs 2 iterations, writing 12 attacker-derived bytes into the adjacent wmem block_fast chunk header.",
"title": "Z39.50 MARC directory count floor/ceil mismatch overflow",
"vendor_severity": "high"
}