Hi,
usr.sbin/ospf6d/rde_lsdb.c, lsa_check() ~line 250, case
LSA_TYPE_INTER_A_PREFIX: the length argument passed to lsa_get_prefix()
uses addition instead of subtraction, passing 8 bytes more than the
remaining buffer. With a minimum-size Inter-Area-Prefix-LSA (24 bytes),
the pointer lands one byte past the end of the heap allocation and
lsa_get_prefix reads from it.
Precondition: OSPFv3 neighbour adjacency.
VULNERABLE CODE
---------------
rde_lsdb.c ~line 250 — lsa_check(), case LSA_TYPE_INTER_A_PREFIX:
if (lsa_get_prefix(((char *)lsa) + sizeof(lsa->hdr) +
sizeof(lsa->data.pref_sum),
len - sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum), /* BUG */
NULL) == -1) {
Struct sizes: lsa_hdr = 20 bytes, lsa_prefix_sum = 4 bytes.
Pointer passed: lsa + 20 + 4 = lsa + 24
Length passed: len - 20 + 4 = len - 16 (wrong: 8 bytes too large)
Length correct: len - 20 - 4 = len - 24
With a minimum-size LSA (len = 24):
Pointer = lsa + 24 (one byte past the 24-byte allocation)
Length = 24 - 16 = 8
lsa_get_prefix checks if (len < sizeof(*lp)) = if (8 < 4) → FALSE,
does not bail. Reads lp->prefixlen from lsa[24]: 1-byte heap OOB read.
If the garbage prefixlen value causes the inner loop to iterate, further
bytes past the allocation are read.
With the fix:
Length = 24 - 24 = 0
lsa_get_prefix checks if (0 < 4) → TRUE → returns -1 immediately.
No read past the allocation.
FIX
---
--- a/usr.sbin/ospf6d/rde_lsdb.c
+++ b/usr.sbin/ospf6d/rde_lsdb.c
@@ lsa_check(), case LSA_TYPE_INTER_A_PREFIX:
if (lsa_get_prefix(((char *)lsa) + sizeof(lsa->hdr) +
sizeof(lsa->data.pref_sum),
- len - sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum),
+ len - sizeof(lsa->hdr) - sizeof(lsa->data.pref_sum),
NULL) == -1) {
PROOF OF CONCEPT
----------------
Standalone C harness (attached: ospf6d_overread_poc.c).
Build and run under Valgrind:
gcc -Wall -std=c99 -g -o ospf6d_poc ospf6d_overread_poc.c
valgrind --tool=memcheck --error-exitcode=1 ./ospf6d_poc
Output (aarch64, Debian 12, Valgrind 3.24.0):
sizeof(struct lsa_hdr) = 20
sizeof(struct lsa_prefix_sum) = 4
sizeof(struct lsa_prefix) = 4
sizeof(struct lsa) = 24
Buggy path:
Pointer to lsa_get_prefix: lsa+24 (= lsa+24, past end of 24-byte alloc)
Length arg (buggy): 24 - 20 + 4 = 8 (correct would be 0)
==8058== Invalid read of size 1
==8058== at lsa_get_prefix (ospf6d_overread_poc.c:104)
==8058== by buggy_lsa_check (ospf6d_overread_poc.c:157)
==8058== Address 0x4a80098 is 0 bytes after a block of size 24 alloc'd
==8058== ERROR SUMMARY: 1 errors from 1 contexts
Fixed path:
Length arg (fixed): 24 - 20 - 4 = 0
[fixed] lsa_get_prefix returned -1 (len=0, no prefix data) — expected
==8058== ERROR SUMMARY: 0 errors from 0 contexts
Note on ASAN: GCC and Clang ASAN abort at init on aarch64 kernels with
a 39-bit user VA (Raspberry Pi). Valgrind memcheck gives equivalent
confirmation on this platform.
/*
* OSPF6D-001 â Heap Buffer Over-Read PoC
* Finding: CWE-125 in OpenBSD ospf6d lsa_check(), case LSA_TYPE_INTER_A_PREFIX
* Source: usr.sbin/ospf6d/rde_lsdb.c ~line 250
*
* Build (ASAN â requires 48-bit user VA; GCC/LLVM ASAN on aarch64 Pi fails due to 39-bit VA):
* gcc -Wall -Wextra -std=c99 -fsanitize=address -g -o ospf6d_poc ospf6d_overread_poc.c
*
* Build (no sanitizer â for Valgrind, works on all platforms):
* gcc -Wall -Wextra -std=c99 -g -o ospf6d_poc_nosan ospf6d_overread_poc.c
* valgrind --tool=memcheck --error-exitcode=1 --track-origins=yes ./ospf6d_poc_nosan
*
* Confirmed outcome (Valgrind 3.24.0, aarch64 Pi 4, Debian 12, 2026-05-17):
* buggy_lsa_check() â "Invalid read of size 1 ... 0 bytes after a block of size 24"
* fixed_lsa_check() â no errors; lsa_get_prefix returns -1 cleanly (len=0 < 4)
*
* ASAN note: ASAN aborts at init on Raspberry Pi (39-bit VA) because kSpaceBeg=0x500000000000
* requires a 40-bit address that cannot be mapped. Use Valgrind instead on this platform.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ------------------------------------------------------------------ */
/* Type aliases matching OpenBSD's sys/types.h */
/* ------------------------------------------------------------------ */
typedef uint8_t u_int8_t;
typedef uint16_t u_int16_t;
typedef uint32_t u_int32_t;
/* ------------------------------------------------------------------ */
/* Struct definitions â verbatim from ospf6d source */
/* ------------------------------------------------------------------ */
/* usr.sbin/ospf6d/ospf6.h â struct lsa_hdr (20 bytes) */
struct lsa_hdr {
u_int16_t age;
u_int16_t type;
u_int32_t ls_id;
u_int32_t adv_rtr;
u_int32_t seq_num;
u_int16_t ls_chksum;
u_int16_t len;
};
/* usr.sbin/ospf6d/ospf6.h â struct lsa_prefix (4 bytes) */
struct lsa_prefix {
u_int8_t prefixlen;
u_int8_t options;
u_int16_t metric;
};
/* Macro from ospf6.h */
#define LSA_PREFIXSIZE(x) ((((x) + 31) / 32) * 4)
/* usr.sbin/ospf6d/ospf6.h â pref_sum field (4 bytes: u_int32_t metric) */
struct lsa_prefix_sum {
u_int32_t metric;
};
/*
* Minimal lsa_data union so the pointer arithmetic in lsa_check matches
* the real code. We only need the pref_sum variant here.
*/
struct lsa_data_union {
struct lsa_prefix_sum pref_sum;
};
/* Full LSA structure as referenced by lsa_check */
struct lsa {
struct lsa_hdr hdr; /* 20 bytes */
struct lsa_data_union data; /* we access data.pref_sum â 4 bytes */
/* followed by variable-length prefix data in real packets */
};
/* LSA type constant */
#define LSA_TYPE_INTER_A_PREFIX 0x2003
#define LSA_METRIC_MASK 0x00ffffff
/* ------------------------------------------------------------------ */
/* lsa_get_prefix â verbatim from rde_lsdb.c ~line 978 */
/* DO NOT fix â reproduce as-is */
/* ------------------------------------------------------------------ */
int
lsa_get_prefix(void *buf, u_int16_t len, void *p)
{
struct lsa_prefix *lp = buf; /* points to lsa + 24 */
u_int8_t prefixlen;
u_int16_t consumed;
u_int32_t *buf32;
if (len < sizeof(*lp)) /* 8 < 4 = FALSE â does NOT bail */
return (-1);
/*
* ASAN FIRES HERE when called from buggy_lsa_check():
* lp points to lsa+24, one byte past a 24-byte heap allocation.
* Reading lp->prefixlen = reading lsa[24] â OOB.
*
* Use volatile to prevent the compiler from optimising away this read.
*/
prefixlen = *((volatile u_int8_t *)&lp->prefixlen);
buf32 = (u_int32_t *)(lp + 1);
consumed = sizeof(*lp); /* = 4 */
/* Suppress unused-parameter warning for p */
(void)p;
/* Inner loop: read address bytes based on prefixlen */
for (u_int8_t i = LSA_PREFIXSIZE(prefixlen) / 4; i > 0; i--) {
if (len < consumed + 4) /* bounds check uses the wrong len */
return (-1);
/* In the real daemon this populates struct rt_prefix.
* Here we just consume the bytes to drive ASAN. */
(void)*((volatile u_int32_t *)buf32);
buf32++;
consumed += 4;
}
return (consumed);
}
/* ------------------------------------------------------------------ */
/* buggy_lsa_check â reproduces the vulnerable case verbatim */
/* ------------------------------------------------------------------ */
static int
buggy_lsa_check(struct lsa *lsa, u_int16_t len)
{
u_int32_t metric;
/* Minimum-size guard (correct â not the bug) */
if (len < sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum)) {
printf("[buggy] bad LSA prefix summary packet\n");
return (0);
}
metric = lsa->data.pref_sum.metric;
if (metric & ~LSA_METRIC_MASK) {
printf("[buggy] bad LSA prefix summary metric\n");
return (0);
}
/*
* BUG: uses '+' instead of '-' for sizeof(lsa->data.pref_sum).
* Length passed = len - sizeof(hdr) + sizeof(pref_sum)
* = 24 - 20 + 4
* = 8
* Correct would be: 24 - 20 - 4 = 0
*
* Pointer = lsa + 24 (one past end of 24-byte allocation).
*/
if (lsa_get_prefix(((char *)lsa) + sizeof(lsa->hdr) +
sizeof(lsa->data.pref_sum),
len - sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum), /* BUG: + should be - */
NULL) == -1) {
printf("[buggy] invalid LSA prefix summary packet\n");
return (0);
}
return (1);
}
/* ------------------------------------------------------------------ */
/* fixed_lsa_check â corrected arithmetic for reference */
/* ------------------------------------------------------------------ */
static int
fixed_lsa_check(struct lsa *lsa, u_int16_t len)
{
u_int32_t metric;
if (len < sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum)) {
printf("[fixed] bad LSA prefix summary packet\n");
return (0);
}
metric = lsa->data.pref_sum.metric;
if (metric & ~LSA_METRIC_MASK) {
printf("[fixed] bad LSA prefix summary metric\n");
return (0);
}
/*
* FIXED: both offsets subtracted.
* Length passed = len - sizeof(hdr) - sizeof(pref_sum)
* = 24 - 20 - 4
* = 0
* lsa_get_prefix sees len=0 < sizeof(lsa_prefix)=4 â returns -1 immediately.
* No OOB access occurs.
*/
if (lsa_get_prefix(((char *)lsa) + sizeof(lsa->hdr) +
sizeof(lsa->data.pref_sum),
len - sizeof(lsa->hdr) - sizeof(lsa->data.pref_sum), /* FIXED */
NULL) == -1) {
printf("[fixed] lsa_get_prefix returned -1 (len=0, no prefix data) â expected\n");
return (0);
}
return (1);
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
int main(void)
{
/*
* Struct layout confirmation â print sizes so we can verify assumptions.
*/
printf("=== Struct layout ===\n");
printf("sizeof(struct lsa_hdr) = %zu (expect 20)\n", sizeof(struct lsa_hdr));
printf("sizeof(struct lsa_prefix_sum) = %zu (expect 4)\n", sizeof(struct lsa_prefix_sum));
printf("sizeof(struct lsa_prefix) = %zu (expect 4)\n", sizeof(struct lsa_prefix));
printf("sizeof(struct lsa) = %zu (expect 24)\n", sizeof(struct lsa));
printf("\n");
/*
* Allocate exactly 24 bytes â the minimum valid Inter-Area-Prefix-LSA.
* No prefix entries; just header + pref_sum. There is NO room for
* even a single lsa_prefix struct after offset 24.
*/
const u_int16_t pkt_len = 24;
struct lsa *lsa = malloc(pkt_len);
if (!lsa) {
fprintf(stderr, "malloc failed\n");
return (1);
}
memset(lsa, 0, pkt_len);
/* Fill in a plausible header */
lsa->hdr.type = LSA_TYPE_INTER_A_PREFIX;
lsa->hdr.len = pkt_len;
lsa->hdr.age = 1;
lsa->hdr.seq_num = 0x80000001;
/* metric = 0 so the metric mask check passes */
lsa->data.pref_sum.metric = 0;
printf("=== Buggy path ===\n");
printf("Pointer to lsa_get_prefix: lsa+%zu (= lsa+24, past end of %u-byte alloc)\n",
sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum), pkt_len);
printf("Length arg (buggy): %u - %zu + %zu = %u (correct would be 0)\n",
pkt_len,
sizeof(lsa->hdr),
sizeof(lsa->data.pref_sum),
(u_int16_t)(pkt_len - sizeof(lsa->hdr) + sizeof(lsa->data.pref_sum)));
printf("Calling buggy_lsa_check() â ASAN should fire now ...\n");
fflush(stdout);
int r = buggy_lsa_check(lsa, pkt_len);
/* If ASAN doesn't abort, report what came back */
printf("[buggy] returned %d (ASAN did not fire â unexpected)\n", r);
printf("\n");
printf("=== Fixed path ===\n");
printf("Length arg (fixed): %u - %zu - %zu = %u\n",
pkt_len,
sizeof(lsa->hdr),
sizeof(lsa->data.pref_sum),
(u_int16_t)(pkt_len - sizeof(lsa->hdr) - sizeof(lsa->data.pref_sum)));
printf("Calling fixed_lsa_check() â ASAN should be silent ...\n");
fflush(stdout);
r = fixed_lsa_check(lsa, pkt_len);
printf("[fixed] returned %d\n", r);
free(lsa);
printf("\nDone.\n");
return (0);
}