Commit Graph
2424 Commits
Author SHA1 Message Date
Simon Kelley 014e909f78 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.
2026-05-10 21:49:39 +01:00
Simon Kelley 29f8712f02 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.
2026-05-10 21:47:12 +01:00
Simon Kelley 0450f9e28a 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
b95d5a2577
to the encapsulated vendor and user options.
2026-05-10 16:49:59 +01:00
Sagie D. 0637bcf010 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.
2026-05-09 21:44:38 +01:00
Simon Kelley 4bcc9650fa 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.
2026-05-04 16:18:33 +01:00
Florian MargaineandClaude Opus 4.6 fc2286805f Preserve existing log file permissions when adding group-write bit.
Commit 1f8f78a49b ("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 <noreply@anthropic.com>
v2.93test10
2026-04-21 22:41:10 +01:00
Simon Kelley d158aeca75 Increase default limit on TCP handler processes from 20 to 30.
There's evidence that modern Chrome may need this. It has been 20
forever, and hardware inflation would support an increase.

https://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/2026q2/018450.html
2026-04-21 22:27:07 +01:00
Simon Kelley 36d081e374 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.
2026-04-21 22:14:41 +01:00
Toliak Purple 9eb0757da9 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/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:488
    #1 0x5563361feeb3 in iface_allowed /opt/dnsmasq/src/network.c:361
    #2 0x556336203aca in iface_allowed_v6 /opt/dnsmasq/src/network.c:622
    #3 0x55633625cc4c in iface_enumerate /opt/dnsmasq/src/netlink.c:291
    #4 0x55633620573c in enumerate_interfaces /opt/dnsmasq/src/network.c:836
    #5 0x55633615c3fe in main /opt/dnsmasq/src/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
```
2026-04-21 21:49:35 +01:00
Simon Kelley 2d0e0c7a54 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.
2026-04-10 13:35:27 +01:00
Simon Kelley cfc2244c2a Bump copyrights to 2026. 2026-04-08 12:08:18 +01:00
Simon Kelley 9ad74926d4 Fix 1-byte buffer overflow in relay_reply4()
Potential SIGSEGV when using DHCPv4-relay.

Thanks to Asim Viladi Oglu Manizada for finding this.
v2.93test9
2026-04-06 22:22:43 +01:00
Simon Kelley a6069da373 Check DNS query via TCP doesn't have QR bit set. 2026-04-05 22:50:43 +01:00
Simon Kelley 14f53171ca Sanity checking on DNS replies via TCP. 2026-04-05 22:25:56 +01:00
Simon Kelley 8fd06d2cc0 OOB buffer check with DNS bitstring labels. 2026-03-30 21:19:26 +01:00
Simon Kelley d152e81e42 Tighten check on TFTP pathnames to avoid directory escape. 2026-03-30 21:18:11 +01:00
Simon Kelley e0bec3342b 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.
2026-03-30 20:23:56 +01:00
Dominik dd707937d5 dump: check lseek() return in pcap record-counting loop
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 <dl6er@dl6er.de>
2026-03-30 20:15:44 +01:00
Dominik c5f46412a3 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 <dl6er@dl6er.de>
2026-03-30 20:06:43 +01:00
DominikandClaude Sonnet 4.6 cd04c0ce20 lease: check parse_hex() return value before use as length
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 <noreply@anthropic.com>
2026-03-30 20:06:34 +01:00
Dominik 0630a45ae1 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 <dl6er@dl6er.de>
2026-03-30 18:24:11 +01:00
Dominik 3ea905b98b rfc3315: fix integer underflow and heap overflow in log6_opts STATUS_CODE
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 <dl6er@dl6er.de>
2026-03-30 18:13:01 +01:00
Dominik ca509cb611 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 <dl6er@dl6er.de>
2026-03-30 18:09:43 +01:00
Dominik 8ceb8ee9b5 dhcp-common: bounds-check label index before reading in OT_RFC1035_NAME decoder
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 <dl6er@dl6er.de>
2026-03-30 18:00:22 +01:00
Dominik 1c4a97edb5 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 <dl6er@dl6er.de>
2026-03-30 17:59:50 +01:00
Dominik 9e3e0465cd 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 <dl6er@dl6er.de>
2026-03-30 17:57:44 +01:00
Dominik b12a9249f6 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 <dl6er@dl6er.de>
2026-03-30 17:34:56 +01:00
Dominik 9dec84ca42 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 <dl6er@dl6er.de>
2026-03-30 17:33:08 +01:00
Dominik 68c149e9dd fix: move fd-match guard out of loop in reply_query()
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 <dl6er@dl6er.de>
2026-03-30 17:32:09 +01:00
Dominik 5ea054b264 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 <dl6er@dl6er.de>
2026-03-30 17:31:20 +01:00
Simon Kelley b95d5a2577 Further tidying of DHCPv6 packet dissection code.
Make things more logical, and put buffer overflow detection in one
new function, opt6_first().
v2.93test8
2026-03-25 12:11:54 +00:00
Simon Kelley 5be5dc1f16 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.
2026-03-24 12:35:46 +00:00
Simon Kelley 48348a2d77 Fix broken DHCPv6 userclass data in DHCP script.
Exactly the same principle as 53313014b5
2026-03-17 13:40:10 +00:00
Simon Kelley b4a0663654 Accept DHCPv6 vendorclasses with any enterprise number in --dhcp-vendorclass
if not enterprise number is specified. Also accept and match on
enterprise number only.
2026-03-17 13:29:49 +00:00
Simon Kelley 53313014b5 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.
2026-03-17 12:09:36 +00:00
Simon Kelley cf08eeee12 Complete 32a54fc8a5 v2.93test7 2026-03-15 16:03:29 +00:00
Simon Kelley 0597828b74 Tighten length checking to avoid possible buffer read-overrun in DHCPv6.
Thanks to Dejan Alvadzijevic for spotting the problem.
2026-03-15 16:01:58 +00:00
Simon Kelley c8168e888e Don't call qsort() with a NULL array when no servers defined. 2026-03-10 21:17:41 +00:00
Simon Kelley 3a052d55fb Fix regression handling non-QUERY requests over TCP.
Regression introduced in 729c16a8ac
2026-03-08 22:19:30 +00:00
Simon Kelley 1eab169173 Fix missed hash->digest calls in 4070a74862 2026-03-01 12:33:38 +00:00
Simon Kelley 4070a74862 Fix FTBFS with nettle 4.0.
Thanks to Andreas Metzler for the heads-up.
2026-03-01 12:19:11 +00:00
Simon Kelley 65adc53130 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.
v2.93test6
2026-02-28 21:30:57 +00:00
Simon Kelley 66d9658f5f 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.
v2.93test5
2026-02-26 15:10:41 +00:00
Thomas Erbesdobler ca30c82647 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.
2026-02-26 13:30:44 +00:00
Simon Kelley 32a54fc8a5 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.
2026-02-16 22:20:05 +00:00
Simon Kelley 3f2bfca3df Log memory allocated by libidn when --log-malloc active. 2026-02-13 13:15:07 +00:00
Simon Kelley e4d0bb4579 Convert hash_init() to use realloc(). v2.93test4 2026-02-06 15:05:59 +00:00
Clayton O'Neill 84415a87be Fix PXE boot server (PXEBS) responses broken in 2.92
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).
2026-02-05 15:38:27 +00:00
Simon Kelley 1537645e8f treat opt_malloc as a wrapper for logging purposes. 2026-02-05 14:53:10 +00:00
Simon Kelley ceab392c60 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.
2026-02-05 13:26:08 +00:00