Skip to content

Apply FRR config before port config on SONiC (work around FRR defect) - #202

Open
chbmuc wants to merge 2 commits into
masterfrom
frr-workaround
Open

Apply FRR config before port config on SONiC (work around FRR defect)#202
chbmuc wants to merge 2 commits into
masterfrom
frr-workaround

Conversation

@chbmuc

@chbmuc chbmuc commented Jul 28, 2026

Copy link
Copy Markdown

Description

Deleting a firewall leaves bgpd with a dangling peer pointer, and the switch crashes with SIGSEGV moments later when the port is bounced.

Sonic.Apply() wrote the port and interface configuration first. When a firewall is deleted its port moves from Ports.Firewalls to Ports.Unprovisioned, so configureUnprovisionedPort() calls ensureNotRouted(), which deletes the INTERFACE entry and then waits until the router interface has disappeared from the ASIC. That removes the interface's link-local address, and FRR responds by clearing peer->su. Only afterwards did the FRR configuration withdraw the neighbor. With peer->su already unset, FRR can no longer find the peer's nexthop cache entry while deleting the peer, so bnc->nht_info is left pointing at the freed peer.

Moving the port into the PXE vlan bounces the interface immediately afterwards. FRR tracks link-local nexthops through interface events rather than through zebra nexthop registration, so bgp_nht_ifp_up() walks every nexthop cache entry whose ifindex matches the interface and calls evaluate_paths() on each, which dereferences the freed peer. Arming and firing therefore happen seconds apart within a single deprovisioning run.

Applying the FRR configuration first withdraws the neighbor while its interface still carries a link-local address, so FRR's cleanup path succeeds and no dangling pointer is left behind.

This works around an FRR defect fixed upstream, but not yet in the currently used Sonic Linux release.

Used AI-Tools ✨

  • TOOL used for crash and FRR analysis: Claude/Opus 5

Deleting a firewall leaves bgpd with a dangling peer pointer, and the
switch crashes with SIGSEGV moments later when the port is bounced.

Sonic.Apply() wrote the port and interface configuration first. When a
firewall is deleted its port moves from Ports.Firewalls to
Ports.Unprovisioned, so configureUnprovisionedPort() calls
ensureNotRouted(), which deletes the INTERFACE entry and then waits
until the router interface has disappeared from the ASIC. That removes
the interface's link-local address, and FRR responds by clearing
peer->su. Only afterwards did the FRR configuration withdraw the
neighbor. With peer->su already unset, FRR can no longer find the peer's
nexthop cache entry while deleting the peer, so bnc->nht_info is left
pointing at the freed peer.

Moving the port into the PXE vlan bounces the interface immediately
afterwards. FRR tracks link-local nexthops through interface events
rather than through zebra nexthop registration, so bgp_nht_ifp_up()
walks every nexthop cache entry whose ifindex matches the interface and
calls evaluate_paths() on each, which dereferences the freed peer.
Arming and firing therefore happen seconds apart within a single
deprovisioning run.

Applying the FRR configuration first withdraws the neighbor while its
interface still carries a link-local address, so FRR's cleanup path
succeeds and no dangling pointer is left behind.

This works around an FRR defect fixed upstream, but not yet in the
currently used Sonic Linux release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chbmuc
chbmuc requested a review from a team as a code owner July 28, 2026 10:45
@majst01
majst01 requested review from iljarotar and mreiger July 28, 2026 11:58
@mwennrich

Copy link
Copy Markdown
Contributor

doesn't work:

Jul 29 07:03:00 fel-wps1t01-r01leaf01 docker[387109]: {"time":"2026-07-29T05:03:00.191783304Z","level":"ERROR","source":{"function":"github.com/metal-stack/metal-core/cmd/internal/core.(*Core).ConstantlyReconfigureSwitch","file":"/work/cmd/internal/core/reconfigure-switch.go","line":52},"msg":"reconfiguration failed","error":"could not apply switch config: reloading frr-reload.service failed: start of frr-reload.service failed"}

Jul 29 06:56:20 fel-wps1t01-r01leaf01 systemd[1]: Failed to start Reload FRR.
Jul 29 06:56:27 fel-wps1t01-r01leaf01 systemd[1]: Starting Reload FRR...
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
Jul 29 06:56:29 fel-wps1t01-r01leaf01 docker[408677]: % VRF Vrf65 not active
...skipping...
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: line 24: % Unknown command[4]:  ipv6 nd ra-interval 6
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: line 27: % Unknown command[4]:  no ipv6 nd suppress-ra
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active
Jul 29 07:04:50 fel-wps1t01-r01leaf01 docker[425040]: % VRF Vrf65 not active

The last commit moved the FRR configuration in front of the port
configuration to keep bgpd from crashing when a firewall is deleted. The
dependency between the two appliers runs both ways, though: a tenant vrf
only becomes usable for FRR once the redis applier has written it to the
CONFIG_DB and vrfmgrd has created the vrf device. Referencing a vrf that
does not exist yet makes frr-reload reject the configuration with "% VRF
Vrf<vni> not active", and every interface line below the rejected
"interface ... vrf ..." is then parsed at the wrong node and reported as
an unknown command.

Because Apply() returned on the first FRR error, the redis applier never
ran, so the vrf was never created, so the next reload failed for the
same reason. reloadFrr() restores the previous frr.conf, so every
reconcile run re-rendered the same configuration and failed identically
- once a machine was provisioned into a new vrf, the switch never
converged again.

Ask the redis applier which order a run needs instead of fixing it. A
port that is about to be deprovisioned and still carries a routing
configuration without a vrf_name is a firewall port whose neighbor has
to be withdrawn from FRR before ensureNotRouted() takes its link-local
address away - only those runs need the FRR configuration first,
everything else keeps the historic order and never references a vrf too
early. The decision is made from the CONFIG_DB rather than from a
remembered configuration, so it also holds for the first run after a
restart. If it cannot be made, FRR goes first: a superfluous frr-reload
is cheaper than a crashing bgpd.

A run that deprovisions a firewall and adds a vrf at the same time
cannot satisfy both requirements. FRR is applied first there as well,
but a failure no longer keeps the port configuration from being applied
and the FRR configuration is retried once the vrfs exist. The protection
against the dangling peer pointer is kept either way, because frr-reload
applies deletions before additions, so the neighbor is withdrawn in the
first pass even when that pass later fails on the missing vrf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chbmuc

chbmuc commented Jul 29, 2026

Copy link
Copy Markdown
Author

Unfortunately it gets a bit more complicated in this case (see commit message).

@iljarotar

Copy link
Copy Markdown
Contributor

Deleting a firewall leaves bgpd with a dangling peer pointer, and the switch crashes with SIGSEGV moments later when the port is bounced.

Where exactly did this occur and what service crashes with SIGSEGV? swss, metal-core or bgpd? I'm not sure I fully understand the problem that this PR is addressing. It introduces some rather high complexity that would need thorough testing in a real environment. And it sets the metal-core up for intended errors in case one firewall gets deleted and a new one gets created within the same reconfiguration.

Does the issue at hand justify the complexity? If so I'd like to discuss if there is a solution that doesn't involve letting the metal-core run into errors on purpose.

@Sven-Ric is currently evaluating whether the mgmt-framework on edgecore SONiC is mature enough to configure BGP entirely via ConfigDB. If that's possible we can get rid of frr-reload completely.

@chbmuc

chbmuc commented Jul 31, 2026

Copy link
Copy Markdown
Author

The reason for this workaround is a known bug in FRR. It causes a SIGSEGV in bgpd when the interface of a underlay device (firewall) goes down before the FRR tables get updated. The consequence is a ~ 70 second network outage for all devices on that leaf switches, as the bug is triggered on both switches of the leaf pair.

The issue is critical for one of our customers, but of course a better way to fix this would be updating FRR with the bugfix (FRRouting/frr@51f3216bee). The bugfix is included in all FRR versions > 8.5. (Edgecore-SONiC still uses a patched FRR version 8.1/8.2).

@iljarotar

iljarotar commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

FRR versions cannot be updated on SONiC as far as I know. Only way I can think of is replacing the bgp container by a newer one but that sounds hacky and probably not intended.

What I don't understand is why did this bug only occur once and only on one leaf pair. From the description of the FRR bug it should be happening all the time whenever a firewall or machine gets deleted. We have the same SONiC version running on all SONiC leafs. It's a guess we can't really validate but I assume the crash was actually caused by a metal-core timeout that left the ConfigDB in a broken state. After that, the BGPD also crashed. But as we haven't cleaned up the config after the first crash it's possible that the same broken state led to the second crash as well.

As long as we don't have a way of reproducing the crash I'd opt for increasing the reconfigure timeout to reduce the risk of breaking the config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants