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

Reply via email to