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