summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHauke Mehrtens2026-06-12 23:57:54 +0000
committerHauke Mehrtens2026-06-16 00:00:27 +0000
commit1b5e7bf1cec775b89f1a6068dc0b9df3593b5986 (patch)
treeb04ad69e2d0a8d55475cf865902a1049903e93b7
parentbd7599d021571e785bd32a75469e7148eb1772e3 (diff)
downloadmdnsd-1b5e7bf1cec775b89f1a6068dc0b9df3593b5986.tar.gz
cache: bound cache size and clamp hostile TTLs
mDNS answers are unsolicited and unauthenticated, so any host on the local segment can spray responses for an unbounded number of unique names. cache_answer()/cache_service() cached every such record with no entry cap; the only eviction is the TTL-based GC, which an attacker defeats by sending a very large TTL. A sustained flood thus grows the heap without bound until the daemon OOMs or crashes. On top of that, the calloc_a() returns were dereferenced without a NULL check, so the failing allocation itself wrote through NULL and SIGSEGVd. Cap the number of cached records and services, clamp the stored TTL so the GC keeps reclaiming hostile records (and the cache recovers once a flood stops), and NULL-check both calloc_a() returns. Reported-by: Puru Kulkarni <puruk@proton.me> Link: https://github.com/openwrt/mdnsd/security/advisories/GHSA-jg8f-fhfw-jg46 Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
-rw-r--r--cache.c41
-rw-r--r--cache.h1
-rw-r--r--main.c11
3 files changed, 51 insertions, 2 deletions
diff --git a/cache.c b/cache.c
index 2c16d34..176ef4c 100644
--- a/cache.c
+++ b/cache.c
@@ -40,6 +40,27 @@
#include "dns.h"
#include "interface.h"
+/*
+ * mDNS answers are unsolicited and unauthenticated, so any host on the
+ * local segment can spray responses for an unbounded number of unique
+ * names. Without these limits the cache grows without bound: every
+ * record/service is kept until its TTL expires, and an attacker is free
+ * to pick an arbitrarily large TTL, which defeats the TTL-only GC. A
+ * sustained flood then exhausts the heap and crashes (or OOMs) the
+ * daemon.
+ *
+ * CACHE_TTL_MAX clamps the stored TTL so the GC keeps reclaiming hostile
+ * records (RFC 6762 recommends 75 minutes for shared records), and
+ * cache_entries_max bounds the number of cached records and services so
+ * memory stays bounded even while a flood is ongoing. The limit defaults
+ * to CACHE_ENTRIES_MAX and can be overridden with the -c command line
+ * option.
+ */
+#define CACHE_TTL_MAX (75 * 60)
+#define CACHE_ENTRIES_MAX 2048
+
+int cache_entries_max = CACHE_ENTRIES_MAX;
+
static struct uloop_timeout cache_gc;
struct avl_tree services;
@@ -178,9 +199,14 @@ cache_service(struct interface *iface, char *entry, size_t hlen, int ttl)
return s;
}
+ if (services.count >= (unsigned int)cache_entries_max)
+ return NULL;
+
s = calloc_a(sizeof(*s),
&entry_buf, strlen(entry) + 1,
&host_buf, hlen ? hlen + sizeof(".local") + 1 : 0);
+ if (!s)
+ return NULL;
s->avl.key = s->entry = strcpy(entry_buf, entry);
s->time = monotonic_time();
@@ -271,6 +297,14 @@ void cache_answer(struct interface *iface, struct sockaddr *from, uint8_t *base,
nlen = strlen(name);
+ /*
+ * A TTL of 0 is a "goodbye" and is handled below; clamp any other
+ * value so a hostile, very large TTL cannot keep a record alive
+ * indefinitely and defeat the GC.
+ */
+ if (a->ttl > CACHE_TTL_MAX)
+ a->ttl = CACHE_TTL_MAX;
+
switch (a->type) {
case TYPE_PTR:
if (a->rdlength < 2)
@@ -364,10 +398,17 @@ void cache_answer(struct interface *iface, struct sockaddr *from, uint8_t *base,
if (!a->ttl)
return;
+ if (records.count >= (unsigned int)cache_entries_max) {
+ DBG(1, "Cache full, dropping %s %s\n", dns_type_string(a->type), name);
+ return;
+ }
+
r = calloc_a(sizeof(*r),
&name_buf, strlen(name) + 1,
&txt_ptr, tlen,
&rdata_ptr, dlen);
+ if (!r)
+ return;
r->avl.key = r->record = strcpy(name_buf, name);
r->type = a->type;
diff --git a/cache.h b/cache.h
index 7c81418..9c12491 100644
--- a/cache.h
+++ b/cache.h
@@ -50,6 +50,7 @@ struct cache_record {
extern struct avl_tree services;
extern struct avl_tree records;
+extern int cache_entries_max;
int cache_init(void);
void cache_update(void);
diff --git a/main.c b/main.c
index 521a266..2150e30 100644
--- a/main.c
+++ b/main.c
@@ -92,13 +92,13 @@ signal_shutdown(int signal)
int
main(int argc, char **argv)
{
- int ch, ttl;
+ int ch, ttl, max;
uloop_init();
udebug_init(&ud);
udebug_auto_connect(&ud, NULL);
- while ((ch = getopt(argc, argv, "t:i:d46n")) != -1) {
+ while ((ch = getopt(argc, argv, "t:i:d46nc:")) != -1) {
switch (ch) {
case 't':
ttl = atoi(optarg);
@@ -107,6 +107,13 @@ main(int argc, char **argv)
else
fprintf(stderr, "invalid ttl\n");
break;
+ case 'c':
+ max = atoi(optarg);
+ if (max > 0)
+ cache_entries_max = max;
+ else
+ fprintf(stderr, "invalid cache size\n");
+ break;
case 'd':
debug++;
break;