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>
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>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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.
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.
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.
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).
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.
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.
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'
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><message>
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.
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.
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.
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.
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.
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.
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.
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.
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.