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);