Summary
FRR bgpd appears to apply treat-as-withdraw to an attribute length framing error even when the malformed UPDATE contains no reachable NLRI. In that case, RFC 7606 Section 5.2 requires session reset, because there is no reachable NLRI for treat-as-withdraw to act on.
This was reproduced against FRR 10.6.1. bgpd detected the attribute length error, logged that the malformed attribute was being treated as a withdrawal, and kept the BGP session Established.
RFC 7606 Behavior
The tested UPDATE has:
Withdrawn Routes Length = 0
Total Path Attribute Length = 4
Path Attribute = ORIGIN, declared length 32
IPv4 reachable NLRI = empty
MP_REACH_NLRI = absent
This is a path attribute length/framing conflict: the enclosed ORIGIN attribute declares a length that exceeds the Total Path Attribute Length.
RFC 7606 Section 4 says that this kind of Total Attribute Length vs. enclosed attribute length conflict normally uses treat-as-withdraw, relying on the Total Attribute Length to locate the NLRI field.
However, RFC 7606 Section 5.2 says that if an UPDATE contains path attributes other than MP_UNREACH_NLRI and does not encode any reachable NLRI, then if any path attribute error is encountered and the error handling is not attribute discard, the session reset approach MUST be used.
So the expected chain is:
attribute length framing error
-> normally treat-as-withdraw per RFC 7606 Section 4
-> but UPDATE has no reachable NLRI
-> RFC 7606 Section 5.2 requires session reset
Steps to Reproduce
- Configure FRR bgpd with an IPv4 unicast peer:
router bgp 65001
bgp router-id 127.0.0.1
neighbor 127.0.0.2 remote-as 65002
neighbor 127.0.0.2 update-source 127.0.0.1
!
address-family ipv4 unicast
neighbor 127.0.0.2 activate
exit-address-family
!
-
Establish the BGP session with OPEN and KEEPALIVE.
-
Send an UPDATE with:
Withdrawn Routes Length = 0
Total Path Attribute Length = 4
Attribute bytes = 40 01 20 00
flags = 0x40
type = 0x01 (ORIGIN)
declared attribute length = 0x20 (32)
IPv4 reachable NLRI = empty
The individual ORIGIN attribute length exceeds the total path attribute block, triggering the attr_endp > endp check in bgp_attr_parse().
Expected Behavior
FRR should detect the attribute length framing error, then notice that the UPDATE carries no reachable NLRI:
IPv4 reachable NLRI length == 0
MP_REACH_NLRI absent or empty
Since there is no reachable NLRI to withdraw, RFC 7606 Section 5.2 requires session reset. bgpd should send an UPDATE Message Error NOTIFICATION and transition the session out of Established.
Actual Behavior
FRR detects the malformed attribute but treats the UPDATE as a withdrawal and keeps the session up:
BGP type 1 length 32 is too large, attribute total length is 4
Attribute ORIGIN, parse error - treating as withdrawal
rcvd UPDATE with errors in attr(s)!! Withdrawing route.
In dynamic testing, no NOTIFICATION was received and the TCP connection remained open. The bgpd log showed a peer KEEPALIVE after the malformed UPDATE, confirming that the session remained Established.
Source Analysis
In bgp_attr_parse() (bgpd/bgp_attr.c), the attribute length overflow path returns BGP_ATTR_PARSE_WITHDRAW:
if (attr_endp > endp) {
flog_warn(...,
"%s: BGP type %d length %d is too large, attribute total length is %d...",
peer->host, type, length, size, attr_endp, endp);
/* RFC 7606 Section 4: treat-as-withdraw for attribute length conflict */
flog_warn(...,
"%s: Attribute %s, parse error - treating as withdrawal",
peer->host, lookup_msg(attr_str, type, NULL));
ret = BGP_ATTR_PARSE_WITHDRAW;
stream_forward_getp(BGP_INPUT(connection), endp - BGP_INPUT_PNT(connection));
goto done;
}
The caller, bgp_update_receive() (bgpd/bgp_packet.c), treats BGP_ATTR_PARSE_WITHDRAW as a signal to process reachable NLRI as withdrawals:
attr_parse_ret = bgp_attr_parse(peer, &attr, attribute_len,
&nlris[NLRI_MP_UPDATE],
&nlris[NLRI_MP_WITHDRAW]);
...
update_len = end - stream_pnt(s);
if (update_len && attribute_len &&
attr_parse_ret != BGP_ATTR_PARSE_MISSING_MANDATORY) {
nlris[NLRI_UPDATE].afi = AFI_IP;
nlris[NLRI_UPDATE].safi = SAFI_UNICAST;
nlris[NLRI_UPDATE].nlri = stream_pnt(s);
nlris[NLRI_UPDATE].length = update_len;
stream_forward_getp(s, update_len);
}
When update_len == 0 and nlris[NLRI_MP_UPDATE].length == 0, there is no reachable NLRI to withdraw. I did not find a later check that applies RFC 7606 Section 5.2 to reset the session in this no-reachable-NLRI case.
BIRD Comparison
BIRD has an explicit RFC 7606 Section 5.2 guard for this condition in bgp_decode_attrs():
withdraw:
/* RFC 7606 5.2 - handle missing NLRI during errors */
if (!s->ip_reach_len && !s->mp_reach_len)
bgp_parse_error(s, 1);
s->err_withdraw = 1;
return NULL;
This check prevents treat-as-withdraw from being used when the malformed UPDATE has no reachable NLRI.
Dynamic Verification
This was reproduced against:
Earlier verification produced the same result in 3 independent runs. I also re-tested the issue on 2026-07-05 with a fresh bgpd instance and the same malformed UPDATE.
Retest PoC observation:
Received KEEPALIVE - session is Established
Sending malformed UPDATE (attribute overflow, NO NLRI)
NOTIFICATION received: False
TCP connection closed: False
VERDICT: VULNERABLE
Retest bgpd log:
2026/07/05 09:46:19 BGP: 127.0.0.2 KEEPALIVE rcvd
2026/07/05 09:46:19 BGP: 127.0.0.2: BGP type 1 length 32 is too large, attribute total length is 4
2026/07/05 09:46:19 BGP: 127.0.0.2: Attribute ORIGIN, parse error - treating as withdrawal
2026/07/05 09:46:19 BGP: 127.0.0.2(Unknown) rcvd UPDATE with errors in attr(s)!! Withdrawing route.
2026/07/05 09:46:20 BGP: 127.0.0.2 KEEPALIVE rcvd
The post-error KEEPALIVE shows that the malformed UPDATE did not cause a NOTIFICATION or session reset.
Suggested Fix Direction
The check likely belongs in bgp_update_receive(), after bgp_attr_parse() returns and after update_len = end - stream_pnt(s) is known. At that point the caller can determine whether any reachable NLRI exists.
Conceptually:
update_len = end - stream_pnt(s);
if ((attr_parse_ret == BGP_ATTR_PARSE_WITHDRAW ||
attr_parse_ret == BGP_ATTR_PARSE_WITHDRAW_IGNORE) &&
update_len == 0 &&
nlris[NLRI_MP_UPDATE].length == 0) {
bgp_notify_send(connection, BGP_NOTIFY_UPDATE_ERR,
BGP_NOTIFY_UPDATE_MAL_ATTR);
bgp_attr_unintern_sub(&attr);
return BGP_Stop;
}
The exact condition should match RFC 7606 Section 5.2:
no IPv4 reachable NLRI
and no MP_REACH_NLRI reachable NLRI
and a path attribute error was encountered
and the applicable error handling is not attribute discard
Other BGP_ATTR_PARSE_WITHDRAW paths should be audited to ensure RFC 7606 Section 5.2 is applied when the UPDATE contains no reachable NLRI.
Impact
This is primarily an RFC 7606 compliance and malformed UPDATE error-handling correctness issue.
The immediate impact is that a malformed UPDATE that RFC 7606 says must reset the session is instead treated as a no-op withdrawal. In the demonstrated case, no bad route is installed because the UPDATE carries no reachable NLRI. The practical consequence is inconsistent behavior with RFC 7606-compliant implementations and reduced visibility into malformed UPDATEs that should terminate the session.
Summary
FRR bgpd appears to apply treat-as-withdraw to an attribute length framing error even when the malformed UPDATE contains no reachable NLRI. In that case, RFC 7606 Section 5.2 requires session reset, because there is no reachable NLRI for treat-as-withdraw to act on.
This was reproduced against FRR 10.6.1. bgpd detected the attribute length error, logged that the malformed attribute was being treated as a withdrawal, and kept the BGP session Established.
RFC 7606 Behavior
The tested UPDATE has:
This is a path attribute length/framing conflict: the enclosed ORIGIN attribute declares a length that exceeds the Total Path Attribute Length.
RFC 7606 Section 4 says that this kind of Total Attribute Length vs. enclosed attribute length conflict normally uses treat-as-withdraw, relying on the Total Attribute Length to locate the NLRI field.
However, RFC 7606 Section 5.2 says that if an UPDATE contains path attributes other than MP_UNREACH_NLRI and does not encode any reachable NLRI, then if any path attribute error is encountered and the error handling is not attribute discard, the session reset approach MUST be used.
So the expected chain is:
Steps to Reproduce
Establish the BGP session with OPEN and KEEPALIVE.
Send an UPDATE with:
The individual ORIGIN attribute length exceeds the total path attribute block, triggering the
attr_endp > endpcheck inbgp_attr_parse().Expected Behavior
FRR should detect the attribute length framing error, then notice that the UPDATE carries no reachable NLRI:
Since there is no reachable NLRI to withdraw, RFC 7606 Section 5.2 requires session reset. bgpd should send an UPDATE Message Error NOTIFICATION and transition the session out of Established.
Actual Behavior
FRR detects the malformed attribute but treats the UPDATE as a withdrawal and keeps the session up:
In dynamic testing, no NOTIFICATION was received and the TCP connection remained open. The bgpd log showed a peer KEEPALIVE after the malformed UPDATE, confirming that the session remained Established.
Source Analysis
In
bgp_attr_parse()(bgpd/bgp_attr.c), the attribute length overflow path returnsBGP_ATTR_PARSE_WITHDRAW:The caller,
bgp_update_receive()(bgpd/bgp_packet.c), treatsBGP_ATTR_PARSE_WITHDRAWas a signal to process reachable NLRI as withdrawals:When
update_len == 0andnlris[NLRI_MP_UPDATE].length == 0, there is no reachable NLRI to withdraw. I did not find a later check that applies RFC 7606 Section 5.2 to reset the session in this no-reachable-NLRI case.BIRD Comparison
BIRD has an explicit RFC 7606 Section 5.2 guard for this condition in
bgp_decode_attrs():This check prevents treat-as-withdraw from being used when the malformed UPDATE has no reachable NLRI.
Dynamic Verification
This was reproduced against:
Earlier verification produced the same result in 3 independent runs. I also re-tested the issue on 2026-07-05 with a fresh bgpd instance and the same malformed UPDATE.
Retest PoC observation:
Retest bgpd log:
The post-error KEEPALIVE shows that the malformed UPDATE did not cause a NOTIFICATION or session reset.
Suggested Fix Direction
The check likely belongs in
bgp_update_receive(), afterbgp_attr_parse()returns and afterupdate_len = end - stream_pnt(s)is known. At that point the caller can determine whether any reachable NLRI exists.Conceptually:
The exact condition should match RFC 7606 Section 5.2:
Other
BGP_ATTR_PARSE_WITHDRAWpaths should be audited to ensure RFC 7606 Section 5.2 is applied when the UPDATE contains no reachable NLRI.Impact
This is primarily an RFC 7606 compliance and malformed UPDATE error-handling correctness issue.
The immediate impact is that a malformed UPDATE that RFC 7606 says must reset the session is instead treated as a no-op withdrawal. In the demonstrated case, no bad route is installed because the UPDATE carries no reachable NLRI. The practical consequence is inconsistent behavior with RFC 7606-compliant implementations and reduced visibility into malformed UPDATEs that should terminate the session.