<feed xmlns='http://www.w3.org/2005/Atom'>
<title>odhcpd, branch master</title>
<subtitle>OpenWrt DHCP Server</subtitle>
<id>https://git.openwrt.org/project/odhcpd/atom?h=master</id>
<link rel='self' href='https://git.openwrt.org/project/odhcpd/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/'/>
<updated>2026-06-03T23:05:46Z</updated>
<entry>
<title>dhcpv6-ia: signal buffer-full from build_ia() IA_ADDR branch</title>
<updated>2026-06-03T23:05:46Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-30T22:25:16Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=1782f3f2aad21cd7971798c898355a12a9611304'/>
<id>urn:sha1:1782f3f2aad21cd7971798c898355a12a9611304</id>
<content type='text'>
In the CONFIRM/RELEASE/DECLINE echo path of build_ia(), the IA_PREFIX branch
returns 0 (the buffer-full failure signal) when the next IA_PREFIX option will
not fit, but the sibling IA_ADDR branch used "continue" instead. That kept the
option loop running and then fell through to writing the IA header length and
returning a non-zero ia_len, so the caller treated the truncated IA -- now
missing the IA_ADDR(s) that did not fit -- as a successfully built option and
put it on the wire. Return 0 from the IA_ADDR branch as well so a full buffer
is reported consistently and a malformed IA is never emitted.

Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>dhcpv6: validate rewritten RELAY_MSG length in update_nested_message()</title>
<updated>2026-06-03T23:05:41Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-30T22:12:10Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=5b1e3befb0b23f7b7506fb6209aed3aa9e04a6f7'/>
<id>urn:sha1:5b1e3befb0b23f7b7506fb6209aed3aa9e04a6f7</id>
<content type='text'>
When building a relay-reply, update_nested_message() walks the nested
RELAY-FORW envelope of the (untrusted) request and adjusts every
OPTION_RELAY_MSG length by pdiff, the signed difference between the assembled
reply options and the original request options. It did this with

	olen += pdiff;                                  /* olen is uint16_t */
	odata[-2] = olen &gt;&gt; 8; odata[-1] = olen;
	update_nested_message(odata, olen - pdiff, ...);

If "olen + pdiff" leaves the 0..65535 range, the uint16_t wraps: the wrapped
value is written into the on-wire RELAY_MSG length field, and "olen - pdiff"
no longer recovers the original (iterator-validated) inner length. The bogus
length is then passed as the recursion's buffer size, so the inner option
walk uses "odata + bogus_len" as its end bound and reads option headers far
past the end of the receive buffer (an out-of-bounds read, plus a 2-byte
out-of-bounds write if a RELAY_MSG pattern is hit) driven by a crafted
multi-level Relay-Forward packet.

Compute the new length in a signed wide type, bail out if it would not fit in
the 16-bit field, and pass the already-validated original olen to the
recursion instead of reconstructing it from the mutated value.

Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>dhcpv4: pass &amp;dest.sin_addr to inet_ntop() when logging</title>
<updated>2026-06-03T23:05:36Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-30T22:12:08Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=c2f94ee5c8e7bc0ed60a0895e717e1fe09799bb0'/>
<id>urn:sha1:c2f94ee5c8e7bc0ed60a0895e717e1fe09799bb0</id>
<content type='text'>
dhcpv4_fr_send() passed &amp;dest (a struct sockaddr_in) to
inet_ntop(AF_INET, ...), which expects a pointer to a 4-byte struct in_addr.
inet_ntop therefore formatted the first four bytes of the sockaddr
(sin_family and half of sin_port) instead of the target IPv4 address, so the
FORCERENEW send / send-failure log lines printed a meaningless address. Point
inet_ntop at dest.sin_addr, like the correct call site elsewhere in the file.

Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>router: keep RA DNS option lengths within the uint8 length field</title>
<updated>2026-06-03T23:05:32Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-30T21:43:36Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=60f488192d5d32023455d51e37ea9f11819e417a'/>
<id>urn:sha1:60f488192d5d32023455d51e37ea9f11819e417a</id>
<content type='text'>
The RDNSS and DNSSL option length fields (RFC 8106) are uint8_t values
counting 8-byte units, but send_router_advert() computed them from the
configured DNS server count / search-list size without any bound. With 128 or
more configured DNS servers, "1 + 2*dns_addrs6_cnt" exceeds 255 and the len
field wrapped; likewise a search list larger than UINT8_MAX*8 bytes wrapped
search_sz/8. In both cases the actual bytes written (sized via dns_sz /
search_sz and the iovec length) used the full untruncated size, so the
emitted option's Length no longer matched its real size and clients would
mis-parse or drop the whole RA.

Cap the RDNSS server count at 127 (1 + 2*127 == 255) and skip the DNSSL
option entirely when it would not fit in a valid uint8 length, matching the
existing UINT8_MAX guard on the DHCPv4 search-list path.

Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>router: restore upstream DNS/MTU between relayed RAs</title>
<updated>2026-06-03T23:05:27Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-30T21:43:21Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=d119e1abc35f61a5fe96a03bbb85811ceabca2e9'/>
<id>urn:sha1:d119e1abc35f61a5fe96a03bbb85811ceabca2e9</id>
<content type='text'>
forward_router_advertisement() forwards a single, shared packet buffer to
every slave interface in turn. The source MAC, the PIO flag bytes and the
RA M/O flags are reset to their upstream state at the top of each iteration,
but the RDNSS addresses (rewritten in place when a slave has
always_rewrite_dns) and the MTU option value (rewritten when a slave has
ra_mtu) were not. Once one slave rewrote them, every subsequent slave in the
same loop was sent the previous slave's values instead of the original
upstream ones -- so a slave without always_rewrite_dns/ra_mtu could leak
another interface's DNS servers or MTU to its clients.

Snapshot the upstream DNS addresses before the loop and restore both the DNS
addresses and the MTU value at the start of each iteration, mirroring how the
PIO/RA flags are already restored.

Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>config: bound DNR 'mandatory' SvcParam key count</title>
<updated>2026-06-03T23:05:22Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-30T21:29:16Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=461043731085761bc067e875a31da569c321f9cd'/>
<id>urn:sha1:461043731085761bc067e875a31da569c321f9cd</id>
<content type='text'>
The mandatory SvcParam parser collects the referenced SvcParamKeys into a
fixed-size stack array mkeys[DNR_SVC_MAX] but only rejected unknown keys and
keys not otherwise present in the entry -- it never bounded the number of
written entries. RFC 9460 requires each key to appear at most once in the
mandatory list, but the parser accepted duplicates, so a config string such
as "mandatory=alpn,alpn,alpn,..." (with a present key repeated more than
DNR_SVC_MAX times) writes past the end of mkeys, smashing the stack.

Reject the entry once DNR_SVC_MAX keys have been recorded.

Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>dhcpv6-ia: validate IA_NA/IA_PD option size before reading header</title>
<updated>2026-06-03T23:05:17Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-23T12:50:42Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=f2275efc72a87a95f971f159babcbadc5d241d42'/>
<id>urn:sha1:f2275efc72a87a95f971f159babcbadc5d241d42</id>
<content type='text'>
dhcpv6_ia_handle_IAs() casts the option payload to dhcpv6_ia_hdr and
reads ia-&gt;iaid (followed later by t1/t2 via build_ia()) without ever
checking that the option is long enough. The dhcpv6_for_each_option
macro only guarantees odata + olen &lt;= end, so a client sending an
IA_NA or IA_PD option with olen &lt; 12 lets us read the iaid/t1/t2
fields out of the next adjacent option (still inside the recv buffer,
so it's not exploitable for code execution, but it pollutes lookups
and breaks lease matching).

RFC8415 §21.4 / §21.21 fix the IA header at exactly 12 bytes plus
sub-options, so reject anything shorter.

Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>dhcpv6: validate length and alignment when reading CLIENT_ARCH option</title>
<updated>2026-06-03T23:05:12Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-23T12:49:22Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=f3fdcf4d3fe2ad4dc6f906aaef0aee485e1e9c2c'/>
<id>urn:sha1:f3fdcf4d3fe2ad4dc6f906aaef0aee485e1e9c2c</id>
<content type='text'>
The Client System Architecture Type option (RFC5970) carries one or
more 16-bit architecture codes. The handler read the first code via
((uint16_t *)odata)[0], which has two problems:

  - if a client sends OPTION_CLIENT_ARCH with olen &lt; 2 (a malformed
    but otherwise validly framed option), this reads two bytes from
    odata where only olen are guaranteed to be in bounds, drifting
    into the next option header,
  - odata is at an arbitrary offset within the wire packet, so the
    uint16_t * cast crashes with SIGBUS on strict-alignment targets.

Require olen to cover the first entry and copy it out via memcpy().

Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>statefiles: clean up tmpfile error path</title>
<updated>2026-06-03T23:05:08Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-23T12:46:56Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=78aa06a30da8cf6ed86383bcd165b0811615d751'/>
<id>urn:sha1:78aa06a30da8cf6ed86383bcd165b0811615d751</id>
<content type='text'>
Two small issues in statefiles_open_tmp_file():
- On openat() failure the err: label still called close(fd) on the
  -1 fd, which sets errno to EBADF. The subsequent error("...: %m")
  then printed "Bad file descriptor" instead of the real openat()
  error.
- On lockf() failure we jumped to err: (not err_del:), so the tmpfile
  was successfully created but never unlinked, leaving stale files in
  the state directory across daemon restarts.

Restructure so the openat() failure reports immediately and the
shared cleanup path (lockf / ftruncate / fdopen failures) always
unlinks the tmpfile and closes the fd, logging before either of
those clobbers errno.

Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>dhcpv6-pxe: free previous default entry on replacement</title>
<updated>2026-06-03T23:05:03Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-23T12:41:19Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/odhcpd/commit/?id=3e1dd3b16c9a9f83a85bc1a889ac636f2528d043'/>
<id>urn:sha1:3e1dd3b16c9a9f83a85bc1a889ac636f2528d043</id>
<content type='text'>
ipv6_pxe_entry_new() stores arch == 0xFFFFFFFF (the "no-arch / default"
case) in the standalone ipv6_pxe_default pointer instead of on the
list. When the function is called with that arch more than once
between ipv6_pxe_clear() runs — e.g. a UCI config with two boot6
sections that omit the arch field — the previous default was simply
overwritten and leaked.

Free the prior default before replacing it.

Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
</feed>
