diff options
| author | Hauke Mehrtens | 2026-06-12 23:57:54 +0000 |
|---|---|---|
| committer | Hauke Mehrtens | 2026-06-16 00:00:27 +0000 |
| commit | 1b5e7bf1cec775b89f1a6068dc0b9df3593b5986 (patch) | |
| tree | b04ad69e2d0a8d55475cf865902a1049903e93b7 | |
| parent | bd7599d021571e785bd32a75469e7148eb1772e3 (diff) | |
| download | mdnsd-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.c | 41 | ||||
| -rw-r--r-- | cache.h | 1 | ||||
| -rw-r--r-- | main.c | 11 |
3 files changed, 51 insertions, 2 deletions
@@ -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; @@ -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); @@ -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; |