summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHauke Mehrtens2026-05-23 12:06:55 +0000
committerHauke Mehrtens2026-06-18 00:34:59 +0000
commitf6571a42f24c7151e99ece24afe97d718ec96d07 (patch)
tree41b1c3f55b2554a7c3b8cc404fabd20d81900f3f
parent6907f28f9ddeef9f69f5ee10bbc9ba8adc0ef57f (diff)
downloadodhcpd-f6571a42f24c7151e99ece24afe97d718ec96d07.tar.gz
dhcpv6: bound nested-relay recursion to HOP_COUNT_LIMIT
handle_nested_message() and update_nested_message() recurse once for every OPTION_RELAY_MSG inside a Relay-Forward message, with no depth limit. A crafted UDP datagram packed full of nested Relay-Forward options can drive recursion ~1700 deep on a ~64KB packet and exhaust the daemon's stack. Each nested Relay-Forward layer is exactly one relay hop: a relay agent wraps the message it received in a new Relay-Forward and increments hop_count. So the recursion depth here is just the relay-chain length seen structurally. RFC8415 bounds that chain via HOP_COUNT_LIMIT (defined in §7.6; a relay discards an over-limit Relay-forward per §19.1.2), so a conforming message can never nest deeper than HOP_COUNT_LIMIT layers. Cap the descent at DHCPV6_HOP_COUNT_LIMIT accordingly. We count the actual recursion depth instead of trusting each header's on-wire hop_count, because an attacker controls the nesting and every per-layer hop_count independently (e.g. nest deeply while stamping hop_count=0 everywhere). This is the same limit relay_client_request() already enforces on the outer header of a forwarded message. Note: DHCPV6_HOP_COUNT_LIMIT is 32, the value from the obsoleted RFC3315 §5.5; RFC8415 §7.6 lowered the default to 8. Either way 32 is a safe structural ceiling that drops only pathologically nested messages, never a legitimate relay chain. Assisted-by: Claude:claude-opus-4-7 Link: https://github.com/openwrt/odhcpd/pull/401 Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> (cherry picked from commit ee0a11f40fa48302482fd6e85458e26d48de6e3a)
-rw-r--r--src/dhcpv6.c23
1 files changed, 17 insertions, 6 deletions
diff --git a/src/dhcpv6.c b/src/dhcpv6.c
index 868740f..b4fea34 100644
--- a/src/dhcpv6.c
+++ b/src/dhcpv6.c
@@ -191,7 +191,7 @@ enum {
IOV_TOTAL
};
-static void handle_nested_message(uint8_t *data, size_t len,
+static void handle_nested_message(uint8_t *data, size_t len, unsigned depth,
struct dhcpv6_client_header **c_hdr, uint8_t **opts,
uint8_t **end, struct iovec iov[IOV_TOTAL])
{
@@ -215,19 +215,26 @@ static void handle_nested_message(uint8_t *data, size_t len,
return;
}
+ /* Each nested Relay-Forward is one relay hop; RFC8415 bounds a relay
+ * chain via HOP_COUNT_LIMIT (defined in §7.6, enforced in §19.1.2),
+ * so refuse to recurse deeper and avoid stack exhaustion on crafted
+ * relay loops. */
+ if (depth >= DHCPV6_HOP_COUNT_LIMIT)
+ return;
+
dhcpv6_for_each_option(r_hdr->options, data + len, otype, olen, odata) {
if (otype == DHCPV6_OPT_RELAY_MSG) {
iov[IOV_RELAY_MSG].iov_base = odata + olen;
iov[IOV_RELAY_MSG].iov_len = (((uint8_t *)iov[IOV_NESTED].iov_base) +
iov[IOV_NESTED].iov_len) - (odata + olen);
- handle_nested_message(odata, olen, c_hdr, opts, end, iov);
+ handle_nested_message(odata, olen, depth + 1, c_hdr, opts, end, iov);
return;
}
}
}
-static void update_nested_message(uint8_t *data, size_t len, ssize_t pdiff)
+static void update_nested_message(uint8_t *data, size_t len, unsigned depth, ssize_t pdiff)
{
struct dhcpv6_relay_header *hdr = (struct dhcpv6_relay_header*)data;
if (hdr->msg_type != DHCPV6_MSG_RELAY_FORW)
@@ -235,6 +242,10 @@ static void update_nested_message(uint8_t *data, size_t len, ssize_t pdiff)
hdr->msg_type = DHCPV6_MSG_RELAY_REPL;
+ /* Bound recursion to mirror handle_nested_message(). */
+ if (depth >= DHCPV6_HOP_COUNT_LIMIT)
+ return;
+
uint16_t otype, olen;
uint8_t *odata;
dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) {
@@ -242,7 +253,7 @@ static void update_nested_message(uint8_t *data, size_t len, ssize_t pdiff)
olen += pdiff;
odata[-2] = (olen >> 8) & 0xff;
odata[-1] = olen & 0xff;
- update_nested_message(odata, olen - pdiff, pdiff);
+ update_nested_message(odata, olen - pdiff, depth + 1, pdiff);
return;
}
}
@@ -656,7 +667,7 @@ static void handle_client_request(void *addr, void *data, size_t len,
};
if (hdr->msg_type == DHCPV6_MSG_RELAY_FORW)
- handle_nested_message(data, len, &hdr, &opts, &opts_end, iov);
+ handle_nested_message(data, len, 0, &hdr, &opts, &opts_end, iov);
if (!IN6_IS_ADDR_MULTICAST((struct in6_addr *)dest_addr) && iov[IOV_NESTED].iov_len == 0 &&
(hdr->msg_type == DHCPV6_MSG_SOLICIT || hdr->msg_type == DHCPV6_MSG_CONFIRM ||
@@ -779,7 +790,7 @@ static void handle_client_request(void *addr, void *data, size_t len,
}
if (iov[IOV_NESTED].iov_len > 0) /* Update length */
- update_nested_message(data, len, iov[IOV_DEST].iov_len + iov[IOV_MAXRT].iov_len +
+ update_nested_message(data, len, 0, iov[IOV_DEST].iov_len + iov[IOV_MAXRT].iov_len +
iov[IOV_RAPID_COMMIT].iov_len + iov[IOV_DNS].iov_len +
iov[IOV_DNS_ADDR].iov_len + iov[IOV_SEARCH].iov_len +
iov[IOV_SEARCH_DOMAIN].iov_len + iov[IOV_PDBUF].iov_len +