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);
}

Reply via email to