Bug#1145007: wsdd2: two remote memory-safety bugs in the WSD/LLMNR packet parsers

Rudi Heitbaum <[email protected]>
Newsgroups gmane.linux.debian.devel.bugs.rc
Message-ID <SY7P282MB4909552C99AA1F6B04D4A869B4A32@SY7P282MB4909.AUSP282.PROD.OUTLOOK.COM>
Package: wsdd2
Version: 1.8.7+dfsg-1.2
Severity: grave
Tags: security upstream patch

wsdd2 has two remotely reachable memory-safety bugs, one in each packet
parser. Both are present in 1.8.7 verbatim.

1. NULL-pointer write, remote DoS. wsd_parse_http_header() does
   *eol = '\0' on the result of strstr(buf, "\r\n") with no NULL check.
   A TCP segment to port 3702 beginning "POST " with no CRLF crashes the
   daemon. It is single-threaded, so one unauthenticated packet ends
   discovery for every host it advertises. Confirmed on hardware.

2. Out-of-bounds read. llmnr_send_response() walks the DNS question
   labels and reads QTYPE/QCLASS without bounding the cursor against the
   received length, so a crafted LLMNR query reads past the packet.

Both found with libFuzzer under ASan/UBSan. Minimal fixes attached
(wsdd2-01-http-null-deref.patch, wsdd2-02-llmnr-oob-read.patch).

Note: upstream github.com/Netgear/wsdd2 and the fork the d/watch tracks
(github.com/Andy2244/wsdd2) are both gone; github.com/oldium/wsdd2 is the
only surviving tree and has these fixes. The package will need to carry
them as debian/patches regardless.
wsdd2-01-http-null-deref.patch (application/octet-stream, 1.7 KB)
From a5d7edcde50af01566fc7d51ca68b70b7c787d3a Mon Sep 17 00:00:00 2001
From: Rudi Heitbaum <[email protected]>
Date: Fri, 21 Aug 2026 22:03:25 +1000
Subject: [PATCH] wsd_parse_http_header: reject a request with no CRLF

The function locates the end of the request line with

	char *eol = strstr(p, "\r\n");

and immediately writes the terminator with *eol = '\0', never checking
that the CRLF was found. A TCP segment on port 3702 whose first five
bytes are "POST " - enough to pass the caller's strncmp(buf, "POST ", 5)
guard - but which contains no CRLF anywhere makes strstr return NULL,
and the store through it is a write to address zero. The daemon takes a
SIGSEGV and dies.

wsdd2 is single-threaded and processes one request at a time, so this is
a remote, unauthenticated, single-packet denial of service: one crafted
segment ends the daemon and every host it was making discoverable drops
off the Windows network until it is restarted.

Found with AddressSanitizer and confirmed with libFuzzer, which crashes
the pre-fix parser on the empty input. Return 400 when the request line
has no CRLF, matching how the rest of the function reports a malformed
header. The later strstr in the header loop is already guarded.
---
 wsd.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/wsd.c b/wsd.c
index 0b276e5..ccf7a78 100644
--- a/wsd.c
+++ b/wsd.c
@@ -901,6 +901,11 @@ static int wsd_parse_http_header(int fd, struct endpoint *ep,
 	if (!endpointlen)
 		endpointlen = strlen(wsd_endpoint);
 
+	if (!eol) {
+		ep->errstr = __FUNCTION__ ": No request line";
+		return 400;
+	}
+
 	*eol = '\0';
 	if (strncmp(p, "POST /", 6) != 0) {
 		ep->errstr = __FUNCTION__ ": Only POST method supported";
wsdd2-02-llmnr-oob-read.patch (application/octet-stream, 2.7 KB)
From dcaafe8e603cccfc12f0145d3fb8d23723421f9e Mon Sep 17 00:00:00 2001
From: Rudi Heitbaum <[email protected]>
Date: Fri, 21 Aug 2026 22:12:41 +1000
Subject: [PATCH] llmnr_send_response: bound the label parser to the packet

The question-section walk trusts the DNS label length bytes and never
checks them against inlen. The loop reads a label length, copies that
many bytes with strncat, and advances in_name_p by length+1, with no
test that the label - or the pointer after it - is still inside the
received packet. After the loop the code reads four more bytes for
QTYPE and QCLASS at in_name_p[1..4] with the same absence of a bound.

A crafted query whose final label length points past the end therefore
walks in_name_p off the buffer. In the daemon the datagram sits in a
fixed 9216+1 byte stack buffer terminated with a NUL, which contains
the common case, but a maximal packet can still push the QTYPE read
past the end of that buffer - an out-of-bounds read of adjacent memory
whose value then steers the type check.

Found with libFuzzer: an exact-sized ASan buffer faults immediately at
the loop condition; the fix survives 25M executions of the same
harness with coverage slightly up. Track the end of the packet and
reject any label, or a question section, that does not fit.
---
 llmnr.c | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/llmnr.c b/llmnr.c
index 77cb81e..ba43291 100644
--- a/llmnr.c
+++ b/llmnr.c
@@ -173,7 +173,8 @@ static int llmnr_send_response(struct endpoint *ep, _saddr_t *sa,
 	in_name = strdup("");
 	in_name_len = 0;
 	in_name_p = &in[12];
-	while (*in_name_p > 0) {
+	const uint8_t *in_end = in + inlen;
+	while (in_name_p < in_end && *in_name_p > 0) {
 		/*
 		 * not supporting message compression
 		 * see section 4.1.4 of RFC 1035
@@ -184,6 +185,13 @@ static int llmnr_send_response(struct endpoint *ep, _saddr_t *sa,
 			return -1;
 		}
 
+		/* the label body must lie within the packet */
+		if (in_name_p + 1 + *in_name_p >= in_end) {
+			DEBUG(1, L, "llmnr: label runs past end of packet");
+			free(in_name);
+			return -1;
+		}
+
 		/* append to the whole name */
 		in_name_len += *in_name_p + (*in_name ? 1 : 0); // '.' if not first
 
@@ -214,6 +222,13 @@ static int llmnr_send_response(struct endpoint *ep, _saddr_t *sa,
 	 * this implementation only supports questions of type A
 	 * or AAAA
 	 */
+	/* need the name terminator plus the 4 bytes of QTYPE and QCLASS */
+	if (in_name_p + 4 >= in_end) {
+		DEBUG(1, L, "llmnr: question section truncated");
+		free(in_name);
+		return -1;
+	}
+
 	qtype = in_name_p[1] * 256 + in_name_p[2];
 	if (qtype != DNS_TYPE_ANY && qtype != DNS_TYPE_A && qtype != DNS_TYPE_AAAA) {
 		DEBUG(1, L, "llmnr: record in question not of type ANY or A or AAAA: %#x", qtype);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.