Commit 5634f9265d for frr

commit 5634f9265d942796a674f26b9cd7735dd396708f
Author: Alejandro Pacheco <alepacheco99@gmail.com>
Date:   Wed Oct 14 19:36:26 2026 +0200

    ospfd: keep opaque LSA TLV display within the LSA

    "show ip ospf database opaque-area" and the LSA debug dumps display
    received Extended Link, Extended Prefix, Traffic Engineering and Router
    Information LSAs by walking their TLVs. Each TLV is checked against the
    length it claims, but not against what is left of the LSA, and a
    claimed length shorter than the fields displayed is accepted:

    - show_vty_link_info() and show_vty_pref_info() compare the TLV body
      length with the remaining length including the TLV header, so the TLV
      may end up to 4 bytes past the LSA, and never check that the body
      holds the fixed part. A TLV shorter than that has its fixed part read
      past the LSA, and subtracting the fixed size wraps the sub-TLV length
      to almost 64 KB, so the sub-TLV walk runs on past the LSA.
    - A TLV or sub-TLV whose header is the last word of the LSA has its
      value read past the LSA: the Traffic Engineering and Router
      Information size checks accept any length up to the expected one, and
      the SID sub-TLVs read their flags, and the PCE Address sub-TLV its
      address type, before checking their length.
    - ospf_router_info_show_info() passes the whole LSA length, not what is
      left of it, to show_vty_unknown_tlv(). show_vty_pce_info() is passed
      the first PCE sub-TLV and walks up to that sub-TLV's length, so the
      remaining sub-TLVs were displayed as top-level TLVs; it now walks the
      PCE TLV's own sub-TLVs and logs instead of using a NULL vty when
      called from the debug dump.

    Add tlv_fits() to ospf_opaque.h: a TLV, header and padded body, lies
    within the bytes left. Check with it that each TLV and sub-TLV lies
    within the LSA (or its parent TLV, padding included) before it is
    displayed, require the fixed part of the Extended Link and Extended
    Prefix TLVs, require the TLV body to hold the fields a Traffic
    Engineering or Router Information TLV displays, and account an Extended
    TLV by its own size. A sub-TLV that does not fit now ends the display of
    its parent TLV only. Well-formed LSAs are displayed as before.

    tests/ospfd/test_ospf_opaque_show displays well-formed LSAs (including a
    label Adj-SID, whose length is not a multiple of 4, and a PCE Address)
    and LSAs whose TLVs claim more than the LSA holds, from an allocation of
    exactly the LSA's size and from a buffer where the LSA is followed by
    bytes that must not be shown.

    Signed-off-by: Alejandro Pacheco <alepacheco99@gmail.com>

diff --git a/ospfd/ospf_ext.c b/ospfd/ospf_ext.c
index 18e0aed912..aeeea80ab3 100644
--- a/ospfd/ospf_ext.c
+++ b/ospfd/ospf_ext.c
@@ -1764,7 +1764,7 @@ static uint16_t show_vty_ext_link_adj_sid(struct vty *vty,
 	struct ext_subtlv_adj_sid *top = (struct ext_subtlv_adj_sid *)tlvh;
 	uint8_t tlv_size;

-	tlv_size = CHECK_FLAG(top->flags, EXT_SUBTLV_LINK_ADJ_SID_VFLG)
+	tlv_size = ntohs(tlvh->length) && CHECK_FLAG(top->flags, EXT_SUBTLV_LINK_ADJ_SID_VFLG)
 			      ? SID_LABEL_SIZE(EXT_SUBTLV_ADJ_SID_SIZE)
 			      : SID_INDEX_SIZE(EXT_SUBTLV_ADJ_SID_SIZE);
 	check_tlv_size(tlv_size, "Adjacency SID");
@@ -1815,7 +1815,7 @@ static uint16_t show_vty_ext_link_lan_adj_sid(struct vty *vty,
 		(struct ext_subtlv_lan_adj_sid *)tlvh;
 	uint8_t tlv_size;

-	tlv_size = CHECK_FLAG(top->flags, EXT_SUBTLV_LINK_ADJ_SID_VFLG)
+	tlv_size = ntohs(tlvh->length) && CHECK_FLAG(top->flags, EXT_SUBTLV_LINK_ADJ_SID_VFLG)
 			      ? SID_LABEL_SIZE(EXT_SUBTLV_LAN_ADJ_SID_SIZE)
 			      : SID_INDEX_SIZE(EXT_SUBTLV_LAN_ADJ_SID_SIZE);
 	check_tlv_size(tlv_size, "LAN-Adjacency SID");
@@ -1905,16 +1905,13 @@ static uint16_t show_vty_link_info(struct vty *vty, struct tlv_header *ext,
 	uint16_t sum = 0;
 	json_object *jadj = NULL, *obj = NULL;

-	/* Verify that TLV length is valid against remaining buffer size */
-	if (length > buf_size) {
+	/* Verify that the TLV, with its fixed part, lies within the LSA */
+	if (!tlv_fits(ext, buf_size) || length < EXT_TLV_LINK_SIZE) {
 		/* Add security check for vty_out. If vty is not available, dump info via zlog. */
-		if (vty != NULL) {
-			vty_out(vty, "  Extended Link TLV size %d exceeds buffer size. Abort!\n",
-				length);
-		} else {
-			zlog_debug("  Extended Link TLV size %d exceeds buffer size. Abort!",
-				   length);
-		}
+		if (vty != NULL)
+			vty_out(vty, "  Extended Link TLV length %u invalid. Abort!\n", length);
+		else
+			zlog_debug("  Extended Link TLV length %u invalid. Abort!", length);
 		return buf_size;
 	}

@@ -1943,10 +1940,12 @@ static uint16_t show_vty_link_info(struct vty *vty, struct tlv_header *ext,
 	}

 	/* Skip Extended TLV and parse sub-TLVs */
-	length -= EXT_TLV_LINK_SIZE;
+	length = TLV_BODY_SIZE(ext) - EXT_TLV_LINK_SIZE;
 	tlvh = (struct tlv_header *)((char *)(ext) + TLV_HDR_SIZE
 				     + EXT_TLV_LINK_SIZE);
 	for (; sum < length && tlvh; tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, length - sum))
+			break;
 		switch (ntohs(tlvh->type)) {
 		case EXT_SUBTLV_ADJ_SID:
 			if (json) {
@@ -1974,7 +1973,7 @@ static uint16_t show_vty_link_info(struct vty *vty, struct tlv_header *ext,
 		}
 	}

-	return sum + sizeof(struct ext_tlv_link);
+	return TLV_SIZE(ext);
 }

 /* Extended Link TLVs */
@@ -1996,6 +1995,8 @@ static void ospf_ext_link_show_info(struct vty *vty, struct json_object *json,

 	for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh;
 	     tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, length - sum))
+			break;
 		switch (ntohs(tlvh->type)) {
 		case EXT_TLV_LINK:
 			sum += show_vty_link_info(vty, tlvh, length - sum,
@@ -2018,7 +2019,7 @@ static uint16_t show_vty_ext_pref_pref_sid(struct vty *vty,
 		(struct ext_subtlv_prefix_sid *)tlvh;
 	uint8_t tlv_size;

-	tlv_size = CHECK_FLAG(top->flags, EXT_SUBTLV_PREFIX_SID_VFLG)
+	tlv_size = ntohs(tlvh->length) && CHECK_FLAG(top->flags, EXT_SUBTLV_PREFIX_SID_VFLG)
 			      ? SID_LABEL_SIZE(EXT_SUBTLV_PREFIX_SID_SIZE)
 			      : SID_INDEX_SIZE(EXT_SUBTLV_PREFIX_SID_SIZE);
 	check_tlv_size(tlv_size, "Prefix SID");
@@ -2068,15 +2069,12 @@ static uint16_t show_vty_pref_info(struct vty *vty, struct tlv_header *ext,
 	uint16_t sum = 0;
 	json_object *jsid = NULL;

-	/* Verify that TLV length is valid against remaining buffer size */
-	if (length > buf_size) {
-		if (vty != NULL) {
-			vty_out(vty, "  Extended Link TLV size %d exceeds buffer size. Abort!\n",
-				length);
-		} else {
-			zlog_debug("  Extended Link TLV size %d exceeds buffer size. Abort!",
-				   length);
-		}
+	/* Verify that the TLV, with its fixed part, lies within the LSA */
+	if (!tlv_fits(ext, buf_size) || length < EXT_TLV_PREFIX_SIZE) {
+		if (vty != NULL)
+			vty_out(vty, "  Extended Prefix TLV length %u invalid. Abort!\n", length);
+		else
+			zlog_debug("  Extended Prefix TLV length %u invalid. Abort!", length);
 		return buf_size;
 	}

@@ -2105,10 +2103,12 @@ static uint16_t show_vty_pref_info(struct vty *vty, struct tlv_header *ext,
 	}

 	/* Skip Extended Prefix TLV and parse sub-TLVs */
-	length -= EXT_TLV_PREFIX_SIZE;
+	length = TLV_BODY_SIZE(ext) - EXT_TLV_PREFIX_SIZE;
 	tlvh = (struct tlv_header *)((char *)(ext) + TLV_HDR_SIZE
 				     + EXT_TLV_PREFIX_SIZE);
 	for (; sum < length && tlvh; tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, length - sum))
+			break;
 		switch (ntohs(tlvh->type)) {
 		case EXT_SUBTLV_PREFIX_SID:
 			sum += show_vty_ext_pref_pref_sid(vty, tlvh, jsid);
@@ -2120,7 +2120,7 @@ static uint16_t show_vty_pref_info(struct vty *vty, struct tlv_header *ext,
 		}
 	}

-	return sum + sizeof(struct ext_tlv_prefix);
+	return TLV_SIZE(ext);
 }

 /* Extended Prefix TLVs */
@@ -2142,6 +2142,8 @@ static void ospf_ext_pref_show_info(struct vty *vty, struct json_object *json,

 	for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh;
 	     tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, length - sum))
+			break;
 		switch (ntohs(tlvh->type)) {
 		case EXT_TLV_PREFIX:
 			sum += show_vty_pref_info(vty, tlvh, length - sum,
diff --git a/ospfd/ospf_opaque.h b/ospfd/ospf_opaque.h
index 54651f8f58..9c0668cbff 100644
--- a/ospfd/ospf_opaque.h
+++ b/ospfd/ospf_opaque.h
@@ -99,6 +99,15 @@ struct tlv_header {
 #define TLV_LEN(tlvh)	tlvh.header.length
 #define TLV_HDR(tlvh)	tlvh.header

+/*
+ * Whether a TLV, header and (padded) body, lies within the 'len' bytes
+ * that start at it: a received TLV must pass this before its body is read.
+ */
+static inline bool tlv_fits(const struct tlv_header *tlvh, uint32_t len)
+{
+	return len >= TLV_HDR_SIZE && TLV_SIZE(tlvh) <= len;
+}
+
 /* Following declaration concerns the Opaque LSA management */
 enum lsa_opcode { REORIGINATE_THIS_LSA, REFRESH_THIS_LSA, FLUSH_THIS_LSA };

diff --git a/ospfd/ospf_ri.c b/ospfd/ospf_ri.c
index e8e9c73eac..58dcdfabb8 100644
--- a/ospfd/ospf_ri.c
+++ b/ospfd/ospf_ri.c
@@ -1204,7 +1204,7 @@ static int ospf_router_info_lsa_update(struct ospf_lsa *lsa)

 #define check_tlv_size(size, msg)                                                                           \
 	do {                                                                                                \
-		if (ntohs(tlvh->length) > size) {                                                           \
+		if (ntohs(tlvh->length) > size || TLV_BODY_SIZE(tlvh) < size) {                             \
 			if (vty != NULL)                                                                    \
 				vty_out(vty,                                                                \
 					"  Wrong %s TLV size: %d(expected %d). Skip subsequent TLVs!\n",    \
@@ -1242,8 +1242,12 @@ static uint16_t show_vty_pce_subtlv_address(struct vty *vty,
 {
 	struct ri_pce_subtlv_address *top =
 		(struct ri_pce_subtlv_address *)tlvh;
+	uint16_t type = 0;

-	if (ntohs(top->address.type) == PCE_ADDRESS_IPV4) {
+	if (ntohs(tlvh->length) >= sizeof(top->address.type))
+		type = ntohs(top->address.type);
+
+	if (type == PCE_ADDRESS_IPV4) {
 		check_tlv_size(PCE_ADDRESS_IPV4_SIZE, "PCE Address");
 		if (vty != NULL)
 			if (!json)
@@ -1256,7 +1260,7 @@ static uint16_t show_vty_pce_subtlv_address(struct vty *vty,
 		else
 			zlog_debug("    PCE Address: %pI4",
 				   &top->address.value);
-	} else if (ntohs(top->address.type) == PCE_ADDRESS_IPV6) {
+	} else if (type == PCE_ADDRESS_IPV6) {
 		check_tlv_size(PCE_ADDRESS_IPV6_SIZE, "PCE Address");
 		if (vty != NULL)
 			if (!json)
@@ -1270,11 +1274,9 @@ static uint16_t show_vty_pce_subtlv_address(struct vty *vty,
 			zlog_debug("    PCE Address: unsupported IPv6");
 	} else {
 		if (vty != NULL)
-			vty_out(vty, "  Wrong PCE Address type: 0x%x\n",
-				ntohs(top->address.type));
+			vty_out(vty, "  Wrong PCE Address type: 0x%x\n", type);
 		else
-			zlog_debug("    Wrong PCE Address type: 0x%x",
-				   ntohs(top->address.type));
+			zlog_debug("    Wrong PCE Address type: 0x%x", type);
 	}

 	return TLV_SIZE(tlvh);
@@ -1449,21 +1451,21 @@ static uint16_t show_vty_unknown_tlv(struct vty *vty, struct tlv_header *tlvh,
 }

 static uint16_t show_vty_pce_info(struct vty *vty, struct tlv_header *ri,
-				  size_t buf_size, json_object *json)
+				  json_object *json)
 {
 	struct tlv_header *tlvh;
-	uint16_t length = ntohs(ri->length);
+	uint16_t length = TLV_BODY_SIZE(ri);
 	uint16_t sum = 0;

-	/* Verify that TLV length is valid against remaining buffer size */
-	if (length > buf_size) {
-		vty_out(vty,
-			"  PCE Info TLV size %d exceeds buffer size. Abort!\n",
-			length);
-		return buf_size;
-	}
-
-	for (tlvh = ri; sum < length; tlvh = TLV_HDR_NEXT(tlvh)) {
+	/* The PCE TLV fits in the LSA: walk its sub-TLVs within it */
+	for (tlvh = TLV_DATA(ri); sum < length; tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, length - sum)) {
+			if (vty != NULL)
+				vty_out(vty, "  PCE sub-TLV exceeds PCE TLV size. Abort!\n");
+			else
+				zlog_debug("  PCE sub-TLV exceeds PCE TLV size. Abort!");
+			break;
+		}
 		switch (ntohs(tlvh->type)) {
 		case RI_PCE_SUBTLV_ADDRESS:
 			sum += show_vty_pce_subtlv_address(vty, tlvh, json);
@@ -1486,7 +1488,7 @@ static uint16_t show_vty_pce_info(struct vty *vty, struct tlv_header *ri,
 			break;
 		}
 	}
-	return sum;
+	return TLV_SIZE(ri);
 }

 /* Display Segment Routing Algorithm TLV information */
@@ -1664,14 +1666,14 @@ static void ospf_router_info_show_info(struct vty *vty,

 	for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh;
 	     tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, length - sum))
+			break;
 		switch (ntohs(tlvh->type)) {
 		case RI_TLV_CAPABILITIES:
 			sum += show_vty_router_cap(vty, tlvh, jri);
 			break;
 		case RI_TLV_PCE:
-			tlvh++;
-			sum += TLV_HDR_SIZE;
-			sum += show_vty_pce_info(vty, tlvh, length - sum, jpce);
+			sum += show_vty_pce_info(vty, tlvh, jpce);
 			break;
 		case RI_SR_TLV_SR_ALGORITHM:
 			sum += show_vty_sr_algorithm(vty, tlvh, jsr);
@@ -1685,7 +1687,7 @@ static void ospf_router_info_show_info(struct vty *vty,
 			break;

 		default:
-			sum += show_vty_unknown_tlv(vty, tlvh, length, jri);
+			sum += show_vty_unknown_tlv(vty, tlvh, length - sum, jri);
 			break;
 		}
 	}
diff --git a/ospfd/ospf_te.c b/ospfd/ospf_te.c
index 5c3432cdf7..de9b2f84b1 100644
--- a/ospfd/ospf_te.c
+++ b/ospfd/ospf_te.c
@@ -3465,7 +3465,7 @@ static void ospf_te_init_ted(struct ls_ted *ted, struct ospf *ospf)
  *------------------------------------------------------------------------*/
 #define check_tlv_size(size, msg)                                                                           \
 	do {                                                                                                \
-		if (ntohs(tlvh->length) > size) {                                                           \
+		if (ntohs(tlvh->length) > size || TLV_BODY_SIZE(tlvh) < size) {                             \
 			if (vty != NULL)                                                                    \
 				vty_out(vty,                                                                \
 					"  Wrong %s TLV size: %d(expected %d). Skip subsequent TLVs!\n",    \
@@ -4239,6 +4239,8 @@ static uint16_t ospf_mpls_te_show_link_subtlv(struct vty *vty,
 	uint16_t sum = subtotal;

 	for (tlvh = tlvh0; sum < total; tlvh = TLV_HDR_NEXT(tlvh)) {
+		if (!tlv_fits(tlvh, total - sum))
+			return total;
 		switch (ntohs(tlvh->type)) {
 		case TE_LINK_SUBTLV_LINK_TYPE:
 			sum += show_vty_link_subtlv_link_type(vty, tlvh, json);
@@ -4341,6 +4343,8 @@ static void ospf_mpls_te_show_info(struct vty *vty, struct json_object *json,

 		next = NULL;
 		sub = total - sum;
+		if (!tlv_fits(tlvh, sub))
+			break;
 		switch (ntohs(tlvh->type)) {
 		case TE_TLV_ROUTER_ADDR:
 			if (json) {
diff --git a/tests/ospfd/.gitignore b/tests/ospfd/.gitignore
index be97550ac7..d2aaddcc07 100644
--- a/tests/ospfd/.gitignore
+++ b/tests/ospfd/.gitignore
@@ -1,4 +1,5 @@
 /*_afl/*
 test_ospf_adj_pacing
 test_ospf_spf
+test_ospf_opaque_show
 core
diff --git a/tests/ospfd/subdir.am b/tests/ospfd/subdir.am
index 4c09079392..48afe67f7b 100644
--- a/tests/ospfd/subdir.am
+++ b/tests/ospfd/subdir.am
@@ -26,8 +26,18 @@ tests_ospfd_test_ospf_adj_pacing_CFLAGS = $(TESTS_CFLAGS)
 tests_ospfd_test_ospf_adj_pacing_CPPFLAGS = $(TESTS_CPPFLAGS)
 tests_ospfd_test_ospf_adj_pacing_LDADD = $(OSPFD_TEST_LDADD)
 tests_ospfd_test_ospf_adj_pacing_SOURCES = tests/ospfd/test_ospf_adj_pacing.c
+
+if OSPFD
+check_PROGRAMS += tests/ospfd/test_ospf_opaque_show
+endif
+tests_ospfd_test_ospf_opaque_show_CFLAGS = $(TESTS_CFLAGS)
+tests_ospfd_test_ospf_opaque_show_CPPFLAGS = $(TESTS_CPPFLAGS)
+tests_ospfd_test_ospf_opaque_show_LDADD = $(OSPFD_TEST_LDADD)
+tests_ospfd_test_ospf_opaque_show_SOURCES = tests/ospfd/test_ospf_opaque_show.c
+
 EXTRA_DIST += \
 	tests/ospfd/test_ospf_adj_pacing.py \
+	tests/ospfd/test_ospf_opaque_show.py \
 	tests/ospfd/test_ospf_spf.py \
 	tests/ospfd/test_ospf_spf.in \
 	tests/ospfd/test_ospf_spf.refout \
diff --git a/tests/ospfd/test_ospf_opaque_show.c b/tests/ospfd/test_ospf_opaque_show.c
new file mode 100644
index 0000000000..46d6c6835c
--- /dev/null
+++ b/tests/ospfd/test_ospf_opaque_show.c
@@ -0,0 +1,350 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * "show ip ospf database opaque-area" stays within each received opaque
+ * LSA: Extended Link, Extended Prefix, Traffic Engineering and Router
+ * Information TLVs whose length claims more than the LSA holds are not
+ * displayed.
+ *
+ * Each LSA is displayed twice: from an allocation of exactly its size (an
+ * address sanitizer build reports any read past it), and from a buffer in
+ * which it is followed by bytes that would display MARK if they were read
+ * as part of the LSA.
+ */
+
+#include <zebra.h>
+
+#include "lib/buffer.h"
+#include "lib/privs.h"
+#include "lib/vrf.h"
+#include "lib/vty.h"
+
+#include "ospfd/ospfd.h"
+#include "ospfd/ospf_asbr.h"
+#include "ospfd/ospf_lsa.h"
+#include "ospfd/ospf_opaque.h"
+#include "ospfd/ospf_sr.h"
+#include "ospfd/ospf_ext.h"
+#include "ospfd/ospf_te.h"
+#include "ospfd/ospf_ri.h"
+#include "ospfd/ospf_vty.h"
+
+/* need these to link in libfrrospf */
+struct event_loop *master;
+struct zebra_privs_t ospfd_privs;
+
+#define TEST_NAME "opaque-show: opaque LSA TLVs are displayed within the LSA"
+
+/* 203.0.113.99, and its display */
+#define MARK_ADDR 0xcb007163
+#define MARK	  "203.0.113.99"
+
+#define BODY_MAX 64
+
+static void failed(const char *what, const char *out)
+{
+	printf("%s\n%s:\n%s\nfailed!\n", TEST_NAME, what, out ? out : "");
+	exit(1);
+}
+
+static void put_tlv_hdr(uint8_t *p, uint16_t type, uint16_t len)
+{
+	struct tlv_header *tlvh = (struct tlv_header *)p;
+
+	tlvh->type = htons(type);
+	tlvh->length = htons(len);
+}
+
+static void put_addr(uint8_t *p, uint32_t addr)
+{
+	addr = htonl(addr);
+	memcpy(p, &addr, sizeof(addr));
+}
+
+static char *show(struct ospf_lsa *lsa)
+{
+	struct vty *vty = vty_new();
+	char *out;
+
+	vty->type = VTY_TERM;
+	show_opaque_info_detail(vty, lsa, NULL);
+	out = buffer_getstr(vty->obuf);
+	vty_close(vty);
+
+	return out;
+}
+
+static void opaque_header_set(struct lsa_header *lsah, uint8_t opaque_type, size_t len)
+{
+	lsah->type = OSPF_OPAQUE_AREA_LSA;
+	lsah->id.s_addr = htonl(SET_OPAQUE_LSID(opaque_type, 1));
+	lsah->adv_router.s_addr = htonl(0x02020202);
+	lsah->ls_seqnum = htonl(OSPF_INITIAL_SEQUENCE_NUMBER);
+	lsah->length = htons(OSPF_LSA_HEADER_SIZE + len);
+}
+
+/*
+ * Display an opaque LSA with the given body, followed in memory by the
+ * given bytes, which are not part of the LSA: MARK must not be shown.
+ */
+static void check(const char *what, uint8_t opaque_type, const uint8_t *body, size_t len,
+		  const uint8_t *after, size_t after_len)
+{
+	uint8_t buf[OSPF_LSA_HEADER_SIZE + 2 * BODY_MAX] = {};
+	struct ospf_lsa lsa = {};
+	struct ospf_lsa *exact;
+	char *out;
+
+	assert(len <= BODY_MAX && after_len <= BODY_MAX);
+
+	exact = ospf_lsa_new_and_data(OSPF_LSA_HEADER_SIZE + len);
+	opaque_header_set(exact->data, opaque_type, len);
+	memcpy((uint8_t *)exact->data + OSPF_LSA_HEADER_SIZE, body, len);
+	out = show(exact);
+	ospf_lsa_discard(exact);
+	if (strstr(out, MARK))
+		failed(what, out);
+	XFREE(MTYPE_TMP, out);
+
+	lsa.data = (struct lsa_header *)buf;
+	lsa.size = OSPF_LSA_HEADER_SIZE + len;
+	opaque_header_set(lsa.data, opaque_type, len);
+	memcpy(buf + OSPF_LSA_HEADER_SIZE, body, len);
+	memcpy(buf + OSPF_LSA_HEADER_SIZE + len, after, after_len);
+	out = show(&lsa);
+	if (strstr(out, MARK))
+		failed(what, out);
+	XFREE(MTYPE_TMP, out);
+}
+
+/* A well-formed Extended Link LSA shows its Remote Interface Address */
+static void test_ext_link_valid(void)
+{
+	uint8_t body[TLV_HDR_SIZE + EXT_TLV_LINK_SIZE + TLV_HDR_SIZE +
+		     EXT_SUBTLV_RMT_ITF_ADDR_SIZE] = {};
+	struct ospf_lsa *lsa;
+	char *out;
+
+	put_tlv_hdr(body, EXT_TLV_LINK, sizeof(body) - TLV_HDR_SIZE);
+	body[TLV_HDR_SIZE] = 1; /* point-to-point */
+	put_tlv_hdr(body + TLV_HDR_SIZE + EXT_TLV_LINK_SIZE, EXT_SUBTLV_RMT_ITF_ADDR,
+		    EXT_SUBTLV_RMT_ITF_ADDR_SIZE);
+	put_addr(body + 2 * TLV_HDR_SIZE + EXT_TLV_LINK_SIZE, MARK_ADDR);
+
+	lsa = ospf_lsa_new_and_data(OSPF_LSA_HEADER_SIZE + sizeof(body));
+	opaque_header_set(lsa->data, OPAQUE_TYPE_EXTENDED_LINK_LSA, sizeof(body));
+	memcpy((uint8_t *)lsa->data + OSPF_LSA_HEADER_SIZE, body, sizeof(body));
+	out = show(lsa);
+	ospf_lsa_discard(lsa);
+	if (!strstr(out, "Remote Interface Address Sub-TLV") || !strstr(out, MARK))
+		failed("well-formed Extended Link TLV", out);
+	XFREE(MTYPE_TMP, out);
+}
+
+/*
+ * A well-formed Extended Link LSA with a label Adj-SID: its sub-TLV length
+ * (7) is not a multiple of 4, so the Extended Link TLV length does not
+ * count the padding of its last sub-TLV.
+ */
+static void test_ext_link_label_valid(void)
+{
+	uint8_t body[2 * TLV_HDR_SIZE + EXT_TLV_LINK_SIZE + EXT_SUBTLV_ADJ_SID_SIZE] = {};
+	struct ext_subtlv_adj_sid *sid;
+	struct ospf_lsa *lsa;
+	char *out;
+
+	put_tlv_hdr(body, EXT_TLV_LINK,
+		    EXT_TLV_LINK_SIZE + TLV_HDR_SIZE + SID_LABEL_SIZE(EXT_SUBTLV_ADJ_SID_SIZE));
+	body[TLV_HDR_SIZE] = 1; /* point-to-point */
+	sid = (struct ext_subtlv_adj_sid *)(body + TLV_HDR_SIZE + EXT_TLV_LINK_SIZE);
+	put_tlv_hdr((uint8_t *)sid, EXT_SUBTLV_ADJ_SID, SID_LABEL_SIZE(EXT_SUBTLV_ADJ_SID_SIZE));
+	sid->flags = EXT_SUBTLV_LINK_ADJ_SID_VFLG;
+	sid->value = htonl(SET_LABEL(16123));
+
+	lsa = ospf_lsa_new_and_data(OSPF_LSA_HEADER_SIZE + sizeof(body));
+	opaque_header_set(lsa->data, OPAQUE_TYPE_EXTENDED_LINK_LSA, sizeof(body));
+	memcpy((uint8_t *)lsa->data + OSPF_LSA_HEADER_SIZE, body, sizeof(body));
+	out = show(lsa);
+	ospf_lsa_discard(lsa);
+	if (!strstr(out, "Label: 16123"))
+		failed("well-formed label Adj-SID", out);
+	XFREE(MTYPE_TMP, out);
+}
+
+/*
+ * An empty Adj-SID sub-TLV in the last word of the LSA: its flags are past
+ * the LSA (reported by an address sanitizer build).
+ */
+static void test_ext_link_empty_subtlv(void)
+{
+	uint8_t body[TLV_HDR_SIZE + EXT_TLV_LINK_SIZE + TLV_HDR_SIZE] = {};
+	uint8_t after[4] = { EXT_SUBTLV_LINK_ADJ_SID_VFLG };
+
+	put_tlv_hdr(body, EXT_TLV_LINK, EXT_TLV_LINK_SIZE + TLV_HDR_SIZE);
+	body[TLV_HDR_SIZE] = 1; /* point-to-point */
+	put_tlv_hdr(body + TLV_HDR_SIZE + EXT_TLV_LINK_SIZE, EXT_SUBTLV_ADJ_SID, 0);
+	check("empty Adj-SID sub-TLV at the end of the LSA", OPAQUE_TYPE_EXTENDED_LINK_LSA, body,
+	      sizeof(body), after, sizeof(after));
+}
+
+/*
+ * An Extended Link TLV shorter than its fixed part. What follows it would
+ * be its fixed part, then a Remote Interface Address sub-TLV showing MARK
+ * and an unknown sub-TLV that ends any sub-TLV walk.
+ */
+static void test_ext_link_short(void)
+{
+	uint8_t body[TLV_HDR_SIZE] = {};
+	uint8_t after[BODY_MAX] = {};
+	uint8_t *p = after + EXT_TLV_LINK_SIZE;
+
+	put_tlv_hdr(body, EXT_TLV_LINK, 0);
+	put_tlv_hdr(p, EXT_SUBTLV_RMT_ITF_ADDR, EXT_SUBTLV_RMT_ITF_ADDR_SIZE);
+	put_addr(p + TLV_HDR_SIZE, MARK_ADDR);
+	p += TLV_HDR_SIZE + EXT_SUBTLV_RMT_ITF_ADDR_SIZE;
+	/* ends the walk an unchecked 0 - EXT_TLV_LINK_SIZE length would make */
+	put_tlv_hdr(p, 0x7fff,
+		    0x10000 - EXT_TLV_LINK_SIZE - 2 * TLV_HDR_SIZE - EXT_SUBTLV_RMT_ITF_ADDR_SIZE);
+	check("Extended Link TLV shorter than its fixed part", OPAQUE_TYPE_EXTENDED_LINK_LSA, body,
+	      sizeof(body), after, p + TLV_HDR_SIZE - after);
+}
+
+/* An Extended Link TLV ending past the LSA, inside its last sub-TLV */
+static void test_ext_link_past_end(void)
+{
+	uint8_t body[TLV_HDR_SIZE + EXT_TLV_LINK_SIZE + TLV_HDR_SIZE] = {};
+	uint8_t after[BODY_MAX] = {};
+
+	put_tlv_hdr(body, EXT_TLV_LINK,
+		    EXT_TLV_LINK_SIZE + TLV_HDR_SIZE + EXT_SUBTLV_RMT_ITF_ADDR_SIZE);
+	body[TLV_HDR_SIZE] = 1; /* point-to-point */
+	put_tlv_hdr(body + TLV_HDR_SIZE + EXT_TLV_LINK_SIZE, EXT_SUBTLV_RMT_ITF_ADDR,
+		    EXT_SUBTLV_RMT_ITF_ADDR_SIZE);
+	put_addr(after, MARK_ADDR);
+	check("Extended Link TLV running past the LSA", OPAQUE_TYPE_EXTENDED_LINK_LSA, body,
+	      sizeof(body), after, 4);
+}
+
+/*
+ * An Extended Prefix TLV shorter than its fixed part. What follows it would
+ * be its fixed part with MARK as the prefix, then an unknown sub-TLV that
+ * ends any sub-TLV walk.
+ */
+static void test_ext_prefix_short(void)
+{
+	uint8_t body[TLV_HDR_SIZE] = {};
+	uint8_t after[BODY_MAX] = {};
+
+	put_tlv_hdr(body, EXT_TLV_PREFIX, 0);
+	put_addr(after + 4, MARK_ADDR);
+	/* ends the walk an unchecked 0 - EXT_TLV_PREFIX_SIZE length would make */
+	put_tlv_hdr(after + EXT_TLV_PREFIX_SIZE, 0x7fff,
+		    0x10000 - EXT_TLV_PREFIX_SIZE - TLV_HDR_SIZE);
+	check("Extended Prefix TLV shorter than its fixed part", OPAQUE_TYPE_EXTENDED_PREFIX_LSA,
+	      body, sizeof(body), after, EXT_TLV_PREFIX_SIZE + TLV_HDR_SIZE);
+}
+
+/* A Traffic Engineering Router Address TLV whose value is past the LSA */
+static void test_te_router_addr_past_end(void)
+{
+	uint8_t body[TLV_HDR_SIZE] = {};
+	uint8_t after[4];
+
+	put_tlv_hdr(body, TE_TLV_ROUTER_ADDR, TE_LINK_SUBTLV_DEF_SIZE);
+	put_addr(after, MARK_ADDR);
+	check("TE Router Address TLV running past the LSA", OPAQUE_TYPE_TRAFFIC_ENGINEERING_LSA,
+	      body, sizeof(body), after, sizeof(after));
+}
+
+/* An empty Traffic Engineering Router Address TLV at the end of the LSA */
+static void test_te_router_addr_empty(void)
+{
+	uint8_t body[TLV_HDR_SIZE] = {};
+	uint8_t after[4];
+
+	put_tlv_hdr(body, TE_TLV_ROUTER_ADDR, 0);
+	put_addr(after, MARK_ADDR);
+	check("empty TE Router Address TLV at the end of the LSA",
+	      OPAQUE_TYPE_TRAFFIC_ENGINEERING_LSA, body, sizeof(body), after, sizeof(after));
+}
+
+/* A Traffic Engineering Link sub-TLV whose value is past the LSA */
+static void test_te_link_subtlv_past_end(void)
+{
+	uint8_t body[2 * TLV_HDR_SIZE] = {};
+	uint8_t after[4];
+
+	put_tlv_hdr(body, TE_TLV_LINK, TLV_HDR_SIZE);
+	put_tlv_hdr(body + TLV_HDR_SIZE, TE_LINK_SUBTLV_LINK_ID, TE_LINK_SUBTLV_DEF_SIZE);
+	put_addr(after, MARK_ADDR);
+	check("TE Link sub-TLV running past the LSA", OPAQUE_TYPE_TRAFFIC_ENGINEERING_LSA, body,
+	      sizeof(body), after, sizeof(after));
+}
+
+/* A well-formed Router Information LSA shows its PCE Address */
+static void test_ri_pce_address_valid(void)
+{
+	uint8_t body[2 * TLV_HDR_SIZE + PCE_ADDRESS_IPV4_SIZE] = {};
+	struct ospf_lsa *lsa;
+	char *out;
+
+	put_tlv_hdr(body, RI_TLV_PCE, TLV_HDR_SIZE + PCE_ADDRESS_IPV4_SIZE);
+	put_tlv_hdr(body + TLV_HDR_SIZE, RI_PCE_SUBTLV_ADDRESS, PCE_ADDRESS_IPV4_SIZE);
+	body[2 * TLV_HDR_SIZE + 1] = PCE_ADDRESS_IPV4;
+	put_addr(body + 2 * TLV_HDR_SIZE + 4, MARK_ADDR);
+
+	lsa = ospf_lsa_new_and_data(OSPF_LSA_HEADER_SIZE + sizeof(body));
+	opaque_header_set(lsa->data, OPAQUE_TYPE_ROUTER_INFORMATION_LSA, sizeof(body));
+	memcpy((uint8_t *)lsa->data + OSPF_LSA_HEADER_SIZE, body, sizeof(body));
+	out = show(lsa);
+	ospf_lsa_discard(lsa);
+	if (!strstr(out, "PCE Address: " MARK))
+		failed("well-formed PCE Address sub-TLV", out);
+	XFREE(MTYPE_TMP, out);
+}
+
+/*
+ * An empty PCE Address sub-TLV in the last word of the LSA: its address
+ * type is past the LSA (reported by an address sanitizer build).
+ */
+static void test_ri_pce_address_empty(void)
+{
+	uint8_t body[2 * TLV_HDR_SIZE] = {};
+	uint8_t after[PCE_ADDRESS_IPV4_SIZE] = { 0, PCE_ADDRESS_IPV4 };
+
+	put_tlv_hdr(body, RI_TLV_PCE, TLV_HDR_SIZE);
+	put_tlv_hdr(body + TLV_HDR_SIZE, RI_PCE_SUBTLV_ADDRESS, 0);
+	put_addr(after + 4, MARK_ADDR);
+	check("empty PCE Address sub-TLV at the end of the LSA",
+	      OPAQUE_TYPE_ROUTER_INFORMATION_LSA, body, sizeof(body), after, sizeof(after));
+}
+
+int main(int argc, char **argv)
+{
+	master = event_master_create(NULL);
+	cmd_init(1);
+	vty_init(master, false);
+	zlog_aux_init("NONE: ", LOG_ERR);
+	vrf_init(NULL, NULL, NULL, NULL);
+	ospf_master_init(master);
+	ospf_vty_init();
+	ospf_opaque_init();
+
+	/* Router Information LSAs are displayed once it is registered */
+	ospf_new_alloc(0, VRF_DEFAULT_NAME);
+	ospf_router_info_update_sr(false, NULL);
+
+	test_ext_link_valid();
+	test_ext_link_label_valid();
+	test_ext_link_short();
+	test_ext_link_empty_subtlv();
+	test_ext_link_past_end();
+	test_ext_prefix_short();
+	test_te_router_addr_past_end();
+	test_te_router_addr_empty();
+	test_te_link_subtlv_past_end();
+	test_ri_pce_address_valid();
+	test_ri_pce_address_empty();
+
+	printf("%s\nOK\n", TEST_NAME);
+	return 0;
+}
diff --git a/tests/ospfd/test_ospf_opaque_show.py b/tests/ospfd/test_ospf_opaque_show.py
new file mode 100644
index 0000000000..bd3b20407e
--- /dev/null
+++ b/tests/ospfd/test_ospf_opaque_show.py
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+import frrtest
+
+
+class TestOspfOpaqueShow(frrtest.TestMultiOut):
+    program = "./test_ospf_opaque_show"
+
+
+TestOspfOpaqueShow.okfail("opaque-show: opaque LSA TLVs are displayed within the LSA")