Commit 32fc143b57 for frr

commit 32fc143b575b6a02e960996a828a1d55c9e10740
Author: Dave Seddon <dave.seddon.ca@gmail.com>
Date:   Sun Nov 8 13:43:49 2026 -0800

    tests: add PIM-DM State Refresh prune indicator test (currently fails)

    RFC 3973 4.7.10 requires a State Refresh sent onto a pruned interface
    to carry the Prune Indicator, and 4.5.1 requires a router forwarding
    one onto such an interface to reset its Prune Timer to the prune
    holdtime.

    The test runs pim_send_staterefresh() and pim_staterefresh_recv()
    against an upstream and a dense-mode downstream interface, with
    pim_msg_send() stubbed to capture the P bit. Forward rows start with
    the Prune Timer about to expire, so a reset shows as its deadline
    moving out to the holdtime.

    The 4 rows with a pruned downstream interface fail today.

    Signed-off-by: Dave Seddon <dave.seddon.ca@gmail.com>

diff --git a/tests/pimd/subdir.am b/tests/pimd/subdir.am
index fdbd6f480a..5b0ae56acb 100644
--- a/tests/pimd/subdir.am
+++ b/tests/pimd/subdir.am
@@ -5,6 +5,7 @@ endif

 if PIMD
 check_PROGRAMS += tests/pimd/test_pim_jp_encode
+check_PROGRAMS += tests/pimd/test_pim_staterefresh
 endif
 tests_pimd_test_pim_jp_encode_CFLAGS = $(TESTS_CFLAGS) -DPIM_IPV=4
 tests_pimd_test_pim_jp_encode_CPPFLAGS = $(TESTS_CPPFLAGS) -I$(top_srcdir)/pimd
@@ -16,3 +17,14 @@ tests_pimd_test_pim_jp_encode_SOURCES = \
 	pimd/pim_tlv.c \
 	# end
 EXTRA_DIST += tests/pimd/test_pim_jp_encode.py
+
+tests_pimd_test_pim_staterefresh_CFLAGS = $(TESTS_CFLAGS) -DPIM_IPV=4
+tests_pimd_test_pim_staterefresh_CPPFLAGS = $(TESTS_CPPFLAGS) -I$(top_srcdir)/pimd
+tests_pimd_test_pim_staterefresh_LDADD = $(ALL_TESTS_LDADD)
+tests_pimd_test_pim_staterefresh_SOURCES = \
+	tests/pimd/test_pim_staterefresh.c \
+	pimd/pim_int.c \
+	pimd/pim_msg.c \
+	pimd/pim_tlv.c \
+	# end
+EXTRA_DIST += tests/pimd/test_pim_staterefresh.py
diff --git a/tests/pimd/test_pim_staterefresh.c b/tests/pimd/test_pim_staterefresh.c
new file mode 100644
index 0000000000..b4d99fbbc0
--- /dev/null
+++ b/tests/pimd/test_pim_staterefresh.c
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * PIM-DM State Refresh prune indicator.
+ *
+ * RFC 3973 4.7.10: a State Refresh sent on an interface whose (S,G) state
+ * is Pruned carries the Prune Indicator (P) bit, whether originated (4.5.2)
+ * or forwarded. 4.5.1: a router that forwards a State Refresh onto such an
+ * interface sets P and resets the Prune Timer to the prune holdtime. The
+ * interface's pruned state lives in bit 23 of the ifchannel flags
+ * (PIM_UPSTREAM_DM_FLAG_MASK_PRUNE).
+ *
+ * The rows run pim_send_staterefresh() (originate) or
+ * pim_staterefresh_recv() (forward) for an upstream interface and one
+ * dense-mode downstream interface with one neighbor. pim_msg_send() is
+ * stubbed to capture what would go on the wire, and the P bit is read from
+ * the trailing State Refresh header. Forward rows start with the downstream
+ * Prune Timer about to expire, so a restart moves its deadline out to the
+ * prune holdtime.
+ *
+ * Each case runs in a child process so that a sanitizer abort fails only
+ * that case.
+ *
+ * Copyright (C) 2026 Dave Seddon
+ */
+#include <zebra.h>
+
+#include <sys/wait.h>
+
+#include "printfrr.h"
+
+#include "pimd/pim_state_refresh.c"
+
+/* pimd globals not linked into this test */
+static struct pim_router test_router;
+struct pim_router *router = &test_router;
+const pim_addr qpim_all_pim_routers_addr;
+
+/* Test topology, set up in main() */
+static struct pim_instance test_pim;
+static struct pim_upstream test_up;
+static struct channel_oil test_oil;
+static struct interface *up_ifp, *down_ifp;
+static struct pim_ifchannel *test_ch; /* NULL: no (S,G) state downstream */
+static bool test_lost_assert;
+
+/* Messages "sent" on the downstream interface */
+static int sent_count;
+static uint8_t sent_msg[1000];
+static int sent_size;
+
+/* Stubs for pimd functions pim_state_refresh.c references */
+struct pim_upstream *pim_upstream_find(struct pim_instance *pim, pim_sgaddr *sg)
+{
+	return &test_up;
+}
+
+void pim_ifchannel_find(struct interface *ifp, pim_sgaddr *sg, struct pim_ifchannel **ch,
+			struct pim_ifchannel **chrpt)
+{
+	*ch = ifp == down_ifp ? test_ch : NULL;
+	*chrpt = NULL;
+}
+
+int pim_macro_ch_lost_assert(const struct pim_ifchannel *ch)
+{
+	return test_lost_assert;
+}
+
+bool pim_is_group_filtered(struct pim_interface *pim_ifp, pim_addr *grp, pim_addr *src)
+{
+	return false;
+}
+
+struct interface *pim_if_find_by_vif_index(struct pim_instance *pim, ifindex_t vif_index)
+{
+	return up_ifp;
+}
+
+void pim_dm_prune_iff_on_timer(struct event *t)
+{
+}
+
+int pim_msg_send(int fd, pim_addr src, pim_addr dst, uint8_t *pim_msg, int pim_msg_size,
+		 struct interface *ifp)
+{
+	assert(ifp == down_ifp);
+	assert(pim_msg_size > 0 && (size_t)pim_msg_size <= sizeof(sent_msg));
+	memcpy(sent_msg, pim_msg, pim_msg_size);
+	sent_size = pim_msg_size;
+	sent_count++;
+	return 0;
+}
+
+/* Test table */
+enum kind { POSITIVE, NEGATIVE, BOUNDARY, CORNER };
+static const char *const kind2str[] = { "positive", "negative", "boundary", "corner" };
+
+enum path {
+	SEND, /* pim_send_staterefresh(): originate */
+	RECV, /* pim_staterefresh_recv(): forward */
+};
+
+/* Child exit codes, distinct from the sanitizers' error exit code (1). */
+#define EXIT_MATCH    10
+#define EXIT_MISMATCH 11
+
+#define PRUNED PIM_UPSTREAM_DM_FLAG_MASK_PRUNE
+#define NO_CH  0xffffffffU /* no (S,G) ifchannel on the downstream interface */
+
+#define PRUNE_HOLDTIME 210 /* seconds */
+#define NEAR_EXPIRY    1   /* seconds left on the Prune Timer before a forward */
+
+struct tcase {
+	const char *description;
+	enum kind kind;
+	enum path path;
+	uint32_t ch_flags; /* downstream ifchannel flags, or NO_CH */
+	bool lost_assert;  /* downstream ifchannel lost the assert */
+	uint8_t ttl;	   /* RECV: TTL of the received State Refresh */
+	/* expected */
+	bool sent;  /* a State Refresh went out downstream */
+	bool p;	    /* its Prune Indicator */
+	bool timer; /* RECV: Prune Timer reset to the prune holdtime */
+};
+
+static const struct tcase tcases[] = {
+	{ "originate: pruned interface sets P", POSITIVE, SEND, PRUNED | 0x5, false, 0, true, true,
+	  false },
+	{ "originate: forwarding interface clears P", NEGATIVE, SEND, 0x5, false, 0, true, false,
+	  false },
+	{ "originate: no (S,G) state clears P", NEGATIVE, SEND, NO_CH, false, 0, true, false,
+	  false },
+	{ "originate: prune flag alone sets P", BOUNDARY, SEND, PRUNED, false, 0, true, true,
+	  false },
+	{ "originate: every flag but prune clears P", CORNER, SEND, ~PRUNED, false, 0, true, false,
+	  false },
+	{ "forward: pruned interface sets P and restarts the prune timer", POSITIVE, RECV,
+	  PRUNED | 0x5, false, 64, true, true, true },
+	{ "forward: forwarding interface clears P, prune timer untouched", NEGATIVE, RECV, 0x5,
+	  false, 64, true, false, false },
+	{ "forward: no (S,G) state clears P", NEGATIVE, RECV, NO_CH, false, 64, true, false,
+	  false },
+	{ "forward: TTL 2 is forwarded with the prune state", BOUNDARY, RECV, PRUNED, false, 2,
+	  true, true, true },
+	{ "forward: TTL 1 is not forwarded", BOUNDARY, RECV, PRUNED, false, 1, false, false,
+	  false },
+	{ "forward: interface that lost the assert is skipped", CORNER, RECV, PRUNED, true, 64,
+	  false, false, false },
+};
+
+static int run_case(const struct tcase *tc)
+{
+	struct pim_ifchannel ch = {};
+	struct pim_staterefresh_header *hdr;
+	unsigned long remain = 0; /* RECV: msec left on the downstream Prune Timer */
+	bool timer = false;
+	int rc = EXIT_MATCH;
+
+	if (tc->ch_flags != NO_CH) {
+		ch.flags = tc->ch_flags;
+		ch.prune_holdtime = PRUNE_HOLDTIME;
+		test_ch = &ch;
+	}
+	if (tc->path == RECV && test_ch)
+		/* A Prune Timer about to expire, as set by an earlier refresh */
+		event_add_timer(router->master, pim_dm_prune_iff_on_timer, test_ch, NEAR_EXPIRY,
+				&test_ch->t_ifjoin_expiry_timer);
+	test_lost_assert = tc->lost_assert;
+
+	if (tc->path == SEND) {
+		pim_send_staterefresh(&test_up);
+	} else {
+		/* The State Refresh as the upstream neighbor sends it */
+		struct pim_interface *pim_up = up_ifp->info;
+		uint8_t msg[1000];
+		int len;
+
+		len = pim_staterefresh_build_msg(msg, sizeof(msg), up_ifp, test_up.sg.grp,
+						 test_up.sg.src, pim_up->primary_address, 0, 0, 0,
+						 IPV4_MAX_BITLEN, tc->ttl, false, false, true, 0,
+						 60);
+		pim_staterefresh_recv(up_ifp, pim_up->primary_address, msg + PIM_MSG_HEADER_LEN,
+				      len - PIM_MSG_HEADER_LEN);
+		/* Restarted: the deadline moved out to the prune holdtime */
+		if (test_ch && test_ch->t_ifjoin_expiry_timer)
+			remain = event_timer_remain_msec(test_ch->t_ifjoin_expiry_timer);
+		timer = remain > (PRUNE_HOLDTIME - NEAR_EXPIRY) * 1000UL;
+	}
+
+	if (sent_count) {
+		/* The State Refresh header is the last 4 bytes */
+		hdr = (struct pim_staterefresh_header *)(sent_msg + sent_size - sizeof(*hdr));
+		printf("    sent %d, P %u", sent_count, hdr->p);
+		if (!tc->sent || hdr->p != tc->p)
+			rc = EXIT_MISMATCH;
+	} else {
+		printf("    not sent");
+		if (tc->sent)
+			rc = EXIT_MISMATCH;
+	}
+	if (tc->path == RECV)
+		printf(", prune timer %lums left", remain);
+	printf("\n");
+	if (timer != tc->timer)
+		rc = EXIT_MISMATCH;
+
+	return rc;
+}
+
+static struct interface *test_if(const char *name, ifindex_t ifindex, const char *addr)
+{
+	struct interface *ifp = if_get_by_name(name, VRF_DEFAULT, VRF_DEFAULT_NAME);
+	struct pim_interface *pim_ifp = XCALLOC(MTYPE_TMP, sizeof(*pim_ifp));
+	struct pim_neighbor *neigh = XCALLOC(MTYPE_TMP, sizeof(*neigh));
+
+	if_set_index(ifp, ifindex);
+	pim_ifp->pim = &test_pim;
+	pim_ifp->pim_enable = true;
+	pim_ifp->pim_mode = PIM_MODE_DENSE;
+	inet_pton(AF_INET, addr, &pim_ifp->primary_address);
+	pim_ifp->pim_neighbor_list = list_new();
+	inet_pton(AF_INET, addr, &neigh->source_addr);
+	listnode_add(pim_ifp->pim_neighbor_list, neigh);
+	ifp->info = pim_ifp;
+	return ifp;
+}
+
+int main(int argc, char **argv)
+{
+	int failures = 0;
+
+	router->master = event_master_create(NULL);
+	vrf_init(NULL, NULL, NULL, NULL);
+
+	up_ifp = test_if("up0", 1, "10.0.1.1");
+	down_ifp = test_if("down0", 2, "10.0.2.1");
+
+	test_pim.staterefresh_time = 60;
+	test_up.pim = &test_pim;
+	inet_pton(AF_INET, "232.1.1.1", &test_up.sg.grp);
+	inet_pton(AF_INET, "10.0.0.1", &test_up.sg.src);
+	test_up.channel_oil = &test_oil;
+	test_oil.iif.index = 1;
+	test_up.rpf.source_nexthop.interface = up_ifp;
+
+	printf("PIM-DM State Refresh prune indicator tests\n\n");
+
+	for (size_t i = 0; i < array_size(tcases); i++) {
+		const struct tcase *tc = &tcases[i];
+		char why[96] = "";
+		int status;
+		pid_t pid;
+
+		fflush(stdout);
+		pid = fork();
+		if (pid == 0) {
+			int rc = run_case(tc);
+
+			fflush(stdout);
+			_exit(rc);
+		}
+
+		waitpid(pid, &status, 0);
+		if (WIFSIGNALED(status))
+			snprintfrr(why, sizeof(why), " (crashed: signal %d)", WTERMSIG(status));
+		else if (WEXITSTATUS(status) == EXIT_MISMATCH)
+			snprintfrr(why, sizeof(why), " (expected %s%s%s)",
+				   tc->sent ? "sent" : "not sent",
+				   tc->sent ? (tc->p ? " with P" : " without P") : "",
+				   tc->path == RECV
+					   ? (tc->timer ? ", prune timer" : ", no prune timer")
+					   : "");
+		else if (WEXITSTATUS(status) != EXIT_MATCH)
+			snprintfrr(why, sizeof(why), " (abnormal exit %d)", WEXITSTATUS(status));
+
+		printf("[%-8s] %s ... %s%s\n", kind2str[tc->kind], tc->description,
+		       why[0] ? "failed" : "OK", why);
+		if (why[0])
+			failures++;
+	}
+
+	printf("\n%d of %zu cases failed\n", failures, array_size(tcases));
+	return failures ? 1 : 0;
+}
diff --git a/tests/pimd/test_pim_staterefresh.py b/tests/pimd/test_pim_staterefresh.py
new file mode 100644
index 0000000000..e961ae3acd
--- /dev/null
+++ b/tests/pimd/test_pim_staterefresh.py
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+import frrtest
+
+
+class TestPimStateRefresh(frrtest.TestMultiOut):
+    program = "./test_pim_staterefresh"
+
+
+TestPimStateRefresh.okfail("originate: pruned interface sets P")
+TestPimStateRefresh.okfail("originate: forwarding interface clears P")
+TestPimStateRefresh.okfail("originate: no (S,G) state clears P")
+TestPimStateRefresh.okfail("originate: prune flag alone sets P")
+TestPimStateRefresh.okfail("originate: every flag but prune clears P")
+TestPimStateRefresh.okfail(
+    "forward: pruned interface sets P and restarts the prune timer"
+)
+TestPimStateRefresh.okfail(
+    "forward: forwarding interface clears P, prune timer untouched"
+)
+TestPimStateRefresh.okfail("forward: no (S,G) state clears P")
+TestPimStateRefresh.okfail("forward: TTL 2 is forwarded with the prune state")
+TestPimStateRefresh.okfail("forward: TTL 1 is not forwarded")
+TestPimStateRefresh.okfail("forward: interface that lost the assert is skipped")