Commit 9f2c7b312f for frr

commit 9f2c7b312f752f444d256d6022b6bd6d3c6e7f9f
Author: Alejandro Pacheco <alepacheco99@gmail.com>
Date:   Wed Oct 14 02:00:14 2026 +0200

    bgpd: Do not free the parsed attr's extra in allowas-in route-map

    When allowas-in is bound to a route-map, bgp_update() runs the route-map
    on a stack copy of the received attr and flushes that copy afterwards.
    The copy was a plain struct assignment, so it shared attr->extra with the
    parsed attr, and bgp_attr_flush() (which frees attr->extra) released it
    while the parsed attr still pointed at it. Any UPDATE whose attributes
    allocate extra, e.g. COMMUNITIES received with the Partial bit, then hit a
    use-after-free later in bgp_update() and a double free when the parsed
    attr is released.

    Make the copy with bgp_attr_dup_into(), as the regular inbound route-map
    path already does, so the flush only frees the copy's own extra. The
    bgp_attr_extra_discard() that followed the flush was a no-op and goes
    away.

    bgp_unreach_info_add() made the same plain copy of a caller's attr before
    interning it; its only caller passes NULL today, but convert it too so no
    plain struct attr copy is left in bgpd.

    Add tests/bgpd/test_allowas_rmap, which drives such an UPDATE through
    bgp_update(); built with --enable-address-sanitizer it reports the
    use-after-free without this change.

    Fixes: e611fde905b4 ("bgpd: Discard temporary extra attributes when using route-map with allowas_in")
    Signed-off-by: Alejandro Pacheco <alepacheco99@gmail.com>

diff --git a/bgpd/bgp_route.c b/bgpd/bgp_route.c
index de66ffd671..96b79cadf0 100644
--- a/bgpd/bgp_route.c
+++ b/bgpd/bgp_route.c
@@ -6125,11 +6125,18 @@ void bgp_update(struct peer *peer, const struct prefix *p, uint32_t addpath_id,
 			 * `set` actions in the route-map do not leak into the
 			 * downstream inbound processing.
 			 */
-			struct attr local_attr = *attr;
+			struct attr local_attr;
 			struct bgp_path_info rmap_path;
 			struct bgp_path_info_extra allowas_extra;
 			route_map_result_t rmap_ret;

+			/*
+			 * A plain struct copy would share attr->extra with the
+			 * parsed attr, and the flush below would then free it
+			 * from under every later user of attr.
+			 */
+			bgp_attr_dup_into(&local_attr, attr);
+
 			prep_for_rmap_apply(&rmap_path, &allowas_extra, dest, NULL, peer, NULL,
 					    &local_attr);

@@ -6153,9 +6160,9 @@ void bgp_update(struct peer *peer, const struct prefix *p, uint32_t addpath_id,

 			/* Always flush; the route-map is for matching only and
 			 * we never carry forward any of its `set` modifications.
+			 * This also frees local_attr's own copy of extra.
 			 */
 			bgp_attr_flush(&local_attr);
-			bgp_attr_extra_discard(&local_attr);

 			if (rmap_ret == RMAP_PERMITMATCH)
 				allowas_in_apply = true;
diff --git a/bgpd/bgp_unreach.c b/bgpd/bgp_unreach.c
index 5f17959f6a..9b56f01028 100644
--- a/bgpd/bgp_unreach.c
+++ b/bgpd/bgp_unreach.c
@@ -907,7 +907,7 @@ int bgp_unreach_info_add(struct bgp *bgp, afi_t afi, struct bgp_unreach_nlri *nl
 				   bgp->name_pretty, &nlri->prefix);
 		/* Initialize attributes (no TLV data in attr) */
 		if (attr) {
-			attr_new = *attr;
+			bgp_attr_dup_into(&attr_new, attr);
 		} else {
 			/* Set default attributes for locally originated route */
 			bgp_attr_default_set(&attr_new, bgp, BGP_ORIGIN_INCOMPLETE);
diff --git a/tests/.gitignore b/tests/.gitignore
index f5bbc3f68e..2a67351f4a 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -4,6 +4,7 @@
 frr-northbound.proto
 frr_northbound*
 .pytest_cache
+/bgpd/test_allowas_rmap
 /bgpd/test_aspath
 /bgpd/test_attr_parse
 /bgpd/test_bgp_table
diff --git a/tests/bgpd/subdir.am b/tests/bgpd/subdir.am
index 0c62bb79e2..6864b2ac14 100644
--- a/tests/bgpd/subdir.am
+++ b/tests/bgpd/subdir.am
@@ -89,3 +89,13 @@ tests_bgpd_test_attr_parse_CPPFLAGS = $(TESTS_CPPFLAGS)
 tests_bgpd_test_attr_parse_LDADD = $(BGP_TEST_LDADD)
 tests_bgpd_test_attr_parse_SOURCES = tests/bgpd/test_attr_parse.c
 EXTRA_DIST += tests/bgpd/test_attr_parse.py
+
+
+if BGPD
+check_PROGRAMS += tests/bgpd/test_allowas_rmap
+endif
+tests_bgpd_test_allowas_rmap_CFLAGS = $(TESTS_CFLAGS)
+tests_bgpd_test_allowas_rmap_CPPFLAGS = $(TESTS_CPPFLAGS)
+tests_bgpd_test_allowas_rmap_LDADD = $(BGP_TEST_LDADD)
+tests_bgpd_test_allowas_rmap_SOURCES = tests/bgpd/test_allowas_rmap.c
+EXTRA_DIST += tests/bgpd/test_allowas_rmap.py
diff --git a/tests/bgpd/test_allowas_rmap.c b/tests/bgpd/test_allowas_rmap.c
new file mode 100644
index 0000000000..09338bfbdd
--- /dev/null
+++ b/tests/bgpd/test_allowas_rmap.c
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Regression test for the use-after-free in the "allowas-in ... route-map"
+ * inbound path.
+ *
+ * bgp_update() runs the allowas-in route-map on a stack copy of the parsed
+ * attr and then calls bgp_attr_flush() on that copy. When the copy is made
+ * with a plain struct assignment it shares attr->extra with the parsed attr,
+ * so the flush frees the extra out from under the parsed attr, and the rest
+ * of bgp_update() reads freed memory.
+ *
+ * The attr below carries COMMUNITIES with the Partial bit set, which is enough
+ * to make bgp_attr_parse() allocate attr->extra (partial_attrs). The test
+ * drives one UPDATE through bgp_update() and then releases the parsed attr the
+ * way bgp_update_receive() does. Under AddressSanitizer this aborts before the
+ * fix; after it, it prints OK.
+ */
+
+#include <zebra.h>
+
+#include "qobj.h"
+#include "vty.h"
+#include "stream.h"
+#include "privs.h"
+#include "routemap.h"
+
+#include "bgpd/bgpd.h"
+#include "bgpd/bgp_attr.h"
+#include "bgpd/bgp_community_alias.h"
+#include "bgpd/bgp_route.h"
+#include "bgpd/bgp_vty.h"
+#include "bgpd/bgp_network.h"
+#include "bgpd/bgp_label.h"
+
+/* need these to link in libbgp */
+struct zebra_privs_t bgpd_privs = {};
+struct event_loop *master;
+
+static const uint8_t attrs[] = {
+	0x40, 0x01, 0x01, 0x00,				      /* ORIGIN igp */
+	0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe9, /* AS_PATH 65001 */
+	0x40, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x02,	      /* NEXT_HOP 10.0.0.2 */
+	0xe0, 0x08, 0x04, 0xfd, 0xe9, 0x00, 0x01,	      /* COMMUNITIES, Partial */
+};
+
+static struct bgp *bgp;
+static as_t asn = 65000;
+
+int main(void)
+{
+	struct peer *peer;
+	struct attr attr = {};
+	struct bgp_nlri mp_update = {};
+	struct bgp_nlri mp_withdraw = {};
+	struct route_map *map;
+	struct prefix p;
+	enum bgp_attr_parse_ret ret;
+	int afi, safi;
+
+	qobj_init();
+	cmd_init(0);
+	bgp_vty_init();
+	master = event_master_create("test allowas rmap");
+	bgp_master_init(master, BGP_SOCKET_SNDBUF_SIZE, list_new());
+	vrf_init(NULL, NULL, NULL, NULL);
+	bgp_option_set(BGP_OPT_NO_LISTEN);
+	bgp_attr_init();
+	bgp_labels_init();
+	bgp_community_alias_init();
+	route_map_init();
+
+	if (bgp_get(&bgp, &asn, NULL, BGP_INSTANCE_TYPE_DEFAULT, NULL, ASNOTATION_PLAIN) < 0)
+		return 1;
+
+	peer = peer_create_accept(bgp, NULL);
+	peer->host = (char *)"foo";
+	peer->connection = bgp_peer_connection_new(peer, NULL, UNKNOWN);
+	peer->connection->status = Established;
+	peer->connection->curr = stream_new(BGP_MAX_PACKET_SIZE);
+	peer->sort = BGP_PEER_EBGP;
+	peer->as = 65001;
+	peer->local_as = 65000;
+	peer->as_type = AS_SPECIFIED;
+	SET_FLAG(peer->cap, PEER_CAP_AS4_RCV);
+	SET_FLAG(peer->cap, PEER_CAP_AS4_ADV);
+	for (afi = AFI_IP; afi < AFI_MAX; afi++)
+		for (safi = SAFI_UNICAST; safi < SAFI_MAX; safi++) {
+			peer->afc[afi][safi] = 1;
+			peer->afc_adv[afi][safi] = 1;
+			peer->afc_nego[afi][safi] = 1;
+		}
+
+	/* neighbor foo allowas-in 3 route-map RM; route-map RM permit 10 */
+	map = route_map_get("RM");
+	route_map_index_get(map, RMAP_PERMIT, 10);
+	SET_FLAG(peer->af_flags[AFI_IP][SAFI_UNICAST], PEER_FLAG_ALLOWAS_IN);
+	peer->allowas_in[AFI_IP][SAFI_UNICAST] = 3;
+	peer->allowas_in_rmap[AFI_IP][SAFI_UNICAST].name = XSTRDUP(MTYPE_ROUTE_MAP_NAME, "RM");
+	peer->allowas_in_rmap[AFI_IP][SAFI_UNICAST].rmap = map;
+
+	stream_reset(peer->connection->curr);
+	stream_write(peer->connection->curr, attrs, sizeof(attrs));
+	stream_set_getp(peer->connection->curr, 0);
+	ret = bgp_attr_parse(peer->connection, &attr, sizeof(attrs), &mp_update, &mp_withdraw,
+			     true);
+	if (ret != BGP_ATTR_PARSE_PROCEED) {
+		printf("allowas-in-rmap-uaf: attr parse failed\nfailed!\n");
+		return 1;
+	}
+
+	/* as bgp_nlri_parse_ip() does before handing the NLRI to bgp_update() */
+	memset(&attr.attr_intern_reuse, 0, sizeof(attr.attr_intern_reuse));
+	attr.attr_intern_reuse.parsed_attr = &attr;
+
+	str2prefix("192.0.2.0/24", &p);
+	bgp_update(peer, &p, 0, &attr, AFI_IP, SAFI_UNICAST, ZEBRA_ROUTE_BGP, BGP_ROUTE_NORMAL,
+		   NULL, NULL, 0, 0, NULL, NULL);
+
+	bgp_attr_unintern_sub(&attr);
+
+	printf("%s: %s\nOK\n", "allowas-in-rmap-uaf",
+	       "UPDATE through allowas-in route-map does not free the parsed attr's extra");
+	return 0;
+}
diff --git a/tests/bgpd/test_allowas_rmap.py b/tests/bgpd/test_allowas_rmap.py
new file mode 100644
index 0000000000..e394879561
--- /dev/null
+++ b/tests/bgpd/test_allowas_rmap.py
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+import frrtest
+
+
+class TestAllowasRmap(frrtest.TestMultiOut):
+    program = "./test_allowas_rmap"
+
+
+TestAllowasRmap.okfail(
+    "allowas-in-rmap-uaf: UPDATE through allowas-in route-map does not free the parsed attr's extra"
+)