From b29f38b181405bfe4a503bc79fd93c314d85b4f3 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 5 Jul 2025 18:11:34 +0100 Subject: [PATCH 001/101] Fix bounds checking in check_ia() A malformed DHCP request can cause out-bounds memory reads, and probably SEGV. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index e1f6189d13..f2838ab8f4 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -1596,9 +1596,14 @@ static void get_context_tag(struct state *state, struct dhcp_context *context) static int check_ia(struct state *state, void *opt, void **endp, void **ia_option) { - state->ia_type = opt6_type(opt); *ia_option = NULL; + /* must be a minimal option to check without stepping outside received packet. */ + if (opt6_ptr(opt, 4) > state->end) + return 0; + + state->ia_type = opt6_type(opt); + if (state->ia_type != OPTION6_IA_NA && state->ia_type != OPTION6_IA_TA) return 0; @@ -1608,7 +1613,10 @@ static int check_ia(struct state *state, void *opt, void **endp, void **ia_optio if (state->ia_type == OPTION6_IA_TA && opt6_len(opt) < 4) return 0; - *endp = opt6_ptr(opt, opt6_len(opt)); + /* Check we don't overflow the received packet. */ + if ((*endp = opt6_ptr(opt, opt6_len(opt))) > state->end) + return 0; + state->iaid = opt6_uint(opt, 0, 4); *ia_option = opt6_find(opt6_ptr(opt, state->ia_type == OPTION6_IA_NA ? 12 : 4), *endp, OPTION6_IAADDR, 24); From f9d82e48b12c7f8bb1f698254e6921d19d14a116 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sat, 13 Dec 2025 11:29:26 +0100 Subject: [PATCH 002/101] Update dnsmasq version Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c5acbf451..7b0cdbb04f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.92rc1) +set(DNSMASQ_VERSION pi-hole-v2.92rc3) add_subdirectory(src) From 34eb0bcee7f7e3efc947ee20d88797cce555f774 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 5 Dec 2025 22:41:37 +0000 Subject: [PATCH 003/101] Tidy up code in in do_tcp_connection() which filters incoming connections. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A general rewrite and consolidation of the code that determines if an incoming TCP connection is allowed, based on --address and --interface. This was prompted by a bug found by Sławomir Zborowski where a TCP connection for a valid IPv4 address on one interface which arrived via a second interface which didn't have an IPv4 address would be wrongly rejected. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 90 ++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 7646642244..f14c847619 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1973,20 +1973,21 @@ static void check_dns_listeners(time_t now) static void do_tcp_connection(struct listener *listener, time_t now, int slot) { - int confd, client_ok = 1; + int confd; struct irec *iface = NULL; pid_t p; union mysockaddr tcp_addr; socklen_t tcp_len = sizeof(union mysockaddr); unsigned char *buff; struct server *s; - int flags, auth_dns; + int flags, auth_dns = 0; struct in_addr netmask; int pipefd[2]; #ifdef HAVE_LINUX_NETWORK unsigned char a = 0; #endif - + netmask.s_addr = 0; + while ((confd = accept(listener->tcpfd, NULL, NULL)) == -1 && errno == EINTR); if (confd == -1) @@ -2013,58 +2014,63 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) enumerate_interfaces(0); - if (option_bool(OPT_NOWILD)) - iface = listener->iface; /* May be NULL */ + if (option_bool(OPT_NOWILD) || option_bool(OPT_CLEVERBIND)) + { + if ((iface = listener->iface)) + { + netmask = iface->netmask; + auth_dns = iface->dns_auth; + } + } else { - int if_index; + int if_index, got_index = 0; char intr_name[IF_NAMESIZE]; - /* if we can find the arrival interface, check it's one that's allowed */ + /* if we can find the arrival interface, check it's one that's allowed + tcp_interface() is not implemented on non-Linux platforms */ if ((if_index = tcp_interface(confd, tcp_addr.sa.sa_family)) != 0 && indextoname(listener->tcpfd, if_index, intr_name)) { union all_addr addr; + + got_index = 1; if (tcp_addr.sa.sa_family == AF_INET6) addr.addr6 = tcp_addr.in6.sin6_addr; else addr.addr4 = tcp_addr.in.sin_addr; - for (iface = daemon->interfaces; iface; iface = iface->next) - if (iface->index == if_index && - iface->addr.sa.sa_family == tcp_addr.sa.sa_family) - break; - - if (!iface && !loopback_exception(listener->tcpfd, tcp_addr.sa.sa_family, &addr, intr_name)) - client_ok = 0; + if (!iface_check(tcp_addr.sa.sa_family, &addr, intr_name, &auth_dns) && + !loopback_exception(listener->tcpfd, tcp_addr.sa.sa_family, &addr, intr_name)) + goto closeconandreturn; } - if (option_bool(OPT_CLEVERBIND)) - iface = listener->iface; /* May be NULL */ - else + /* When binding the wildcard address, try and get the + netmask of the interface for localisation. */ + for (iface = daemon->interfaces; iface; iface = iface->next) + if (sockaddr_isequal(&iface->addr, &tcp_addr)) + { + netmask = iface->netmask; + break; + } + + /* On platforms where tcp_interface() doesn't work, we rely + of the presence of the local address of the connection in the + interface list to check if we're accepting this connection and + for its auth status. */ + if (!got_index) { - /* Check for allowed interfaces when binding the wildcard address: - we do this by looking for an interface with the same address as - the local address of the TCP connection, then looking to see if that's - an allowed interface. As a side effect, we get the netmask of the - interface too, for localisation. */ - - for (iface = daemon->interfaces; iface; iface = iface->next) - if (sockaddr_isequal(&iface->addr, &tcp_addr)) - break; - if (!iface) - client_ok = 0; + goto closeconandreturn; + + auth_dns = iface->dns_auth; } } - if (!client_ok) - goto closeconandreturn; - if (!option_bool(OPT_DEBUG)) { - /* The code in edns0.c qthat decorates queries with the source MAC address depends + /* The code in edns0.c that decorates queries with the source MAC address depends on the code in arp.c, which populates a cache with the contents of the ARP table using netlink. Since the child process can't use netlink, we pre-populate the cache with the ARP table entry for our source here, including a negative entry @@ -2132,23 +2138,9 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) return; } - } - - if (iface) - { - netmask = iface->netmask; - auth_dns = iface->dns_auth; - } - else - { - netmask.s_addr = 0; - auth_dns = 0; - } - - /* Arrange for SIGALRM after CHILD_LIFETIME seconds to - terminate the process. */ - if (!option_bool(OPT_DEBUG)) - { + + /* Arrange for SIGALRM after CHILD_LIFETIME seconds to + terminate the process. */ #ifdef HAVE_LINUX_NETWORK /* See comment above re: netlink socket. */ close(daemon->netlinkfd); From 546cacbec13f1b4ce2a2beeadb2d72baad55632b Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 19 Jan 2026 20:29:01 +0100 Subject: [PATCH 004/101] Prepare for next patch Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index f14c847619..58ea235abc 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1353,13 +1353,7 @@ static void sig_handler(int sig) { /* alarm is used to kill TCP children after a fixed time. */ if (sig == SIGALRM) - { - /*** Pi-hole modification ***/ - // TCP workers ignore all signals except SIGALRM - FTL_TCP_worker_terminating(false); - /*** Pi-hole modification ***/ - _exit(0); - } + _exit(0); } else { From bd4f8af4fc7894da93721669224d149e781380d9 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 6 Dec 2025 00:20:44 +0000 Subject: [PATCH 005/101] Terminate TCP child processes that arise from UDP truncated replies. These can't be held forever by a client that opens a connection and then sends nothing, but we can still be DoSsed by a server which accepts a connection and never replies. If the TCP process times out, it sends that information to the parent so that the UDP query can be unblocked. As the TCP timeout is 150s it's highly unlikely that any client will still be waiting, but the point is to free resources. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 25 ++++++++++++++++--------- src/dnsmasq/dnsmasq.c | 41 ++++++++++++++++++++++++++++++++++++++--- src/dnsmasq/dnsmasq.h | 4 ++++ src/dnsmasq/forward.c | 6 +++++- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 4f5dacfa93..808752d1ec 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -872,7 +872,7 @@ void cache_end_insert(void) #ifdef HAVE_DNSSEC void cache_update_hwm(void) { - /* Sneak out possibly updated crypto HWM values. */ + /* Sneak out possibly updated crypto HWM values. */ unsigned char op = PIPE_OP_STATS; read_write(daemon->pipe_to_parent, &op, sizeof(op), RW_WRITE); @@ -1017,6 +1017,7 @@ int cache_recv_insert(time_t now, int fd) } case PIPE_OP_RESULT: + case PIPE_OP_KILLED: { /* UDP validation moved to TCP to avoid truncation. Restart UDP validation process with the returned result. */ @@ -1030,11 +1031,14 @@ int cache_recv_insert(time_t now, int fd) !read_write(fd, (unsigned char *)&ret_len, sizeof(ret_len), RW_READ) || !read_write(fd, (unsigned char *)daemon->packet, ret_len, RW_READ) || !read_write(fd, (unsigned char *)&forward, sizeof(forward), RW_READ) || - !read_write(fd, (unsigned char *)&uid, sizeof(uid), RW_READ) || - !read_write(fd, (unsigned char *)&keycount, sizeof(keycount), RW_READ) || - !read_write(fd, (unsigned char *)&keycountp, sizeof(keycountp), RW_READ) || - !read_write(fd, (unsigned char *)&validatecount, sizeof(validatecount), RW_READ) || - !read_write(fd, (unsigned char *)&validatecountp, sizeof(validatecountp), RW_READ)) + !read_write(fd, (unsigned char *)&uid, sizeof(uid), RW_READ)) + return 0; + + if (op == PIPE_OP_RESULT && + (!read_write(fd, (unsigned char *)&keycount, sizeof(keycount), RW_READ) || + !read_write(fd, (unsigned char *)&keycountp, sizeof(keycountp), RW_READ) || + !read_write(fd, (unsigned char *)&validatecount, sizeof(validatecount), RW_READ) || + !read_write(fd, (unsigned char *)&validatecountp, sizeof(validatecountp), RW_READ))) return 0; /* There's a tiny chance that the frec may have been freed @@ -1042,9 +1046,12 @@ int cache_recv_insert(time_t now, int fd) the uid field which is unique modulo 2^32 for each use. */ if (uid == forward->uid) { - /* repatriate the work counters from the child process. */ - *keycountp = keycount; - *validatecountp = validatecount; + if (op == PIPE_OP_RESULT) + { + /* repatriate the work counters from the child process. */ + *keycountp = keycount; + *validatecountp = validatecount; + } if (!forward->dependent) return_reply(now, forward, (struct dns_header *)daemon->packet, ret_len, status); diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 58ea235abc..4b3ecb39d3 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1349,11 +1349,42 @@ static void sig_handler(int sig) if (sig == SIGTERM || sig == SIGINT) exit(EC_MISC); } - else if (pid != getpid()) + else if (daemon->pipe_to_parent != -1) { /* alarm is used to kill TCP children after a fixed time. */ if (sig == SIGALRM) - _exit(0); + { +#ifdef HAVE_DNSSEC + if (!daemon->forward_to_tcp) +#endif + _exit(0); /* Normal TCP child */ +#ifdef HAVE_DNSSEC + else + { + /* udp_to_tcp transfer. + If daemon->header_to_tcp is NULL the waiting is over and + we can let things take their course, otherwise, send a failure + return down the pipe to unblock the UDP transaction and kill + the process. */ + if (daemon->header_to_tcp) + { + unsigned char op = PIPE_OP_KILLED; + int status = STAT_ABANDONED; + + read_write(daemon->pipe_to_parent, &op, sizeof(op), RW_WRITE); + read_write(daemon->pipe_to_parent, (unsigned char *)&status, sizeof(status), RW_WRITE); + read_write(daemon->pipe_to_parent, (unsigned char *)(&daemon->plen_to_tcp), sizeof(daemon->plen_to_tcp), RW_WRITE); + read_write(daemon->pipe_to_parent, (unsigned char *)(daemon->header_to_tcp), daemon->plen_to_tcp, RW_WRITE); + read_write(daemon->pipe_to_parent, (unsigned char *)(&daemon->forward_to_tcp), sizeof(daemon->forward_to_tcp), RW_WRITE); + read_write(daemon->pipe_to_parent, (unsigned char *)(&daemon->forward_to_tcp->uid), sizeof(daemon->forward_to_tcp->uid), RW_WRITE); + + my_syslog(LOG_INFO, _("TCP process for DNSSEC validation timed out")); + + _exit(0); + } + } +#endif + } } else { @@ -2270,8 +2301,12 @@ int swap_to_tcp(struct frec *forward, time_t now, int status, struct dns_header // Pi-hole modification daemon->netlinkfd = -1; #endif + alarm(CHILD_LIFETIME); close(pipefd[0]); /* close read end in child. */ - daemon->pipe_to_parent = pipefd[1]; + daemon->pipe_to_parent = pipefd[1]; + daemon->forward_to_tcp = forward; + daemon->header_to_tcp = header; + daemon->plen_to_tcp = *plen; } } diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index ccf0a1e5c8..8f8850d5fc 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -561,6 +561,7 @@ struct crec { #define PIPE_OP_STATS 3 /* Update parent's stats */ #define PIPE_OP_IPSET 4 /* Update IPset */ #define PIPE_OP_NFTSET 5 /* Update NFTset */ +#define PIPE_OP_KILLED 6 /* child killed by SIGALARM */ /* struct sockaddr is not large enough to hold any address, and specifically not big enough to hold an IPv6 address. @@ -1286,6 +1287,9 @@ extern struct daemon { int dnssec_no_time_check; int back_to_the_future; int limit[LIMIT_MAX]; + struct frec *forward_to_tcp; + struct dns_header *header_to_tcp; + ssize_t plen_to_tcp; #endif struct frec *frec_list; struct frec_src *free_frec_src; diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index 4bc1e39311..3e68b765c2 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -2327,7 +2327,7 @@ int tcp_from_udp(time_t now, int status, struct dns_header *header, ssize_t *ple if (n >= daemon->edns_pktsz) { - /* still too bIg, strip optional sections and try again. */ + /* still too big, strip optional sections and try again. */ new_header->nscount = htons(0); new_header->arcount = htons(0); n = resize_packet(new_header, n, NULL, 0); @@ -2341,6 +2341,10 @@ int tcp_from_udp(time_t now, int status, struct dns_header *header, ssize_t *ple } } + /* we have succeeded and are no longer blocked talking + on a TCP connection, so if the watchdog alarm goes off, + ignore it. */ + daemon->forward_to_tcp = NULL; /* return the stripped or truncated reply. */ memcpy(header, new_header, n); *plenp = n; From aa95eb66b9c98260cc50c43c926f43a8deec2f51 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 19 Jan 2026 20:35:45 +0100 Subject: [PATCH 006/101] Reimplement necessary Pi-hole changes Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 4b3ecb39d3..fe3ba2c345 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1357,7 +1357,13 @@ static void sig_handler(int sig) #ifdef HAVE_DNSSEC if (!daemon->forward_to_tcp) #endif + { + /*** Pi-hole modification ***/ + // TCP workers ignore all signals except SIGALRM + FTL_TCP_worker_terminating(false); + /****************************/ _exit(0); /* Normal TCP child */ + } #ifdef HAVE_DNSSEC else { @@ -1380,6 +1386,11 @@ static void sig_handler(int sig) my_syslog(LOG_INFO, _("TCP process for DNSSEC validation timed out")); + /*** Pi-hole modification ***/ + // TCP workers ignore all signals except SIGALRM + FTL_TCP_worker_terminating(false); + /****************************/ + _exit(0); } } From 898ff362e2945439eafb7bf8623a2ff0e0ce5435 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 12 Jan 2026 21:55:41 +0000 Subject: [PATCH 007/101] Fix a corner-case in DNSSEC validation with wildcards. If we have a wildcard record *.example.com and recieve a query for a.example.com then that's OK, but we have to check that there isn't an actual a.example.com record. The corner case is when we get a query for *.example.com in that case the non-existence check is not required, but was being done. Thanks to Jan Breig for spotting this. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 7 +++---- src/dnsmasq/dnssec.c | 47 +++++++++++++++++++++---------------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index 8f8850d5fc..ce539a48e7 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -776,10 +776,9 @@ struct dyndir { #define STAT_NEED_DS 0x40000 #define STAT_NEED_KEY 0x50000 #define STAT_TRUNCATED 0x60000 -#define STAT_SECURE_WILDCARD 0x70000 -#define STAT_OK 0x80000 -#define STAT_ABANDONED 0x90000 -#define STAT_ASYNC 0xa0000 +#define STAT_OK 0x70000 +#define STAT_ABANDONED 0x80000 +#define STAT_ASYNC 0x90000 #define DNSSEC_FAIL_NYV 0x0001 /* key not yet valid */ #define DNSSEC_FAIL_EXP 0x0002 /* key expired */ diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index a90d3ce0d7..90cedca6db 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -439,8 +439,6 @@ int dec_counter(int *counter, char *message) /* Validate a single RRset (class, type, name) in the supplied DNS reply Return code: STAT_SECURE if it validates. - STAT_SECURE_WILDCARD if it validates and is the result of wildcard expansion. - (In this case *wildcard_out points to the "body" of the wildcard within name.) STAT_BOGUS signature is wrong, bad packet. STAT_ABANDONED validation abandoned do to excess resource usage. STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname) @@ -456,7 +454,7 @@ int dec_counter(int *counter, char *message) ttl_out is the floor on TTL, based on TTL and orig_ttl and expiration of sig used to validate. */ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, int sigidx, int rrsetidx, - char *name, char *keyname, char **wildcard_out, struct blockdata *key, int keylen, + char *name, char *keyname, int *wildcard_offset_out, struct blockdata *key, int keylen, int algo_in, int keytag_in, unsigned long *ttl_out, int *validate_counter) { unsigned char *p; @@ -469,8 +467,8 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in unsigned long curtime = time(0); int time_check = is_check_date(curtime); - if (wildcard_out) - *wildcard_out = NULL; + if (wildcard_offset_out) + *wildcard_offset_out = 0; name_labels = count_labels(name); /* For 4035 5.3.2 check */ @@ -581,7 +579,9 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in name_start = name; /* if more labels than in RRsig name, hash *. 4035 5.3.2 */ - if (labels < name_labels) + /* If the name is already the wildcard, we're not going to change it. */ + if (labels < name_labels && + !(name_labels - labels == 1 && name_start[0] == '*' && name_start[1] == '.')) { for (j = name_labels - labels; j != 0; j--) { @@ -591,11 +591,11 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in name_start++; } - if (wildcard_out) - *wildcard_out = name_start+1; - + if (wildcard_offset_out) + *wildcard_offset_out = name_start - name + 1; + name_start--; - *name_start = '*'; + *name_start = '*'; } wire_len = to_wire(name_start); @@ -694,7 +694,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in return STAT_ABANDONED; if (verify(crecp->addr.key.keydata, crecp->addr.key.keylen, sig, sig_len, digest, hash->digest_size, algo)) - return (labels < name_labels) ? STAT_SECURE_WILDCARD : STAT_SECURE; + return STAT_SECURE; /* An attacker can waste a lot of our CPU by setting up a giant DNSKEY RRSET full of failing keys, all of which we have to try. Since many failing keys is not likely for @@ -1552,7 +1552,7 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige /* returns 0 on success, or DNSSEC_FAIL_* value on failure. */ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count, char *workspace1, - char *workspace2, char *name, int type, char *wildname, int *nons, int *validate_counter) + char *workspace2, char *name, int type, int wild_offset, int *nons, int *validate_counter) { unsigned char *salt, *p, *digest; int digest_len, i, iterations, salt_len, base32_len, algo = 0; @@ -1650,9 +1650,9 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns if (*closest_encloser == '.') closest_encloser++; - if (wildname && hostname_isequal(closest_encloser, wildname)) + if (wild_offset != 0 && name - closest_encloser == wild_offset) break; - + if (dec_counter(validate_counter, NULL)) return DNSSEC_FAIL_WORK; @@ -1694,7 +1694,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns return DNSSEC_FAIL_NONSEC; /* Finally, check that there's no seat of wildcard synthesis */ - if (!wildname) + if (wild_offset == 0) { if (!(wildcard = strchr(next_closest, '.')) || wildcard == next_closest) return DNSSEC_FAIL_NONSEC; @@ -1717,7 +1717,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns /* returns 0 on success, or DNSSEC_FAIL_* value on failure. */ static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass, - char *wildname, int *nons, int *nsec_ttl, int *validate_counter) + int wild_offset, int *nons, int *nsec_ttl, int *validate_counter) { static unsigned char **nsecset = NULL, **rrsig_labels = NULL; static int nsecset_sz = 0, rrsig_labels_sz = 0; @@ -1865,7 +1865,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key if (type_found == T_NSEC) return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons); else if (type_found == T_NSEC3) - return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons, validate_counter); + return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wild_offset, nons, validate_counter); else return DNSSEC_FAIL_NONSEC; } @@ -2190,8 +2190,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch else { /* Not done, validate now */ - int sigcnt, rrcnt; - char *wildname; + int sigcnt, rrcnt, wild_offset; if (!explore_rrset(header, plen, class1, type1, name, keyname, &sigcnt, &rrcnt)) return STAT_BOGUS; @@ -2245,7 +2244,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch { unsigned long sig_ttl; rc = validate_rrset(now, header, plen, class1, type1, sigcnt, - rrcnt, name, keyname, &wildname, NULL, 0, 0, 0, &sig_ttl, validate_counter); + rrcnt, name, keyname, &wild_offset, NULL, 0, 0, 0, &sig_ttl, validate_counter); if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS) || STAT_ISEQUAL(rc, STAT_ABANDONED)) { @@ -2254,7 +2253,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch return rc; } - /* rc is now STAT_SECURE or STAT_SECURE_WILDCARD */ + /* rc is now STAT_SECURE */ /* Note that RR is validated */ daemon->rr_status[i] = sig_ttl; @@ -2280,8 +2279,8 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch Note that we may not yet have validated the NSEC/NSEC3 RRsets. That's not a problem since if the RRsets later fail we'll return BOGUS then. */ - if (STAT_ISEQUAL(rc, STAT_SECURE_WILDCARD) && - ((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL, validate_counter))) != 0) + if (wild_offset != 0 && + ((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wild_offset, NULL, NULL, validate_counter))) != 0) return (rc_nsec & DNSSEC_FAIL_WORK) ? STAT_ABANDONED : (STAT_BOGUS | rc_nsec); rc = STAT_SECURE; @@ -2309,7 +2308,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch the answer is in an unsigned zone, or there's NSEC records. For a DS record, we return INSECURE, which almost always turns into BOGUS in the caller. */ - if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons, nsec_ttl, validate_counter)) != 0) + if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, 0, nons, nsec_ttl, validate_counter)) != 0) { if (rc_nsec & DNSSEC_FAIL_WORK) return STAT_ABANDONED; From d899a20975aa1d835c61f3c14e409ef769ede9a4 Mon Sep 17 00:00:00 2001 From: Matthias Andree Date: Wed, 14 Jan 2026 20:08:41 +0000 Subject: [PATCH 008/101] Support Inotify in FreeBSD. FreeBSD 15.0 has added Linux-compatible inotify support, so enable it by looking if the version matches. Since FreeBSD inotify has seen a few bug fixes in 2025H2, so only enable it if __FreeBSD_version >= 1500068. The latter can be checked through osreldate.h or sys/param.h; the latter defines more macros that clash with dnsmasq's, such as MIN and MAX, so use the former. Signed-off-by: Dominik --- src/dnsmasq/config.h | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/config.h b/src/dnsmasq/config.h index 5361667bec..4e8a949e5f 100644 --- a/src/dnsmasq/config.h +++ b/src/dnsmasq/config.h @@ -144,7 +144,8 @@ HAVE_LOOP include functionality to probe for and remove DNS forwarding loops. HAVE_INOTIFY - use the Linux inotify facility to efficiently re-read configuration files. + use the Linux and FreeBSD >= 15 inotify facility + to efficiently re-read configuration files. NO_ID Don't report *.bind CHAOS info to clients, forward such requests upstream instead. @@ -388,8 +389,15 @@ HAVE_SOCKADDR_SA_LEN #undef HAVE_DUMPFILE #endif -#if defined (HAVE_LINUX_NETWORK) && !defined(NO_INOTIFY) -#define HAVE_INOTIFY +#if !defined(NO_INOTIFY) +# if defined (HAVE_LINUX_NETWORK) +# define HAVE_INOTIFY +# elif defined (__FreeBSD__) && __FreeBSD__ + 0 >= 15 +# include +# if __FreeBSD_version >= 1500068 /* 15.0.0 */ +# define HAVE_INOTIFY +# endif +# endif #endif /* This never compiles code, it's only used by the makefile to fingerprint builds. */ From 456e24e5e4861bdf77b1853c6193923c0912b283 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 15 Jan 2026 14:30:05 +0000 Subject: [PATCH 009/101] Fix DNSSEC failure with spurious RRSIGs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The presence of wrong RRSIG RRs in replies causes DNSSEC validation to fail even when the RRs do not require validation because the zone is unsigned. This patch solves the problem and tidies up the logic. Included is some fixes for hostname_issubdomain() which suffered some confusions about argument order :) I've clarified that and checked every to the function to make sure they are using the correct argument order. Note that, at the time of this commit, Google DNS appears to have the same bug, so if you're using 8.8.8.8 or friends as upstream, resolving the broken zones (eg rivcoed.org) will still fail. Thanks to Petr Menšík for the bug report. Signed-off-by: Dominik --- src/dnsmasq/dnssec.c | 119 +++++++++++++++++++------------------------ src/dnsmasq/util.c | 10 ++-- 2 files changed, 57 insertions(+), 72 deletions(-) diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 90cedca6db..7b21e32409 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -371,45 +371,42 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int GETSHORT(type_covered, p); p += 16; /* algo, labels, orig_ttl, sig_expiration, sig_inception, key_tag */ - - if (gotkey) - { - /* If there's more than one SIG, ensure they all have same keyname */ - if (extract_name(header, plen, &p, keyname, EXTR_NAME_COMPARE, 0) != 1) - return 0; - } - else + + if (type_covered == type) { - gotkey = 1; - - if (!extract_name(header, plen, &p, keyname, EXTR_NAME_EXTRACT, 0)) + if (!extract_name(header, plen, &p, daemon->workspacename, EXTR_NAME_EXTRACT, 0)) return 0; - + /* RFC 4035 5.3.1 says that the Signer's Name field MUST equal the name of the zone containing the RRset. We can't tell that for certain, but we can check that the RRset name is equal to or encloses the signers name, which should be enough to stop an attacker using signatures made with the key of an unrelated - zone he controls. Note that the root key is always allowed. */ - if (*keyname != 0) + zone he controls. Note that the root key is always allowed. + Ignore sigs which aren't valid */ + if (*daemon->workspacename == 0 || hostname_issubdomain(name, daemon->workspacename) != 0) { - char *name_start; - for (name_start = name; !hostname_isequal(name_start, keyname); ) - if ((name_start = strchr(name_start, '.'))) - name_start++; /* chop a label off and try again */ - else - return 0; + if (gotkey) + { + /* If there's more than one valid SIG, they must all have same keyname */ + if (!hostname_isequal(keyname, daemon->workspacename)) + return 0; + } + else + { + strcpy(keyname, daemon->workspacename); + gotkey = 1; + } + + if (gotkey) + { + if (!expand_workspace(&sigs, &sig_sz, sigidx)) + return 0; + + sigs[sigidx++] = pdata; + } } } - - - if (type_covered == type) - { - if (!expand_workspace(&sigs, &sig_sz, sigidx)) - return 0; - - sigs[sigidx++] = pdata; - } p = pdata + 6; /* restore for ADD_RDLEN */ } @@ -462,11 +459,13 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in struct crec *crecp = NULL; short *rr_desc = rrfilter_desc(type); u32 sig_expiration, sig_inception; - int failflags = DNSSEC_FAIL_NOSIG | DNSSEC_FAIL_NYV | DNSSEC_FAIL_EXP | DNSSEC_FAIL_NOKEYSUP; - - unsigned long curtime = time(0); + unsigned long curtime = time(0); int time_check = is_check_date(curtime); + int failflags = DNSSEC_FAIL_NOSIG; + if (sigidx != 0) + failflags |= DNSSEC_FAIL_NYV | DNSSEC_FAIL_EXP | DNSSEC_FAIL_NOKEYSUP; + if (wildcard_offset_out) *wildcard_offset_out = 0; @@ -2110,8 +2109,8 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch CNAME must be . CNAME target must be . s must match for name and target. */ - if (hostname_issubdomain(name, daemon->cname) == 1 && - hostname_issubdomain(keyname, daemon->workspacename) == 1 && + if (hostname_issubdomain(daemon->cname, name) == 1 && + hostname_issubdomain(daemon->workspacename, keyname) == 1 && name_prefix_len == strlen(daemon->workspacename) - strlen(keyname)) { char save = daemon->cname[name_prefix_len]; @@ -2195,41 +2194,27 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch if (!explore_rrset(header, plen, class1, type1, name, keyname, &sigcnt, &rrcnt)) return STAT_BOGUS; - /* No signatures for RRset. We can be configured to assume this is OK and return an INSECURE result. */ - if (sigcnt == 0) - { - /* NSEC and NSEC3 records must be signed. We make this assumption elsewhere. */ - if (type1 == T_NSEC || type1 == T_NSEC3) - return STAT_BOGUS | DNSSEC_FAIL_NOSIG; - else if (nons && i >= ntohs(header->ancount)) - /* If we're validating a DS reply, rather than looking for the value of AD bit, - we only care that NSEC and NSEC3 RRs in the auth section are signed. - Return SECURE even if others (SOA....) are not. */ - rc = STAT_SECURE; - else - { - /* unsigned RRsets in auth section are not BOGUS, but do make reply insecure. */ - if (check_unsigned && i < ntohs(header->ancount)) - { - rc = zone_status(name, class1, keyname, now); - if (STAT_ISEQUAL(rc, STAT_SECURE)) - rc = STAT_BOGUS | DNSSEC_FAIL_NOSIG; - - if (class) - *class = class1; /* Class for NEED_DS or NEED_KEY */ - } - else - rc = STAT_INSECURE; - - if (!STAT_ISEQUAL(rc, STAT_INSECURE)) - return rc; - } - } + /* NSEC and NSEC3 records must be signed. We make this assumption elsewhere. */ + if (sigcnt == 0 && (type1 == T_NSEC || type1 == T_NSEC3)) + return STAT_BOGUS | DNSSEC_FAIL_NOSIG; + else if (sigcnt == 0 && nons && i >= ntohs(header->ancount)) + /* If we're validating a DS reply, rather than looking for the value of AD bit, + we only care that NSEC and NSEC3 RRs in the auth section are signed. + Return SECURE even if others (SOA....) are not. */ + rc = STAT_SECURE; + else if (sigcnt == 0 && (!check_unsigned || i >= ntohs(header->ancount))) + /* unsigned RRsets in auth section are not BOGUS, but do make reply insecure. */ + rc = STAT_INSECURE; else { - /* explore_rrset() gives us key name from sigs in keyname. + /* explore_rrset() gives us zone name from sigs in keyname, if + it didn't find a key, use the name we're validating. Can't overwrite name here. */ - strcpy(daemon->workspacename, keyname); + if (sigcnt == 0) + strcpy(daemon->workspacename, name); + else + strcpy(daemon->workspacename, keyname); + rc = zone_status(daemon->workspacename, class1, keyname, now); if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS)) @@ -2239,7 +2224,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch return rc; } - /* Zone is insecure, don't need to validate RRset */ + /* If zone is insecure, don't need to validate RRset, and rc remains as STAT_INSECURE*/ if (STAT_ISEQUAL(rc, STAT_SECURE)) { unsigned long sig_ttl; diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index bb5e3462e7..240e3cc732 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -447,8 +447,8 @@ int hostname_issubdomain(char *a, char *b) for (ap = a; *ap; ap++); for (bp = b; *bp; bp++); - /* a shorter than b or a empty. */ - if ((bp - b) < (ap - a) || ap == a) + /* a shorter than b */ + if ((ap - a) < (bp - b)) return 0; do @@ -463,12 +463,12 @@ int hostname_issubdomain(char *a, char *b) if (c1 != c2) return 0; - } while (ap != a); + } while (bp != b); - if (bp == b) + if (ap == a) return 2; - if (*(--bp) == '.') + if (*(--ap) == '.') return 1; return 0; From 92d0ae82a713211910954cbd268082de772ded42 Mon Sep 17 00:00:00 2001 From: Pavel Bozhko Date: Fri, 19 Dec 2025 19:21:59 +0300 Subject: [PATCH 010/101] The only_failed argument has been added to the log-queries parameter. This may be useful for embedded systems for example: log-queries without only_failed places a heavy load on the NAND flash memory. At the same time, logging requests is necessary to troubleshoot network issues. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 12 ++++++++++++ src/dnsmasq/dnsmasq.h | 3 ++- src/dnsmasq/option.c | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 808752d1ec..69eb3316ca 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -2372,6 +2372,15 @@ const char *edestr(int ede) } } +static int error_occured(unsigned int flags) { + if (flags & F_RCODE) + return 1; + else if (flags & F_NEG) + return 1; + else + return 0; +} + /**** P-hole modified: Added file and line and serve log_query via macro defined in dnsmasq.h ****/ void _log_query(unsigned int flags, char *name, union all_addr *addr, char *arg, unsigned short type, const char *file, const int line) { @@ -2387,6 +2396,9 @@ void _log_query(unsigned int flags, char *name, union all_addr *addr, char *arg, if (!option_bool(OPT_LOG)) return; + if(option_bool(OPT_LOG_ONLY_FAILED) && !error_occured(flags)) + return; + /* F_NOERR is reused here to indicate logs arrising from auth queries */ if (!(flags & F_NOERR) && option_bool(OPT_AUTH_LOG)) return; diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index ce539a48e7..da1af57f10 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -292,7 +292,8 @@ struct event_desc { #define OPT_DO_0x20 75 #define OPT_AUTH_LOG 76 #define OPT_LEASEQUERY 77 -#define OPT_LAST 78 +#define OPT_LOG_ONLY_FAILED 78 +#define OPT_LAST 79 #define OPTION_BITS (sizeof(unsigned int)*8) #define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) ) diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 741925816a..fbc1541dfa 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -3466,6 +3466,8 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma } else if (strcmp(arg, "auth") == 0) set_option_bool(OPT_AUTH_LOG); + else if (strcmp(arg, "only_failed") == 0) + set_option_bool(OPT_LOG_ONLY_FAILED); } break; From a72e626f34f658ff1f5efab6fb13615b455f4617 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 18 Jan 2026 12:50:57 +0000 Subject: [PATCH 011/101] Log SERVFAIL from usptream servers. If we're doing DNSSEC validation and fail because the upstream reply to our query is SERVFAIL, log this as the reason in validation result line. This will make maintainers lives easier when they get reports of "wrong" validation failure, which is sometimes an upstream problem. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 1 + src/dnsmasq/dns-protocol.h | 1 + src/dnsmasq/dnsmasq.h | 1 + src/dnsmasq/dnssec.c | 10 +++++++--- src/dnsmasq/forward.c | 5 +++++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 69eb3316ca..3e7a2718df 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -2368,6 +2368,7 @@ const char *edestr(int ede) case EDE_UNS_NS3_ITER: return "unsupported NSEC3 iterations value"; case EDE_UNABLE_POLICY: return "uanble to conform to policy"; case EDE_SYNTHESIZED: return "synthesized"; + case EDE_US_SERVFAIL: return "upstream returned SERVFAIL"; default: return "unknown"; } } diff --git a/src/dnsmasq/dns-protocol.h b/src/dnsmasq/dns-protocol.h index 0db5913f20..e71bedc46f 100644 --- a/src/dnsmasq/dns-protocol.h +++ b/src/dnsmasq/dns-protocol.h @@ -86,6 +86,7 @@ #define EDNS0_OPTION_UMBRELLA 20292 /* Cisco Umbrella temporary assignment */ /* RFC-8914 extended errors, negative values are our definitions */ +#define EDE_US_SERVFAIL -2 /* SERVFAIL from usptream */ #define EDE_UNSET -1 /* No extended DNS error available */ #define EDE_OTHER 0 /* Other */ #define EDE_USUPDNSKEY 1 /* Unsupported DNSKEY algo */ diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index da1af57f10..139ecdc689 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -793,6 +793,7 @@ struct dyndir { #define DNSSEC_FAIL_NSEC3_ITERS 0x0200 /* too many iterations in NSEC3 */ #define DNSSEC_FAIL_BADPACKET 0x0400 /* bad packet */ #define DNSSEC_FAIL_WORK 0x0800 /* too much crypto */ +#define DNSSEC_FAIL_UPSTREAM 0x1000 /* SERVFAIL from UPSTREAM */ #define STAT_ISEQUAL(a, b) (((a) & 0xffff0000) == (b)) diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 7b21e32409..ea2ad3224a 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -2000,8 +2000,11 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch if (neganswer) *neganswer = 0; + + if (RCODE(header) == SERVFAIL) + return STAT_BOGUS | DNSSEC_FAIL_UPSTREAM; - if (RCODE(header) == SERVFAIL || ntohs(header->qdcount) != 1) + if (ntohs(header->qdcount) != 1) return STAT_BOGUS; if (RCODE(header) != NXDOMAIN && RCODE(header) != NOERROR) @@ -2372,8 +2375,9 @@ int errflags_to_ede(int status) /* We can end up with more than one flag set for some errors, so this encodes a rough priority so the (eg) No sig is reported before no-unexpired-sig. */ - - if (status & DNSSEC_FAIL_NYV) + if (status & DNSSEC_FAIL_UPSTREAM) + return EDE_US_SERVFAIL; + else if (status & DNSSEC_FAIL_NYV) return EDE_SIG_NYV; else if (status & DNSSEC_FAIL_EXP) return EDE_SIG_EXP; diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index 3e68b765c2..df16f58520 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -1445,6 +1445,9 @@ void return_reply(time_t now, struct frec *forward, struct dns_header *header, s a.log.ede = ede; log_query(F_SECSTAT, domain, &a, result, 0); + + if (ede == EDE_US_SERVFAIL) + ede = EDE_DNSSEC_BOGUS; } } @@ -2807,6 +2810,8 @@ unsigned char *tcp_request(int confd, time_t now, a.log.ede = ede; log_query(F_SECSTAT, domain, &a, result, 0); + if (ede == EDE_US_SERVFAIL) + ede = EDE_DNSSEC_BOGUS; if ((daemon->limit[LIMIT_CRYPTO] - validatecount) > (int)daemon->metrics[METRIC_CRYPTO_HWM]) daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - validatecount; From 707bc2d25c27fc1a6c14d3fb8d383eea1ef57e5c Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 18 Jan 2026 18:57:08 +0000 Subject: [PATCH 012/101] Fix DNSSEC fail with CNAME replies to DS queries. A CNAME reply to a DNSSEC query was confusing the validation logic. It now accepts a signed CNAME reply to a DS query as proof that no DS exists at the domain. This fixes the DS/zone break detection logic. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 2 +- src/dnsmasq/dnssec.c | 77 ++++++++++++++++++++++++------------------- src/dnsmasq/forward.c | 4 +-- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index 139ecdc689..f68c3ef99b 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1486,7 +1486,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class, int *validate_count); int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class, - int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_count); + int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_count); int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen); size_t filter_rrsigs(struct dns_header *header, size_t plen); int setup_timestamp(void); diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index ea2ad3224a..4557174eb3 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -998,7 +998,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char char *keyname, int class, int *validate_counter) { unsigned char *p = (unsigned char *)(header+1); - int qtype, qclass, rc, i, neganswer = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0; + int qtype, qclass, rc, i, neganswer = 0, prim_ok = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0; int aclass, atype, rdlen, flags; unsigned long ttl; union all_addr a; @@ -1009,7 +1009,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char if (RCODE(header) == SERVFAIL) servfail = neganswer = nons = 1; else - rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons, &neg_ttl, validate_counter); + rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &prim_ok, &nons, &neg_ttl, validate_counter); p = (unsigned char *)(header+1); if (ntohs(header->qdcount) != 1 || @@ -1026,27 +1026,32 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char { if (STAT_ISEQUAL(rc, STAT_INSECURE)) { - if (option_bool(OPT_BOGUSPRIV) && - (flags = in_arpa_name_2_addr(name, &a)) && - ((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0)))) + /* A INSECURE DS answer is OK if it's negative and there's a CNAME answer to the DS answer which is + signed, since that's enough to prove that the DS record doesn't exist. */ + if (!neganswer || !prim_ok) { - my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name); - neganswer = 1; - nons = 0; /* If we're faking a DS, fake one with an NS. */ - neg_ttl = DNSSEC_ASSUMED_DS_TTL; - } - else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) - { - my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name); - neganswer = 1; - nons = 0; /* If we're faking a DS, fake one with an NS. */ - neg_ttl = DNSSEC_ASSUMED_DS_TTL; - } - else - { - my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); - log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0); - return STAT_BOGUS | DNSSEC_FAIL_INDET; + if (option_bool(OPT_BOGUSPRIV) && + (flags = in_arpa_name_2_addr(name, &a)) && + ((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0)))) + { + my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name); + neganswer = 1; + nons = 0; /* If we're faking a DS, fake one with an NS. */ + neg_ttl = DNSSEC_ASSUMED_DS_TTL; + } + else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) + { + my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name); + neganswer = 1; + nons = 0; /* If we're faking a DS, fake one with an NS. */ + neg_ttl = DNSSEC_ASSUMED_DS_TTL; + } + else + { + my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); + log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0); + return STAT_BOGUS | DNSSEC_FAIL_INDET; + } } } else @@ -1971,7 +1976,7 @@ static int zone_status(char *name, int class, char *keyname, time_t now) if the nons argument is non-NULL. */ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, - int *class, int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_counter) + int *class, int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_counter) { static unsigned char **targets = NULL; static int target_sz = 0; @@ -2280,6 +2285,20 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch secure = STAT_INSECURE; } + + /* For a DS record, we are interested also in if the answer to the DS query was + a CNAME RRset which validated. That's proof that the DS doesn't exist, + even if it's a CNAME which is not signed, and therefore we have no proof + of what it actually _is_. This return tells us that the answer to + primary query is secure, even is the whole answer is insecure, because + something down the CNAME list doesn't validate or doesn't exist. + Note that prim_ok is only valid when neganswer is true, ie either + the answer is the requested record or it's a CNAME that ends + in a missing answer or an unsigned zone. + */ + if (prim_ok) + *prim_ok = !targets[0]; + /* OK, all the RRsets validate, now see if we have a missing answer or CNAME target. */ for (j = 0; j namebuff, daemon->keyname, forward->class, &orig->validate_counter); else status = dnssec_validate_reply(now, header, plen, daemon->namebuff, daemon->keyname, &forward->class, - !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, &orig->validate_counter); + !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, NULL, &orig->validate_counter); if (STAT_ISEQUAL(status, STAT_ABANDONED)) log_resource = 1; @@ -2383,7 +2383,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount); else new_status = dnssec_validate_reply(now, header, n, name, keyname, &class, - !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, validatecount); + !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, NULL, validatecount); if (!STAT_ISEQUAL(new_status, STAT_NEED_DS) && !STAT_ISEQUAL(new_status, STAT_NEED_KEY) && !STAT_ISEQUAL(new_status, STAT_ABANDONED)) break; From cc286debfc99e4ab9f9bb6d8d8171fa56559619f Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 19 Jan 2026 20:42:26 +0100 Subject: [PATCH 013/101] Update embedded dnsmasq version to 2.93test1 Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b0cdbb04f..3a13171da3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.92rc3) +set(DNSMASQ_VERSION pi-hole-v2.93test1) add_subdirectory(src) From 350f2b0f806ae757e4006738237d3792194ed25d Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 27 Jan 2026 19:28:08 +0100 Subject: [PATCH 014/101] Update expected dnsmasq warnings Signed-off-by: Dominik --- test/dnsmasq_warnings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dnsmasq_warnings b/test/dnsmasq_warnings index 541c88aaa0..e66c5d6e59 100644 --- a/test/dnsmasq_warnings +++ b/test/dnsmasq_warnings @@ -87,7 +87,7 @@ src/dnsmasq/dnsmasq.c src/dnsmasq/dnssec.c my_syslog(LOG_WARNING, "limit exceeded: %s", message ? message : _("per-query crypto work")); src/dnsmasq/dnssec.c - my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); + my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); src/dnsmasq/dnssec.c my_syslog(LOG_WARNING, _("Negative DS reply without NS record received for %s, assuming non-DNSSEC domain-specific server."), name); src/dnsmasq/forward.c From bb8207b83d8126cb3ce4b3ee509d4604203d1e5b Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 24 Jan 2026 22:00:35 +0000 Subject: [PATCH 015/101] Fix memory allocation in blockdata_retrieve() This was functionally correct, but every call malloced a new buffer and freed the previous one, rather than only doing that when the buffer needed expansion. Signed-off-by: Dominik --- src/dnsmasq/blockdata.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/blockdata.c b/src/dnsmasq/blockdata.c index 668d63ff8e..dc02d46dc1 100644 --- a/src/dnsmasq/blockdata.c +++ b/src/dnsmasq/blockdata.c @@ -202,11 +202,15 @@ void *blockdata_retrieve(struct blockdata *block, size_t len, void *data) { if (len > buff_len) { - if (!(new = whine_malloc(len))) + blen = len + 1024; + if (!(new = whine_malloc(blen))) return NULL; + if (buff) free(buff); + buff = new; + buff_len = blen; } data = buff; } From 3c42f538b49cd2f978dbbdf07aef0f887da8efbc Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 24 Jan 2026 22:10:55 +0000 Subject: [PATCH 016/101] Add --log-malloc debugging option. Log all blocks malloced and all blocks freed. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 11 ++++++--- src/dnsmasq/option.c | 3 +++ src/dnsmasq/util.c | 56 +++++++++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index f68c3ef99b..1013ba45ea 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -293,7 +293,8 @@ struct event_desc { #define OPT_AUTH_LOG 76 #define OPT_LEASEQUERY 77 #define OPT_LOG_ONLY_FAILED 78 -#define OPT_LAST 79 +#define OPT_LOG_MALLOC 79 +#define OPT_LAST 80 #define OPTION_BITS (sizeof(unsigned int)*8) #define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) ) @@ -1514,8 +1515,12 @@ unsigned char *do_rfc1035_name(unsigned char *p, char *sval, char *limit); void *safe_malloc(size_t size); void safe_strncpy(char *dest, const char *src, size_t size); void safe_pipe(int *fd, int read_noblock); -void *whine_malloc(size_t size); -void *whine_realloc(void *ptr, size_t size); +#define whine_malloc(x) whine_malloc_real(__func__, __LINE__, (x)) +#define whine_realloc(x, y) whine_realloc_real(__func__, __LINE__, (x), (y)) +#define free(x) free_real(__func__, __LINE__, (x)) +void free_real(const char *func, unsigned int line, void *ptr); +void *whine_malloc_real(const char *func, unsigned int line, size_t size); +void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t size); int sa_len(union mysockaddr *addr); int sockaddr_isequal(const union mysockaddr *s1, const union mysockaddr *s2); int sockaddr_isnull(const union mysockaddr *s); diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index fbc1541dfa..4a765d60f3 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -201,6 +201,7 @@ struct myoption { #define LOPT_DO_ENCODE 388 #define LOPT_LEASEQUERY 389 #define LOPT_SPLIT_RELAY 390 +#define LOPT_LOG_MALLOC 391 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -402,6 +403,7 @@ static const struct myoption opts[] = { "no-ident", 0, 0, LOPT_NO_IDENT }, { "max-tcp-connections", 1, 0, LOPT_MAX_PROCS }, { "leasequery", 2, 0, LOPT_LEASEQUERY }, + { "log-malloc", 0, 0, LOPT_LOG_MALLOC }, { NULL, 0, 0, 0 } }; @@ -610,6 +612,7 @@ static struct { { LOPT_NO_IDENT, OPT_NO_IDENT, NULL, gettext_noop("Do not add CHAOS TXT records."), NULL }, { LOPT_CACHE_RR, ARG_DUP, "", gettext_noop("Cache this DNS resource record type."), NULL }, { LOPT_MAX_PROCS, ARG_ONE, "", gettext_noop("Maximum number of concurrent tcp connections."), NULL }, + { LOPT_LOG_MALLOC, OPT_LOG_MALLOC, NULL, gettext_noop("Log memory allocation for debugging."), NULL }, { 0, 0, NULL, NULL, NULL } }; diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 240e3cc732..0639c8bb61 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -345,26 +345,6 @@ void safe_pipe(int *fd, int read_noblock) die(_("cannot create pipe: %s"), NULL, EC_MISC); } -void *whine_malloc(size_t size) -{ - void *ret = calloc(1, size); - - if (!ret) - my_syslog(LOG_ERR, _("failed to allocate %d bytes"), (int) size); - - return ret; -} - -void *whine_realloc(void *ptr, size_t size) -{ - void *ret = realloc(ptr, size); - - if (!ret) - my_syslog(LOG_ERR, _("failed to reallocate %d bytes"), (int) size); - - return ret; -} - int sockaddr_isequal(const union mysockaddr *s1, const union mysockaddr *s2) { if (s1->sa.sa_family == s2->sa.sa_family) @@ -958,3 +938,39 @@ int kernel_version(void) return version * 256 + (split ? atoi(split) : 0); } #endif + +#define hash_ptr(x) (((unsigned int)(((char *)(x)) - ((char *)NULL))) & 0xffffff) + +void *whine_malloc_real(const char *func, unsigned int line, size_t size) +{ + void *ret = calloc(1, size); + + if (!ret) + my_syslog(LOG_ERR, _("failed to allocate %d bytes"), (int) size); + else if (option_bool(OPT_LOG_MALLOC)) + my_syslog(LOG_INFO, _("malloc: %s:%u %zu bytes at %x"), func, line, size, hash_ptr(ret)); + + return ret; +} + +void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t size) +{ + unsigned int old = hash_ptr(ptr); + void *ret = realloc(ptr, size); + + if (!ret) + my_syslog(LOG_ERR, _("failed to reallocate %d bytes"), (int) size); + else if (option_bool(OPT_LOG_MALLOC)) + my_syslog(LOG_INFO, _("realloc: %s:%u %zu bytes from %x to %x"), func, line, size, old, hash_ptr(ret)); + + return ret; +} + +void free_real(const char *func, unsigned int line, void *ptr) +{ + if (option_bool(OPT_LOG_MALLOC)) + my_syslog(LOG_INFO, _("free: %s:%u block at %x"), func, line, hash_ptr(ptr)); + +#undef free + free(ptr); +} From b36cbec5a2078daeefa8ee0c37e61c5105ebb125 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 24 Jan 2026 23:07:40 +0000 Subject: [PATCH 017/101] Don't log free(NULL) calls. Signed-off-by: Dominik --- src/dnsmasq/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 0639c8bb61..f6bd26ccbd 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -968,7 +968,7 @@ void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t void free_real(const char *func, unsigned int line, void *ptr) { - if (option_bool(OPT_LOG_MALLOC)) + if (ptr && option_bool(OPT_LOG_MALLOC)) my_syslog(LOG_INFO, _("free: %s:%u block at %x"), func, line, hash_ptr(ptr)); #undef free From ace64db17252808a2006df1fb6ec933315188012 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 24 Jan 2026 23:59:06 +0000 Subject: [PATCH 018/101] Don't start malloc() logging until the log system is configured. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 3 +++ src/dnsmasq/dnsmasq.h | 1 + src/dnsmasq/util.c | 15 +++++++++------ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index fe3ba2c345..659620902b 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -873,6 +873,9 @@ int main_dnsmasq (int argc, char **argv) } #endif + /* Don't start logging malloc before logging is set up. */ + daemon->log_malloc = option_bool(OPT_LOG_MALLOC); + if (daemon->port == 0) my_syslog(LOG_INFO, _("started, version %s DNS disabled"), VERSION); else diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index 1013ba45ea..807448f435 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1223,6 +1223,7 @@ extern struct daemon { int log_fac; /* log facility */ char *log_file; /* optional log file */ int max_logs; /* queue limit */ + int log_malloc; /* log malloc/realloc/free */ int randport_limit; /* Maximum number of source ports for query. */ int cachesize, ftabsize; int port, query_port, min_port, max_port; diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index f6bd26ccbd..943fd884de 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -947,7 +947,7 @@ void *whine_malloc_real(const char *func, unsigned int line, size_t size) if (!ret) my_syslog(LOG_ERR, _("failed to allocate %d bytes"), (int) size); - else if (option_bool(OPT_LOG_MALLOC)) + else if (daemon->log_malloc) my_syslog(LOG_INFO, _("malloc: %s:%u %zu bytes at %x"), func, line, size, hash_ptr(ret)); return ret; @@ -960,17 +960,20 @@ void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t if (!ret) my_syslog(LOG_ERR, _("failed to reallocate %d bytes"), (int) size); - else if (option_bool(OPT_LOG_MALLOC)) + else if (daemon->log_malloc) my_syslog(LOG_INFO, _("realloc: %s:%u %zu bytes from %x to %x"), func, line, size, old, hash_ptr(ret)); return ret; } +#undef free void free_real(const char *func, unsigned int line, void *ptr) { - if (ptr && option_bool(OPT_LOG_MALLOC)) - my_syslog(LOG_INFO, _("free: %s:%u block at %x"), func, line, hash_ptr(ptr)); + if (ptr) + { + if (daemon->log_malloc) + my_syslog(LOG_INFO, _("free: %s:%u block at %x"), func, line, hash_ptr(ptr)); -#undef free - free(ptr); + free(ptr); + } } From 34c01e1dd2b36e066bd4a44bb7862715e43ec3fe Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 26 Jan 2026 15:25:22 +0000 Subject: [PATCH 019/101] Rationalise DNS TCP buffer use. This fixes the plethora of 64k buffers that got allocated when doing DNSSEC over TCP. By using the UDP buffer is pass the query into tcp_talk() and allowing tcp_talk to allocate its output buffer one the size of the reply is known, we only need to allocate as much memory as is required. The final reply to the TCP query still needs the 64k buffer because answer_request() and answer_auth() are not capable of extending their output buffers. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 8 +- src/dnsmasq/dnsmasq.h | 4 +- src/dnsmasq/forward.c | 263 +++++++++++++++++++++--------------------- 3 files changed, 136 insertions(+), 139 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 659620902b..d05840ae66 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -2017,11 +2017,11 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) pid_t p; union mysockaddr tcp_addr; socklen_t tcp_len = sizeof(union mysockaddr); - unsigned char *buff; struct server *s; int flags, auth_dns = 0; struct in_addr netmask; int pipefd[2]; + struct iovec tcpbuff; #ifdef HAVE_LINUX_NETWORK unsigned char a = 0; #endif @@ -2202,14 +2202,12 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) FTL_iface(iface, NULL, 0); /**********************************************/ - buff = tcp_request(confd, now, &tcp_addr, netmask, auth_dns); /************ Pi-hole modification ************/ FTL_TCP_worker_terminating(true); /**********************************************/ - - if (buff) - free(buff); + tcp_request(confd, now, &tcpbuff, &tcp_addr, netmask, auth_dns); + free(tcpbuff.iov_base); for (s = daemon->servers; s; s = s->next) if (s->tcpfd != -1) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index 807448f435..9979b796a1 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1588,8 +1588,8 @@ int tcp_from_udp(time_t now, int status, struct dns_header *header, ssize_t *n, int class, char *name, struct server *server, int *keycount, int *validatecount); #endif -unsigned char *tcp_request(int confd, time_t now, - union mysockaddr *local_addr, struct in_addr netmask, int auth_dns); +void tcp_request(int confd, time_t now, struct iovec *bigbuff, + union mysockaddr *local_addr, struct in_addr netmask, int auth_dns); void server_gone(struct server *server); int send_from(int fd, int nowild, char *packet, size_t len, union mysockaddr *to, union all_addr *source, diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index e12537c433..a8d58bffd5 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -143,9 +143,9 @@ static void _log_query_mysockaddr(unsigned int flags, char *name, union mysockad } static void server_send(struct server *server, int fd, - const void *header, size_t plen, int flags) + const void *header, size_t plen) { - while (retry_send(sendto(fd, header, plen, flags, + while (retry_send(sendto(fd, header, plen, 0, &server->addr.sa, sa_len(&server->addr)))); } @@ -1118,7 +1118,7 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header, set_outgoing_mark(orig, fd); #endif - server_send(server, fd, header, nn, 0); + server_send(server, fd, header, nn); server->queries++; #ifdef HAVE_DUMPFILE dump_packet_udp(DUMP_SEC_QUERY, (void *)header, (size_t)nn, NULL, &server->addr, fd); @@ -2116,23 +2116,24 @@ void receive_query(struct listener *listen, time_t now) } -/* Send query in packet, qsize to a server determined by first,last,start and - get the reply. return reply size. */ -static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, size_t qsize, - int have_mark, unsigned int mark, struct server **servp) +/* Send query in header, qsize to a server determined by first,last,start and + get the reply into the buffer passed by outbuff. Return reply size. */ +static ssize_t tcp_talk(int first, int last, int start, struct dns_header *header, size_t qsize, + struct iovec *recvbuff, int have_mark, unsigned int mark, struct server **servp) { int firstsendto = -1; - u16 *length = (u16 *)packet; - unsigned char *payload = &packet[2]; - struct dns_header *header = (struct dns_header *)payload; - unsigned int rsize; + u16 length; + unsigned int rsize = 0; int class, rclass, type, rtype; unsigned char *p; - struct blockdata *saved_question; struct timeval tv; // Pi-hole char where = 0; +#ifdef MSG_FASTOPEN + struct msghdr msg; + struct iovec sendio[2]; +#endif (void)mark; (void)have_mark; @@ -2144,10 +2145,6 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, GETSHORT(type, p); GETSHORT(class, p); - /* Save question for retry. */ - if (!(saved_question = blockdata_alloc((char *)header, (size_t)qsize))) - return 0; - while (1) { int data_sent = 0, fatal = 0; @@ -2169,9 +2166,7 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, *servp = serv = daemon->serverarray[start]; retry: - blockdata_retrieve(saved_question, qsize, header); - - *length = htons(qsize); + length = htons(qsize); if (serv->tcpfd == -1) { @@ -2205,8 +2200,20 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, #endif #ifdef MSG_FASTOPEN - server_send(serv, serv->tcpfd, packet, qsize + sizeof(u16), MSG_FASTOPEN); - + sendio[0].iov_base = (unsigned char *)&length; + sendio[0].iov_len = sizeof(length); + sendio[1].iov_base = (unsigned char *)header; + sendio[1].iov_len = qsize; + msg.msg_name = &serv->addr.sa; + msg.msg_namelen = sa_len(&serv->addr); + msg.msg_iov = sendio; + msg.msg_iovlen = 2; + msg.msg_control = NULL; + msg.msg_controllen = 0; + msg.msg_flags = 0; + + while (retry_send(sendmsg(serv->tcpfd, &msg, MSG_FASTOPEN))); + if (errno == 0) data_sent = 1; else if (errno == ETIMEDOUT || errno == EHOSTUNREACH || errno == EINPROGRESS || errno == ECONNREFUSED) @@ -2235,12 +2242,15 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, serv->flags &= ~SERV_GOT_TCP; } - /* We us the _ONCE veriant of read_write() here because we've set a timeout on the tcp socket + /* We us the _ONCE variant of read_write() here because we've set a timeout on the tcp socket and wish to abort if the whole data is not read/written within the timeout. */ - if ((!data_sent && (where = 2) && !read_write(serv->tcpfd, (unsigned char *)packet, qsize + sizeof(u16), RW_WRITE_ONCE)) || - ((where = 3) && !read_write(serv->tcpfd, (unsigned char *)length, sizeof (*length), RW_READ_ONCE)) || - ((where = 4) && !read_write(serv->tcpfd, payload, (rsize = ntohs(*length)), RW_READ_ONCE))) - { + if ((!data_sent && + ((where = 2) && !read_write(serv->tcpfd, (unsigned char *)&length, sizeof(length), RW_WRITE_ONCE) || + ((where = 3) && !read_write(serv->tcpfd, (unsigned char *)header, qsize, RW_WRITE_ONCE)))) || + ((where = 4) && !read_write(serv->tcpfd, (unsigned char *)&length, sizeof(length), RW_READ_ONCE)) || + ((where = 5) && !expand_buf(recvbuff, (rsize = ntohs(length)))) || + ((where = 6) && !read_write(serv->tcpfd, recvbuff->iov_base, rsize, RW_READ_ONCE))) + { /* We get data then EOF, reopen connection to same server, else try next. This avoids DoS from a server which accepts connections and then closes them. */ @@ -2253,28 +2263,28 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, else goto failed; } + else + { + /* If the question section of the reply doesn't match the question we sent, then + someone might be attempting to insert bogus values into the cache by + sending replies containing questions and bogus answers. + Try another server, or give up */ + p = (unsigned char *)(((struct dns_header *)recvbuff->iov_base)+1); + if (extract_name(((struct dns_header *)recvbuff->iov_base), rsize, &p, daemon->namebuff, EXTR_NAME_COMPARE, 4) != 1) + continue; + GETSHORT(rtype, p); + GETSHORT(rclass, p); - /* If the question section of the reply doesn't match the question we sent, then - someone might be attempting to insert bogus values into the cache by - sending replies containing questions and bogus answers. - Try another server, or give up */ - p = (unsigned char *)(header+1); - if (extract_name(header, rsize, &p, daemon->namebuff, EXTR_NAME_COMPARE, 4) != 1) - continue; - GETSHORT(rtype, p); - GETSHORT(rclass, p); - - if (type != rtype || class != rclass) - continue; + if (type != rtype || class != rclass) + continue; + } serv->flags |= SERV_GOT_TCP; *servp = serv; - blockdata_free(saved_question); return rsize; } - blockdata_free(saved_question); return 0; } @@ -2286,19 +2296,16 @@ int tcp_from_udp(time_t now, int status, struct dns_header *header, ssize_t *ple int class, char *name, struct server *server, int *keycount, int *validatecount) { - unsigned char *packet = whine_malloc(65536 + MAXDNAME + RRFIXEDSZ + sizeof(u16)); - struct dns_header *new_header = (struct dns_header *)&packet[2]; int start, first, last, new_status; ssize_t n = *plenp; int log_save = daemon->log_display_id; + struct iovec recvbuff; + + recvbuff.iov_len = 0; + recvbuff.iov_base = NULL; *plenp = 0; - if (!packet) - return STAT_ABANDONED; - - memcpy(new_header, header, n); - /* Set TCP flag in logs. */ daemon->log_display_id = -daemon->log_display_id; @@ -2316,10 +2323,11 @@ int tcp_from_udp(time_t now, int status, struct dns_header *header, ssize_t *ple log_query_mysockaddr(F_NOEXTRA | F_DNSSEC | F_SERVER, name, &server->addr, STAT_ISEQUAL(status, STAT_NEED_KEY) ? "dnssec-query[DNSKEY]" : "dnssec-query[DS]", 0); - if ((n = tcp_talk(first, last, start, packet, n, 0, 0, &server)) == 0) + if ((n = tcp_talk(first, last, start, header, n, &recvbuff, 0, 0, &server)) == 0) new_status = STAT_ABANDONED; else { + struct dns_header *new_header = (struct dns_header *)recvbuff.iov_base; new_status = tcp_key_recurse(now, status, new_header, n, class, daemon->namebuff, daemon->keyname, server, 0, 0, keycount, validatecount); if (STAT_ISEQUAL(status, STAT_OK)) @@ -2356,7 +2364,7 @@ int tcp_from_udp(time_t now, int status, struct dns_header *header, ssize_t *ple } daemon->log_display_id = log_save; - free(packet); + free(recvbuff.iov_base); return new_status; } @@ -2366,11 +2374,14 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si int have_mark, unsigned int mark, int *keycount, int *validatecount) { int first, last, start, new_status; - unsigned char *packet = NULL; - struct dns_header *new_header = NULL; + struct iovec new_packet; + struct dns_header *query_header = NULL, *new_header; FTL_header_analysis(header, server, daemon->log_display_id); + new_packet.iov_base = NULL; + new_packet.iov_len = 0; + while (1) { size_t m; @@ -2404,34 +2415,21 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si } /* Can't validate because we need a key/DS whose name now in keyname. - Make query for same, and recurse to validate */ - if (!packet) - { - packet = whine_malloc(65536 + MAXDNAME + RRFIXEDSZ + sizeof(u16)); - new_header = (struct dns_header *)&packet[2]; - } - - if (!packet) - { - new_status = STAT_ABANDONED; - break; - } - - m = dnssec_generate_query(new_header, ((unsigned char *) new_header) + 65536, keyname, class, 0, + Make query for same in UDP packet buffer, recurse to validate*/ + query_header = (struct dns_header *)daemon->packet; + daemon->srv_save = NULL; + + m = dnssec_generate_query(query_header, ((unsigned char *)query_header) + daemon->edns_pktsz, keyname, class, 0, STAT_ISEQUAL(new_status, STAT_NEED_KEY) ? T_DNSKEY : T_DS); - if ((start = dnssec_server(server, keyname, STAT_ISEQUAL(new_status, STAT_NEED_DS), &first, &last)) == -1) + if ((start = dnssec_server(server, keyname, STAT_ISEQUAL(new_status, STAT_NEED_DS), &first, &last)) == -1 || + (m = tcp_talk(first, last, start, query_header, m, &new_packet, have_mark, mark, &server)) == 0) { new_status = STAT_ABANDONED; break; } - if ((m = tcp_talk(first, last, start, packet, m, have_mark, mark, &server)) == 0) - { - new_status = STAT_ABANDONED; - break; - } - + new_header = new_packet.iov_base; log_save = daemon->log_display_id; daemon->log_display_id = -(++daemon->log_id); @@ -2448,8 +2446,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si break; } - if (packet) - free(packet); + free(new_packet.iov_base); return new_status; } @@ -2459,11 +2456,11 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si /* The daemon forks before calling this: it should deal with one connection, blocking as necessary, and then return. Note, need to be a bit careful about resources for debug mode, when the fork is suppressed: that's - done by the caller. */ -unsigned char *tcp_request(int confd, time_t now, - union mysockaddr *local_addr, struct in_addr netmask, int auth_dns) + done by the caller, which also frees bigbuff. */ +void tcp_request(int confd, time_t now, struct iovec *bigbuff, + union mysockaddr *local_addr, struct in_addr netmask, int auth_dns) { - size_t size = 0, saved_size = 0; + size_t size = 0; int norebind = 0; #ifdef HAVE_CONNTRACK int allowed = 1; @@ -2472,16 +2469,10 @@ unsigned char *tcp_request(int confd, time_t now, int local_auth = 0; #endif int checking_disabled, do_bit = 0, ad_reqd = 0, have_pseudoheader = 0; - struct blockdata *saved_question = NULL; unsigned short qtype; unsigned int gotname = 0; - /* Max TCP packet + slop + size */ - unsigned char *packet = whine_malloc(65536 + MAXDNAME + RRFIXEDSZ + sizeof(u16)); - unsigned char *payload = &packet[2]; - u16 tcp_len; - /* largest field in header is 16-bits, so this is still sufficiently aligned */ - struct dns_header *header = (struct dns_header *)payload; - u16 *length = (u16 *)packet; + u16 tcp_len, out_len; + struct dns_header *header, *out_header; struct server *serv; struct in_addr dst_addr_4; union mysockaddr peer_addr; @@ -2496,8 +2487,11 @@ unsigned char *tcp_request(int confd, time_t now, bool piholeblocked = false; /**********************************************/ - if (!packet || getpeername(confd, (struct sockaddr *)&peer_addr, &peer_len) == -1) - return packet; + bigbuff->iov_base = NULL; + bigbuff->iov_len = 0; + + if (getpeername(confd, (struct sockaddr *)&peer_addr, &peer_len) == -1) + return; #ifdef HAVE_CONNTRACK /* Get connection mark of incoming query to set on outgoing connections. */ @@ -2518,7 +2512,7 @@ unsigned char *tcp_request(int confd, time_t now, if (option_bool(OPT_LOCAL_SERVICE)) { struct addrlist *addr; - + if (peer_addr.sa.sa_family == AF_INET6) { for (addr = daemon->interface_addrs; addr; addr = addr->next) @@ -2541,7 +2535,7 @@ unsigned char *tcp_request(int confd, time_t now, { prettyprint_addr(&peer_addr, daemon->addrbuff); my_syslog(LOG_WARNING, _("ignoring query from non-local network %s"), daemon->addrbuff); - return packet; + return; } } @@ -2559,18 +2553,26 @@ unsigned char *tcp_request(int confd, time_t now, if (query_count >= TCP_MAX_QUERIES) break; + /* Now get the query into the normal UDP packet buffer. + Ignore queries long than this. If we're answering locally, + copy the query into the output buffer, but for forwarding, tcp_talk() + wants the query in a a different buffer from the reply. + Note that we overwrote any saved UDP query - this onlt matters in debug mode. */ + daemon->srv_save = NULL; if (!read_write(confd, (unsigned char *)&tcp_len, sizeof(tcp_len), RW_READ) || - !(size = ntohs(tcp_len)) || - !read_write(confd, payload, size, RW_READ)) + !(size = ntohs(tcp_len)) || size > (size_t)daemon->packet_buff_sz || + !read_write(confd, (unsigned char *)daemon->packet, size, RW_READ)) break; - + if (size < (int)sizeof(struct dns_header)) continue; - /* Clear buffer beyond request to avoid risk of - information disclosure. */ - memset(payload + size, 0, 65536 - size); + /* Make sure we have a buffer big enough for the largest answer. */ + expand_buf(bigbuff, 65536 + MAXDNAME + RRFIXEDSZ); + out_header = bigbuff->iov_base; + /* header == query */ + header = (struct dns_header *)daemon->packet; query_count++; /* log_query gets called indirectly all over the place, so @@ -2596,9 +2598,6 @@ unsigned char *tcp_request(int confd, time_t now, ede = EDE_INVALID_DATA; else { - if (saved_question) - blockdata_free(saved_question); - do_bit = 0; if (find_pseudoheader(header, (size_t)size, NULL, &pheader, NULL, NULL)) @@ -2613,9 +2612,12 @@ unsigned char *tcp_request(int confd, time_t now, do_bit = 1; /* do bit */ } - size = add_edns0_config(header, size, ((unsigned char *) header) + 65536, &peer_addr, now, &cacheable); - saved_question = blockdata_alloc((char *)header, (size_t)size); - saved_size = size; + size = add_edns0_config(header, size, ((unsigned char *) header) + daemon->edns_pktsz, &peer_addr, now, &cacheable); + + /* Clear buffer to avoid risk of information disclosure. */ + memset(bigbuff->iov_base, 0, bigbuff->iov_len); + /* Copy query into output buffer for local answering */ + memcpy(out_header, header, size); log_query_mysockaddr((auth_dns ? F_NOERR | F_AUTH : 0) | F_QUERY | F_FORWARD, daemon->namebuff, &peer_addr, NULL, qtype); @@ -2677,7 +2679,7 @@ unsigned char *tcp_request(int confd, time_t now, #endif #ifdef HAVE_AUTH else if (auth_dns) - m = answer_auth(header, ((char *) header) + 65536, (size_t)size, now, &peer_addr, local_auth); + m = answer_auth(out_header, ((char *) out_header) + 65536, (size_t)size, now, &peer_addr, local_auth); #endif /************ Pi-hole modification ************/ // Interface name is known from before forking @@ -2702,7 +2704,7 @@ unsigned char *tcp_request(int confd, time_t now, } /**********************************************/ else - m = answer_request(header, ((char *) header) + 65536, (size_t)size, + m = answer_request(out_header, ((char *) out_header) + 65536, (size_t)size, dst_addr_4, netmask, now, ad_reqd, do_bit, !cacheable, &stale, &filtered); } } @@ -2710,15 +2712,12 @@ unsigned char *tcp_request(int confd, time_t now, /* Do this by steam now we're not in the select() loop */ check_log_writer(1); - if (m == 0 && ede == EDE_UNSET && saved_question) + if (m == 0 && ede == EDE_UNSET) { struct server *master; int start; int no_cache_dnssec = 0, cache_secure = 0, bogusanswer = 0; - blockdata_retrieve(saved_question, (size_t)saved_size, header); - size = saved_size; - /* save state of "cd" flag in query */ checking_disabled = header->hb4 & HB4_CD; @@ -2747,7 +2746,7 @@ unsigned char *tcp_request(int confd, time_t now, #ifdef HAVE_DNSSEC if (option_bool(OPT_DNSSEC_VALID)) { - size = add_do_bit(header, size, ((unsigned char *) header) + 65536); + size = add_do_bit(header, size, ((unsigned char *) header) + daemon->edns_pktsz); /* For debugging, set Checking Disabled, otherwise, have the upstream check too, this allows it to select auth servers when one is returning bad data. */ @@ -2757,12 +2756,14 @@ unsigned char *tcp_request(int confd, time_t now, #endif /* Loop round available servers until we succeed in connecting to one. */ - if ((m = tcp_talk(first, last, start, packet, size, have_mark, mark, &serv)) == 0) + if ((m = tcp_talk(first, last, start, header, size, bigbuff, have_mark, mark, &serv)) == 0) ede = EDE_NETERR; else { + /* just in case tcp_talk() expanded buffer - should never happen */ + out_header = bigbuff->iov_base; /* get query name again for logging - may have been overwritten */ - if (!extract_name(header, (unsigned int)size, NULL, daemon->namebuff, EXTR_NAME_EXTRACT, 0)) + if (!extract_name(out_header, (unsigned int)size, NULL, daemon->namebuff, EXTR_NAME_EXTRACT, 0)) strcpy(daemon->namebuff, "query"); log_query_mysockaddr(F_SERVER | F_FORWARD, daemon->namebuff, &serv->addr, NULL, 0); @@ -2778,7 +2779,8 @@ unsigned char *tcp_request(int confd, time_t now, { int keycount = daemon->limit[LIMIT_WORK]; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */ int validatecount = daemon->limit[LIMIT_CRYPTO]; - int status = tcp_key_recurse(now, STAT_OK, header, m, 0, daemon->namebuff, daemon->keyname, + /* tcp_key_recurse() may overwrite packetbuf, and thuse *header is now invalid */ + int status = tcp_key_recurse(now, STAT_OK, out_header, m, 0, daemon->namebuff, daemon->keyname, serv, have_mark, mark, &keycount, &validatecount); char *result, *domain = "result"; @@ -2804,7 +2806,7 @@ unsigned char *tcp_request(int confd, time_t now, no_cache_dnssec = 1; bogusanswer = 1; - if (extract_name(header, m, NULL, daemon->namebuff, EXTR_NAME_EXTRACT, 0)) + if (extract_name(out_header, m, NULL, daemon->namebuff, EXTR_NAME_EXTRACT, 0)) domain = daemon->namebuff; } @@ -2828,18 +2830,18 @@ unsigned char *tcp_request(int confd, time_t now, /* restore CD bit to the value in the query */ if (checking_disabled) - header->hb4 |= HB4_CD; + out_header->hb4 |= HB4_CD; else - header->hb4 &= ~HB4_CD; + out_header->hb4 &= ~HB4_CD; /* Never cache answers which are contingent on the source or MAC address EDSN0 option, since the cache is ignorant of such things. */ if (!cacheable) no_cache_dnssec = 1; - m = process_reply(header, now, serv, (unsigned int)m, + m = process_reply(out_header, now, serv, (unsigned int)m, option_bool(OPT_NO_REBIND) && !norebind, no_cache_dnssec, cache_secure, bogusanswer, - ad_reqd, do_bit, !have_pseudoheader, &peer_addr, ((unsigned char *)header) + 65536, ede); + ad_reqd, do_bit, !have_pseudoheader, &peer_addr, ((unsigned char *)out_header) + 65536, ede); /* process_reply() adds pheader itself */ have_pseudoheader = 0; @@ -2854,8 +2856,8 @@ unsigned char *tcp_request(int confd, time_t now, /* In case of local answer or no connections made. */ if (m == 0 && !piholeblocked) // Pi-hole modified to ensure we don't provide local answers when dropping the reply { - if (!(m = make_local_answer(flags, gotname, size, header, daemon->namebuff, - ((char *) header) + 65536, first, last, ede))) + if (!(m = make_local_answer(flags, gotname, size, out_header, daemon->namebuff, + ((char *) out_header) + 65536, first, last, ede))) break; } else if (ede == EDE_UNSET) @@ -2871,14 +2873,12 @@ unsigned char *tcp_request(int confd, time_t now, u16 swap = htons((u16)ede); if (ede != EDE_UNSET) - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, EDNS0_OPTION_EDE, (unsigned char *)&swap, 2, do_bit, 0); + m = add_pseudoheader(out_header, m, ((unsigned char *) out_header) + 65536, EDNS0_OPTION_EDE, (unsigned char *)&swap, 2, do_bit, 0); else - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, 0, NULL, 0, do_bit, 0); + m = add_pseudoheader(out_header, m, ((unsigned char *) out_header) + 65536, 0, NULL, 0, do_bit, 0); } - - check_log_writer(1); - *length = htons(m); + check_log_writer(1); #if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS) #ifdef HAVE_AUTH @@ -2888,7 +2888,9 @@ unsigned char *tcp_request(int confd, time_t now, report_addresses(header, m, mark); #endif - if (!read_write(confd, packet, m + sizeof(u16), RW_WRITE)) + out_len = htons(m); + if (!read_write(confd, (unsigned char *)&out_len, sizeof(out_len), RW_WRITE) | + !read_write(confd, bigbuff->iov_base, m, RW_WRITE)) break; /* If we answered with stale data, this process will now try and get fresh data into @@ -2904,18 +2906,15 @@ unsigned char *tcp_request(int confd, time_t now, daemon->log_source_addr = NULL; } } - - /* If we ran once to get fresh data, confd is already closed. */ + +/* If we ran once to get fresh data, confd is already closed. */ if (!do_stale) { shutdown(confd, SHUT_RDWR); close(confd); } - - blockdata_free(saved_question); + check_log_writer(1); - - return packet; } /* return a UDP socket bound to a random port, have to cope with straying into @@ -3416,7 +3415,7 @@ void resend_query(void) { if (daemon->srv_save) server_send(daemon->srv_save, daemon->fd_save, - daemon->packet, daemon->packet_len, 0); + daemon->packet, daemon->packet_len); } /* A server record is going away, remove references to it */ From 0456c8e40d93bb3e88817b33e232f400bb3f20b3 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 26 Jan 2026 21:44:27 +0000 Subject: [PATCH 020/101] Optimise TCP send. In the DNS TCP code, there are a couple of places where we have a buffer containing a message which we need to send via TCP. The DNS protocol is that this is sent as <16-bit length in network order> Making two write calls, one for the length and one for the message causes the TCP stack to send two packets, one for each. A single packet containing both is preferable from a performance POV. Implement a scatter-gather version of our read_write() wrapper and use it where necessary to send TCP DNS messages. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 1 + src/dnsmasq/forward.c | 38 +++++++++++++++-------------- src/dnsmasq/util.c | 57 ++++++++++++++++++++++++++++--------------- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index 9979b796a1..c972bf50c3 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1546,6 +1546,7 @@ int memcmp_masked(unsigned char *a, unsigned char *b, int len, int expand_buf(struct iovec *iov, size_t size); char *print_mac(char *buff, unsigned char *mac, int len); int read_write(int fd, unsigned char *packet, int size, int rw); +int read_writev(int fd, struct iovec *iov, int iovcnt, int rw); void close_fds(long max_fd, int spare1, int spare2, int spare3); int wildcard_match(const char* wildcard, const char* match); int wildcard_matchn(const char* wildcard, const char* match, int num); diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index a8d58bffd5..1f43bb51f7 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -2130,9 +2130,9 @@ static ssize_t tcp_talk(int first, int last, int start, struct dns_header *heade // Pi-hole char where = 0; + struct iovec sendio[2]; #ifdef MSG_FASTOPEN struct msghdr msg; - struct iovec sendio[2]; #endif (void)mark; @@ -2145,6 +2145,12 @@ static ssize_t tcp_talk(int first, int last, int start, struct dns_header *heade GETSHORT(type, p); GETSHORT(class, p); + length = htons(qsize); + sendio[0].iov_base = &length; + sendio[0].iov_len = sizeof(length); + sendio[1].iov_base = header; + sendio[1].iov_len = qsize; + while (1) { int data_sent = 0, fatal = 0; @@ -2166,8 +2172,6 @@ static ssize_t tcp_talk(int first, int last, int start, struct dns_header *heade *servp = serv = daemon->serverarray[start]; retry: - length = htons(qsize); - if (serv->tcpfd == -1) { if ((serv->tcpfd = socket(serv->addr.sa.sa_family, SOCK_STREAM, 0)) == -1) @@ -2198,12 +2202,7 @@ static ssize_t tcp_talk(int first, int last, int start, struct dns_header *heade tv.tv_sec += TCP_TIMEOUT; setsockopt(serv->tcpfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); #endif - #ifdef MSG_FASTOPEN - sendio[0].iov_base = (unsigned char *)&length; - sendio[0].iov_len = sizeof(length); - sendio[1].iov_base = (unsigned char *)header; - sendio[1].iov_len = qsize; msg.msg_name = &serv->addr.sa; msg.msg_namelen = sa_len(&serv->addr); msg.msg_iov = sendio; @@ -2242,14 +2241,12 @@ static ssize_t tcp_talk(int first, int last, int start, struct dns_header *heade serv->flags &= ~SERV_GOT_TCP; } - /* We us the _ONCE variant of read_write() here because we've set a timeout on the tcp socket + /* We use the _ONCE variant of read_write() here because we've set a timeout on the tcp socket and wish to abort if the whole data is not read/written within the timeout. */ - if ((!data_sent && - ((where = 2) && !read_write(serv->tcpfd, (unsigned char *)&length, sizeof(length), RW_WRITE_ONCE) || - ((where = 3) && !read_write(serv->tcpfd, (unsigned char *)header, qsize, RW_WRITE_ONCE)))) || - ((where = 4) && !read_write(serv->tcpfd, (unsigned char *)&length, sizeof(length), RW_READ_ONCE)) || - ((where = 5) && !expand_buf(recvbuff, (rsize = ntohs(length)))) || - ((where = 6) && !read_write(serv->tcpfd, recvbuff->iov_base, rsize, RW_READ_ONCE))) + if ((!data_sent && (where = 2) && !read_writev(serv->tcpfd, sendio, 2, RW_WRITE_ONCE)) || + ((where = 3) && !read_write(serv->tcpfd, (unsigned char *)&length, sizeof(length), RW_READ_ONCE)) || + ((where = 4) && !expand_buf(recvbuff, (rsize = ntohs(length)))) || + ((where = 5) && !read_write(serv->tcpfd, recvbuff->iov_base, rsize, RW_READ_ONCE))) { /* We get data then EOF, reopen connection to same server, else try next. This avoids DoS from a server which accepts @@ -2486,7 +2483,8 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, /************ Pi-hole modification ************/ bool piholeblocked = false; /**********************************************/ - + struct iovec out_iov[2]; + bigbuff->iov_base = NULL; bigbuff->iov_len = 0; @@ -2888,9 +2886,13 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, report_addresses(header, m, mark); #endif + /* use scatter-gather IO so that length doesn't end up in separate packet. */ out_len = htons(m); - if (!read_write(confd, (unsigned char *)&out_len, sizeof(out_len), RW_WRITE) | - !read_write(confd, bigbuff->iov_base, m, RW_WRITE)) + out_iov[0].iov_len = sizeof(out_len); + out_iov[0].iov_base = &out_len; + out_iov[1].iov_len = m; + out_iov[1].iov_base = bigbuff->iov_base; + if (!read_writev(confd, out_iov, 2, RW_WRITE)) break; /* If we answered with stale data, this process will now try and get fresh data into diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 943fd884de..632091f583 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -761,43 +761,60 @@ int retry_send(ssize_t rc) "once" fails on EAGAIN, as this a timeout. This indicates a timeout of a TCP socket. */ -int read_write(int fd, unsigned char *packet, int size, int rw) +int read_writev(int fd, struct iovec *iov, int iovcnt, int rw) { - ssize_t n, done; - - for (done = 0; done < size; done += n) + int cur = 0; + ssize_t n, done = 0; + + while (cur < iovcnt) { + iov[cur].iov_len -= done; + iov[cur].iov_base = ((char *)iov[cur].iov_base) + done; + if (rw & 1) - n = read(fd, &packet[done], (size_t)(size - done)); + n = readv(fd, &iov[cur], iovcnt - cur); else - n = write(fd, &packet[done], (size_t)(size - done)); - - if (n == 0) - return 0; + n = writev(fd, &iov[cur], iovcnt - cur); + iov[cur].iov_len += done; + iov[cur].iov_base = ((char *)iov[cur].iov_base) - done; + if (n == -1) { - n = 0; /* don't mess with counter when we loop. */ - if (errno == EINTR || errno == ENOMEM || errno == ENOBUFS) continue; - - if (errno == EAGAIN || errno == EWOULDBLOCK) - { - /* "once" variant */ - if (rw & 2) - return 0; - - continue; - } + + if (!(rw & 2) && (errno == EAGAIN || errno == EWOULDBLOCK)) + continue; return 0; } + + if (n == 0 && (rw & 1)) + return 0; + + done += n; + while ((size_t)done >= iov[cur].iov_len) + done -= iov[cur++].iov_len; } return 1; } +int read_write(int fd, unsigned char *packet, int size, int rw) +{ + struct iovec iov; + + /* size == 0 is not an error, just a NOOP. */ + if (size == 0) + return 1; + + iov.iov_len = (size_t)size; + iov.iov_base = packet; + + return read_writev(fd, &iov, 1, rw); +} + /* close all fds except STDIN, STDOUT and STDERR, spare1, spare2 and spare3 */ void close_fds(long max_fd, int spare1, int spare2, int spare3) { From f08d1dde3d25f7d31cce584a126be5b01341c370 Mon Sep 17 00:00:00 2001 From: Dominik Date: Wed, 28 Jan 2026 20:12:30 +0100 Subject: [PATCH 021/101] Update embedded dnsmasq to v2.93test2 Signed-off-by: Dominik --- CMakeLists.txt | 2 +- src/FTL.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a13171da3..10173e41ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test1) +set(DNSMASQ_VERSION pi-hole-v2.93test2) add_subdirectory(src) diff --git a/src/FTL.h b/src/FTL.h index 180b2bfba0..3e47228cd0 100644 --- a/src/FTL.h +++ b/src/FTL.h @@ -177,6 +177,7 @@ // caused by insufficient memory or by code bugs (not properly dealing // with NULL pointers) much easier. #undef strdup // strdup() is a macro in itself, it needs special handling +#undef free #define free(ptr) { FTLfree(ptr, __FILE__, __FUNCTION__, __LINE__); ptr = NULL; } #define strdup(str_in) FTLstrdup(str_in, __FILE__, __FUNCTION__, __LINE__) #define calloc(numer_of_elements, element_size) FTLcalloc(numer_of_elements, element_size, __FILE__, __FUNCTION__, __LINE__) From a7cede6b4f293ea2b571b2db1df762904a5cc8a0 Mon Sep 17 00:00:00 2001 From: Dominik Date: Wed, 28 Jan 2026 20:22:47 +0100 Subject: [PATCH 022/101] Prevent macro leakage into system headers: document and guard push/pop of free/strdup Signed-off-by: Dominik --- src/FTL.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/FTL.h b/src/FTL.h index 3e47228cd0..50933a50ae 100644 --- a/src/FTL.h +++ b/src/FTL.h @@ -12,6 +12,29 @@ #define __USE_XOPEN #define _GNU_SOURCE +#ifdef __GNUC__ +/* + * Protect system headers from project-local macro redefinitions. + * + * `dnsmasq/dnsmasq.h` (and other third-party headers) define very common + * identifiers like `free`/`strdup` as macros that expand to internal + * wrappers (e.g. `free_real(__func__, __LINE__, (x))`). If such macros are + * active while system headers are included, they can be expanded inside + * libc prototypes and inline functions and produce invalid code, causing + * spurious compiler errors. + * + * To avoid this we push any existing macro definition on a stack, undefine + * the name while including system headers, and restore the original macro + * afterwards with `#pragma pop_macro`. + * + * Note: `#pragma push_macro`/`pop_macro` is a GCC/Clang extension, so we + * guard its use with `#ifdef __GNUC__` for portability. + */ +#pragma push_macro("free") +#pragma push_macro("strdup") +#undef free +#undef strdup +#endif #include // variable argument lists #include @@ -176,6 +199,10 @@ // and report accordingly in the log. This will make debugging FTL crash // caused by insufficient memory or by code bugs (not properly dealing // with NULL pointers) much easier. +#ifdef __GNUC__ +#pragma pop_macro("strdup") +#pragma pop_macro("free") +#endif #undef strdup // strdup() is a macro in itself, it needs special handling #undef free #define free(ptr) { FTLfree(ptr, __FILE__, __FUNCTION__, __LINE__); ptr = NULL; } From a680941d034322b93cc8c4eb890e52cf6faebbcf Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 31 Jan 2026 15:56:08 +0000 Subject: [PATCH 023/101] Remove DHCPv6 UseMulticast option code. This almost certainly never worked and was never use, and it's rendered obsolete in RFC 9915. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index f2838ab8f4..ad38544bb5 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -34,8 +34,8 @@ struct state { }; static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t sz, - struct in6_addr *client_addr, int is_unicast, time_t now); -static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbuff, size_t sz, int is_unicast, time_t now); + struct in6_addr *client_addr, time_t now); +static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbuff, size_t sz, time_t now); static void log6_opts(int nest, unsigned int xid, void *start_opts, void *end_opts); static void log6_packet(struct state *state, char *type, struct in6_addr *addr, char *string); static void log6_quiet(struct state *state, char *type, struct in6_addr *addr, char *string); @@ -97,8 +97,7 @@ unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int state.tags = NULL; state.link_address = NULL; - if (dhcp6_maybe_relay(&state, daemon->dhcp_packet.iov_base, sz, client_addr, - IN6_IS_ADDR_MULTICAST(client_addr), now)) + if (dhcp6_maybe_relay(&state, daemon->dhcp_packet.iov_base, sz, client_addr, now)) return msg_type == DHCP6RELAYFORW ? DHCPV6_SERVER_PORT : DHCPV6_CLIENT_PORT; return 0; @@ -106,7 +105,7 @@ unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int /* This cost me blood to write, it will probably cost you blood to understand - srk. */ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t sz, - struct in6_addr *client_addr, int is_unicast, time_t now) + struct in6_addr *client_addr, time_t now) { uint8_t *end = inbuff + sz; uint8_t *opts = inbuff + 34; @@ -184,7 +183,7 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t return 0; } - return dhcp6_no_relay(state, msg_type, inbuff, sz, is_unicast, now); + return dhcp6_no_relay(state, msg_type, inbuff, sz, now); } /* must have at least msg_type+hopcount+link_address+peer_address+minimal size option @@ -250,9 +249,7 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t /* RFC6221 para 4 */ if (!IN6_IS_ADDR_UNSPECIFIED(&align)) state->link_address = &align; - /* zero is_unicast since that is now known to refer to the - relayed packet, not the original sent by the client */ - if (!dhcp6_maybe_relay(state, opt6_ptr(opt, 0), opt6_len(opt), client_addr, 0, now)) + if (!dhcp6_maybe_relay(state, opt6_ptr(opt, 0), opt6_len(opt), client_addr, now)) return 0; } else @@ -264,7 +261,7 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t return 1; } -static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbuff, size_t sz, int is_unicast, time_t now) +static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbuff, size_t sz, time_t now) { void *opt; int i, o, o1, start_opts, start_msg; @@ -370,18 +367,6 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu put_opt6(daemon->duid, daemon->duid_len); end_opt6(o); - if (is_unicast && - (msg_type == DHCP6REQUEST || msg_type == DHCP6RENEW || msg_type == DHCP6RELEASE || msg_type == DHCP6DECLINE)) - - { - outmsgtype = DHCP6REPLY; - o1 = new_opt6(OPTION6_STATUS_CODE); - put_opt6_short(DHCP6USEMULTI); - put_opt6_string("Use multicast"); - end_opt6(o1); - goto done; - } - /* match vendor and user class options */ for (vendor = daemon->dhcp_vendors; vendor; vendor = vendor->next) { @@ -1304,7 +1289,6 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu log_tags(tagif, state->xid); - done: /* Fill in the message type. Note that we store the offset, not a direct pointer, since the packet memory may have been reallocated. */ From 5d8dfd7c6e929bec73c80bcd605b8da55ca67385 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 31 Jan 2026 20:40:21 +0000 Subject: [PATCH 024/101] Tidy up check for muticast DHCPv6 requests. Anything sent by a client must be multicast. A relay can unicast to a server. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index ad38544bb5..94ce42d30f 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -21,7 +21,7 @@ struct state { unsigned char *clid; - int multicast_dest, clid_len, ia_type, interface, hostname_auth, lease_allocate; + int clid_len, ia_type, interface, hostname_auth, lease_allocate; char *client_hostname, *hostname, *domain, *send_domain; struct dhcp_context *context; struct in6_addr *link_address, *fallback, *ll_addr, *ula_addr; @@ -80,6 +80,10 @@ unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int return 0; msg_type = *((unsigned char *)daemon->dhcp_packet.iov_base); + + /* request from a client must be multicast RFC-9915 section 16 */ + if (msg_type != DHCP6RELAYFORW && !multicast_dest) + return 0; /* Mark these so we only match each at most once, to avoid tangled linked lists */ for (vendor = daemon->dhcp_vendors; vendor; vendor = vendor->next) @@ -87,7 +91,6 @@ unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int reset_counter(); state.context = context; - state.multicast_dest = multicast_dest; state.interface = interface; state.iface_name = iface_name; state.fallback = fallback; @@ -337,22 +340,17 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu opt = opt6_find(state->packet_options, state->end, OPTION6_SERVER_ID, 1); - if (msg_type == DHCP6SOLICIT || msg_type == DHCP6CONFIRM || msg_type == DHCP6REBIND || msg_type == DHCP6IREQ) + if (msg_type == DHCP6SOLICIT || msg_type == DHCP6CONFIRM || msg_type == DHCP6REBIND) { - /* Above message types must be multicast 3315 Section 15. */ - if (!state->multicast_dest) + /* SOLICIT, CONFIRM and REBIND messages MUST NOT have a server-id. 3315 para 15.x */ + if (opt) return 0; - - /* server-id must match except for SOLICIT, CONFIRM and REBIND messages, which MUST NOT - have a server-id. 3315 para 15.x */ - if (msg_type == DHCP6IREQ) - { - /* If server-id provided in IREQ, it must match. */ - if (opt && (opt6_len(opt) != daemon->duid_len || - memcmp(opt6_ptr(opt, 0), daemon->duid, daemon->duid_len) != 0)) - return 0; - } - else if (opt) + } + else if (msg_type == DHCP6IREQ) + { + /* If server-id provided in IREQ, it must match. */ + if (opt && (opt6_len(opt) != daemon->duid_len || + memcmp(opt6_ptr(opt, 0), daemon->duid, daemon->duid_len) != 0)) return 0; } else From 269c57b9ca78344fecb703d0df607c27db7cf0b3 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 31 Jan 2026 21:20:17 +0000 Subject: [PATCH 025/101] Tidy up memory allocation in read_event() Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index d05840ae66..38fc7cee73 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1475,8 +1475,6 @@ void send_event(int fd, int event, int data, char *msg) while (writev(fd, iov, msg ? 2 : 1) == -1 && errno == EINTR); } -/* NOTE: the memory used to return msg is leaked: use msgs in events only - to describe fatal errors. */ static int read_event(int fd, struct event_desc *evp, char **msg) { char *buf; @@ -1486,12 +1484,22 @@ static int read_event(int fd, struct event_desc *evp, char **msg) *msg = NULL; - if (evp->msg_sz != 0 && - (buf = malloc(evp->msg_sz + 1)) && - read_write(fd, (unsigned char *)buf, evp->msg_sz, RW_READ)) + if (evp->msg_sz != 0) { - buf[evp->msg_sz] = 0; - *msg = buf; + if (!(buf = whine_malloc(evp->msg_sz + 1))) + { + int i; + unsigned char a; + + /* Keep the stream synchronised if malloc fails. */ + for (i = 0; i < evp->msg_sz; i++) + read_write(fd, &a, 1, RW_READ); + } + else if (read_write(fd, (unsigned char *)buf, evp->msg_sz, RW_READ)) + { + buf[evp->msg_sz] = 0; + *msg = buf; + } } return 1; @@ -1554,9 +1562,6 @@ static void async_event(int pipe, time_t now) int wstatus, i, check = 0; char *msg; - /* NOTE: the memory used to return msg is leaked: use msgs in events only - to describe fatal errors. */ - if (read_event(pipe, &ev, &msg)) switch (ev.event) { @@ -1668,7 +1673,6 @@ static void async_event(int pipe, time_t now) case EVENT_SCRIPT_LOG: my_syslog(MS_SCRIPT | LOG_DEBUG, "%s", msg ? msg : ""); free(msg); - msg = NULL; break; /* necessary for fatal errors in helper */ From 2b0d90906391c901d64c5319e8c51f0b08132ff3 Mon Sep 17 00:00:00 2001 From: Matthias Andree Date: Sat, 31 Jan 2026 23:22:30 +0100 Subject: [PATCH 026/101] base32_decode: avoid shifting into the sign bit While this won't do harm on systems that do 2's completement, it triggers the compilers' undefined-behavior sanitizer and fixes sanitizer error such as the one below (where the 1694... will vary) and is distracting while debugging. dnssec.c:1427:12: runtime error: left shift of 1694604366 by 1 places cannot be represented in type 'int' Signed-off-by: Dominik --- src/dnsmasq/dnssec.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 4557174eb3..8ff554060b 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -1430,7 +1430,10 @@ static int base32_decode(char *in, unsigned char *out) oc |= 1; mask = mask >> 1; if (((++on) & 7) == 0) - *p++ = oc; + { + *p++ = oc; + oc = 0; + } oc = oc << 1; } } From 37bd6c5192adb4ab1fa28ba30f5afcac8042eb22 Mon Sep 17 00:00:00 2001 From: Matthias Andree Date: Sat, 31 Jan 2026 23:25:22 +0100 Subject: [PATCH 027/101] Avoid uninitialized-value warnings from the compiler Signed-off-by: Dominik --- src/dnsmasq/dnssec.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 8ff554060b..97734b75be 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -1563,7 +1563,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns { unsigned char *salt, *p, *digest; int digest_len, i, iterations, salt_len, base32_len, algo = 0; - struct nettle_hash const *hash; + struct nettle_hash const *hash = NULL; char *closest_encloser, *next_closest, *wildcard; if (nons) From 9dd5f0be84a66ff58edcdbafc5075c5d437962f3 Mon Sep 17 00:00:00 2001 From: Matthias Andree Date: Sat, 31 Jan 2026 23:25:33 +0100 Subject: [PATCH 028/101] read_writev: avoid reading past the last iovec elem If iovcnt is exhausted and the first vector element's operation is satisfied, the while loop will read past the end of the iovec array. This triggers the address sanitizer and leads to undefined program state. Avoid reading too far. Signed-off-by: Dominik --- src/dnsmasq/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 632091f583..78ad0b7703 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -794,7 +794,7 @@ int read_writev(int fd, struct iovec *iov, int iovcnt, int rw) return 0; done += n; - while ((size_t)done >= iov[cur].iov_len) + while (cur < iovcnt && (size_t)done >= iov[cur].iov_len) done -= iov[cur++].iov_len; } From 9113bbf79d090fde92435571086e2c0f82fbfa04 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 1 Feb 2026 15:38:06 +0000 Subject: [PATCH 029/101] Fix compiler warning. Signed-off-by: Dominik --- src/dnsmasq/edns0.c | 2 +- src/dnsmasq/option.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/edns0.c b/src/dnsmasq/edns0.c index b4ef82c408..5a5f59856f 100644 --- a/src/dnsmasq/edns0.c +++ b/src/dnsmasq/edns0.c @@ -103,7 +103,7 @@ unsigned char *find_pseudoheader(struct dns_header *header, size_t plen, size_t size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned char *limit, int optno, unsigned char *opt, size_t optlen, int set_do, int replace) { - unsigned char *lenp, *datap, *p, *udp_len, *buff = NULL; + unsigned char *lenp = NULL, *datap = NULL, *p, *udp_len, *buff = NULL; int rdlen = 0, is_sign, is_last; unsigned short flags = set_do ? 0x8000 : 0, rcode = 0; diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 4a765d60f3..50be7a28e2 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -5737,7 +5737,7 @@ struct hostsfile *expand_filelist(struct hostsfile *list) struct dirent **namelist; /* find largest used index */ - for (i = SRC_AH, ah = list; ah; ah = ah->next) + for (last = NULL, i = SRC_AH, ah = list; ah; ah = ah->next) { last = ah; From ebb859d632fe1015753cb6d63be48fa9a2c843aa Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 1 Feb 2026 21:46:19 +0000 Subject: [PATCH 030/101] Rewrite blockdata_retrieve() and expand_buf() to use realloc(). Signed-off-by: Dominik --- src/dnsmasq/blockdata.c | 14 ++++++-------- src/dnsmasq/util.c | 8 +------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/dnsmasq/blockdata.c b/src/dnsmasq/blockdata.c index dc02d46dc1..00b86d5715 100644 --- a/src/dnsmasq/blockdata.c +++ b/src/dnsmasq/blockdata.c @@ -193,22 +193,20 @@ void *blockdata_retrieve(struct blockdata *block, size_t len, void *data) { size_t blen; struct blockdata *b; - uint8_t *new, *d; + uint8_t *d; - static unsigned int buff_len = 0; - static unsigned char *buff = NULL; - if (!data) { + static unsigned int buff_len = 0; + static unsigned char *buff = NULL; + uint8_t *new; + if (len > buff_len) { blen = len + 1024; - if (!(new = whine_malloc(blen))) + if (!(new = whine_realloc(buff, blen))) return NULL; - if (buff) - free(buff); - buff = new; buff_len = blen; } diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 78ad0b7703..48dcb78011 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -684,18 +684,12 @@ int expand_buf(struct iovec *iov, size_t size) if (size <= (size_t)iov->iov_len) return 1; - if (!(new = whine_malloc(size))) + if (!(new = whine_realloc(iov->iov_base, size))) { errno = ENOMEM; return 0; } - if (iov->iov_base) - { - memcpy(new, iov->iov_base, iov->iov_len); - free(iov->iov_base); - } - iov->iov_base = new; iov->iov_len = size; From cbf4666d90cf1165e092c7ccd8580f5fee348ac3 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 2 Feb 2026 21:05:52 +0100 Subject: [PATCH 031/101] Update embedded dnsmasq version to v2.93test3 Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10173e41ac..54e677296c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test2) +set(DNSMASQ_VERSION pi-hole-v2.93test3) add_subdirectory(src) From 87e8480c8f8352e185a93bfd20779fddfabe512c Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 3 Feb 2026 22:33:32 +0000 Subject: [PATCH 032/101] Enhance --log-malloc Log callers of wrapper functions of realloc() These are expand_buf() and expand_workspace() Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 10 +++--- src/dnsmasq/rrfilter.c | 21 ------------- src/dnsmasq/util.c | 69 ++++++++++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index c972bf50c3..a80b24d5e8 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1517,11 +1517,15 @@ void *safe_malloc(size_t size); void safe_strncpy(char *dest, const char *src, size_t size); void safe_pipe(int *fd, int read_noblock); #define whine_malloc(x) whine_malloc_real(__func__, __LINE__, (x)) -#define whine_realloc(x, y) whine_realloc_real(__func__, __LINE__, (x), (y)) +#define whine_realloc(x, y) whine_realloc_real(NULL, __func__, __LINE__, (x), (y)) +#define expand_buf(x, y) expand_buf_real(__func__, __LINE__, (x), (y)) +#define expand_workspace(x, y, z) expand_workspace_real(__func__, __LINE__, (x), (y), (z)) #define free(x) free_real(__func__, __LINE__, (x)) void free_real(const char *func, unsigned int line, void *ptr); void *whine_malloc_real(const char *func, unsigned int line, size_t size); -void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t size); +void *whine_realloc_real(const char *wrapper, const char *func, unsigned int line, void *ptr, size_t size); +int expand_buf_real(const char *func, unsigned int line, struct iovec *iov, size_t size); +int expand_workspace_real(const char *func, unsigned int line, unsigned char ***wkspc, int *szp, int new); int sa_len(union mysockaddr *addr); int sockaddr_isequal(const union mysockaddr *s1, const union mysockaddr *s2); int sockaddr_isnull(const union mysockaddr *s); @@ -1543,7 +1547,6 @@ int parse_hex(char *in, unsigned char *out, int maxlen, unsigned int *wildcard_mask, int *mac_type); int memcmp_masked(unsigned char *a, unsigned char *b, int len, unsigned int mask); -int expand_buf(struct iovec *iov, size_t size); char *print_mac(char *buff, unsigned char *mac, int len); int read_write(int fd, unsigned char *packet, int size, int rw); int read_writev(int fd, struct iovec *iov, int iovcnt, int rw); @@ -1935,7 +1938,6 @@ int do_poll(int timeout); /* rrfilter.c */ size_t rrfilter(struct dns_header *header, size_t *plen, int mode); short *rrfilter_desc(int type); -int expand_workspace(unsigned char ***wkspc, int *szp, int new); int to_wire(char *name); void from_wire(char *name); /* modes. */ diff --git a/src/dnsmasq/rrfilter.c b/src/dnsmasq/rrfilter.c index d13ed221b3..29f69c74a3 100644 --- a/src/dnsmasq/rrfilter.c +++ b/src/dnsmasq/rrfilter.c @@ -337,27 +337,6 @@ short *rrfilter_desc(int type) return p+1; } -int expand_workspace(unsigned char ***wkspc, int *szp, int new) -{ - unsigned char **p; - int old = *szp; - - if (old >= new+1) - return 1; - - new += 5; - - if (!(p = whine_realloc(*wkspc, new * sizeof(unsigned char *)))) - return 0; - - memset(p+old, 0, new-old); - - *wkspc = p; - *szp = new; - - return 1; -} - /* Convert from presentation format to wire format, in place. Also map UC -> LC. Note that using extract_name to get presentation format diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 48dcb78011..e360efaee9 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -676,26 +676,6 @@ int memcmp_masked(unsigned char *a, unsigned char *b, int len, unsigned int mask return count; } -/* _note_ may copy buffer */ -int expand_buf(struct iovec *iov, size_t size) -{ - void *new; - - if (size <= (size_t)iov->iov_len) - return 1; - - if (!(new = whine_realloc(iov->iov_base, size))) - { - errno = ENOMEM; - return 0; - } - - iov->iov_base = new; - iov->iov_len = size; - - return 1; -} - char *print_mac(char *buff, unsigned char *mac, int len) { char *p = buff; @@ -964,7 +944,7 @@ void *whine_malloc_real(const char *func, unsigned int line, size_t size) return ret; } -void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t size) +void *whine_realloc_real(const char *wrapper, const char *func, unsigned int line, void *ptr, size_t size) { unsigned int old = hash_ptr(ptr); void *ret = realloc(ptr, size); @@ -972,11 +952,56 @@ void *whine_realloc_real(const char *func, unsigned int line, void *ptr, size_t if (!ret) my_syslog(LOG_ERR, _("failed to reallocate %d bytes"), (int) size); else if (daemon->log_malloc) - my_syslog(LOG_INFO, _("realloc: %s:%u %zu bytes from %x to %x"), func, line, size, old, hash_ptr(ret)); + { + if (!wrapper) + wrapper = "realloc"; + my_syslog(LOG_INFO, _("%s: %s:%u %zu bytes from %x to %x"), wrapper, func, line, size, old, hash_ptr(ret)); + } return ret; } +/* _note_ may copy buffer */ +int expand_buf_real(const char *func, unsigned int line, struct iovec *iov, size_t size) +{ + void *new; + + if (size <= (size_t)iov->iov_len) + return 1; + + if (!(new = whine_realloc_real("expand_buf", func, line, iov->iov_base, size))) + { + errno = ENOMEM; + return 0; + } + + iov->iov_base = new; + iov->iov_len = size; + + return 1; +} + +int expand_workspace_real(const char *func, unsigned int line, unsigned char ***wkspc, int *szp, int new) +{ + unsigned char **p; + int old = *szp; + + if (old >= new+1) + return 1; + + new += 5; + + if (!(p = whine_realloc_real("expand_workspace", func, line, *wkspc, new * sizeof(unsigned char *)))) + return 0; + + memset(p+old, 0, new-old); + + *wkspc = p; + *szp = new; + + return 1; +} + #undef free void free_real(const char *func, unsigned int line, void *ptr) { From b8ad9b37e544e1fbd2ff9ed34fe226bf16a940d8 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 5 Feb 2026 12:54:31 +0000 Subject: [PATCH 033/101] Improve memory allocation for /etc/hosts etc. Clearing and reloading the DNS cache involved freeing and re-allocating many small memory blocks. Doing this frequently with a significant number of static DNS configuraions or DHCP leases could result in excessive heap fragmentation and memory use. This patch extends the pool memory allocation technique used else where to this memory. On cache clear, the memory is not freed, but added to a pool of useable but unused data structures which get re-used to store the new data. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 242 +++++++++++++++++++++++++------------------ src/dnsmasq/config.h | 1 + 2 files changed, 144 insertions(+), 99 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 3e7a2718df..36229b498c 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -19,13 +19,16 @@ #include "webserver/webserver.h" static struct crec *cache_head = NULL, *cache_tail = NULL, **hash_table = NULL; -#ifdef HAVE_DHCP -static struct crec *dhcp_spare = NULL; -#endif +static struct crec *config_spare = NULL; static struct crec *new_chain = NULL; static int insert_error; static union bigname *big_free = NULL; static int bignames_left, hash_size; +struct nameblock { + struct nameblock *next; + unsigned int last, index; + char data[NAMEBLOCK_CHARS]; +} *hostblocks = NULL; static void make_non_terminals(struct crec *source); static struct crec *really_insert(char *name, union all_addr *addr, unsigned short class, @@ -209,6 +212,61 @@ void cache_init(void) rehash(daemon->cachesize); } +static struct crec *get_config_crec(void) +{ + struct crec *ret; + + if (config_spare) + { + ret = config_spare; + config_spare = config_spare->next; + } + else + ret = whine_malloc(SIZEOF_POINTER_CREC); + + return ret; +} + +static void free_config_crec(struct crec *p) +{ + p->next = config_spare; + config_spare = p; +} + +static char *store_name(unsigned int namelen, unsigned int index) +{ + struct nameblock *block; + char *ret = NULL; + + for (block = hostblocks; block; block = block->next) + if (block->index == index && NAMEBLOCK_CHARS - block->last >= namelen) + break; + + if (!block && ((block = whine_malloc(sizeof(struct nameblock))))) + { + block->next = hostblocks; + block->index = index; + hostblocks = block; + } + + if (block && NAMEBLOCK_CHARS - block->last >= namelen) + { + ret = &block->data[block->last]; + block->last += namelen; + } + + return ret; +} + +static void free_names(unsigned int index) +{ + struct nameblock *block; + + for (block = hostblocks; block; block = block->next) + if (index == UID_NONE || block->index == index) + block->last = 0; +} + /* In most cases, we create the hash table once here by calling this with (hash_table == NULL) but if the hosts file(s) are big (some people have 50000 ad-block entries), the table will be much too small, so the hosts reading code calls rehash every 1000 addresses, to @@ -458,12 +516,14 @@ unsigned int cache_remove_uid(const unsigned int uid) if ((crecp->flags & (F_HOSTS | F_DHCP | F_CONFIG)) && crecp->uid == uid) { *up = tmp; - free(crecp); + free_config_crec(crecp); removed++; } else up = &crecp->hash_next; } + + free_names(uid); return removed; } @@ -1278,7 +1338,7 @@ void add_hosts_entry(struct crec *cache, union all_addr *addr, int addrlen, while ((lookup = cache_find_by_name(lookup, cache_get_name(cache), 0, cache->flags & (F_IPV4 | F_IPV6)))) if ((lookup->flags & F_HOSTS) && memcmp(&lookup->addr, addr, addrlen) == 0) { - free(cache); + free_config_crec(cache); return; } @@ -1401,13 +1461,13 @@ int read_hostsfile(char *filename, unsigned int index, int cache_size, struct cr { if (inet_pton(AF_INET, token, &addr) > 0) { - flags = F_HOSTS | F_IMMORTAL | F_FORWARD | F_REVERSE | F_IPV4; + flags = F_NAMEP | F_HOSTS | F_IMMORTAL | F_FORWARD | F_REVERSE | F_IPV4; addrlen = INADDRSZ; domain_suffix = get_domain(addr.addr4); } else if (inet_pton(AF_INET6, token, &addr) > 0) { - flags = F_HOSTS | F_IMMORTAL | F_FORWARD | F_REVERSE | F_IPV6; + flags = F_NAMEP | F_HOSTS | F_IMMORTAL | F_FORWARD | F_REVERSE | F_IPV6; addrlen = IN6ADDRSZ; domain_suffix = get_domain6(&addr.addr6); } @@ -1441,27 +1501,37 @@ int read_hostsfile(char *filename, unsigned int index, int cache_size, struct cr if ((canon = canonicalise(token, &nomem))) { /* If set, add a version of the name with a default domain appended */ - if (option_bool(OPT_EXPAND) && domain_suffix && !fqdn && - (cache = whine_malloc(SIZEOF_BARE_CREC + strlen(canon) + 2 + strlen(domain_suffix)))) + if (option_bool(OPT_EXPAND) && domain_suffix && !fqdn && (cache = get_config_crec())) { - strcpy(cache->name.sname, canon); - strcat(cache->name.sname, "."); - strcat(cache->name.sname, domain_suffix); - cache->flags = flags; - cache->ttd = daemon->local_ttl; - add_hosts_entry(cache, &addr, addrlen, index, rhash, hashsz); - name_count++; - names_done++; + if (!(cache->name.namep = store_name(strlen(canon) + 2 + strlen(domain_suffix), index))) + free_config_crec(cache); + else + { + strcpy(cache->name.namep, canon); + strcat(cache->name.namep, "."); + strcat(cache->name.namep, domain_suffix); + cache->flags = flags; + cache->ttd = daemon->local_ttl; + add_hosts_entry(cache, &addr, addrlen, index, rhash, hashsz); + name_count++; + names_done++; + } } - if ((cache = whine_malloc(SIZEOF_BARE_CREC + strlen(canon) + 1))) + if ((cache = get_config_crec())) { - strcpy(cache->name.sname, canon); - cache->flags = flags; - cache->ttd = daemon->local_ttl; - add_hosts_entry(cache, &addr, addrlen, index, rhash, hashsz); - name_count++; - names_done++; + if (!(cache->name.namep = store_name(strlen(canon) + 1, index))) + free_config_crec(cache); + else + { + strcpy(cache->name.namep, canon); + cache->flags = flags; + cache->ttd = daemon->local_ttl; + add_hosts_entry(cache, &addr, addrlen, index, rhash, hashsz); + name_count++; + names_done++; + } } + free(canon); } @@ -1512,7 +1582,7 @@ void cache_reload(void) if (cache->flags & (F_HOSTS | F_CONFIG)) { *up = cache->hash_next; - free(cache); + free_config_crec(cache); } else if (!(cache->flags & F_DHCP)) { @@ -1527,11 +1597,13 @@ void cache_reload(void) else up = &cache->hash_next; } + + free_names(UID_NONE); /* free everything */ /* Add locally-configured CNAMEs to the cache */ for (a = daemon->cnames; a; a = a->next) if (a->alias[1] != '*' && - ((cache = whine_malloc(SIZEOF_POINTER_CREC)))) + ((cache = get_config_crec()))) { cache->flags = F_FORWARD | F_NAMEP | F_CNAME | F_IMMORTAL | F_CONFIG; cache->ttd = a->ttl; @@ -1545,25 +1617,30 @@ void cache_reload(void) #ifdef HAVE_DNSSEC for (ds = daemon->ds; ds; ds = ds->next) - if ((cache = whine_malloc(SIZEOF_POINTER_CREC)) && - (cache->addr.ds.keydata = blockdata_alloc(ds->digest, ds->digestlen))) + if ((cache = get_config_crec())) { - cache->flags = F_FORWARD | F_IMMORTAL | F_DS | F_CONFIG | F_NAMEP; - cache->ttd = daemon->local_ttl; - cache->name.namep = ds->name; - cache->uid = ds->class; - if (ds->digestlen != 0) + + if (!(cache->addr.ds.keydata = blockdata_alloc(ds->digest, ds->digestlen))) + free_config_crec(cache); + else { - cache->addr.ds.keylen = ds->digestlen; - cache->addr.ds.algo = ds->algo; - cache->addr.ds.keytag = ds->keytag; - cache->addr.ds.digest = ds->digest_type; + cache->flags = F_FORWARD | F_IMMORTAL | F_DS | F_CONFIG | F_NAMEP; + cache->ttd = daemon->local_ttl; + cache->name.namep = ds->name; + cache->uid = ds->class; + if (ds->digestlen != 0) + { + cache->addr.ds.keylen = ds->digestlen; + cache->addr.ds.algo = ds->algo; + cache->addr.ds.keytag = ds->keytag; + cache->addr.ds.digest = ds->digest_type; + } + else + cache->flags |= F_NEG | F_DNSSECOK | F_NO_RR; + + cache_hash(cache); + make_non_terminals(cache); } - else - cache->flags |= F_NEG | F_DNSSECOK | F_NO_RR; - - cache_hash(cache); - make_non_terminals(cache); } #endif @@ -1578,7 +1655,7 @@ void cache_reload(void) for (nl = hr->names; nl; nl = nl->next) { if ((hr->flags & HR_4) && - (cache = whine_malloc(SIZEOF_POINTER_CREC))) + (cache = get_config_crec())) { cache->name.namep = nl->name; cache->ttd = hr->ttl; @@ -1587,7 +1664,7 @@ void cache_reload(void) } if ((hr->flags & HR_6) && - (cache = whine_malloc(SIZEOF_POINTER_CREC))) + (cache = get_config_crec())) { cache->name.namep = nl->name; cache->ttd = hr->ttl; @@ -1679,8 +1756,7 @@ void cache_unhash_dhcp(void) if (cache->flags & F_DHCP) { *up = cache->hash_next; - cache->next = dhcp_spare; - dhcp_spare = cache; + free_config_crec(cache); } else up = &cache->hash_next; @@ -1751,12 +1827,7 @@ void cache_add_dhcp_entry(char *host_name, int prot, else flags |= F_REVERSE; - if ((crec = dhcp_spare)) - dhcp_spare = dhcp_spare->next; - else /* need new one */ - crec = whine_malloc(SIZEOF_POINTER_CREC); - - if (crec) /* malloc may fail */ + if ((crec = get_config_crec())) { crec->flags = flags | F_NAMEP | F_DHCP | F_FORWARD; if (ttd == 0) @@ -1803,15 +1874,7 @@ static void make_non_terminals(struct crec *source) hostname_isequal(name, cache_get_name(crecp))) { *up = crecp->hash_next; -#ifdef HAVE_DHCP - if (type & F_DHCP) - { - crecp->next = dhcp_spare; - dhcp_spare = crecp; - } - else -#endif - free(crecp); + free_config_crec(crecp); break; } else @@ -1844,17 +1907,7 @@ static void make_non_terminals(struct crec *source) continue; } -#ifdef HAVE_DHCP - if ((source->flags & F_DHCP) && dhcp_spare) - { - crecp = dhcp_spare; - dhcp_spare = dhcp_spare->next; - } - else -#endif - crecp = whine_malloc(SIZEOF_POINTER_CREC); - - if (crecp) + if ((crecp = get_config_crec())) { crecp->flags = (source->flags | F_NAMEP) & ~(F_IPV4 | F_IPV6 | F_CNAME | F_RR | F_DNSKEY | F_DS | F_REVERSE); if (!(crecp->flags & F_IMMORTAL)) @@ -2283,9 +2336,9 @@ char *querystr(char *desc, unsigned short type) unsigned int i; int len = 10; /* strlen("type=xxxxx") */ const char *types = NULL; - static char *buff = NULL; - static int bufflen = 0; - + static struct iovec buff = { NULL, 0 }; + char *buffp; + for (i = 0; i < (sizeof(typestr)/sizeof(typestr[0])); i++) if (typestr[i].type == type) { @@ -2300,37 +2353,28 @@ char *querystr(char *desc, unsigned short type) len += strlen(desc); } len++; /* terminator */ + + if (!expand_buf(&buff, len)) + return ""; + + buffp = buff.iov_base; - if (!buff || bufflen < len) + if (desc) { - if (buff) - free(buff); - else if (len < 20) - len = 20; - - buff = whine_malloc(len); - bufflen = len; + if (types) + sprintf(buffp, "%s[%s]", desc, types); + else + sprintf(buffp, "%s[type=%d]", desc, type); } - - if (buff) + else { - if (desc) - { - if (types) - sprintf(buff, "%s[%s]", desc, types); - else - sprintf(buff, "%s[type=%d]", desc, type); - } + if (types) + sprintf(buffp, "<%s>", types); else - { - if (types) - sprintf(buff, "<%s>", types); - else - sprintf(buff, "", type); - } + sprintf(buffp, "", type); } - return buff ? buff : ""; + return buffp; } /**** Pi-hole modified: removed static and added prototype to dnsmasq.h ****/ diff --git a/src/dnsmasq/config.h b/src/dnsmasq/config.h index 4e8a949e5f..a6413a0c73 100644 --- a/src/dnsmasq/config.h +++ b/src/dnsmasq/config.h @@ -22,6 +22,7 @@ #define TCP_BACKLOG 32 /* kernel backlog limit for TCP connections */ #define EDNS_PKTSZ 1232 /* default max EDNS.0 UDP packet from from /dnsflagday.net/2020 */ #define KEYBLOCK_LEN 40 /* choose to minimise fragmentation when storing DNSSEC keys */ +#define NAMEBLOCK_CHARS 1500 /* quantum of memory allocation for names from /etc/hosts */ #define DNSSEC_LIMIT_WORK 40 /* Max number of queries to validate one question */ #define DNSSEC_LIMIT_SIG_FAIL 20 /* Number of signature that can fail to validate in one answer */ #define DNSSEC_LIMIT_CRYPTO 200 /* max no. of crypto operations to validate one query. */ From 7e5ac70288a6541e82459c86b78cbf1d56fcafff Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 5 Feb 2026 14:53:10 +0000 Subject: [PATCH 034/101] treat opt_malloc as a wrapper for logging purposes. Signed-off-by: Dominik --- src/dnsmasq/option.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 50be7a28e2..29fb724768 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -27,6 +27,9 @@ static volatile int mem_recover = 0; static jmp_buf mem_jmp; static int one_file(char *file, int hard_opt); +static void *opt_malloc_real(const char *func, unsigned int line, size_t size); +#define opt_malloc(x) opt_malloc_real(__func__, __LINE__, (x)) + /* Solaris headers don't have facility names. */ #ifdef HAVE_SOLARIS_NETWORK static const struct { @@ -659,13 +662,13 @@ static void unhide_metas(char *cp) *cp = unhide_meta(*cp); } -static void *opt_malloc(size_t size) +static void *opt_malloc_real(const char *func, unsigned int line, size_t size) { void *ret; if (mem_recover) { - ret = whine_malloc(size); + ret = whine_malloc_real(func, line, size); if (!ret) longjmp(mem_jmp, 1); } From 8b6ad15c29e6e8ca17d9f9b9c6bbb6d0e6b7aaa6 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Thu, 5 Feb 2026 15:38:27 +0000 Subject: [PATCH 035/101] Fix PXE boot server (PXEBS) responses broken in 2.92 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I think I've found a regression in dnsmasq 2.92 that breaks PXE boot server (PXEBS) responses when running in proxy DHCP mode. Fair warning: I'm not familiar with the dnsmasq codebase and used AI tooling to help trace through the source and identify the issue, so please take the analysis below with appropriate skepticism. PXE boot works fine on 2.91 but fails on 2.92 — the client gets the initial proxy DHCPOFFER, but the PXEBS ACK on port 4011 never reaches it. My setup is dnsmasq in proxy DHCP mode serving iPXE to Proxmox VMs via their virtio-net ROM. Here's a stripped-down version of my config: port=0 enable-tftp tftp-root=/tftpboot dhcp-range=172.19.74.0,proxy,255.255.255.0 interface=eno1 bind-interfaces dhcp-match=set:ipxe,175 pxe-service=tag:ipxe,x86PC,"Network Boot",http://server:8081/boot.ipxe pxe-service=tag:!ipxe,x86PC,"Network Boot",undionly.kpxe log-dhcp The issue seems to be in src/dhcp.c in the response routing logic after dhcp_reply() returns. In 2.91, the destination selection was an if/else-if chain: if (pxe_fd) { ... } else if (mess->giaddr.s_addr && !is_relay_reply) { ... } else if (mess->ciaddr.s_addr) { ... } else { ... broadcast to 255.255.255.255:68 ... } In 2.92, the else between the pxe_fd block and the giaddr/relay check was removed in commit 4fbe1ad ("Implement RFC-4388 DHCPv4 leasequery") to accommodate the new is_relay_use_source logic: if (pxe_fd) { ... } if ((is_relay_use_source || mess->giaddr.s_addr) && !is_relay_reply) { ... } else if (mess->ciaddr.s_addr) { ... } else { ... broadcast to 255.255.255.255:68 ... } For PXEBS responses, dhcp_reply() in rfc2131.c (around line 924-925) does: mess->yiaddr = mess->ciaddr; mess->ciaddr.s_addr = 0; So after dhcp_reply() returns for a PXEBS request, ciaddr is 0, giaddr is 0 (no relay), and is_relay_use_source is 0. In 2.91, the pxe_fd block runs and the rest of the chain is skipped — dest stays as received from recvmsg, and the response goes back to the client correctly. In 2.92, the pxe_fd block runs but then falls through to the standalone if, which is false, so the else block runs and sets dest to 255.255.255.255 port 68. The client is listening on port 4011 and ignores it. Here are the relevant dnsmasq logs. With 2.91 (working), I see normal proxy DHCP and PXE boot server exchanges: dnsmasq-dhcp: DHCPDISCOVER(eno1) bc:24:11:59:85:90 dnsmasq-dhcp: DHCPOFFER(eno1) 172.19.74.60 bc:24:11:59:85:90 dnsmasq-dhcp: DHCPREQUEST(eno1) 172.19.74.60 bc:24:11:59:85:90 dnsmasq-dhcp: DHCPACK(eno1) 172.19.74.60 bc:24:11:59:85:90 dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 http://infra1.oneill.net:8081/boot.ipxe With 2.92 (broken), the DHCPDISCOVER/OFFER/REQUEST/ACK cycle and the proxy PXE response work, but the PXEBS response never reaches the client — it times out after repeated attempts. The dnsmasq side shows it sending the response, but the client keeps retrying: dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe I tested by restoring the else keyword and the fix appears to work — 2.92 with the patch below PXE boots successfully. I believe this change preserves the leasequery behavior since that path only applies when pxe_fd is false (normal DHCP handling, not port 4011). Signed-off-by: Dominik --- src/dnsmasq/dhcp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/dhcp.c b/src/dnsmasq/dhcp.c index 4a47d8a6af..0a1e33ce98 100644 --- a/src/dnsmasq/dhcp.c +++ b/src/dnsmasq/dhcp.c @@ -399,7 +399,7 @@ void dhcp_packet(time_t now, int pxe_fd) if (mess->ciaddr.s_addr != 0) dest.sin_addr = mess->ciaddr; } - if ((is_relay_use_source || mess->giaddr.s_addr) && !is_relay_reply) + else if ((is_relay_use_source || mess->giaddr.s_addr) && !is_relay_reply) { /* Send to BOOTP relay. */ if (is_relay_use_source) From 2d563e104d6a37645d95ed6a1fda7e1eb2b74329 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 6 Feb 2026 15:05:59 +0000 Subject: [PATCH 036/101] Convert hash_init() to use realloc(). Signed-off-by: Dominik --- src/dnsmasq/crypto.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/dnsmasq/crypto.c b/src/dnsmasq/crypto.c index 6a5f0c1031..92de2d99fd 100644 --- a/src/dnsmasq/crypto.c +++ b/src/dnsmasq/crypto.c @@ -126,20 +126,18 @@ int hash_init(const struct nettle_hash *hash, void **ctxp, unsigned char **diges if (ctx_sz < hash->context_size) { - if (!(new = whine_malloc(hash->context_size))) + if (!(new = whine_realloc(ctx, hash->context_size))) return 0; - if (ctx) - free(ctx); + ctx = new; ctx_sz = hash->context_size; } if (digest_sz < hash->digest_size) { - if (!(new = whine_malloc(hash->digest_size))) + if (!(new = whine_realloc(digest, hash->digest_size))) return 0; - if (digest) - free(digest); + digest = new; digest_sz = hash->digest_size; } From f0f0feb1d23fb71bc4a4cb8c29ea18606052b839 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sat, 14 Feb 2026 12:44:28 +0100 Subject: [PATCH 037/101] Update embedded dnsmasq to v2.93test4 Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 54e677296c..69f1b3a056 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test3) +set(DNSMASQ_VERSION pi-hole-v2.93test4) add_subdirectory(src) From e333c9511b06577522fb72a369b633afb5be1273 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 13 Feb 2026 12:54:54 +0000 Subject: [PATCH 038/101] Log memory allocated by libidn when --log-malloc active. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 2 ++ src/dnsmasq/util.c | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index a80b24d5e8..d286ebb204 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1516,11 +1516,13 @@ unsigned char *do_rfc1035_name(unsigned char *p, char *sval, char *limit); void *safe_malloc(size_t size); void safe_strncpy(char *dest, const char *src, size_t size); void safe_pipe(int *fd, int read_noblock); +#define malloc_log(x, y) malloc_log_real(__func__, __LINE__, (x), (y)) #define whine_malloc(x) whine_malloc_real(__func__, __LINE__, (x)) #define whine_realloc(x, y) whine_realloc_real(NULL, __func__, __LINE__, (x), (y)) #define expand_buf(x, y) expand_buf_real(__func__, __LINE__, (x), (y)) #define expand_workspace(x, y, z) expand_workspace_real(__func__, __LINE__, (x), (y), (z)) #define free(x) free_real(__func__, __LINE__, (x)) +void malloc_log_real(const char *func, unsigned int line, void *mem, size_t size); void free_real(const char *func, unsigned int line, void *ptr); void *whine_malloc_real(const char *func, unsigned int line, size_t size); void *whine_realloc_real(const char *wrapper, const char *func, unsigned int line, void *ptr, size_t size); diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index e360efaee9..659c9296b2 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -270,6 +270,10 @@ char *canonicalise(char *in, int *nomem) return NULL; } + + /* IDN library doesnt call our malloc wrapper, so log this by steam */ + if (ret) + malloc_log(ret, strlen(ret)+1); return ret; } @@ -1002,6 +1006,12 @@ int expand_workspace_real(const char *func, unsigned int line, unsigned char *** return 1; } +void malloc_log_real(const char *func, unsigned int line, void *mem, size_t size) +{ + if (mem && daemon->log_malloc) + my_syslog(LOG_INFO, _("malloc: %s:%u %zu bytes at %x"), func, line, size, hash_ptr(mem)); +} + #undef free void free_real(const char *func, unsigned int line, void *ptr) { From eca40c51fa7ffafda4789fd5e8f116ae29d5887d Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 16 Feb 2026 22:20:05 +0000 Subject: [PATCH 039/101] Remove duplicate configured trust anchors. Well-known trust anchors can turn up in multiple config files. Leaving the duplicates makes logging messy and costs some CPU checking the same DS twice. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 2 +- src/dnsmasq/dnsmasq.c | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 36229b498c..247dbf76f6 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -1617,7 +1617,7 @@ void cache_reload(void) #ifdef HAVE_DNSSEC for (ds = daemon->ds; ds; ds = ds->next) - if ((cache = get_config_crec())) + if (ds->name && (cache = get_config_crec())) { if (!(cache->addr.ds.keydata = blockdata_alloc(ds->digest, ds->digestlen))) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 38fc7cee73..655a50faf9 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -955,9 +955,27 @@ int main_dnsmasq (int argc, char **argv) my_syslog(LOG_INFO, _("DNSSEC signature timestamps not checked until system time valid")); for (ds = daemon->ds; ds; ds = ds->next) - my_syslog(LOG_INFO, - ds->digestlen == 0 ? _("configured with negative trust anchor for %s") : _("configured with trust anchor for %s keytag %u"), - ds->name[0] == 0 ? "" : ds->name, ds->keytag); + { + struct ds_config *ds1; + + for (ds1 = ds->next; ds1; ds1 = ds1->next) + if (strcmp(ds->name, ds1->name) == 0 && + ds->digestlen == ds1->digestlen && + (ds->digestlen == 0 || + (ds->algo == ds1->algo && + ds->keytag == ds1->keytag && + ds->digest_type == ds1->digest_type && + memcmp(ds->digest, ds1->digest, ds->digestlen) == 0))) + { + ds->name = NULL; /* Mark as duplicate */ + break; + } + + if (ds->name) + my_syslog(LOG_INFO, + ds->digestlen == 0 ? _("configured with negative trust anchor for %s") : _("configured with trust anchor for %s keytag %u"), + ds->name[0] == 0 ? "" : ds->name, ds->keytag); + } } #endif From fac3a2a4ba4241b2c962f69601f0e97d56f50cec Mon Sep 17 00:00:00 2001 From: Thomas Erbesdobler Date: Thu, 26 Feb 2026 13:30:44 +0000 Subject: [PATCH 040/101] Fix broken NS responses in certain auth-zone configurations. If dnsmasq is configured as an authoritatve server for zone transfer _only_, (ie no interface name or address in --auth-server) and secondary auth servers are configured, then queries for NS RRs at the auth zone will get mangled answers. This problem doesn't occur in AXFR or SOA queries, only NS queries. Thanks to Thomas Erbesdobler for finding and analysing this problem. The patch here is substantially his, with a little but of collateral code tidying by srk. Signed-off-by: Dominik --- src/dnsmasq/auth.c | 14 +++++++------- src/dnsmasq/rfc1035.c | 5 +++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/dnsmasq/auth.c b/src/dnsmasq/auth.c index 7c34522318..c318b4d17c 100644 --- a/src/dnsmasq/auth.c +++ b/src/dnsmasq/auth.c @@ -591,7 +591,7 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n if (auth && zone) { char *authname; - int newoffset, offset = 0; + int newoffset = ansp - (unsigned char *)header, offset = 0; if (!subnet) authname = zone->domain; @@ -631,8 +631,7 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n } /* handle NS and SOA in auth section or for explicit queries */ - newoffset = ansp - (unsigned char *)header; - if (((anscount == 0 && !ns) || soa) && + if (((anscount == 0 && !ns) || soa) && add_resource_record(header, limit, &trunc, 0, &ansp, daemon->auth_ttl, NULL, T_SOA, C_IN, "ddlllll", authname, daemon->authserver, daemon->hostmaster, @@ -650,11 +649,10 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n if (anscount != 0 || ns) { struct name_list *secondary; - + /* Only include the machine running dnsmasq if it's acting as an auth server */ if (daemon->authinterface) { - newoffset = ansp - (unsigned char *)header; if (add_resource_record(header, limit, &trunc, -offset, &ansp, daemon->auth_ttl, NULL, T_NS, C_IN, "d", offset == 0 ? authname : NULL, daemon->authserver)) { @@ -669,9 +667,11 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n if (!subnet) for (secondary = daemon->secondary_forward_server; secondary; secondary = secondary->next) - if (add_resource_record(header, limit, &trunc, offset, &ansp, - daemon->auth_ttl, NULL, T_NS, C_IN, "d", secondary->name)) + if (add_resource_record(header, limit, &trunc, -offset, &ansp, + daemon->auth_ttl, NULL, T_NS, C_IN, "d", offset == 0 ? authname : NULL, secondary->name)) { + if (offset == 0) + offset = newoffset; if (ns) anscount++; else diff --git a/src/dnsmasq/rfc1035.c b/src/dnsmasq/rfc1035.c index 908130ec7b..2fe53bd6af 100644 --- a/src/dnsmasq/rfc1035.c +++ b/src/dnsmasq/rfc1035.c @@ -1460,6 +1460,11 @@ int check_for_ignored_address(struct dns_header *header, size_t qlen) return check_bad_address(header, qlen, daemon->ignore_addr, NULL, NULL); } +/* Nameoffset > 0 means that the name of the new record already exists at the given offset, + so use a "jump" to that. + Nameoffset == 0 means use the first variable argument as the name of the new record. + nameoffset < 0 means use the first variable argument as the start of the new record name, + then "jump" to -nameoffset to complete it. */ int add_resource_record(struct dns_header *header, char *limit, int *truncp, int nameoffset, unsigned char **pp, unsigned long ttl, int *offset, unsigned short type, unsigned short class, char *format, ...) { From ee09c81606f8a76867539babc9c6b18e7c52ca6e Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 26 Feb 2026 15:10:41 +0000 Subject: [PATCH 041/101] Fix crash with empty DHCP SNAME option. dhcp-option=66, sets the servername to nothing, and causes a segfault at DHCP transaction time. There may be other ways to provoke this, for instance by using an empty filename. The patch make safe_strncpy() even safer, by handing a NULL src argument. Thanks to Jeff Allen for spotting and reporting this. Signed-off-by: Dominik --- src/dnsmasq/util.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 659c9296b2..8d74a7086a 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -331,13 +331,15 @@ void *safe_malloc(size_t size) } /* Ensure limited size string is always terminated. - * Can be replaced by (void)strlcpy() on some platforms */ + Can be replaced by (void)strlcpy() on some platforms. + src may be NULL in which case we return an empty string. */ void safe_strncpy(char *dest, const char *src, size_t size) { if (size != 0) { - dest[size-1] = '\0'; - strncpy(dest, src, size-1); + dest[0] = dest[size-1] = '\0'; + if (src) + strncpy(dest, src, size-1); } } From a1ff716b0d17f1300a08714d92fe0e0641149a40 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sat, 28 Feb 2026 21:30:57 +0000 Subject: [PATCH 042/101] Modify the inotify implementation so that inotify watches are only created after dnsmasq has changed permissions and userid. This means that the permissions used when creating the watches are the same as used for accessing watched files, which makes more sense and avoids odd and confusing error conditions. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 36 ++++++++++++++++++++++++++++-------- src/dnsmasq/dnsmasq.h | 7 ++++++- src/dnsmasq/inotify.c | 35 +++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 655a50faf9..59af701fa2 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -444,14 +444,6 @@ int main_dnsmasq (int argc, char **argv) daemon->tcp_pipes[i] = -1; } -#ifdef HAVE_INOTIFY - if ((daemon->port != 0 && !option_bool(OPT_NO_RESOLV)) || - daemon->dynamic_dirs) - inotify_dnsmasq_init(); - else - daemon->inotifyfd = -1; -#endif - if (daemon->dump_file) #ifdef HAVE_DUMPFILE dump_init(); @@ -873,6 +865,14 @@ int main_dnsmasq (int argc, char **argv) } #endif +#ifdef HAVE_INOTIFY + if ((daemon->port != 0 && !option_bool(OPT_NO_RESOLV)) || + daemon->dynamic_dirs) + inotify_dnsmasq_init(err_pipe[1]); + else + daemon->inotifyfd = -1; +#endif + /* Don't start logging malloc before logging is set up. */ daemon->log_malloc = option_bool(OPT_LOG_MALLOC); @@ -1570,6 +1570,26 @@ static void fatal_event(struct event_desc *ev, char *msg) /* fall through */ case EVENT_TIME_ERR: die(_("cannot create timestamp file %s: %s" ), msg, EC_BADCONF); + + /* fall through */ + case EVENT_LINK_ERR: + die(_("cannot access path %s: %s" ), msg, EC_MISC); + + /* fall through */ + case EVENT_INOTFY_ERR: + die(_("failed to create inotify: %s" ), NULL, EC_MISC); + + /* fall through */ + case EVENT_TMSL_ERR: + die(_("too many symlinks following %s"), msg, EC_MISC); + + /* fall through */ + case EVENT_RESOLV_ERR: + die(_("directory %s for resolv-file is missing, cannot poll"), msg, EC_MISC); + + /* fall through */ + case EVENT_IFILE_ERR: + die(_("failed to create inotify for %s: %s"), msg, EC_MISC); } } diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index d286ebb204..b95fd4e402 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -201,6 +201,11 @@ struct event_desc { #define EVENT_TIME_ERR 24 #define EVENT_SCRIPT_LOG 25 #define EVENT_TIME 26 +#define EVENT_LINK_ERR 27 +#define EVENT_INOTFY_ERR 28 +#define EVENT_TMSL_ERR 29 +#define EVENT_RESOLV_ERR 30 +#define EVENT_IFILE_ERR 31 // Pi-hole #define EVENT_SIGNAL 255 @@ -1926,7 +1931,7 @@ int detect_loop(char *query, int type); /* inotify.c */ #ifdef HAVE_INOTIFY -void inotify_dnsmasq_init(void); +void inotify_dnsmasq_init(int errfd); int inotify_check(time_t now); void set_dynamic_inotify(int flag, int total_size, struct crec **rhash, int revhashsz); #endif diff --git a/src/dnsmasq/inotify.c b/src/dnsmasq/inotify.c index f70daeadd7..2ec6e3976f 100644 --- a/src/dnsmasq/inotify.c +++ b/src/dnsmasq/inotify.c @@ -40,7 +40,7 @@ static char *inotify_buffer; points to, made absolute if relative. If path doesn't exist or is not a symlink, return NULL. Return value is malloc'ed */ -static char *my_readlink(char *path) +static char *my_readlink(int errfd, char *path) { ssize_t rc, size = 64; char *buf; @@ -59,7 +59,10 @@ static char *my_readlink(char *path) return NULL; } else - die(_("cannot access path %s: %s"), path, EC_MISC); + { + send_event(errfd, EVENT_LINK_ERR, errno, path); + _exit(0); + } } else if (rc < size-1) { @@ -85,15 +88,18 @@ static char *my_readlink(char *path) } } -void inotify_dnsmasq_init() +void inotify_dnsmasq_init(int errfd) { struct resolvc *res; inotify_buffer = safe_malloc(INOTIFY_SZ); daemon->inotifyfd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (daemon->inotifyfd == -1) - die(_("failed to create inotify: %s"), NULL, EC_MISC); - + { + send_event(errfd, EVENT_INOTFY_ERR, errno, NULL); + _exit(0); + } + if (daemon->port == 0 || option_bool(OPT_NO_RESOLV)) return; @@ -105,10 +111,13 @@ void inotify_dnsmasq_init() strcpy(path, res->name); /* Follow symlinks until we reach a non-symlink, or a non-existent file. */ - while ((new_path = my_readlink(path))) + while ((new_path = my_readlink(errfd, path))) { if (links-- == 0) - die(_("too many symlinks following %s"), res->name, EC_MISC); + { + send_event(errfd, EVENT_TMSL_ERR, 0, res->name); + _exit(0); + } free(path); path = new_path; } @@ -124,12 +133,18 @@ void inotify_dnsmasq_init() *d = '/'; if (res->wd == -1 && errno == ENOENT) - die(_("directory %s for resolv-file is missing, cannot poll"), res->name, EC_MISC); + { + send_event(errfd, EVENT_RESOLV_ERR, 0, res->name); + _exit(0); + } } if (res->wd == -1) - die(_("failed to create inotify for %s: %s"), res->name, EC_MISC); - + { + send_event(errfd, EVENT_IFILE_ERR, errno, res->name); + _exit(0); + } + } } From 86225142b35f424d75ee6cfefa53f74b68b17ae8 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 1 Mar 2026 12:01:12 +0000 Subject: [PATCH 043/101] Fix FTBFS with nettle 4.0. Thanks to Andreas Metzler for the heads-up. Signed-off-by: Dominik --- src/dnsmasq/crypto.c | 23 ++++++++++++++++++++++- src/dnsmasq/dnsmasq.h | 1 + src/dnsmasq/dnssec.c | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/crypto.c b/src/dnsmasq/crypto.c index 92de2d99fd..002aae3ef0 100644 --- a/src/dnsmasq/crypto.c +++ b/src/dnsmasq/crypto.c @@ -93,7 +93,15 @@ static void null_hash_update(void *ctxv, size_t length, const uint8_t *src) memcpy(null_hash_buff + ctx->len, src, length); ctx->len += length; } - + +/* The prototype changes in nettle 4.0 to omit the length argument */ +#if MIN_VERSION(4, 0) +static void null_hash_digest(void *ctx, uint8_t *dst) +{ + ((struct null_hash_digest *)dst)->buff = null_hash_buff; + ((struct null_hash_digest *)dst)->len = ((struct null_hash_ctx *)ctx)->len; +} +#else static void null_hash_digest(void *ctx, size_t length, uint8_t *dst) { (void)length; @@ -101,6 +109,7 @@ static void null_hash_digest(void *ctx, size_t length, uint8_t *dst) ((struct null_hash_digest *)dst)->buff = null_hash_buff; ((struct null_hash_digest *)dst)->len = ((struct null_hash_ctx *)ctx)->len; } +#endif static struct nettle_hash null_hash = { "null_hash", @@ -501,4 +510,16 @@ const struct nettle_hash *hash_find(char *name) #endif } +/* The prototype changes in nettle 4.0 to omit the length argument */ +void nettle_digest_wrapper(const struct nettle_hash *hash, void *ctx, size_t length, uint8_t *dst) +{ +#if MIN_VERSION(4, 0) + (void)length; + hash->digest(ctx, dst); +#else + hash->digest(ctx, length, dst); +#endif +} + + #endif /* defined(HAVE_DNSSEC) */ diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index b95fd4e402..2b16599d1a 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1508,6 +1508,7 @@ int verify(struct blockdata *key_data, unsigned int key_len, unsigned char *sig, char *ds_digest_name(int digest); char *algo_digest_name(int algo); char *nsec3_digest_name(int digest); +void nettle_digest_wrapper(const struct nettle_hash *hash, void *ctx, size_t length, uint8_t *dst); /* util.c */ void rand_init(void); diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 97734b75be..c0d46e235c 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -663,7 +663,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in } } - hash->digest(ctx, hash->digest_size, digest); + nettle_digest_wrapper(hash, ctx, hash->digest_size, digest); /* namebuff used for workspace above, restore to leave unchanged on exit */ p = (unsigned char*)(rrset[0]); From a034611643787bfc389f7d4af014651b78315d0b Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 1 Mar 2026 12:33:38 +0000 Subject: [PATCH 044/101] Fix missed hash->digest calls in 4070a74862c3c956a676d2b931ff186e14f5d9f5 Signed-off-by: Dominik --- src/dnsmasq/dnssec.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index c0d46e235c..5c1e17fd33 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -843,7 +843,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch rather then O(keys x DSs) */ hash->update(ctx, (unsigned int)wire_len, (unsigned char *)name); hash->update(ctx, (unsigned int)rdlen, psave); - hash->digest(ctx, hash->digest_size, digest); + nettle_digest_wrapper(hash, ctx, hash->digest_size, digest); from_wire(name); @@ -1392,13 +1392,13 @@ static int hash_name(char *in, unsigned char **out, struct nettle_hash const *ha hash->update(ctx, to_wire(in), (unsigned char *)in); hash->update(ctx, salt_len, salt); - hash->digest(ctx, hash->digest_size, digest); + nettle_digest_wrapper(hash, ctx, hash->digest_size, digest); for(i = 0; i < iterations; i++) { hash->update(ctx, hash->digest_size, digest); hash->update(ctx, salt_len, salt); - hash->digest(ctx, hash->digest_size, digest); + nettle_digest_wrapper(hash, ctx, hash->digest_size, digest); } from_wire(in); From 64852549b54d6724e90016e21f96fcf7f1749d51 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 1 Mar 2026 19:45:46 +0100 Subject: [PATCH 045/101] Update dnsmasq tag Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 69f1b3a056..8b81d2bdba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test4) +set(DNSMASQ_VERSION pi-hole-v2.93test6) add_subdirectory(src) From f70ff5a6a550b60e0d4bc3467a68f8bd72d4a8a1 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 2 Mar 2026 20:58:49 +0100 Subject: [PATCH 046/101] Make FTL nettle v4.0 compatible Signed-off-by: Dominik --- src/CMakeLists.txt | 27 +++++++++++++++++++++++++++ src/api/2fa.c | 5 ++++- src/config/password.c | 8 ++++++++ src/files.c | 9 ++++++++- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b35cd73a0b..19eb8386ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -328,6 +328,33 @@ find_library(LIBNETTLE NAMES libnettle${LIBRARY_SUFFIX} nettle HINTS /usr/local/ find_library(LIBIDN2 NAMES libidn2${LIBRARY_SUFFIX} idn2) find_library(LIBUNISTRING NAMES libunistring${LIBRARY_SUFFIX} unistring) +# Echo library search results to the console +if(LIBHOGWEED) + message(STATUS "Found libhogweed: ${LIBHOGWEED}") +else() + message(WARNING "libhogweed not found, DNSSEC support will be disabled") +endif() +if(LIBGMP) + message(STATUS "Found libgmp: ${LIBGMP}") +else() + message(WARNING "libgmp not found, DNSSEC support will be disabled") +endif() +if(LIBNETTLE) + message(STATUS "Found libnettle: ${LIBNETTLE}") +else() + message(WARNING "libnettle not found, DNSSEC support will be disabled") +endif() +if(LIBIDN2) + message(STATUS "Found libidn2: ${LIBIDN2}") +else() + message(WARNING "libidn2 not found, IDN support will be disabled") +endif() +if(LIBUNISTRING) + message(STATUS "Found libunistring: ${LIBUNISTRING}") +else() + message(WARNING "libunistring not found, IDN support will be disabled") +endif() + target_link_libraries(pihole-FTL rt Threads::Threads ${LIBHOGWEED} ${LIBGMP} ${LIBNETTLE} ${LIBIDN2} ${LIBUNISTRING}) if(LUA_DL STREQUAL "true") diff --git a/src/api/2fa.c b/src/api/2fa.c index 510cf73333..f6a67d24d4 100644 --- a/src/api/2fa.c +++ b/src/api/2fa.c @@ -33,8 +33,11 @@ static uint32_t hotp(const uint8_t *key, size_t key_len, const uint64_t counter, // Compute HMAC-SHA1 hmac_sha1_update(&ctx, sizeof(counter_be), (uint8_t*)&counter_be); uint8_t out[SHA1_DIGEST_SIZE]; +#if NETTLE_VERSION_MAJOR >= 4 + hmac_sha1_digest(&ctx, out); +#else hmac_sha1_digest(&ctx, SHA1_DIGEST_SIZE, out); - +#endif // Truncate HMAC-SHA1 for ease of use // RFC 6238 (section 5.3): offset = last nibble of hash const uint8_t offset = out[SHA1_DIGEST_SIZE-1] & 0x0F; diff --git a/src/config/password.c b/src/config/password.c index a5f7035d44..cc9e488428 100644 --- a/src/config/password.c +++ b/src/config/password.c @@ -76,7 +76,11 @@ static char * __attribute__((malloc)) double_sha256_password(const char *passwor strlen(password), (uint8_t*)password); +#if NETTLE_VERSION_MAJOR >= 4 + sha256_digest(&ctx, raw_response); +#else sha256_digest(&ctx, SHA256_DIGEST_SIZE, raw_response); +#endif sha256_raw_to_hex(raw_response, response); // Hash password a second time @@ -85,7 +89,11 @@ static char * __attribute__((malloc)) double_sha256_password(const char *passwor strlen(response), (uint8_t*)response); +#if NETTLE_VERSION_MAJOR >= 4 + sha256_digest(&ctx, raw_response); +#else sha256_digest(&ctx, SHA256_DIGEST_SIZE, raw_response); +#endif sha256_raw_to_hex(raw_response, response); return strdup(response); diff --git a/src/files.c b/src/files.c index 91b738fb43..aa3aaeeb77 100644 --- a/src/files.c +++ b/src/files.c @@ -38,6 +38,10 @@ // flock(), LOCK_SH #include +// crypto library +#include +#include + // chmod_file() changes the file mode bits of a given file (relative // to the directory file descriptor) according to mode. mode is an // octal number representing the bit pattern for the new mode bits @@ -755,8 +759,11 @@ bool sha256sum(const char *path, uint8_t checksum[SHA256_DIGEST_SIZE], const boo } // Finalize SHA256 context +#if NETTLE_VERSION_MAJOR >= 4 + sha256_digest(&ctx, checksum); +#else sha256_digest(&ctx, SHA256_DIGEST_SIZE, checksum); - +#endif // Close file fclose(fp); From 1b43af985acf5d168b9bc86a47f6417f28543d17 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 3 Mar 2026 19:08:16 +0100 Subject: [PATCH 047/101] Fix TCP replies being broken. THis fixes a regression of 13e120b29b7c664ccc0bc82b4584613e7586dd74 Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 4 ++-- src/dnsmasq/forward.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 59af701fa2..e88e7ff630 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -2244,12 +2244,12 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) FTL_iface(iface, NULL, 0); /**********************************************/ + tcp_request(confd, now, &tcpbuff, &tcp_addr, netmask, auth_dns); + free(tcpbuff.iov_base); /************ Pi-hole modification ************/ FTL_TCP_worker_terminating(true); /**********************************************/ - tcp_request(confd, now, &tcpbuff, &tcp_addr, netmask, auth_dns); - free(tcpbuff.iov_base); for (s = daemon->servers; s; s = s->next) if (s->tcpfd != -1) diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index 1f43bb51f7..ae720d43b9 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -2687,16 +2687,16 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, size_t ede_len = 0; stale = 0; // Generate DNS packet for reply - m = FTL_make_answer(header, ((char *) header) + 65536, size, ede_data, &ede_len); + m = FTL_make_answer(out_header, ((char *) out_header) + 65536, size, ede_data, &ede_len); // The pseudoheader may contain important information such as EDNS0 version important for // some DNS resolvers (such as systemd-resolved) to work properly. We should not discard them. if (have_pseudoheader && m > 0) { if (ede_len > 0) // Add EDNS0 option EDE if applicable - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, + m = add_pseudoheader(out_header, m, ((unsigned char *) out_header) + 65536, EDNS0_OPTION_EDE, ede_data, ede_len, do_bit, 0); else - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, + m = add_pseudoheader(out_header, m, ((unsigned char *) out_header) + 65536, 0, NULL, 0, do_bit, 0); } } From ed8e049f695c3b0d18295fbf49237d0df6492c1a Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 3 Mar 2026 20:21:10 +0100 Subject: [PATCH 048/101] Tweak zone update testing script Signed-off-by: Dominik --- test/zone_update.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/zone_update.py b/test/zone_update.py index dad9d90ff3..fcb96571db 100644 --- a/test/zone_update.py +++ b/test/zone_update.py @@ -14,19 +14,20 @@ import dns.update import dns.rcode +proto = sys.argv[1] if len(sys.argv) > 1 else 'tcp' +server = sys.argv[2] if len(sys.argv) > 2 else '127.0.0.1' +port = int(sys.argv[3]) if len(sys.argv) > 3 else 53 + # Create a new update object update = dns.update.Update('example.com') # Add a new A record -update.add('www.example.com', 300, 'A', '127.0.0.1') +update.add('www.example.com', 300, 'A', server) # Send the update to the DNS server and print the response -if sys.argv[1] == 'udp': - response = dns.query.udp(update, '127.0.0.1') +if proto == 'udp': + response = dns.query.udp(update, server, port = port) print("UDP response: " + dns.rcode.to_text(response.rcode())) -elif sys.argv[1] == 'tcp': - response = dns.query.tcp(update, '127.0.0.1') +elif proto == 'tcp': + response = dns.query.tcp(update, server, port = port) print("TCP response: " + dns.rcode.to_text(response.rcode())) -else: - print("Invalid argument, use 'udp' or 'tcp'") - sys.exit(1) From c1cd0139e2556164ee03c16e6ac2ba9899e153c5 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 5 Mar 2026 19:27:04 +0100 Subject: [PATCH 049/101] Improve zone testing script Signed-off-by: Dominik --- build.sh | 10 ++++++++++ test/zone_update.py | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 34ea13493c..a07a274057 100755 --- a/build.sh +++ b/build.sh @@ -95,6 +95,16 @@ if [[ -n "${debug}" ]]; then restart=1 fi +# If we are building in debug mode, ensure CMake is configured for a Debug build +# This appends the cache entry so callers can still pass other -D options. +if [[ -n "${debug}" ]]; then + if [[ -n "${cmake_args}" ]]; then + cmake_args="${cmake_args} -DCMAKE_BUILD_TYPE=Debug" + else + cmake_args="-DCMAKE_BUILD_TYPE=Debug" + fi +fi + # If we are in dev mode, we want to build, install, restart, and tail the logs # by default if [[ -n "${dev}" ]]; then diff --git a/test/zone_update.py b/test/zone_update.py index fcb96571db..090644c4f1 100644 --- a/test/zone_update.py +++ b/test/zone_update.py @@ -14,9 +14,12 @@ import dns.update import dns.rcode +# Usage: python3 zone_update.py [proto = tcp] [port = 5300] [server = 127.0.0.1] + +# Get the protocol, server, and port from command line arguments or use defaults proto = sys.argv[1] if len(sys.argv) > 1 else 'tcp' -server = sys.argv[2] if len(sys.argv) > 2 else '127.0.0.1' -port = int(sys.argv[3]) if len(sys.argv) > 3 else 53 +port = int(sys.argv[2]) if len(sys.argv) > 2 else 5300 +server = sys.argv[3] if len(sys.argv) > 3 else '127.0.0.1' # Create a new update object update = dns.update.Update('example.com') From 3c6e24477d5357c4f3230cbbe6543b4ae6d8e04f Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 8 Mar 2026 22:19:30 +0000 Subject: [PATCH 050/101] Fix regression handling non-QUERY requests over TCP. Regression introduced in 729c16a8ace49d472bc29cc37f87ca39ade920b6 Signed-off-by: Dominik --- src/dnsmasq/forward.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index ae720d43b9..a82a562dc8 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -2552,10 +2552,10 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, break; /* Now get the query into the normal UDP packet buffer. - Ignore queries long than this. If we're answering locally, + Ignore queries longer than this. If we're answering locally, copy the query into the output buffer, but for forwarding, tcp_talk() - wants the query in a a different buffer from the reply. - Note that we overwrote any saved UDP query - this onlt matters in debug mode. */ + wants the query in different buffer from the reply. + Note that we overwrote any saved UDP query - this only matters in debug mode. */ daemon->srv_save = NULL; if (!read_write(confd, (unsigned char *)&tcp_len, sizeof(tcp_len), RW_READ) || !(size = ntohs(tcp_len)) || size > (size_t)daemon->packet_buff_sz || @@ -2571,6 +2571,15 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, /* header == query */ header = (struct dns_header *)daemon->packet; + + /* Add edns0 pheader to query */ + size = add_edns0_config(header, size, ((unsigned char *) header) + daemon->edns_pktsz, &peer_addr, now, &cacheable); + + /* Clear buffer to avoid risk of information disclosure. */ + memset(bigbuff->iov_base, 0, bigbuff->iov_len); + /* Copy query into output buffer for local answering */ + memcpy(out_header, header, size); + query_count++; /* log_query gets called indirectly all over the place, so @@ -2610,13 +2619,6 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, do_bit = 1; /* do bit */ } - size = add_edns0_config(header, size, ((unsigned char *) header) + daemon->edns_pktsz, &peer_addr, now, &cacheable); - - /* Clear buffer to avoid risk of information disclosure. */ - memset(bigbuff->iov_base, 0, bigbuff->iov_len); - /* Copy query into output buffer for local answering */ - memcpy(out_header, header, size); - log_query_mysockaddr((auth_dns ? F_NOERR | F_AUTH : 0) | F_QUERY | F_FORWARD, daemon->namebuff, &peer_addr, NULL, qtype); @@ -2672,7 +2674,7 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, else if (!allowed) { ede = EDE_BLOCKED; - m = answer_disallowed(header, size, (u32)mark, daemon->namebuff); + m = answer_disallowed(out_header, size, (u32)mark, daemon->namebuff); } #endif #ifdef HAVE_AUTH @@ -2710,7 +2712,7 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, /* Do this by steam now we're not in the select() loop */ check_log_writer(1); - if (m == 0 && ede == EDE_UNSET) + if (!flags && m == 0 && ede == EDE_UNSET) { struct server *master; int start; From b90bd2e2b143f9a6b57ef0f48d00ea4ff3e9d25c Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 10 Mar 2026 21:17:41 +0000 Subject: [PATCH 051/101] Don't call qsort() with a NULL array when no servers defined. Signed-off-by: Dominik --- src/dnsmasq/domain-match.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/domain-match.c b/src/dnsmasq/domain-match.c index 935a2d3b0e..a5afb35f70 100644 --- a/src/dnsmasq/domain-match.c +++ b/src/dnsmasq/domain-match.c @@ -79,7 +79,9 @@ void build_server_array(void) for (serv = daemon->local_domains; serv; serv = serv->next, count++) daemon->serverarray[count] = serv; - qsort(daemon->serverarray, daemon->serverarraysz, sizeof(struct server *), order_qsort); + /* serverarray may be unallocated if we have no servers yet. */ + if (daemon->serverarray) + qsort(daemon->serverarray, daemon->serverarraysz, sizeof(struct server *), order_qsort); /* servers need the location in the array to find all the whole set of equivalent servers from a pointer to a single one. */ From 1f85ef275959bea60dce9dc24641b22e950a9562 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 10 Mar 2026 22:06:50 +0000 Subject: [PATCH 052/101] Tighten length checking to avoid possible buffer read-overrun in DHCPv6. Thanks to Dejan Alvadzijevic for spotting the problem. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index 94ce42d30f..b1ecac8f95 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -40,6 +40,7 @@ static void log6_opts(int nest, unsigned int xid, void *start_opts, void *end_op static void log6_packet(struct state *state, char *type, struct in6_addr *addr, char *string); static void log6_quiet(struct state *state, char *type, struct in6_addr *addr, char *string); static void *opt6_find (uint8_t *opts, uint8_t *end, unsigned int search, unsigned int minsize); +static void *opt6_first(uint8_t *opt, uint8_t *end); static void *opt6_next(uint8_t *opts, uint8_t *end); static unsigned int opt6_uint(unsigned char *opt, int offset, int size); static void get_context_tag(struct state *state, struct dhcp_context *context); @@ -112,11 +113,18 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t { uint8_t *end = inbuff + sz; uint8_t *opts = inbuff + 34; - int msg_type = *inbuff; + int msg_type; unsigned char *outmsgtypep; uint8_t *opt; struct dhcp_vendor *vendor; + /* must have at least msg_type+trans_id + which is 1 + 3 = 4 */ + if (sz < 4) + return 0; + + msg_type = *inbuff; + /* if not an encapsulated relayed message, just do the stuff */ if (msg_type != DHCP6RELAYFORW) { @@ -233,11 +241,8 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t memcpy(&state->mac[0], opt6_ptr(opt, 2), state->mac_len); } - for (opt = opts; opt; opt = opt6_next(opt, end)) + for (opt = opt6_first(opts, end); opt; opt = opt6_next(opt, end)) { - if ((uint8_t *)opt6_ptr(opt, 0) + opt6_len(opt) > end) - return 0; - /* Don't copy MAC address into reply. */ if (opt6_type(opt) != OPTION6_CLIENT_MAC) { @@ -290,7 +295,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu state->hostname = NULL; state->client_hostname = NULL; state->fqdn_flags = 0x01; /* default to send if we receive no FQDN option */ - + /* set tag with name == interface */ iface_id.net = state->iface_name; iface_id.next = state->tags; @@ -2113,6 +2118,19 @@ static void *opt6_find (uint8_t *opts, uint8_t *end, unsigned int search, unsign } } +static void *opt6_first(uint8_t *opt, uint8_t *end) +{ + /* make sure we have option number and length. */ + if ((uint8_t *)opt6_ptr(opt, 0) > end) + return NULL; + + /* make sure we have bytes promised by length. */ + if ((uint8_t *)opt6_ptr(opt, opt6_len(opt)) > end) + return NULL; + + return opt; +} + static void *opt6_next(uint8_t *opts, uint8_t *end) { u16 opt_len; From caab737d645be022ed1ffb626ff400c7a470605e Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 15 Mar 2026 16:03:29 +0000 Subject: [PATCH 053/101] Complete 32a54fc8a5b7fee715b963f6e5b28f41289e6a4f Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index e88e7ff630..1fb4752a56 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -206,7 +206,7 @@ int main_dnsmasq (int argc, char **argv) /* Must have at least a root trust anchor, or the DNSSEC code can loop forever. */ for (ds = daemon->ds; ds; ds = ds->next) - if (ds->name[0] == 0) + if (ds->name && ds->name[0] == 0) break; if (!ds) From 0038e89f13e4e39579ab54708d04436db24fd7d8 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 17 Mar 2026 06:53:44 +0100 Subject: [PATCH 054/101] Update embedded dnsmasq version Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b81d2bdba..079185928c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test6) +set(DNSMASQ_VERSION pi-hole-v2.93test7) add_subdirectory(src) From 73548bdf4f7c63c382100d7913a702a4fbe7e5d8 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 17 Mar 2026 07:05:32 +0100 Subject: [PATCH 055/101] Update zone update test script Signed-off-by: Dominik --- test/zone_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/zone_update.py b/test/zone_update.py index 090644c4f1..65fdc5c2fb 100644 --- a/test/zone_update.py +++ b/test/zone_update.py @@ -18,7 +18,7 @@ # Get the protocol, server, and port from command line arguments or use defaults proto = sys.argv[1] if len(sys.argv) > 1 else 'tcp' -port = int(sys.argv[2]) if len(sys.argv) > 2 else 5300 +port = int(sys.argv[2]) if len(sys.argv) > 2 else 53 server = sys.argv[3] if len(sys.argv) > 3 else '127.0.0.1' # Create a new update object From c8f1e1cc5a2ab1fb4a329498ed225864040b0a23 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 17 Mar 2026 12:09:36 +0000 Subject: [PATCH 056/101] Fix broken DHCPv6 vendorclass data in DHCP script. The code sending vendorclass data to the DHCP script was confused about the format of the vendorclass in the DHCP packet, resulting in broken data passed to the packet. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index b1ecac8f95..31a88b071f 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -1918,10 +1918,10 @@ static void update_leases(struct state *state, struct dhcp_context *context, str lease_add_extradata(lease, (unsigned char *)daemon->dhcp_buff2, strlen(daemon->dhcp_buff2), 0); if (opt6_len(opt) >= 6) - for (enc_opt = opt6_ptr(opt, 4); enc_opt; enc_opt = opt6_next(enc_opt, enc_end)) + for (enc_opt = opt6_ptr(opt, 4); enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) { lease->vendorclass_count++; - lease_add_extradata(lease, opt6_ptr(enc_opt, 0), opt6_len(enc_opt), 0); + lease_add_extradata(lease, opt6_user_vendor_ptr(enc_opt, 0), opt6_user_vendor_len(enc_opt), 0); } } From 5811f5eccc83cc6e8daa64ec47770b2f984c2ce1 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 17 Mar 2026 12:12:59 +0000 Subject: [PATCH 057/101] Accept DHCPv6 vendorclasses with any enterprise number in --dhcp-vendorclass if not enterprise number is specified. Also accept and match on enterprise number only. Signed-off-by: Dominik --- src/dnsmasq/option.c | 58 ++++++++++++++++++++++--------------------- src/dnsmasq/rfc3315.c | 21 +++++++++++++--- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 29fb724768..c091379067 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -4583,39 +4583,41 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma only allowed for agent-options. */ arg = comma; - if ((comma = split(arg))) + if (option == 'U' && strstr(arg, "enterprise:") == arg) { - if (option != 'U' || strstr(arg, "enterprise:") != arg) - { - free(new->netid.net); - ret_err_free(gen_err, new); - } - else - new->enterprise = atoi(arg+11); + comma = split(arg); + new->enterprise = atoi(arg+11); + arg = comma; } - else - comma = arg; - for (dig = 0, colon = 0, p = (unsigned char *)comma; *p; p++) - if (isxdigit(*p)) - dig = 1; - else if (*p == ':') - colon = 1; - else - break; - - unhide_metas(comma); - if (option == 'U' || option == 'j' || *p || !dig || !colon) + if (arg) { - new->len = strlen(comma); - new->data = opt_malloc(new->len); - memcpy(new->data, comma, new->len); + for (dig = 0, colon = 0, p = (unsigned char *)arg; *p; p++) + if (isxdigit(*p)) + dig = 1; + else if (*p == ':') + colon = 1; + else + break; + + unhide_metas(arg); + if (option == 'U' || option == 'j' || *p || !dig || !colon) + { + new->len = strlen(arg); + new->data = opt_malloc(new->len); + memcpy(new->data, arg, new->len); + } + else + { + new->len = parse_hex(comma, (unsigned char *)arg, strlen(arg), NULL, NULL); + new->data = opt_malloc(new->len); + memcpy(new->data, arg, new->len); + } } - else + else if (option != 'U' || new->enterprise == 0) { - new->len = parse_hex(comma, (unsigned char *)comma, strlen(comma), NULL, NULL); - new->data = opt_malloc(new->len); - memcpy(new->data, comma, new->len); + free(new->netid.net); + ret_err_free(gen_err, new); } switch (option) @@ -4638,7 +4640,7 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma } new->next = daemon->dhcp_vendors; daemon->dhcp_vendors = new; - + break; } diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index 31a88b071f..cc2510f115 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -392,13 +392,26 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu if (opt6_len(opt) < 4) continue; - if (vendor->enterprise != opt6_uint(opt, 0, 4)) + if (vendor->enterprise != 0 && vendor->enterprise != opt6_uint(opt, 0, 4)) continue; - + + /* matching enterprise, no string match. */ + if (vendor->enterprise != 0 && vendor->len == 0) + { + vendor->netid.next = state->tags; + state->tags = &vendor->netid; + break; + } + offset = 4; + + /* If we're going to search the strings below, there must be at least one empty string to search + I think a vendor_class option with just the enterprise number is valid. */ + if (opt6_len(opt) < 6) + continue; } - - /* Note that format if user/vendor classes is different to DHCP options - no option types. */ + + /* Note that format if user/vendor classes is different to DHCP options - no option types. */ for (enc_opt = opt6_ptr(opt, offset); enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) for (i = 0; i <= (opt6_user_vendor_len(enc_opt) - vendor->len); i++) if (memcmp(vendor->data, opt6_user_vendor_ptr(enc_opt, i), vendor->len) == 0) From fc1bcdb454d5f2a2edd4d7e656dfb65f785b1af9 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 17 Mar 2026 13:40:10 +0000 Subject: [PATCH 058/101] Fix broken DHCPv6 userclass data in DHCP script. Exactly the same principle as 53313014b50f256ae3aaa40990f46d927ae8c101 Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index cc2510f115..9d67d5c6b6 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -1991,8 +1991,8 @@ static void update_leases(struct state *state, struct dhcp_context *context, str if ((opt = opt6_find(state->packet_options, state->end, OPTION6_USER_CLASS, 2))) { void *enc_opt, *enc_end = opt6_ptr(opt, opt6_len(opt)); - for (enc_opt = opt6_ptr(opt, 0); enc_opt; enc_opt = opt6_next(enc_opt, enc_end)) - lease_add_extradata(lease, opt6_ptr(enc_opt, 0), opt6_len(enc_opt), 0); + for (enc_opt = opt6_ptr(opt, 0); enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) + lease_add_extradata(lease, opt6_user_vendor_ptr(enc_opt, 0), opt6_user_vendor_len(enc_opt), 0); } } #endif From 2c40dd7fac410346f8ee2e6ea5f3c9d59d3c7903 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 24 Mar 2026 12:30:45 +0000 Subject: [PATCH 059/101] New heuristic for disabling DNSSEC for domain-specific nameservers. If there's no configured server for the parent of the domain covered by a domain-specific server, assume that said domain is not signed. This extends the existing logic which makes similar decisions for replies to DS queries from tghe parent server. Signed-off-by: Dominik --- src/dnsmasq/dnsmasq.h | 1 + src/dnsmasq/dnssec.c | 31 +++++++++++++++++-------------- src/dnsmasq/forward.c | 13 +++++++++++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index 2b16599d1a..eedeeb2400 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1492,6 +1492,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch char *keyname, int class, int *validate_count); int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class, int *validate_count); +int cache_neg_ds(char *name, int flags, int class, time_t now, int neg_ttl); int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class, int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_count); int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen); diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 5c1e17fd33..42b5f064ad 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -1034,21 +1034,21 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char (flags = in_arpa_name_2_addr(name, &a)) && ((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0)))) { - my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name); + my_syslog(LOG_INFO, _("insecure reply received for DS %s, assuming that's OK for a RFC-1918 address"), name); neganswer = 1; nons = 0; /* If we're faking a DS, fake one with an NS. */ neg_ttl = DNSSEC_ASSUMED_DS_TTL; } else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) { - my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name); + my_syslog(LOG_INFO, _("insecure reply received for DS %s, assuming non-DNSSEC domain-specific server"), name); neganswer = 1; nons = 0; /* If we're faking a DS, fake one with an NS. */ neg_ttl = DNSSEC_ASSUMED_DS_TTL; } else { - my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); + my_syslog(LOG_WARNING, _("insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0); return STAT_BOGUS | DNSSEC_FAIL_INDET; } @@ -1148,44 +1148,47 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char } flags = F_FORWARD | F_DS | F_NEG | F_DNSSECOK; - + if (neganswer) { if (RCODE(header) == NXDOMAIN) flags |= F_NXDOMAIN; - /* We only cache validated DS records, DNSSECOK flag hijacked - to store presence/absence of NS. */ if (nons) { if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) { - my_syslog(LOG_WARNING, _("Negative DS reply without NS record received for %s, assuming non-DNSSEC domain-specific server."), name); + my_syslog(LOG_WARNING, _("negative DS reply without NS record received for %s, assuming non-DNSSEC domain-specific server"), name); nons = 0; + neg_ttl = DNSSEC_ASSUMED_DS_TTL; } else /* We only cache validated DS records, DNSSECOK flag hijacked to store presence/absence of NS. */ flags &= ~F_DNSSECOK; } + + log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, + servfail ? "SERVFAIL" : (nons ? "no DS/cut" : "no DS"), 0); } - + + return cache_neg_ds(name, flags, class, now, neg_ttl); + +} + +int cache_neg_ds(char *name, int flags, int class, time_t now, int ttl) +{ cache_start_insert(); /* Use TTL from NSEC for negative cache entries */ - if (!cache_insert(name, NULL, class, now, neg_ttl, flags)) + if (!cache_insert(name, NULL, class, now, ttl, flags)) return STAT_ABANDONED; cache_end_insert(); - if (neganswer) - log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, - servfail ? "SERVFAIL" : (nons ? "no DS/cut" : "no DS"), 0); - return STAT_OK; } - /* 4034 6.1 */ static int hostname_cmp(const char *a, const char *b) { diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index a82a562dc8..8d3fa5e8a7 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -998,6 +998,7 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header, /* As soon as anything returns BOGUS, we stop and unwind, to do otherwise would invite infinite loops, since the answers to DNSKEY and DS queries will not be cached, so they'll be repeated. */ + ds_retry: if (forward->flags & FREC_DNSKEY_QUERY) status = dnssec_validate_by_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter); else if (forward->flags & FREC_DS_QUERY) @@ -1127,6 +1128,18 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header, STAT_ISEQUAL(status, STAT_NEED_KEY) ? "dnssec-query[DNSKEY]" : "dnssec-query[DS]", 0); return; } + + /* If there's no server for the parent of a domain-specific server's domain, + assume that said server's contents it legitimately unsigned, as if the parent + contained a negative DS record. This is part of the same logic that's found + in dnssec_validate_ds() when it gets a negative DS repsonse. */ + if (STAT_ISEQUAL(status, STAT_NEED_DS) && serverind == -1 && lookup_domain(daemon->keyname, F_DOMAINSRV, NULL, NULL) && + cache_neg_ds(daemon->keyname, F_FORWARD | F_DS | F_NEG | F_DNSSECOK, forward->class, now, DNSSEC_ASSUMED_DS_TTL) == STAT_OK) + { + my_syslog(LOG_WARNING, _("no server for parent domain of %s, assuming unsigned domain"), daemon->keyname); + blockdata_free(stash); + goto ds_retry; + } /* error unwind */ free_rfds(&rfds); From a0be547655a7370de2e6cdd6696010fdd4083e57 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 24 Mar 2026 22:07:49 +0000 Subject: [PATCH 060/101] Further tidying of DHCPv6 packet dissection code. Make things more logical, and put buffer overflow detection in one new function, opt6_first(). Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 79 ++++++++++++------------------------------- 1 file changed, 21 insertions(+), 58 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index 9d67d5c6b6..66d91926d9 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -39,9 +39,8 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu static void log6_opts(int nest, unsigned int xid, void *start_opts, void *end_opts); static void log6_packet(struct state *state, char *type, struct in6_addr *addr, char *string); static void log6_quiet(struct state *state, char *type, struct in6_addr *addr, char *string); -static void *opt6_find (uint8_t *opts, uint8_t *end, unsigned int search, unsigned int minsize); +static void *opt6_find (uint8_t *opts, uint8_t *end, unsigned int search, int minsize); static void *opt6_first(uint8_t *opt, uint8_t *end); -static void *opt6_next(uint8_t *opts, uint8_t *end); static unsigned int opt6_uint(unsigned char *opt, int offset, int size); static void get_context_tag(struct state *state, struct dhcp_context *context); static int check_ia(struct state *state, void *opt, void **endp, void **ia_option); @@ -63,6 +62,7 @@ static void calculate_times(struct dhcp_context *context, unsigned int *min_time #define opt6_len(opt) ((int)(opt6_uint(opt, -2, 2))) #define opt6_type(opt) (opt6_uint(opt, -4, 2)) #define opt6_ptr(opt, i) ((void *)&(((uint8_t *)(opt))[4+(i)])) +#define opt6_next(opt, end) (opt6_first(opt6_ptr((opt), opt6_len((opt))), (end))) #define opt6_user_vendor_ptr(opt, i) ((void *)&(((uint8_t *)(opt))[2+(i)])) #define opt6_user_vendor_len(opt) ((int)(opt6_uint(opt, -4, 2))) @@ -675,7 +675,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu for (c = state->context; c; c = c->current) c->flags &= ~CONTEXT_CONF_USED; - for (opt = state->packet_options; opt; opt = opt6_next(opt, state->end)) + for (opt = opt6_first(state->packet_options, state->end); opt; opt = opt6_next(opt, state->end)) { void *ia_option, *ia_end; unsigned int min_time = 0xffffffff; @@ -839,7 +839,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu if (ignore) return 0; - for (opt = state->packet_options; opt; opt = opt6_next(opt, state->end)) + for (opt = opt6_first(state->packet_options, state->end); opt; opt = opt6_next(opt, state->end)) { void *ia_option, *ia_end; unsigned int min_time = 0xffffffff; @@ -950,7 +950,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu log6_quiet(state, msg_type == DHCP6RENEW ? "DHCPRENEW" : "DHCPREBIND", NULL, NULL); - for (opt = state->packet_options; opt; opt = opt6_next(opt, state->end)) + for (opt = opt6_first(state->packet_options, state->end); opt; opt = opt6_next(opt, state->end)) { void *ia_option, *ia_end; unsigned int min_time = 0xffffffff; @@ -1085,7 +1085,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu log6_quiet(state, "DHCPCONFIRM", NULL, NULL); - for (opt = state->packet_options; opt; opt = opt6_next(opt, state->end)) + for (opt = opt6_first(state->packet_options, state->end); opt; opt = opt6_next(opt, state->end)) { void *ia_option, *ia_end; @@ -1161,7 +1161,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu log6_quiet(state, "DHCPRELEASE", NULL, NULL); - for (opt = state->packet_options; opt; opt = opt6_next(opt, state->end)) + for (opt = opt6_first(state->packet_options, state->end); opt; opt = opt6_next(opt, state->end)) { void *ia_option, *ia_end; int made_ia = 0; @@ -1226,7 +1226,7 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu log6_quiet(state, "DHCPDECLINE", NULL, NULL); - for (opt = state->packet_options; opt; opt = opt6_next(opt, state->end)) + for (opt = opt6_first(state->packet_options, state->end); opt; opt = opt6_next(opt, state->end)) { void *ia_option, *ia_end; int made_ia = 0; @@ -1598,10 +1598,7 @@ static int check_ia(struct state *state, void *opt, void **endp, void **ia_optio { *ia_option = NULL; - /* must be a minimal option to check without stepping outside received packet. */ - if (opt6_ptr(opt, 4) > state->end) - return 0; - + /* callee ensures packet is long enough for opt6_len(opt) to be valid and believe-able. */ state->ia_type = opt6_type(opt); if (state->ia_type != OPTION6_IA_NA && state->ia_type != OPTION6_IA_TA) @@ -1613,10 +1610,7 @@ static int check_ia(struct state *state, void *opt, void **endp, void **ia_optio if (state->ia_type == OPTION6_IA_TA && opt6_len(opt) < 4) return 0; - /* Check we don't overflow the received packet. */ - if ((*endp = opt6_ptr(opt, opt6_len(opt))) > state->end) - return 0; - + *endp = opt6_ptr(opt, opt6_len(opt)); state->iaid = opt6_uint(opt, 0, 4); *ia_option = opt6_find(opt6_ptr(opt, state->ia_type == OPTION6_IA_NA ? 12 : 4), *endp, OPTION6_IAADDR, 24); @@ -2007,10 +2001,10 @@ static void log6_opts(int nest, unsigned int xid, void *start_opts, void *end_op void *opt; char *desc = nest ? "nest" : "sent"; - if (!option_bool(OPT_LOG_OPTS) || start_opts == end_opts) + if (!option_bool(OPT_LOG_OPTS)) return; - for (opt = start_opts; opt; opt = opt6_next(opt, end_opts)) + for (opt = opt6_first(start_opts, end_opts); opt; opt = opt6_next(opt, end_opts)) { int type = opt6_type(opt); void *ia_options = NULL; @@ -2104,35 +2098,20 @@ static void log6_packet(struct state *state, char *type, struct in6_addr *addr, string ? string : ""); } -static void *opt6_find (uint8_t *opts, uint8_t *end, unsigned int search, unsigned int minsize) +static void *opt6_find(uint8_t *opts, uint8_t *end, unsigned int search, int minsize) { - u16 opt, opt_len; - void *start; - - if (!opts) - return NULL; - - while (1) - { - if (end - opts < 4) - return NULL; - - start = opts; - GETSHORT(opt, opts); - GETSHORT(opt_len, opts); - - if (opt_len > (end - opts)) - return NULL; - - if (opt == search && (opt_len >= minsize)) - return start; - - opts += opt_len; - } + for (opts = opt6_first(opts, end); opts; opts = opt6_next(opts, end)) + if (opt6_type(opts) == search && opt6_len(opts) >= minsize) + return opts; + + return NULL; } static void *opt6_first(uint8_t *opt, uint8_t *end) { + if (!opt) + return NULL; + /* make sure we have option number and length. */ if ((uint8_t *)opt6_ptr(opt, 0) > end) return NULL; @@ -2144,22 +2123,6 @@ static void *opt6_first(uint8_t *opt, uint8_t *end) return opt; } -static void *opt6_next(uint8_t *opts, uint8_t *end) -{ - u16 opt_len; - - if (end - opts < 4) - return NULL; - - opts += 2; - GETSHORT(opt_len, opts); - - if (opt_len >= (end - opts)) - return NULL; - - return opts + opt_len; -} - static unsigned int opt6_uint(unsigned char *opt, int offset, int size) { /* this worries about unaligned data and byte order */ From 41d855ae7d2c7043b65e97ddb5bbf32723d22fb0 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 26 Mar 2026 21:29:26 +0100 Subject: [PATCH 061/101] Update dnsmasq tag fo 2.93test8 Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 079185928c..527b357596 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test7) +set(DNSMASQ_VERSION pi-hole-v2.93test8) add_subdirectory(src) From 816f627dab8428fd32ce3c0ee70702e55a05f904 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 30 Mar 2026 20:23:56 +0100 Subject: [PATCH 062/101] Move logging on TCP timeout from child process to main process. Callong my_syslog from a signal handler is not good. Thanks to Dominik Derigs for spotting this. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 3 +++ src/dnsmasq/dnsmasq.c | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 247dbf76f6..21b9cc9a5a 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -1100,6 +1100,9 @@ int cache_recv_insert(time_t now, int fd) !read_write(fd, (unsigned char *)&validatecount, sizeof(validatecount), RW_READ) || !read_write(fd, (unsigned char *)&validatecountp, sizeof(validatecountp), RW_READ))) return 0; + + if (op == PIPE_OP_KILLED) + my_syslog(LOG_INFO, _("TCP process for DNSSEC validation timed out")); /* There's a tiny chance that the frec may have been freed and reused before the TCP process returns. Detect that with diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 1fb4752a56..966b7ddb39 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1405,8 +1405,6 @@ static void sig_handler(int sig) read_write(daemon->pipe_to_parent, (unsigned char *)(&daemon->forward_to_tcp), sizeof(daemon->forward_to_tcp), RW_WRITE); read_write(daemon->pipe_to_parent, (unsigned char *)(&daemon->forward_to_tcp->uid), sizeof(daemon->forward_to_tcp->uid), RW_WRITE); - my_syslog(LOG_INFO, _("TCP process for DNSSEC validation timed out")); - /*** Pi-hole modification ***/ // TCP workers ignore all signals except SIGALRM FTL_TCP_worker_terminating(false); From 37f3f9efdeca4a2fb8183a2ecbf2c71a13c02fd7 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 30 Mar 2026 21:18:11 +0100 Subject: [PATCH 063/101] Tighten check on TFTP pathnames to avoid directory escape. Signed-off-by: Dominik --- src/dnsmasq/tftp.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/tftp.c b/src/dnsmasq/tftp.c index 966e696f48..b035a43fe9 100644 --- a/src/dnsmasq/tftp.c +++ b/src/dnsmasq/tftp.c @@ -545,8 +545,13 @@ static struct tftp_file *check_tftp_fileperm(char *packet, ssize_t *len, char *p int fd = -1; /* trick to ban moving out of the subtree */ - if (prefix && strstr(namebuff, "/../")) - goto perm; + if (prefix) + { + char *suspect = strstr(namebuff, "/.."); + + if (suspect && (suspect[3] == '/' || suspect[3] == 0)) + goto perm; + } if ((fd = open(namebuff, O_RDONLY)) == -1) { From 7e87df2e987a4b08e5c9d6ee9c952f3d6dfb3847 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 30 Mar 2026 21:19:26 +0100 Subject: [PATCH 064/101] OOB buffer check with DNS bitstring labels. Signed-off-by: Dominik --- src/dnsmasq/rfc1035.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/rfc1035.c b/src/dnsmasq/rfc1035.c index 2fe53bd6af..1e2141f300 100644 --- a/src/dnsmasq/rfc1035.c +++ b/src/dnsmasq/rfc1035.c @@ -318,7 +318,7 @@ unsigned char *skip_name(unsigned char *ansp, struct dns_header *header, size_t else if (label_type == 0x40) { /* Extended label type */ - unsigned int count; + unsigned int count, llen; if (!CHECK_LEN(header, ansp, plen, 2)) return NULL; @@ -329,9 +329,12 @@ unsigned char *skip_name(unsigned char *ansp, struct dns_header *header, size_t count = *(ansp++); /* Bits in bitstring */ if (count == 0) /* count == 0 means 256 bits */ - ansp += 32; + llen = 32; else - ansp += ((count-1)>>3)+1; + llen = ((count-1)>>3)+1; + + if (!ADD_RDLEN(header, ansp, plen, llen)) + return NULL; } else { /* label type == 0 Bottom six bits is length */ From 3970e70662967fdb18e75410fc958bd3cce6049a Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 5 Apr 2026 22:25:56 +0100 Subject: [PATCH 065/101] Sanity checking on DNS replies via TCP. Signed-off-by: Dominik --- src/dnsmasq/forward.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index 8d3fa5e8a7..a223b04e61 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -2279,9 +2279,12 @@ static ssize_t tcp_talk(int first, int last, int start, struct dns_header *heade someone might be attempting to insert bogus values into the cache by sending replies containing questions and bogus answers. Try another server, or give up */ - p = (unsigned char *)(((struct dns_header *)recvbuff->iov_base)+1); - if (extract_name(((struct dns_header *)recvbuff->iov_base), rsize, &p, daemon->namebuff, EXTR_NAME_COMPARE, 4) != 1) + struct dns_header *header = (struct dns_header *)recvbuff->iov_base; + p = (unsigned char *)(header+1); + if (rsize < (unsigned int)sizeof(struct dns_header) || !(header->hb3 & HB3_QR) || ntohs(header->qdcount) != 1 || + extract_name(header, rsize, &p, daemon->namebuff, EXTR_NAME_COMPARE, 4) != 1) continue; + GETSHORT(rtype, p); GETSHORT(rclass, p); From 2b77ae70e8b8ee0bd96c7b6f874beef959aa48c7 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 5 Apr 2026 22:50:43 +0100 Subject: [PATCH 066/101] Check DNS query via TCP doesn't have QR bit set. Signed-off-by: Dominik --- src/dnsmasq/forward.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index a223b04e61..3ff3b10a37 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -2578,18 +2578,18 @@ void tcp_request(int confd, time_t now, struct iovec *bigbuff, !read_write(confd, (unsigned char *)daemon->packet, size, RW_READ)) break; - if (size < (int)sizeof(struct dns_header)) + /* header == query */ + header = (struct dns_header *)daemon->packet; + + if (size < (int)sizeof(struct dns_header) || (header->hb3 & HB3_QR)) continue; /* Make sure we have a buffer big enough for the largest answer. */ expand_buf(bigbuff, 65536 + MAXDNAME + RRFIXEDSZ); out_header = bigbuff->iov_base; - /* header == query */ - header = (struct dns_header *)daemon->packet; - /* Add edns0 pheader to query */ - size = add_edns0_config(header, size, ((unsigned char *) header) + daemon->edns_pktsz, &peer_addr, now, &cacheable); + size = add_edns0_config(header, size, ((unsigned char *) header) + daemon->packet_buff_sz, &peer_addr, now, &cacheable); /* Clear buffer to avoid risk of information disclosure. */ memset(bigbuff->iov_base, 0, bigbuff->iov_len); From 28d75113c7aec9a5db5f3109fcae75db12348bab Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 6 Apr 2026 22:22:43 +0100 Subject: [PATCH 067/101] Fix 1-byte buffer overflow in relay_reply4() Potential SIGSEGV when using DHCPv4-relay. Thanks to Asim Viladi Oglu Manizada for finding this. Signed-off-by: Dominik --- src/dnsmasq/rfc2131.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/rfc2131.c b/src/dnsmasq/rfc2131.c index 9ff28da73c..af5c5101d8 100644 --- a/src/dnsmasq/rfc2131.c +++ b/src/dnsmasq/rfc2131.c @@ -3248,7 +3248,7 @@ unsigned int relay_reply4(struct dhcp_packet *mess, size_t sz, char *arrival_int /* delete agent info before return RFC 3046 para 2.1 */ *opt = OPTION_END; - memset(opt + 1, 0, option_len(opt) + 2); + memset(opt + 1, 0, option_len(opt) + 1); } } else if (mess->giaddr.s_addr == relay->local.addr4.s_addr) From 47957b96a8b99e2050969b6dd67db7bf30b4ad40 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 7 Apr 2026 10:49:17 +0200 Subject: [PATCH 068/101] Update dnsmasq version to v2.93test9 Signed-off-by: Dominik --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 527b357596..905982c54c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.93test8) +set(DNSMASQ_VERSION pi-hole-v2.93test9) add_subdirectory(src) From 61376812d7097aff8184b35785aa61e63f2cae06 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Wed, 8 Apr 2026 12:08:18 +0100 Subject: [PATCH 069/101] Bump copyrights to 2026. Signed-off-by: Dominik --- src/dnsmasq/arp.c | 2 +- src/dnsmasq/auth.c | 2 +- src/dnsmasq/blockdata.c | 2 +- src/dnsmasq/bpf.c | 2 +- src/dnsmasq/cache.c | 2 +- src/dnsmasq/config.h | 2 +- src/dnsmasq/conntrack.c | 2 +- src/dnsmasq/crypto.c | 2 +- src/dnsmasq/dbus.c | 2 +- src/dnsmasq/dhcp-common.c | 2 +- src/dnsmasq/dhcp-protocol.h | 2 +- src/dnsmasq/dhcp.c | 2 +- src/dnsmasq/dhcp6-protocol.h | 2 +- src/dnsmasq/dhcp6.c | 2 +- src/dnsmasq/dns-protocol.h | 2 +- src/dnsmasq/dnsmasq.c | 2 +- src/dnsmasq/dnsmasq.h | 4 ++-- src/dnsmasq/dnssec.c | 2 +- src/dnsmasq/domain-match.c | 2 +- src/dnsmasq/domain.c | 2 +- src/dnsmasq/dump.c | 2 +- src/dnsmasq/edns0.c | 2 +- src/dnsmasq/forward.c | 2 +- src/dnsmasq/helper.c | 2 +- src/dnsmasq/inotify.c | 2 +- src/dnsmasq/ip6addr.h | 2 +- src/dnsmasq/lease.c | 2 +- src/dnsmasq/log.c | 2 +- src/dnsmasq/loop.c | 2 +- src/dnsmasq/metrics.c | 2 +- src/dnsmasq/metrics.h | 2 +- src/dnsmasq/netlink.c | 2 +- src/dnsmasq/network.c | 2 +- src/dnsmasq/nftset.c | 2 +- src/dnsmasq/option.c | 2 +- src/dnsmasq/outpacket.c | 2 +- src/dnsmasq/pattern.c | 2 +- src/dnsmasq/poll.c | 2 +- src/dnsmasq/radv-protocol.h | 2 +- src/dnsmasq/radv.c | 2 +- src/dnsmasq/rfc1035.c | 2 +- src/dnsmasq/rfc2131.c | 2 +- src/dnsmasq/rfc3315.c | 2 +- src/dnsmasq/rrfilter.c | 2 +- src/dnsmasq/slaac.c | 2 +- src/dnsmasq/tftp.c | 2 +- src/dnsmasq/ubus.c | 2 +- 47 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/dnsmasq/arp.c b/src/dnsmasq/arp.c index a74c2cdc22..0f548933cc 100644 --- a/src/dnsmasq/arp.c +++ b/src/dnsmasq/arp.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/auth.c b/src/dnsmasq/auth.c index c318b4d17c..94c281c185 100644 --- a/src/dnsmasq/auth.c +++ b/src/dnsmasq/auth.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/blockdata.c b/src/dnsmasq/blockdata.c index 00b86d5715..ebaa82fafd 100644 --- a/src/dnsmasq/blockdata.c +++ b/src/dnsmasq/blockdata.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/bpf.c b/src/dnsmasq/bpf.c index dd67735dcd..f8c8b35bbf 100644 --- a/src/dnsmasq/bpf.c +++ b/src/dnsmasq/bpf.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 21b9cc9a5a..34c31ef3d9 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/config.h b/src/dnsmasq/config.h index a6413a0c73..a18308d44e 100644 --- a/src/dnsmasq/config.h +++ b/src/dnsmasq/config.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/conntrack.c b/src/dnsmasq/conntrack.c index 24935041d8..0c6d51abe2 100644 --- a/src/dnsmasq/conntrack.c +++ b/src/dnsmasq/conntrack.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/crypto.c b/src/dnsmasq/crypto.c index 002aae3ef0..cb71319466 100644 --- a/src/dnsmasq/crypto.c +++ b/src/dnsmasq/crypto.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dbus.c b/src/dnsmasq/dbus.c index 0e2243f382..afdadf2055 100644 --- a/src/dnsmasq/dbus.c +++ b/src/dnsmasq/dbus.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dhcp-common.c b/src/dnsmasq/dhcp-common.c index 192d374393..0cab942862 100644 --- a/src/dnsmasq/dhcp-common.c +++ b/src/dnsmasq/dhcp-common.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dhcp-protocol.h b/src/dnsmasq/dhcp-protocol.h index 72c420d977..adf8d08be7 100644 --- a/src/dnsmasq/dhcp-protocol.h +++ b/src/dnsmasq/dhcp-protocol.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dhcp.c b/src/dnsmasq/dhcp.c index 0a1e33ce98..e758c96a5f 100644 --- a/src/dnsmasq/dhcp.c +++ b/src/dnsmasq/dhcp.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dhcp6-protocol.h b/src/dnsmasq/dhcp6-protocol.h index ad5183de86..d55f1475e0 100644 --- a/src/dnsmasq/dhcp6-protocol.h +++ b/src/dnsmasq/dhcp6-protocol.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dhcp6.c b/src/dnsmasq/dhcp6.c index 480ecebfdb..3b81e5acbe 100644 --- a/src/dnsmasq/dhcp6.c +++ b/src/dnsmasq/dhcp6.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dns-protocol.h b/src/dnsmasq/dns-protocol.h index e71bedc46f..9ef4083f81 100644 --- a/src/dnsmasq/dns-protocol.h +++ b/src/dnsmasq/dns-protocol.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dnsmasq.c b/src/dnsmasq/dnsmasq.c index 966b7ddb39..96e0b8df12 100644 --- a/src/dnsmasq/dnsmasq.c +++ b/src/dnsmasq/dnsmasq.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index eedeeb2400..d7295302ff 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ along with this program. If not, see . */ -#define COPYRIGHT "Copyright (c) 2000-2025 Simon Kelley" +#define COPYRIGHT "Copyright (c) 2000-2026 Simon Kelley" /* We do defines that influence behavior of stdio.h, so complain if included too early. */ diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index 42b5f064ad..b99d3451d9 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -1,5 +1,5 @@ /* dnssec.c is Copyright (c) 2012 Giovanni Bajo - and Copyright (c) 2012-2025 Simon Kelley + and Copyright (c) 2012-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/domain-match.c b/src/dnsmasq/domain-match.c index a5afb35f70..208cd210ce 100644 --- a/src/dnsmasq/domain-match.c +++ b/src/dnsmasq/domain-match.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/domain.c b/src/dnsmasq/domain.c index ce4929bed3..9647a85e2f 100644 --- a/src/dnsmasq/domain.c +++ b/src/dnsmasq/domain.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/dump.c b/src/dnsmasq/dump.c index aa91458d5f..55ab43dbe9 100644 --- a/src/dnsmasq/dump.c +++ b/src/dnsmasq/dump.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/edns0.c b/src/dnsmasq/edns0.c index 5a5f59856f..04b5bcf4c1 100644 --- a/src/dnsmasq/edns0.c +++ b/src/dnsmasq/edns0.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index 3ff3b10a37..4dd4fc3414 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/helper.c b/src/dnsmasq/helper.c index cd73785380..729d5e19df 100644 --- a/src/dnsmasq/helper.c +++ b/src/dnsmasq/helper.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/inotify.c b/src/dnsmasq/inotify.c index 2ec6e3976f..eb8524179c 100644 --- a/src/dnsmasq/inotify.c +++ b/src/dnsmasq/inotify.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/ip6addr.h b/src/dnsmasq/ip6addr.h index edf3baa50f..d821bcdeb2 100644 --- a/src/dnsmasq/ip6addr.h +++ b/src/dnsmasq/ip6addr.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/lease.c b/src/dnsmasq/lease.c index 00748ad9e6..dd6babd3cd 100644 --- a/src/dnsmasq/lease.c +++ b/src/dnsmasq/lease.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/log.c b/src/dnsmasq/log.c index 4d17a423bd..0f24c45679 100644 --- a/src/dnsmasq/log.c +++ b/src/dnsmasq/log.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/loop.c b/src/dnsmasq/loop.c index b7e0db1d85..b005cba0a1 100644 --- a/src/dnsmasq/loop.c +++ b/src/dnsmasq/loop.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/metrics.c b/src/dnsmasq/metrics.c index 864b34cfa5..0712808a43 100644 --- a/src/dnsmasq/metrics.c +++ b/src/dnsmasq/metrics.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/metrics.h b/src/dnsmasq/metrics.h index 79017e2859..62330dc11d 100644 --- a/src/dnsmasq/metrics.h +++ b/src/dnsmasq/metrics.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/netlink.c b/src/dnsmasq/netlink.c index 389325e08e..385d4200c7 100644 --- a/src/dnsmasq/netlink.c +++ b/src/dnsmasq/netlink.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/network.c b/src/dnsmasq/network.c index 5641167dd2..e8a856f05f 100644 --- a/src/dnsmasq/network.c +++ b/src/dnsmasq/network.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/nftset.c b/src/dnsmasq/nftset.c index 7130eeb70e..78ea2932f7 100644 --- a/src/dnsmasq/nftset.c +++ b/src/dnsmasq/nftset.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index c091379067..4779f4273c 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/outpacket.c b/src/dnsmasq/outpacket.c index c0163514ec..fbb966d396 100644 --- a/src/dnsmasq/outpacket.c +++ b/src/dnsmasq/outpacket.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/pattern.c b/src/dnsmasq/pattern.c index 0d297f1604..10db9bc000 100644 --- a/src/dnsmasq/pattern.c +++ b/src/dnsmasq/pattern.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/poll.c b/src/dnsmasq/poll.c index ff46a7145b..d2d1fb925a 100644 --- a/src/dnsmasq/poll.c +++ b/src/dnsmasq/poll.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/radv-protocol.h b/src/dnsmasq/radv-protocol.h index dee03886ff..06eb4186e5 100644 --- a/src/dnsmasq/radv-protocol.h +++ b/src/dnsmasq/radv-protocol.h @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/radv.c b/src/dnsmasq/radv.c index aaf6b71e27..fc4d1d15c9 100644 --- a/src/dnsmasq/radv.c +++ b/src/dnsmasq/radv.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/rfc1035.c b/src/dnsmasq/rfc1035.c index 1e2141f300..c7cac1bedd 100644 --- a/src/dnsmasq/rfc1035.c +++ b/src/dnsmasq/rfc1035.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/rfc2131.c b/src/dnsmasq/rfc2131.c index af5c5101d8..bcaba821c2 100644 --- a/src/dnsmasq/rfc2131.c +++ b/src/dnsmasq/rfc2131.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index 66d91926d9..325ad8a65b 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/rrfilter.c b/src/dnsmasq/rrfilter.c index 29f69c74a3..8583fb8e68 100644 --- a/src/dnsmasq/rrfilter.c +++ b/src/dnsmasq/rrfilter.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/slaac.c b/src/dnsmasq/slaac.c index 8b089c85e7..ca92ac0aec 100644 --- a/src/dnsmasq/slaac.c +++ b/src/dnsmasq/slaac.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/tftp.c b/src/dnsmasq/tftp.c index b035a43fe9..3436bab9a2 100644 --- a/src/dnsmasq/tftp.c +++ b/src/dnsmasq/tftp.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/dnsmasq/ubus.c b/src/dnsmasq/ubus.c index b30f2b6d2b..ec910ec3fa 100644 --- a/src/dnsmasq/ubus.c +++ b/src/dnsmasq/ubus.c @@ -1,4 +1,4 @@ -/* dnsmasq is Copyright (c) 2000-2025 Simon Kelley +/* dnsmasq is Copyright (c) 2000-2026 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From f08e9130c24b39bcd36859e9271ba38325d8b5b0 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 10 Apr 2026 12:40:29 +0100 Subject: [PATCH 070/101] Tweak DHCPv6 replay decapsulation. RFC 8415 doesn't exclude the possiblity that a relay-forward message could contain _more_than_one relay-message option. The existing code could, under certain circumstances, access freed stack memory in this case. Thanks to Dejan Alvadzijevic and his fuzzer for finding this condition. This patch fixes things so that dnsmasq does at least sensible things with multiple relay-message options, and no longer has the (remote) possibility of accessing dead memory. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 46 ++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index 325ad8a65b..a65b32adb1 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -24,7 +24,7 @@ struct state { int clid_len, ia_type, interface, hostname_auth, lease_allocate; char *client_hostname, *hostname, *domain, *send_domain; struct dhcp_context *context; - struct in6_addr *link_address, *fallback, *ll_addr, *ula_addr; + struct in6_addr *relay_address, *fallback, *ll_addr, *ula_addr; unsigned int xid, fqdn_flags, iaid; char *iface_name; void *packet_options, *end; @@ -34,7 +34,7 @@ struct state { }; static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t sz, - struct in6_addr *client_addr, time_t now); + struct in6_addr *client_addr, struct in6_addr *link_address, time_t now); static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbuff, size_t sz, time_t now); static void log6_opts(int nest, unsigned int xid, void *start_opts, void *end_opts); static void log6_packet(struct state *state, char *type, struct in6_addr *addr, char *string); @@ -99,9 +99,8 @@ unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int state.ula_addr = ula_addr; state.mac_len = 0; state.tags = NULL; - state.link_address = NULL; - - if (dhcp6_maybe_relay(&state, daemon->dhcp_packet.iov_base, sz, client_addr, now)) + + if (dhcp6_maybe_relay(&state, daemon->dhcp_packet.iov_base, sz, client_addr, NULL, now)) return msg_type == DHCP6RELAYFORW ? DHCPV6_SERVER_PORT : DHCPV6_CLIENT_PORT; return 0; @@ -109,7 +108,7 @@ unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int /* This cost me blood to write, it will probably cost you blood to understand - srk. */ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t sz, - struct in6_addr *client_addr, time_t now) + struct in6_addr *client_addr, struct in6_addr *link_address, time_t now) { uint8_t *end = inbuff + sz; uint8_t *opts = inbuff + 34; @@ -136,7 +135,7 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t link_address == NULL means there's no relay in use, so we try and find the client's MAC address from the local ND cache. */ - if (!state->link_address) + if (!link_address) get_client_mac(client_addr, state->interface, state->mac, &state->mac_len, &state->mac_type, now); else { @@ -144,9 +143,9 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t struct shared_network *share = NULL; state->context = NULL; - if (!IN6_IS_ADDR_LOOPBACK(state->link_address) && - !IN6_IS_ADDR_LINKLOCAL(state->link_address) && - !IN6_IS_ADDR_MULTICAST(state->link_address)) + if (!IN6_IS_ADDR_LOOPBACK(link_address) && + !IN6_IS_ADDR_LINKLOCAL(link_address) && + !IN6_IS_ADDR_MULTICAST(link_address)) for (c = daemon->dhcp6; c; c = c->next) { for (share = daemon->shared_networks; share; share = share->next) @@ -155,7 +154,7 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t continue; if (share->if_index != 0 || - !IN6_ARE_ADDR_EQUAL(state->link_address, &share->match_addr6)) + !IN6_ARE_ADDR_EQUAL(link_address, &share->match_addr6)) continue; if ((c->flags & CONTEXT_DHCP) && @@ -168,8 +167,8 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t if (share || ((c->flags & CONTEXT_DHCP) && !(c->flags & (CONTEXT_TEMPLATE | CONTEXT_OLD)) && - is_same_net6(state->link_address, &c->start6, c->prefix) && - is_same_net6(state->link_address, &c->end6, c->prefix))) + is_same_net6(link_address, &c->start6, c->prefix) && + is_same_net6(link_address, &c->end6, c->prefix))) { c->preferred = c->valid = 0xffffffff; c->current = state->context; @@ -179,7 +178,7 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t if (!state->context) { - inet_ntop(AF_INET6, state->link_address, daemon->addrbuff, ADDRSTRLEN); + inet_ntop(AF_INET6, link_address, daemon->addrbuff, ADDRSTRLEN); my_syslog(MS_DHCP | LOG_WARNING, _("no address range available for DHCPv6 request from relay at %s"), daemon->addrbuff); @@ -194,6 +193,8 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t return 0; } + state->relay_address = link_address; + return dhcp6_no_relay(state, msg_type, inbuff, sz, now); } @@ -253,11 +254,12 @@ static int dhcp6_maybe_relay(struct state *state, unsigned char *inbuff, size_t /* the packet data is unaligned, copy to aligned storage */ memcpy(&align, inbuff + 2, IN6ADDRSZ); - - /* RFC6221 para 4 */ - if (!IN6_IS_ADDR_UNSPECIFIED(&align)) - state->link_address = &align; - if (!dhcp6_maybe_relay(state, opt6_ptr(opt, 0), opt6_len(opt), client_addr, now)) + /* RFC6221 para 4 says if link_address in encapulation + is zero, ignore it, and, by implication, use the link + address of any enclosing encapsulation or, failing that + of the arrival interface on the the server. */ + if (!dhcp6_maybe_relay(state, opt6_ptr(opt, 0), opt6_len(opt), client_addr, + IN6_IS_ADDR_UNSPECIFIED(&align) ? link_address : &align, now)) return 0; } else @@ -1977,10 +1979,10 @@ static void update_leases(struct state *state, struct dhcp_context *context, str } } - if (state->link_address) - inet_ntop(AF_INET6, state->link_address, daemon->addrbuff, ADDRSTRLEN); + if (state->relay_address) + inet_ntop(AF_INET6, state->relay_address, daemon->addrbuff, ADDRSTRLEN); - lease_add_extradata(lease, (unsigned char *)daemon->addrbuff, state->link_address ? strlen(daemon->addrbuff) : 0, 0); + lease_add_extradata(lease, (unsigned char *)daemon->addrbuff, state->relay_address ? strlen(daemon->addrbuff) : 0, 0); if ((opt = opt6_find(state->packet_options, state->end, OPTION6_USER_CLASS, 2))) { From 9a72d8dc0cf32faebf0a798e72e2c169240c5d2e Mon Sep 17 00:00:00 2001 From: Toliak Purple Date: Tue, 21 Apr 2026 21:49:35 +0100 Subject: [PATCH 071/101] Fix crash with mal-formed config option. Reproduction Steps. Both commands cause a segmentation fault: ``` dnsmasq --interface-name=, dnsmasq --dynamic-host=,::, ``` Stack Trace (ASAN, v2.90) ``` ==1817==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f0203d22845 bp 0x7ffc3316b920 sp 0x7ffc3316b0a0 T0) ==1817==The signal is caused by a READ memory access. ==1817==Hint: address points to the zero page. #0 0x7f0203d22845 in __interceptor_strncmp ../../../../src/dnsmasq/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:488 #1 0x5563361feeb3 in iface_allowed /opt/dnsmasq/src/dnsmasq/network.c:361 #2 0x556336203aca in iface_allowed_v6 /opt/dnsmasq/src/dnsmasq/network.c:622 #3 0x55633625cc4c in iface_enumerate /opt/dnsmasq/src/dnsmasq/netlink.c:291 #4 0x55633620573c in enumerate_interfaces /opt/dnsmasq/src/dnsmasq/network.c:836 #5 0x55633615c3fe in main /opt/dnsmasq/src/dnsmasq/dnsmasq.c:367 ``` The fault occurs due to a null-pointer dereference in iface_allowed() at line 361: ``` if (strncmp(label, int_name->intr, IF_NAMESIZE) == 0) //// here int_name->intr is NULL ``` That occurs due to a null-pointer assignment in the option.c at line 4856: ``` new->intr = opt_string_alloc(arg); //// if arg is empty string, the opt_string_alloc will return NULL ``` Signed-off-by: Dominik --- src/dnsmasq/option.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 4779f4273c..20026552c1 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -4963,7 +4963,8 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma arg = NULL; /* provoke error below */ } - if (!domain || !arg || !(new->name = canonicalise_opt(domain))) + if (!domain || !arg || !new->intr || + !(new->name = canonicalise_opt(domain))) ret_err(option == LOPT_DYNHOST ? _("bad dynamic host") : _("bad interface name")); From 135ab752d5a7f5c98656d229765e71d5028eec66 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 21 Apr 2026 22:14:41 +0100 Subject: [PATCH 072/101] Fix buffer overlow in log_query() The addition of "(not supported)" to logs of DS and DNSKEY replies overflows the buffer used to construct the string. Re-arrange things to avoid this, and add checks to avoid the same problem if the logging calls change in the future. Thanks to Yiwei Hou for finding this. The problem exists is DNSSEC is enabled and query logging is also enabled. The overwrite is of bounded length and the bytes written are not in control of an attacker, so this is not considered a likely remote-execution vector. Signed-off-by: Dominik --- src/dnsmasq/cache.c | 28 +++++++++++++++++----------- src/dnsmasq/dnssec.c | 7 ++----- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 34c31ef3d9..305e26cad8 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -2452,7 +2452,7 @@ void _log_query(unsigned int flags, char *name, union all_addr *addr, char *arg, return; /* build query type string if requested */ - if (!(flags & (F_SERVER | F_IPSET | F_QUERY)) && type > 0) + if (!(flags & (F_SERVER | F_IPSET | F_QUERY | F_KEYTAG | F_RR)) && type > 0) arg = querystr(arg, type); dest = arg; @@ -2468,19 +2468,25 @@ void _log_query(unsigned int flags, char *name, union all_addr *addr, char *arg, { dest = daemon->addrbuff; - if (flags & F_RR) - { - if (flags & F_KEYTAG) - dest = querystr(NULL, addr->rrblock.rrtype); - else - dest = querystr(NULL, addr->rrdata.rrtype); - } - else if (flags & F_KEYTAG) - sprintf(daemon->addrbuff, arg, addr->log.keytag, addr->log.algo, addr->log.digest); + if (flags & F_RR) + { + if (flags & F_KEYTAG) + dest = querystr(NULL, addr->rrblock.rrtype); + else + dest = querystr(NULL, addr->rrdata.rrtype); + } +#ifdef HAVE_DNSSEC + else if (flags & F_KEYTAG) + { + snprintf(daemon->addrbuff, ADDRSTRLEN, arg, addr->log.keytag, addr->log.algo, addr->log.digest); + if (type) + extra = " (not supported)"; + } +#endif else if (flags & F_RCODE) { unsigned int rcode = addr->log.rcode; - + if (rcode == SERVFAIL) dest = "SERVFAIL"; else if (rcode == REFUSED) diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index b99d3451d9..0756163988 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -957,10 +957,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch a.log.keytag = keytag; a.log.algo = algo; - if (algo_digest_name(algo)) - log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu", 0); - else - log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu (not supported)", 0); + log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu", !algo_digest_name(algo)); } } @@ -1108,7 +1105,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char a.log.keytag = keytag; a.log.algo = algo; a.log.digest = digest; - log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS for keytag %hu, algo %hu, digest %hu (not supported)", 0); + log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS for keytag %hu, algo %hu, digest %hu", 1); neg_ttl = ttl; } else if ((key = blockdata_alloc((char*)p, rdlen - 4))) From ce5a896ea23c613a711a75b70ce79f9bfe5fd786 Mon Sep 17 00:00:00 2001 From: Florian Margaine Date: Tue, 21 Apr 2026 22:41:10 +0100 Subject: [PATCH 073/101] Preserve existing log file permissions when adding group-write bit. Commit 1f8f78a49b8fd ("Add root group writeable flag to log file") introduced a fchmod() call in log_start() that resets the file mode to a hardcoded value (0660), discarding any pre-existing permissions. This broke our usage of dnsmasq where we create the log file with specific permissions before starting dnsmasq in an LXC container namespace, so that unprivileged users inside the container can read the log. The hardcoded mode strips those permissions on startup. Use the existing stat result to OR in S_IWGRP instead, equivalent to chmod g+w, so that only the group-write bit is added without disturbing other permission bits. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Dominik --- src/dnsmasq/log.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/log.c b/src/dnsmasq/log.c index 0f24c45679..121d724da8 100644 --- a/src/dnsmasq/log.c +++ b/src/dnsmasq/log.c @@ -118,7 +118,7 @@ int log_start(struct passwd *ent_pw, int errfd) struct stat ls; if (getgid() == 0 && fstat(log_fd, &ls) == 0 && ls.st_gid == 0 && (ls.st_mode & S_IWGRP) == 0) - (void)fchmod(log_fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); + (void)fchmod(log_fd, ls.st_mode | S_IWGRP); if (fchown(log_fd, ent_pw->pw_uid, -1) != 0) ret = errno; } From a8819db02a1fc1e74283cbc9896a7b59fd7a2545 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 4 May 2026 16:18:33 +0100 Subject: [PATCH 074/101] Fix memory leak reading ARP cache on *BSD. arp_enumerate() on *BSD platforms has a bad memory leak. Thanks to Sagie Duchovne-Nave for finding this problem. His description is included below: arp_enumerate() allocates a heap buffer via expand_buf() to hold the kernel ARP table dump retrieved through sysctl(NET_RT_FLAGS). This buffer is never freed on any return path -- neither the early error returns nor the normal return after iteration -- causing a leak on every call. The leak is most acute in the DHCPv6 path. get_client_mac() calls find_mac() up to five times per packet with lazy=0. Because the 'updated' flag is local to each find_mac() invocation, a cached ARP_EMPTY entry for an unresolvable IPv6 address does not short- circuit the kernel lookup: each call falls through to iface_enumerate() -> arp_enumerate(), leaking one buffer per call. This yields up to five leaked allocations per DHCPv6 SOLICIT packet. The leak size per call equals the full system-wide IPv4 ARP table dump across all interfaces. The condition is readily triggered by a DHCPv6 client whose MAC address cannot be resolved via NDP -- which is the common case on FreeBSD, because arp_enumerate() queries NET_RT_FLAGS/RTF_LLINFO, which returns IPv4 ARP entries only; IPv6 NDP neighbour entries are not included. As a result every IPv6 MAC lookup fails unconditionally on FreeBSD, every failed lookup produces an ARP_EMPTY record that is never promoted, and every subsequent packet for that client leaks five buffers. This fix is by Simon Kelley, not Sagie, so any bugs are my fault. The fix is to make the buffer statically allocated, so that expand_buf() is normally a NOOP, but when the ARP cache grows, it grows to match. This avoids fragmenting the heap with many malloc/free calls. It's a common design pattern in dnsmasq, but the original author of this code missed the importance of that static allocation. Since that was before dnsmasq moved into git, the details are lost to time. Signed-off-by: Dominik --- src/dnsmasq/bpf.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/dnsmasq/bpf.c b/src/dnsmasq/bpf.c index f8c8b35bbf..658a34ac2e 100644 --- a/src/dnsmasq/bpf.c +++ b/src/dnsmasq/bpf.c @@ -47,7 +47,7 @@ static union all_addr del_addr; #if defined(HAVE_BSD_NETWORK) && !defined(__APPLE__) -int arp_enumerate(void *parm, callback_t callback) +static int arp_enumerate(void *parm, callback_t callback) { int mib[6]; size_t needed; @@ -55,12 +55,9 @@ int arp_enumerate(void *parm, callback_t callback) struct rt_msghdr *rtm; struct sockaddr_inarp *sin2; struct sockaddr_dl *sdl; - struct iovec buff; + static struct iovec buff; int rc; - buff.iov_base = NULL; - buff.iov_len = 0; - mib[0] = CTL_NET; mib[1] = PF_ROUTE; mib[2] = 0; From 7d63856295c28ce43d94137ca001c0f030d7a756 Mon Sep 17 00:00:00 2001 From: "Sagie D." Date: Sat, 9 May 2026 21:44:38 +0100 Subject: [PATCH 075/101] Add support for IPV6 when reading ARP/ND table on *BSD. The arp_enumerate() function is extended into a per-family helper arp_enumerate_family(), called sequentially for AF_INET and AF_INET6, allowing the NDP neighbour cache to be enumerated alongside the ARP table. An empty table for either family is treated as vacuous success rather than an error. Both tables are acquired in *BSD via PF_ROUTE sysctl calls that return raw kernel structures; consequently, for IPv6, the sockaddr_in6 peer address extracted from them has an embedded link-local scope ID. This ID is extracted into sin6_scope_id, and bytes 2-3 of the address are then cleared, per the KAME API contract. Signed-off-by: Sagie D. Signed-off-by: Dominik --- src/dnsmasq/bpf.c | 51 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/dnsmasq/bpf.c b/src/dnsmasq/bpf.c index 658a34ac2e..01e4fc9134 100644 --- a/src/dnsmasq/bpf.c +++ b/src/dnsmasq/bpf.c @@ -47,21 +47,20 @@ static union all_addr del_addr; #if defined(HAVE_BSD_NETWORK) && !defined(__APPLE__) -static int arp_enumerate(void *parm, callback_t callback) +static int arp_enumerate_family(int family, void *parm, callback_t callback) { int mib[6]; size_t needed; char *next; struct rt_msghdr *rtm; - struct sockaddr_inarp *sin2; struct sockaddr_dl *sdl; - static struct iovec buff; + static struct iovec buff = { NULL, 0 }; int rc; mib[0] = CTL_NET; mib[1] = PF_ROUTE; mib[2] = 0; - mib[3] = AF_INET; + mib[3] = family; mib[4] = NET_RT_FLAGS; #ifdef RTF_LLINFO mib[5] = RTF_LLINFO; @@ -69,8 +68,8 @@ static int arp_enumerate(void *parm, callback_t callback) mib[5] = 0; #endif if (sysctl(mib, 6, NULL, &needed, NULL, 0) == -1 || needed == 0) - return 0; - + return 1; /* not a failure: unsupported or empty table */ + while (1) { if (!expand_buf(&buff, needed)) @@ -80,20 +79,50 @@ static int arp_enumerate(void *parm, callback_t callback) break; needed += needed / 8; } + if (rc == -1) return 0; for (next = buff.iov_base ; next < (char *)buff.iov_base + needed; next += rtm->rtm_msglen) { rtm = (struct rt_msghdr *)next; - sin2 = (struct sockaddr_inarp *)(rtm + 1); - sdl = (struct sockaddr_dl *)((char *)sin2 + SA_SIZE(sin2)); - if (!callback.af_unspec(AF_INET, &sin2->sin_addr, LLADDR(sdl), sdl->sdl_alen, parm)) - return 0; + if (family == AF_INET) + { + struct sockaddr_inarp *sin2 = (struct sockaddr_inarp *)(rtm + 1); + sdl = (struct sockaddr_dl *)((char *)sin2 + SA_SIZE(sin2)); + if (!callback.af_unspec(AF_INET, &sin2->sin_addr, + LLADDR(sdl), sdl->sdl_alen, parm)) + return 0; + } + else + { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)(rtm + 1); + sdl = (struct sockaddr_dl *)((char *)sin6 + SA_SIZE(sin6)); + if (IN6_IS_ADDR_LINKLOCAL(&sin6->sin6_addr)) + { + /* PF_ROUTE sysctl returns raw kernel structures with the interface + index embedded in bytes 2-3 of link-local addresses. Extract it + into sin6_scope_id per the KAME API contract before clearing. */ + sin6->sin6_scope_id = + ((uint32_t)(sin6->sin6_addr.s6_addr[2]) << 8) | sin6->sin6_addr.s6_addr[3]; + sin6->sin6_addr.s6_addr[2] = 0; + sin6->sin6_addr.s6_addr[3] = 0; + } + if (!callback.af_unspec(AF_INET6, &sin6->sin6_addr, + LLADDR(sdl), sdl->sdl_alen, parm)) + return 0; + } } - + return 1; } + +static int arp_enumerate(void *parm, callback_t callback) +{ + if (!arp_enumerate_family(AF_INET, parm, callback)) + return 0; + return arp_enumerate_family(AF_INET6, parm, callback); +} #endif /* defined(HAVE_BSD_NETWORK) && !defined(__APPLE__) */ From 8d0a92196c2d1a563b304a481962447d2d6f897e Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 10 May 2026 16:49:59 +0100 Subject: [PATCH 076/101] Fix OOB read when parsing DHCPv6 userclass or vendorclass options. Credit to Haiyang Huang for finding this. The fix extends the improved framework for DHCPv6 options added in b95d5a25777cc8910d2c2a921c68c778a3b30498 to the encapsulated vendor and user options. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index a65b32adb1..fecec7fe43 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -41,6 +41,7 @@ static void log6_packet(struct state *state, char *type, struct in6_addr *addr, static void log6_quiet(struct state *state, char *type, struct in6_addr *addr, char *string); static void *opt6_find (uint8_t *opts, uint8_t *end, unsigned int search, int minsize); static void *opt6_first(uint8_t *opt, uint8_t *end); +static void *opt6_user_vendor_first(uint8_t *opt, uint8_t *end); static unsigned int opt6_uint(unsigned char *opt, int offset, int size); static void get_context_tag(struct state *state, struct dhcp_context *context); static int check_ia(struct state *state, void *opt, void **endp, void **ia_option); @@ -65,9 +66,9 @@ static void calculate_times(struct dhcp_context *context, unsigned int *min_time #define opt6_next(opt, end) (opt6_first(opt6_ptr((opt), opt6_len((opt))), (end))) #define opt6_user_vendor_ptr(opt, i) ((void *)&(((uint8_t *)(opt))[2+(i)])) -#define opt6_user_vendor_len(opt) ((int)(opt6_uint(opt, -4, 2))) -#define opt6_user_vendor_next(opt, end) (opt6_next(((uint8_t *) opt) - 2, end)) - +#define opt6_user_vendor_len(opt) ((int)(opt6_uint((opt), -4, 2))) +#define opt6_user_vendor_next(opt, end) (opt6_user_vendor_first(opt6_user_vendor_ptr((opt), opt6_user_vendor_len((opt))), (end))) + unsigned short dhcp6_reply(struct dhcp_context *context, int multicast_dest, int interface, char *iface_name, struct in6_addr *fallback, struct in6_addr *ll_addr, struct in6_addr *ula_addr, @@ -414,7 +415,8 @@ static int dhcp6_no_relay(struct state *state, int msg_type, unsigned char *inbu } /* Note that format if user/vendor classes is different to DHCP options - no option types. */ - for (enc_opt = opt6_ptr(opt, offset); enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) + for (enc_opt = opt6_user_vendor_first(opt6_ptr(opt, offset), enc_end); + enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) for (i = 0; i <= (opt6_user_vendor_len(enc_opt) - vendor->len); i++) if (memcmp(vendor->data, opt6_user_vendor_ptr(enc_opt, i), vendor->len) == 0) { @@ -1927,7 +1929,8 @@ static void update_leases(struct state *state, struct dhcp_context *context, str lease_add_extradata(lease, (unsigned char *)daemon->dhcp_buff2, strlen(daemon->dhcp_buff2), 0); if (opt6_len(opt) >= 6) - for (enc_opt = opt6_ptr(opt, 4); enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) + for (enc_opt = opt6_user_vendor_first(opt6_ptr(opt, 4), enc_end); + enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) { lease->vendorclass_count++; lease_add_extradata(lease, opt6_user_vendor_ptr(enc_opt, 0), opt6_user_vendor_len(enc_opt), 0); @@ -1987,7 +1990,8 @@ static void update_leases(struct state *state, struct dhcp_context *context, str if ((opt = opt6_find(state->packet_options, state->end, OPTION6_USER_CLASS, 2))) { void *enc_opt, *enc_end = opt6_ptr(opt, opt6_len(opt)); - for (enc_opt = opt6_ptr(opt, 0); enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) + for (enc_opt = opt6_user_vendor_first(opt6_ptr(opt, 0), enc_end); + enc_opt; enc_opt = opt6_user_vendor_next(enc_opt, enc_end)) lease_add_extradata(lease, opt6_user_vendor_ptr(enc_opt, 0), opt6_user_vendor_len(enc_opt), 0); } } @@ -2124,7 +2128,23 @@ static void *opt6_first(uint8_t *opt, uint8_t *end) return opt; } - + +static void *opt6_user_vendor_first(uint8_t *opt, uint8_t *end) +{ + if (!opt) + return NULL; + + /* make sure we have length. */ + if ((uint8_t *)opt6_user_vendor_ptr(opt, 0) > end) + return NULL; + + /* make sure we have bytes promised by length. */ + if ((uint8_t *)opt6_user_vendor_ptr(opt, opt6_user_vendor_len(opt)) > end) + return NULL; + + return opt; +} + static unsigned int opt6_uint(unsigned char *opt, int offset, int size) { /* this worries about unaligned data and byte order */ From eb4090dc5f18cd367d187665f023741f2ead4780 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 10 May 2026 21:47:12 +0100 Subject: [PATCH 077/101] Fix OOB read/write of rr_status. Crafted packets can write zeros outside the limits of the rr_status array, causing a crash and DoS. Thanks to Haiyang Huang for finding and reprting this. Signed-off-by: Dominik --- src/dnsmasq/rfc1035.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/rfc1035.c b/src/dnsmasq/rfc1035.c index c7cac1bedd..2334710a5e 100644 --- a/src/dnsmasq/rfc1035.c +++ b/src/dnsmasq/rfc1035.c @@ -499,7 +499,7 @@ int do_doctor(struct dns_header *header, size_t qlen, char *namebuff) header->hb3 &= ~HB3_AA; #ifdef HAVE_DNSSEC /* remove validated flag from this RR, since we changed it! */ - if (option_bool(OPT_DNSSEC_VALID) && i < ntohs(header->ancount)) + if (option_bool(OPT_DNSSEC_VALID) && i < daemon->rr_status_sz && i < ntohs(header->ancount)) daemon->rr_status[i] = 0; #endif done = 1; @@ -611,7 +611,9 @@ static int find_soa(struct dns_header *header, size_t qlen, char *name, int *sub addr.rrblock.datalen += 20; #ifdef HAVE_DNSSEC - if (option_bool(OPT_DNSSEC_VALID) && daemon->rr_status[i + ntohs(header->ancount)] != 0) + if (option_bool(OPT_DNSSEC_VALID) && + i + ntohs(header->ancount) < daemon->rr_status_sz && + daemon->rr_status[i + ntohs(header->ancount)] != 0) { secflag = F_DNSSECOK; From 21672477db815834ea841ee914604c73d28e2a96 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 20:59:11 +0100 Subject: [PATCH 078/101] fix: zero correct byte count in expand_workspace_real() memset(p+old, 0, new-old) was missing the sizeof(unsigned char *) multiplier. p is unsigned char**, so each slot is pointer-sized (8 instead of 1 byte on 64-bit), yet only new-old bytes were zeroed, leaving newly allocated DNSSEC workspace pointer slots with garbage values. Signed-off-by: Dominik --- src/dnsmasq/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 8d74a7086a..48d19c15d6 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -1000,7 +1000,7 @@ int expand_workspace_real(const char *func, unsigned int line, unsigned char *** if (!(p = whine_realloc_real("expand_workspace", func, line, *wkspc, new * sizeof(unsigned char *)))) return 0; - memset(p+old, 0, new-old); + memset(p+old, 0, (new-old) * sizeof(unsigned char *)); *wkspc = p; *szp = new; From 9fabcd4e367bff070f2f13a01fd03ce7a21ca336 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 21:08:47 +0100 Subject: [PATCH 079/101] fix: move fd-match guard out of loop in reply_query() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `if (serv == last) return;` was inside the loop `for (serv = first; serv != last; ...)`, making it dead code — the loop condition ensures the body never runs when serv == last. Replies arriving on an unrecognised file descriptor were therefore never rejected by the fd check. Moving the guard after the loop causes it to fire when no server socket matches. Signed-off-by: Dominik --- src/dnsmasq/forward.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index 4dd4fc3414..ef19fe1012 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -1249,10 +1249,10 @@ void reply_query(int fd, time_t now) server = daemon->serverarray[serv]; if (server->sfd && server->sfd->fd == fd) break; - - if (serv == last) - return; } + + if (serv == last) + return; } /* spoof check: answer must come from known server, also From 8f035d108f7292411bd03067c83fba185fa8b9a9 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 21:26:11 +0100 Subject: [PATCH 080/101] fix: rand64() must share global outleft with rand16()/rand32() rand64() declared its own `static int outleft`, shadowing the global one while all three functions share the same out[] SURF output buffer. When rand64() called surf() to refill out[], the global outleft still indexed into the now-overwritten buffer, so subsequent rand32() or rand16() calls could return the same words rand64() had already consumed. Drop the local static so all three functions coordinate through the same counter. Security: rand64() is used to generate DHCPv6 temporary address interface IDs, which are visible on the local network. Because of the shared buffer issue, an observer could reconstruct the out[] words consumed by rand64() and predict the next rand32() outputs. rand32() drives the 0x20 case-scrambling bitmap used to harden DNS queries against cache-poisoning, so this leaks PRNG state across the DHCPv6 and DNS code paths. I'd still say the overall severity is rather low as local network access required, and 0x20 encoding is really a defence-in-depth and not some kind of essential security. Signed-off-by: Dominik --- src/dnsmasq/util.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 48d19c15d6..73ec721fd2 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -109,8 +109,6 @@ u32 rand32(void) u64 rand64(void) { - static int outleft = 0; - if (outleft < 2) { if (!++in[0]) if (!++in[1]) if (!++in[2]) ++in[3]; From a77d8f91041787432a2d73ea3281bc243bca3065 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 21:35:47 +0100 Subject: [PATCH 081/101] fix: NUL-terminate buf in prettyprint_time() when t == 0 When t == 0 every conditional branch is skipped and buf is never written, leaving it without a NUL terminator. The caller in dhcp-common.c log_opts() does `p += strlen(p)` immediately after, scanning uninitialised stack memory until it finds a stray zero byte. Reachable from the network: a local attacker can craft a DHCP packet with any time-typed option (e.g. option 51) set to 0x00000000, triggering this path via the option-logging code. Impact is limited to stack bytes appearing in syslog output (information disclosure), not reachable for code execution. Fix by zeroing buf[0] first so the buffer is always well-formed. This bug does not look like it could be a remotely triggerable DoS vector as strlen(buf) on an uninitialised heap buffer will almost certainly find a NUL byte in adjacent heap metadata before hitting an unmapped page. Signed-off-by: Dominik --- src/dnsmasq/util.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dnsmasq/util.c b/src/dnsmasq/util.c index 73ec721fd2..74c27cc0a8 100644 --- a/src/dnsmasq/util.c +++ b/src/dnsmasq/util.c @@ -583,6 +583,7 @@ void prettyprint_time(char *buf, unsigned int t) else { unsigned int x, p = 0; + buf[0] = '\0'; if ((x = t/86400)) p += sprintf(&buf[p], "%ud", x); if ((x = (t/3600)%24)) From e886e2475bb5b8549b253ec2b3f18f51f1a03609 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 21:45:11 +0100 Subject: [PATCH 082/101] helper: fix OOB read in grab_extradata_lua bounds check The loop in grab_extradata_lua checked *next != 0 in the loop condition before the body could test next == end, so when next reached end the byte *end was read out-of-bounds. The non-Lua sibling grab_extradata had the correct order (test next == end before dereferencing). Fix by adopting the same idiom. Security: a DHCP client can supply a vendor-class option whose content fills the extradata buffer with no trailing NUL, causing a 1-byte OOB read in the Lua helper child (already non-root). Requires HAVE_LUASCRIPT and a configured Lua script. Not feasible for triggering a crash from remote reliably. Signed-off-by: Dominik --- src/dnsmasq/helper.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/helper.c b/src/dnsmasq/helper.c index 729d5e19df..39fcb7207c 100644 --- a/src/dnsmasq/helper.c +++ b/src/dnsmasq/helper.c @@ -761,9 +761,11 @@ static unsigned char *grab_extradata_lua(unsigned char *buf, unsigned char *end, if (!buf || (buf == end)) return NULL; - for (next = buf; *next != 0; next++) + for (next = buf; ; next++) if (next == end) return NULL; + else if (*next == 0) + break; if (next != buf) { From 5af69629f551e1eff46a4c360b2a48bca2e11ebe Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 21:51:47 +0100 Subject: [PATCH 083/101] rfc2131: fix off-by-one in BOOTP filename netid NUL termination After copying the 128-byte DHCP file field into dhcp_buff2, the sentinel was written to index 129 instead of 128. The byte at index 128 was therefore left holding stale data from the previous DHCP exchange, so a client that fills the entire file field with non-NUL bytes would produce a 129-character netid tag whose last byte is heap garbage. In theory this allows incorrect netid-tag matching; in practice it requires the client to know the heap state of a prior transaction and BOOTP use is uncommon. Security: very-low-severity heap-content exposure that could influence DHCP option selection via tag mismatch; no memory corruption or crash. Signed-off-by: Dominik --- src/dnsmasq/rfc2131.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/rfc2131.c b/src/dnsmasq/rfc2131.c index bcaba821c2..a1d43f9445 100644 --- a/src/dnsmasq/rfc2131.c +++ b/src/dnsmasq/rfc2131.c @@ -597,7 +597,7 @@ size_t dhcp_reply(struct dhcp_context *context, char *iface_name, int int_index, if (mess->file[0]) { memcpy(daemon->dhcp_buff2, mess->file, sizeof(mess->file)); - daemon->dhcp_buff2[sizeof(mess->file) + 1] = 0; /* ensure zero term. */ + daemon->dhcp_buff2[sizeof(mess->file)] = 0; /* ensure zero term. */ id.net = (char *)daemon->dhcp_buff2; id.next = netid; netid = &id; From f50fe7a7f6abe85a56b6cd09e13287ef829d1f6e Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 21:57:11 +0100 Subject: [PATCH 084/101] dhcp-common: bounds-check label index before reading in OT_RFC1035_NAME decoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In option_string(), the RFC1035-name label decoder sets i = l (the position past the current label) and then reads val[i] to decide whether to append a '.' separator — but without first checking that i < opt_len. If the last label ends exactly at the option boundary (no NUL terminator), i == opt_len and val[i] reads one byte beyond the option data. The read lands inside the packet buffer so no crash occurs, but the out-of-bounds byte (typically the next DHCPv6 option's type field) could produce a spurious trailing '.' in --log-dhcp output when a client sends a domain-search/nis-domain option without a trailing NUL. Security: OOB read of 1 byte from a network-supplied DHCPv6 packet; reachable only with --log-dhcp enabled; no memory corruption, crash, or meaningful information disclosure. Signed-off-by: Dominik --- src/dnsmasq/dhcp-common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnsmasq/dhcp-common.c b/src/dnsmasq/dhcp-common.c index 0cab942862..795bc4e649 100644 --- a/src/dnsmasq/dhcp-common.c +++ b/src/dnsmasq/dhcp-common.c @@ -882,7 +882,7 @@ char *option_string(int prot, unsigned int opt, unsigned char *val, int opt_len, buf[j++] = c; } i = l; - if (val[i] != 0 && j < buf_len) + if (i < opt_len && val[i] != 0 && j < buf_len) buf[j++] = '.'; } } From f47243d08ab9b8208ae2670bf50091ca66e42c41 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 22:11:06 +0100 Subject: [PATCH 085/101] dhcp-common: fix OOB reads in DHCPv6 option_string() decoders Two decoders in option_string() read beyond the end of the supplied option-value buffer when a malformed DHCPv6 option is received: 1. OT_RFC1035_NAME: after advancing i past the last label (i = l), val[i] was read at line 885 to decide whether to append a '.'. If the label ends exactly at opt_len, i == opt_len and val[i] is one byte past the option data. Fixed by guarding with i < opt_len before the read. 2. OT_CSTRING: the loop opened with while(1) and called GETSHORT (a 2-byte read) before checking that two bytes remained, so a 1- or 0-byte option value caused an immediate 1-byte OOB read. Additionally, if the length field exceeded the remaining option data the inner body-copy loop read past the end. Fixed by changing the loop condition to while(i+2 <= opt_len) and adding a check that the body fits before copying. Both reads land inside the received packet buffer (no crash), but they allow a DHCPv6 client to cause dnsmasq to read unrelated packet bytes and emit them in syslog output. Security: OOB reads of up to option-length bytes from a network-supplied DHCPv6 packet; reachable only with --log-dhcp enabled; no memory corruption, crash, or sensitive data disclosure beyond adjacent packet bytes. Signed-off-by: Dominik --- src/dnsmasq/dhcp-common.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/dnsmasq/dhcp-common.c b/src/dnsmasq/dhcp-common.c index 795bc4e649..0dfbc62493 100644 --- a/src/dnsmasq/dhcp-common.c +++ b/src/dnsmasq/dhcp-common.c @@ -892,24 +892,23 @@ char *option_string(int prot, unsigned int opt, unsigned char *val, int opt_len, unsigned char *p; i = 0, j = 0; - while (1) + while (i + 2 <= opt_len) { p = &val[i]; GETSHORT(len, p); + if (i + 2 + len > opt_len) + break; /* malformed: body extends beyond option */ for (k = 0; k < len && j < buf_len; k++) { char c = *p++; if (isprint((unsigned char)c)) buf[j++] = c; } - i += len +2; - if (i >= opt_len) - break; - - if (j < buf_len) + i += len + 2; + if (i < opt_len && j < buf_len) buf[j++] = ','; } - } + } #endif else if ((ot[o].size & (OT_DEC | OT_TIME)) && opt_len != 0) { From abdfb23a004dcb84d1c2059f9f2de5aca8365352 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 22:33:02 +0100 Subject: [PATCH 086/101] rfc3315: fix integer underflow and heap overflow in log6_opts STATUS_CODE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In log6_opts(), the OPTION6_STATUS_CODE branch did: memcpy(daemon->namebuff + len, opt6_ptr(opt, 2), opt6_len(opt)-2); daemon->namebuff[len + opt6_len(opt) - 2] = 0; Two bugs: 1. Integer underflow: if opt6_len(opt) < 2, the subtraction wraps to a huge size_t, making memcpy write gigabytes and crash with SIGSEGV. 2. Heap overflow: no upper-bound check. opt6_len() can return up to 65535, but daemon->namebuff is only (MAXDNAME*2)+1 = 2051 bytes. A STATUS_CODE payload larger than ~2045 bytes would overflow the heap buffer. Exploitability: log6_opts() is only ever called on daemon->outpacket, the outgoing DHCPv6 reply built by dnsmasq itself — NOT on incoming client packets. Consequently, these bugs are NOT directly triggerable by a remote attacker under the current code. The fix is defensive: put_opt6_short() (which writes STATUS_CODE options in outgoing packets) always produces opt6_len >= 2, so neither bug fires in practice today. However, the unchecked arithmetic is a latent hazard that could become exploitable if future refactoring passes incoming option data through log6_opts(): - Guard the block with opt6_len(opt) >= 2 to prevent underflow. - Cap slen to the remaining space in daemon->namebuff. Signed-off-by: Dominik --- src/dnsmasq/rfc3315.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/dnsmasq/rfc3315.c b/src/dnsmasq/rfc3315.c index fecec7fe43..f0bff019bb 100644 --- a/src/dnsmasq/rfc3315.c +++ b/src/dnsmasq/rfc3315.c @@ -2043,9 +2043,15 @@ static void log6_opts(int nest, unsigned int xid, void *start_opts, void *end_op } else if (type == OPTION6_STATUS_CODE) { - int len = sprintf(daemon->namebuff, "%u ", opt6_uint(opt, 0, 2)); - memcpy(daemon->namebuff + len, opt6_ptr(opt, 2), opt6_len(opt)-2); - daemon->namebuff[len + opt6_len(opt) - 2] = 0; + if (opt6_len(opt) >= 2) + { + int slen = opt6_len(opt) - 2; + int len = sprintf(daemon->namebuff, "%u ", opt6_uint(opt, 0, 2)); + if (slen > (MAXDNAME * 2) - len - 1) + slen = (MAXDNAME * 2) - len - 1; + memcpy(daemon->namebuff + len, opt6_ptr(opt, 2), slen); + daemon->namebuff[len + slen] = 0; + } optname = "status"; } else From c7b2ae13fc60c8dc75a33ad8a22a69bb1c0c0450 Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 22:41:55 +0100 Subject: [PATCH 087/101] edns0: bounds-check option length before memcmp in check_source() The EDNS0 option iteration loop read a 2-byte len field from the packet and used it directly in: opt.scope_netmask = p[3]; // requires len >= 4 memcmp(p, &opt, len) // reads len bytes from packet Neither access was guarded against len exceeding the remaining RDATA. A crafted DNS reply whose OPT RDATA contains a CLIENT_SUBNET option with an oversized len field would cause memcmp to read past the end of the packet buffer, leaking heap memory. Risk: currently the only caller that reaches this branch is forward.c:process_reply(), which (before the companion fix) passed an OPT-RR-scoped plen that made skip_name() return NULL first, so the unsafe code was never reached. With that companion fix applied the path is live: a rogue upstream server or on-path attacker can craft a reply with a malformed CLIENT_SUBNET len and trigger the OOB read, potentially leaking adjacent heap contents. Fix: break out of the loop if the option body would extend past the RDATA boundary. Signed-off-by: Dominik --- src/dnsmasq/edns0.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dnsmasq/edns0.c b/src/dnsmasq/edns0.c index 04b5bcf4c1..e1621e85a7 100644 --- a/src/dnsmasq/edns0.c +++ b/src/dnsmasq/edns0.c @@ -469,6 +469,8 @@ int check_source(struct dns_header *header, size_t plen, unsigned char *pseudohe { GETSHORT(code, p); GETSHORT(len, p); + if (i + 4 + len > rdlen) + break; /* malformed: option body extends beyond RDATA */ if (code == EDNS0_OPTION_CLIENT_SUBNET) { if (peer) From db50e3fdffe39629c7e647800600f68c307f23df Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 23:11:46 +0100 Subject: [PATCH 088/101] lease: check parse_hex() return value before use as length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_hex() returns -1 on malformed input. Three call sites in read_leases() used the return value directly as a length argument without checking for the error sentinel: opt_len → lease_set_vendorclass() / lease_set_agent_id() whine_malloc(-1) attempts a ~4 GB allocation, likely crashing the process. hw_len → lease_set_hwaddr() memcpy(lease->hwaddr, hwaddr, (size_t)-1) performs an ~18 EB write, causing an immediate crash. clid_len → lease_set_hwaddr() (same path as hw_len) Exploitation requires a corrupted or attacker-controlled lease file; it is not directly network-triggerable. However, any process that can write to the lease file (e.g. a compromised helper) can crash or potentially gain code execution in the dnsmasq process at next startup or lease-file re-read. Fix: mirror the existing duid path (which already guards its parse_hex call) by checking for < 0. For opt_len, skip the record (continue) to match the "invalid line" behaviour. For hw_len and clid_len, treat the failure as a zero-length field so the lease is still created with the correct address. Co-Authored-By: Claude Sonnet 4.6 --- src/dnsmasq/lease.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dnsmasq/lease.c b/src/dnsmasq/lease.c index dd6babd3cd..9bf99e809e 100644 --- a/src/dnsmasq/lease.c +++ b/src/dnsmasq/lease.c @@ -73,7 +73,9 @@ static int read_leases(time_t now, FILE *leasestream) if (lease) { opt_len = parse_hex(daemon->packet, (unsigned char *)daemon->packet, 255, NULL, NULL); - + if (opt_len < 0) + continue; + if (strcmp(daemon->dhcp_buff3, "vendorclass") == 0) lease_set_vendorclass(lease, (unsigned char *)daemon->packet, opt_len); else if (strcmp(daemon->dhcp_buff3, "agent-info") == 0) @@ -99,6 +101,8 @@ static int read_leases(time_t now, FILE *leasestream) hw_len = parse_hex(daemon->dhcp_buff2, (unsigned char *)daemon->dhcp_buff2, DHCP_CHADDR_MAX, NULL, &hw_type); + if (hw_len < 0) + hw_len = 0; /* For backwards compatibility, no explicit MAC address type means ether. */ if (hw_type == 0 && hw_len != 0) hw_type = ARPHRD_ETHER; @@ -131,7 +135,11 @@ static int read_leases(time_t now, FILE *leasestream) die (_("too many stored leases"), NULL, EC_MISC); if (strcmp(daemon->packet, "*") != 0) - clid_len = parse_hex(daemon->packet, (unsigned char *)daemon->packet, 255, NULL, NULL); + { + clid_len = parse_hex(daemon->packet, (unsigned char *)daemon->packet, 255, NULL, NULL); + if (clid_len < 0) + clid_len = 0; + } lease_set_hwaddr(lease, (unsigned char *)daemon->dhcp_buff2, (unsigned char *)daemon->packet, hw_len, hw_type, clid_len, now, 0); From 922e45079f42b30fdf084a8060c96d8a55ff4c0c Mon Sep 17 00:00:00 2001 From: Dominik Date: Thu, 19 Mar 2026 23:41:22 +0100 Subject: [PATCH 089/101] domain: fix strncat buffer-size argument in is_rev_synth() strncat(dst, src, n) truncates src to n bytes, but n should be the remaining space in dst, not the total buffer size. Passing MAXDNAME as n allowed the combined prefix + address + "." + domain to overflow the MAXDNAME-byte namebuff if c->domain was close to MAXDNAME chars. Replace MAXDNAME with MAXDNAME - strlen(name) - 1 at each call site (lines 178-179 for IPv4, 204 for the IPv6 hex-fragment loop, 208-209 for the IPv6 suffix) so the append is always bounded by actual remaining capacity. Security implications: strncat(name, c->domain, MAXDNAME) passes the full buffer size instead of remaining space. Overflow requires a local admin to configure a --synth-domain with a domain name > ~960 chars. The actual exploitable window is narrow: the admin would need a --synth-domain with a domain name close to 1024 chars (which check_name permits) combined with a non-trivial prefix or IPv6 address. Not remotely exploitable. Signed-off-by: Dominik --- src/dnsmasq/domain.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dnsmasq/domain.c b/src/dnsmasq/domain.c index 9647a85e2f..955dbc7be5 100644 --- a/src/dnsmasq/domain.c +++ b/src/dnsmasq/domain.c @@ -175,8 +175,8 @@ int is_rev_synth(int flag, union all_addr *addr, char *name) *p = '-'; } - strncat(name, ".", MAXDNAME); - strncat(name, c->domain, MAXDNAME); + strncat(name, ".", MAXDNAME - strlen(name) - 1); + strncat(name, c->domain, MAXDNAME - strlen(name) - 1); return 1; } @@ -201,16 +201,16 @@ int is_rev_synth(int flag, union all_addr *addr, char *name) for (i = 0; i < 16; i += 2) { sprintf(frag, "%s%02x%02x", i == 0 ? "" : "-", addr->addr6.s6_addr[i], addr->addr6.s6_addr[i+1]); - strncat(name, frag, MAXDNAME); + strncat(name, frag, MAXDNAME - strlen(name) - 1); } } - strncat(name, ".", MAXDNAME); - strncat(name, c->domain, MAXDNAME); - + strncat(name, ".", MAXDNAME - strlen(name) - 1); + strncat(name, c->domain, MAXDNAME - strlen(name) - 1); + return 1; } - + return 0; } From b8603b34e96446adad64c765fd77c6c52071c05a Mon Sep 17 00:00:00 2001 From: Dominik Date: Fri, 20 Mar 2026 16:21:00 +0100 Subject: [PATCH 090/101] dump: check lseek() return in pcap record-counting loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When --dump-file points to an existing pcap file, dump_init_file() counts the existing records by reading each pcap record header and seeking past the payload. The lseek() return value was not checked: if it fails (e.g. EOVERFLOW on a 32-bit build with a very large incl_len, or any other seek error) the file position does not advance, the next read_write() call reads the same header again, and the loop spins forever — hanging daemon startup. Fix: break out of the loop on lseek failure. An undercount of existing records is harmless; a hanging startup is not. Signed-off-by: Dominik --- src/dnsmasq/dump.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dnsmasq/dump.c b/src/dnsmasq/dump.c index 55ab43dbe9..b2dfa9e701 100644 --- a/src/dnsmasq/dump.c +++ b/src/dnsmasq/dump.c @@ -85,7 +85,8 @@ void dump_init(void) /* count existing records */ while (read_write(daemon->dumpfd, (void *)&pcap_header, sizeof(pcap_header), RW_READ)) { - lseek(daemon->dumpfd, pcap_header.incl_len, SEEK_CUR); + if (lseek(daemon->dumpfd, pcap_header.incl_len, SEEK_CUR) == (off_t)-1) + break; packet_count++; } } From 37de5e9ea064d56456ccf23c93b54e5e61ec8746 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 12 Feb 2026 16:23:42 +0000 Subject: [PATCH 091/101] Rework storage allocation for domain names. CVE-2026-2291 This is the stable release fix for CVE-2026-2291 Dnsmasq recieves and sends domain names within DNS packets in RFC-1035 format, a string of counted labels terminated by a zero length label. Internally to dnsmasq, domain names are represented as zero-terminated C strings with the labels seperated by '.' characters, like the normal presentation format for domain names. The RFC-1035 limit for a domain name in wire format is 255 octets, and when converted to presentation format, this makes a string of maximum 253 characters. However dnsmasq needs to be able to represent any 8-bit character within a label, and this causes problems with /000 (which would terminate the c-string early) and '.' (which would terminate a label early). To avoid this, dnsmasq escapes both of these characters, which means that they take two bytes in the string. The domain name with must charaters in it has four labels and the 255 octet wire length limits these four labels to a total of 250 characters. The other five octets are the four label length bytes and the zero length byte terminator. If all 250 characters need escaping that makes the maximum length of the dnsmasq internal format 503 characters (250 escaped characters in labels, amd three dots between four labels) Since this format is zero-terminated c-string, buffers have to be allocated as 504 bytes. This arrangement grew in a somewhat ad-hoc manner and early dnsmasq didn't do the escaping trick, and didn't really differentiate between the lengths of the wire format and the presentation format, since they were very similar. It also, for reasons lost in time, inherited a defintion for MAXDNAME from early BIND headers, set at 1025 bytes. This value was used as the buffer size for both wire and presentation formats. This patch makes everything consistent. It declares two constants MAXDNAME 255 /* max size of wire format domain name */ MAXDNAMESTR 503 /* max size of internal format created from a 255 octet wire format name */ All internal name buffers are allocated as MAXDNAMESTR+1 bytes, to hold a maximum size internal format name and zero termination. Names parsed out of incoming packets are rejected if their wire format is greater than 255 bytes, which ensures that their internal representation cannot exceed 503 characters. Names inserted into outgoing packets are failed similarly if they exceed 255 bytes. (this would in theory be possible for a legal internal-representaion name if it has at least one unescaped character in a label. --- src/dnsmasq/cache.c | 8 ++++++-- src/dnsmasq/config.h | 2 +- src/dnsmasq/dbus.c | 2 +- src/dnsmasq/dhcp-common.c | 4 ++-- src/dnsmasq/dhcp.c | 2 +- src/dnsmasq/dns-protocol.h | 32 ++++++++++++++++++++++++++++++-- src/dnsmasq/dnsmasq.c | 9 ++------- src/dnsmasq/dnsmasq.h | 8 ++++---- src/dnsmasq/dnssec.c | 2 +- src/dnsmasq/domain.c | 18 +++++++++--------- src/dnsmasq/helper.c | 2 +- src/dnsmasq/lease.c | 2 +- src/dnsmasq/network.c | 4 ++-- src/dnsmasq/option.c | 22 +++++++++------------- src/dnsmasq/radv.c | 4 ++-- src/dnsmasq/rfc1035.c | 19 ++++++++++++------- src/dnsmasq/rfc2131.c | 6 +++--- src/dnsmasq/rfc3315.c | 6 +++--- src/dnsmasq/rrfilter.c | 26 ++++++++++---------------- src/dnsmasq/tftp.c | 14 +++++++------- src/dnsmasq/util.c | 24 ++++++++++++++++++------ 21 files changed, 125 insertions(+), 91 deletions(-) diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 305e26cad8..d4273227a0 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -24,6 +24,7 @@ static struct crec *new_chain = NULL; static int insert_error; static union bigname *big_free = NULL; static int bignames_left, hash_size; + struct nameblock { struct nameblock *next; unsigned int last, index; @@ -238,6 +239,9 @@ static char *store_name(unsigned int namelen, unsigned int index) struct nameblock *block; char *ret = NULL; + if (namelen > NAMEBLOCK_CHARS) + return NULL; + for (block = hostblocks; block; block = block->next) if (block->index == index && NAMEBLOCK_CHARS - block->last >= namelen) break; @@ -1435,7 +1439,7 @@ static int gettok(FILE *f, char *token) return eatspace(f); } - if (count < (MAXDNAME - 1)) + if (count < (MAXDNAMESTR - 1)) { token[count++] = c; token[count] = 0; @@ -1810,7 +1814,7 @@ void cache_add_dhcp_entry(char *host_name, int prot, /* Name in hosts, address doesn't match */ if (fail_crec) { - inet_ntop(prot, &fail_crec->addr, daemon->namebuff, MAXDNAME); + inet_ntop(prot, &fail_crec->addr, daemon->namebuff, MAXDNAMESTR); my_syslog(MS_DHCP | LOG_WARNING, _("not giving name %s to the DHCP lease of %s because " "the name exists in %s with address %s"), diff --git a/src/dnsmasq/config.h b/src/dnsmasq/config.h index a18308d44e..79fa2ec1d2 100644 --- a/src/dnsmasq/config.h +++ b/src/dnsmasq/config.h @@ -43,7 +43,7 @@ #define PING_CACHE_TIME 30 /* Ping test assumed to be valid this long. */ #define DECLINE_BACKOFF 600 /* disable DECLINEd static addresses for this long */ #define DHCP_PACKET_MAX 16384 /* hard limit on DHCP packet size */ -#define SMALLDNAME 50 /* most domain names are smaller than this */ +#define SMALLDNAME 75 /* most domain names are smaller than this */ #define CNAME_CHAIN 10 /* chains longer than this atr dropped for loop protection */ #define DNSSEC_MIN_TTL 60 /* DNSKEY and DS records in cache last at least this long */ #define HOSTSFILE "/etc/hosts" diff --git a/src/dnsmasq/dbus.c b/src/dnsmasq/dbus.c index afdadf2055..aa59eb82e4 100644 --- a/src/dnsmasq/dbus.c +++ b/src/dnsmasq/dbus.c @@ -736,7 +736,7 @@ static void add_dict_entry(DBusMessageIter *container, const char *key, const ch static void add_dict_int(DBusMessageIter *container, const char *key, const unsigned int val) { - snprintf(daemon->namebuff, MAXDNAME, "%u", val); + snprintf(daemon->namebuff, MAXDNAMESTR, "%u", val); add_dict_entry(container, key, daemon->namebuff); } diff --git a/src/dnsmasq/dhcp-common.c b/src/dnsmasq/dhcp-common.c index 0dfbc62493..7e794e8211 100644 --- a/src/dnsmasq/dhcp-common.c +++ b/src/dnsmasq/dhcp-common.c @@ -278,9 +278,9 @@ void log_tags(struct dhcp_netid *netid, u32 xid) if (!n) { - strncat (s, netid->net, (MAXDNAME-1) - strlen(s)); + strncat (s, netid->net, MAXDNAMESTR - strlen(s)); if (netid->next) - strncat (s, ", ", (MAXDNAME-1) - strlen(s)); + strncat (s, ", ", MAXDNAMESTR - strlen(s)); } } my_syslog(MS_DHCP | LOG_INFO, _("%u tags: %s"), xid, s); diff --git a/src/dnsmasq/dhcp.c b/src/dnsmasq/dhcp.c index e758c96a5f..cb87a301a6 100644 --- a/src/dnsmasq/dhcp.c +++ b/src/dnsmasq/dhcp.c @@ -958,7 +958,7 @@ void dhcp_read_ethers(void) up = &config->next; } - while (fgets(buff, MAXDNAME, f)) + while (fgets(buff, MAXDNAMESTR, f)) { char *host = NULL; diff --git a/src/dnsmasq/dns-protocol.h b/src/dnsmasq/dns-protocol.h index 9ef4083f81..43dddcfa03 100644 --- a/src/dnsmasq/dns-protocol.h +++ b/src/dnsmasq/dns-protocol.h @@ -22,8 +22,34 @@ #define IN6ADDRSZ 16 #define INADDRSZ 4 +/* Dnsmasq handles domains names internally as NULL-terminated C strings. + The '.' characters in these strings are significant as label-seperators. + '.' and /0 characters _within_ labels are escaped and take up two + characters to allow labels to contain arbitrary characters. + + The maximum length of a domain name in wire format is 255 bytes. + and the maximum length of this when coverted to a