The EVPN protocol is prepared to start before its VXLAN tunnel device
exists: evpn_start() returns PS_START to wait for the interface and
evpn_if_notify() brings the protocol up once it appears. But evpn_start()
also attaches the VLAN request topic, which is keyed by the name of the
bridge the tunnel device is enslaved to.
The configuration parser resolves the tunnel device with if_get_by_name(),
which returns a placeholder for an interface that does not exist yet. Such
a placeholder has no master, so attaching the topic dereferences a NULL
pointer and BIRD crashes during configuration commit.
Attach the topic only once the bridge is known, and retry from
evpn_started(), which already runs when the interface shows up.
While the topic is unattached there is nothing to publish to, so skip
publishing VLAN requests and re-issue them when attaching later. This also
covers evpn_shutdown(), which withdraws VLANs unconditionally and would
otherwise trip the assertion in ps_publish() on the way out.
Reproducible on both branches with any evpn protocol whose tunnel device
is created after BIRD starts, which happens with a restarted daemon or a
rebuilt bridge.
---
Rooted in thread-next; cherry-picks onto master without conflicts if you
want it in 2.x as well.
Reproducer, a bridge and an evpn protocol pointing at a VXLAN device that
is never created:
ip link add lo0 type dummy
ip -6 addr add fd00::1/128 dev lo0 nodad
ip link set lo0 up
ip link add name br-red type bridge
ip link set br-red up
bird -f -c bird.conf
protocol bridge bridge_red {
eth { table etab_red; export all; };
bridge device "br-red";
}
protocol evpn evpn_red {
eth { table etab_red; };
evpn;
encapsulation vxlan {
tunnel device "vx-red";
router address fd00::1;
};
rd 10.0.0.1:100;
route target (rt, 1, 100);
vni 100;
}
Without this commit, at configuration commit:
#1 ps_get_topic (name=0x10 <error: Cannot access memory at address 0x10>)
at lib/pubsub.c:94
#2 ps_attach_topic (name=0x10 ...) at ./lib/pubsub.h:64
#3 evpn_start (P=...) at proto/evpn/evpn.c:1092
#4 proto_start ... #13 main
0x10 is offsetof(struct iface, name) applied to a NULL master.
Built and run at the branch heads of 2026-08-08, each against its own
unpatched parent, with the device missing and with it appearing late:
3.3.0 and 2.19.0 segfault before the fix, run after it, and evpn_red
reaches up when the device shows up. Tarballs 3.3.1 and 2.19.2 match their
heads. Rebasing onto the current thread-next moved hunk offsets only.
The path where the device exists at startup is unaffected: a three-leaf
two-tenant fabric on patched 3.3.1 still passes its reachability, tenant
isolation and MAC learning suite.
Nothing added under netlab/ in bird-tools; happy to do so if you want this
case covered there.
proto/evpn/evpn.c | 41 ++++++++++++++++++++++++++++++++++++++++-
proto/evpn/evpn.h | 1 +
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/proto/evpn/evpn.c b/proto/evpn/evpn.c
index 32326ea..c8487d9 100644
--- a/proto/evpn/evpn.c
+++ b/proto/evpn/evpn.c
@@ -750,6 +750,13 @@ evpn_remove_vlan(struct evpn_proto *p, struct evpn_vlan *v)
static void
evpn_publish_vlan_request(struct evpn_proto *p, struct vlan_request *req, bool
update, int vlan_count)
{
+ /* The topic is attached only once the tunnel device and its bridge are
known,
+ so there may be nothing to publish to yet. Requests skipped here are
+ re-issued from evpn_started(); the withdraw on shutdown has nothing to
+ withdraw. */
+ if (!p->vlan_pub_attached)
+ return;
+
struct evpn_encap *encap = evpn_get_encap(p);
/* Fill header */
@@ -1068,6 +1075,28 @@ evpn_init(struct proto_config *CF)
return P;
}
+/*
+ * Attach the VLAN request topic, which is keyed by the name of the bridge the
+ * tunnel device is enslaved to.
+ *
+ * Neither the tunnel device nor its bridge has to exist when the protocol
+ * starts: evpn_start() returns PS_START precisely to wait for the interface,
+ * and evpn_if_notify() picks it up later. So this may be called before the
+ * bridge is known, and is a no-op until it is.
+ */
+static void
+evpn_attach_vlan_topic(struct evpn_proto *p, struct evpn_encap *encap)
+{
+ if (p->vlan_pub_attached)
+ return;
+
+ if (!encap->tunnel_dev || !encap->tunnel_dev->master)
+ return;
+
+ ps_attach_topic(p->vlan_pub, &vlan_requests,
encap->tunnel_dev->master->name);
+ p->vlan_pub_attached = true;
+}
+
static int
evpn_start(struct proto *P)
{
@@ -1089,7 +1118,8 @@ evpn_start(struct proto *P)
struct evpn_encap *encap = evpn_get_encap(p);
p->vlan_pub = ps_publisher_new(p->p.pool, evpn_vlan_subscribe_hook, p);
- ps_attach_topic(p->vlan_pub, &vlan_requests,
encap->tunnel_dev->master->name);
+ p->vlan_pub_attached = false;
+ evpn_attach_vlan_topic(p, encap);
init_list(&p->vlans);
memset(&p->vlan_tag_hash, 0, sizeof(p->vlan_tag_hash));
@@ -1123,6 +1153,15 @@ evpn_started(struct evpn_proto *p, struct iface *i)
if (!evpn_validate_iface_attrs(p, i))
return;
+ /* The bridge may only become known now; VLAN requests made before the topic
+ was attached went nowhere, so re-issue them once it is. */
+ if (!p->vlan_pub_attached)
+ {
+ evpn_attach_vlan_topic(p, evpn_get_encap(p));
+ if (p->vlan_pub_attached)
+ evpn_request_vlans(p);
+ }
+
proto_notify_state(&p->p, PS_UP);
evpn_announce_imet(p, EVPN_ROOT_VLAN(p), 1);
diff --git a/proto/evpn/evpn.h b/proto/evpn/evpn.h
index 8103dfb..3fe527b 100644
--- a/proto/evpn/evpn.h
+++ b/proto/evpn/evpn.h
@@ -81,6 +81,7 @@ struct evpn_proto {
HASH(struct evpn_vlan) vlan_tag_hash;
HASH(struct evpn_vlan) vlan_vid_hash;
ps_publisher *vlan_pub;
+ bool vlan_pub_attached; /* VLAN topic attached (needs the
bridge name) */
};
struct evpn_encap {
base-commit: 06ca9bf5c588df6c0587eb64bd375eb0ade526ab
--
2.55.0