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"
+)