Skip to content

packet/bmp: bound the mirrored BGP message by its Route Mirroring TLV length - #3608

Merged
fujita merged 1 commit into
osrg:masterfrom
Dev-next-gen:bmp-route-mirroring-tlv-length
Sep 16, 2026
Merged

fujita merged 1 commit into
osrg:masterfrom
Dev-next-gen:bmp-route-mirroring-tlv-length

Conversation

@Dev-next-gen

Copy link
Copy Markdown
Contributor

I was reading the Route Mirroring TLV parsers in pkg/packet/bmp/bmp.go after seeing Test_TLV16RejectsShortLength, which locks down the case where a fixed-width TLV reads past its own value into the following TLV. The BGP Message TLV turns out to have the same shape of mistake, just from the other direction.

BMPRouteMirrTLVBGPMsg.ParseValue calls bgp.ParseBGPMessage(data) on the whole remainder of the body. Every other ParseValue in the file slices data[:s.Length] first. Because ParseBGPMessage trusts the length field inside the message it is given, the embedded BGP header ends up deciding where the mirrored PDU ends, not the TLV that contains it. The enclosing loop still advances by the declared tl.Length, so the bytes the over-read consumed are parsed a second time as the next TLV.

RFC 7854 Section 4.7 is explicit that the BGP Message TLV carries a PDU the monitoring station is mirroring, including ones it could not process itself, so the length inside that PDU is exactly the field you cannot rely on. The TLV Length is the framing that matters.

Here is what I measured on master. A Route Mirroring body made of a BGP Message TLV with Length = 19 whose embedded header claims 23 octets, followed by an Information TLV (type = 1, length = 2, value = 0x0001):

Info[0] BGPMsg declared TLV Length=19, header.Len=23, type=5,
        ROUTE-REFRESH AFI=1 SAFI=2
Info[1] *bmp.BMPRouteMirrTLV16 {Type:1 Length:2 Value:1}

ParseBody returned no error. The mirrored ROUTE-REFRESH is a fabrication: its AFI and its SAFI are the four header bytes 00 01 00 02 of the Information TLV, which was then also returned as a TLV in its own right. Nineteen octets of value produced a 23-octet message.

The change slices the value to s.Length before parsing, with the same "value length is not enough" guard the sibling parsers use. With it, ParseBGPMessage sees 19 octets for a message that declares 23 and rejects it, which is what the wire actually holds.

Test_RouteMirroringBGPMsgTLVHonoursItsLength covers that body, and also asserts the other half: a mirrored ROUTE-REFRESH that does fit inside its TLV still decodes, and the Information TLV after it is still parsed on its own. It fails on master at the require.Error and passes with the change. go test ./pkg/... ./internal/... is green, as are gofmt -l and go vet on the package.

Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.

… length

BMPRouteMirrTLVBGPMsg.ParseValue handed the whole remainder of the
Route Mirroring body to bgp.ParseBGPMessage instead of the Length
octets the TLV declared. Every other value parser in this file slices
data[:s.Length] first, so this one is the odd one out: the embedded BGP
header's own length field, not the TLV framing, decided where the
mirrored message ended.

RFC 7854 Section 4.7 says the BGP Message TLV carries a complete BGP
PDU, and that a monitoring station mirrors PDUs it may not have been
able to process, so the length inside the mirrored message is not
trustworthy. The enclosing loop still advances by the declared
tl.Length, so a message announcing more octets than its TLV holds was
decoded out of the TLVs that follow it, and those same octets were then
parsed a second time as the next TLV.

Concretely, a Route Mirroring body of a 19-octet BGP Message TLV whose
embedded header claims 23 octets, followed by a 2-octet Information
TLV, decoded without error into a ROUTE-REFRESH for AFI 1 / SAFI 2.
Both the AFI and the SAFI came from the four header bytes of the
Information TLV, which was then also returned as a second TLV of its
own. Slicing the value to s.Length makes ParseBGPMessage see 19 octets
for a message that declares 23 and reject it, which is what the wire
actually contains.

Test_TLV16RejectsShortLength already locks down the same "read past its
own value into the following TLV" mistake for the fixed-width
Termination Reason and Route Mirroring Information TLVs.
Test_RouteMirroringBGPMsgTLVHonoursItsLength covers the BGP Message TLV
and also asserts that a mirrored message that does fit inside its TLV
still decodes, together with the TLV that follows it.
@Dev-next-gen

Copy link
Copy Markdown
Contributor Author

The zebra check is red on this PR and I don't believe it belongs to the change, so here is what I found rather than leaving it for whoever picks this up. The failure is test/scenario_test/bgp_zebra_test.py::GoBGPTestBase::test_09_mpath_test_check_mpath_injected, which polls ip route show 10.0.10.0/24 inside g1 for 30 seconds and raises with whatever it last saw; the log ends on Exception: [], so zebra had installed no route at all rather than a wrong set of nexthops. unit and unit386 passed in the same run.

The changed function cannot run in that container. grep -rn "ParseBMPMessage" --include="*.go" . | grep -v _test.go returns only the two definitions at pkg/packet/bmp/bmp.go:1140 and :1144 — nothing in the tree parses BMP outside the unit tests, since gobgpd emits it and never reads it, so BMPRouteMirrTLVBGPMsg.ParseValue is never reached by a scenario test. I re-ran the package on cf5ad1d to be sure of the other half: Test_RouteMirroring, Test_TLV16RejectsShortLength and Test_RouteMirroringBGPMsgTLVHonoursItsLength all pass.

What I cannot show you is the same job red on the base branch, and I want to be plain about that: zebra is green on the latest master run (35037455750) and on each of the previous nineteen pull requests I checked, so this reads as a one-off rather than a known-flaky job, and I have no reproduction of it. If it is worth settling, a re-run of the zebra job would do it — gh run rerun declines for me with "Must have admin rights to Repository".

Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.

@Dev-next-gen

Copy link
Copy Markdown
Contributor Author

I said in my previous comment that I had no reproduction of the zebra failure. I have one now, and the mechanism is already named in the tree: the comment at pkg/config/config.go:301 says zebra.NewClient dials the zserv socket once and never retries, and since 5f4a6b5 a failed dial is logged instead of aborting startup. I built gobgpd at cf5ad1d and started it with zebra.config.enabled = true pointing at a socket that did not exist yet:

INFO  gobgpd started
WARN  cannot connect to Zebra with message version  Version=2
 ... versions 3, 4, 5, 6 ...
ERROR failed to set zebra config  Error="dial unix /root/zrepro/run/zserv.api: connect: no such file or directory"

The process stayed up, and the dial happened 23 ms after gobgpd started. I then bound that socket myself and left it accepting for 20 seconds: ZSERV accepted 0 connection(s) in 20s. So a gobgpd that loses the start race runs the rest of the test with zebra quietly off, and ip route show returns nothing rather than the wrong nexthops — which is the shape of the Exception: [] in the job. test/lib/gobgp.py:168-176 starts zebra with a detached docker exec, starts gobgpd on the next line, and only waits for vtysh -d zebra -c 'show run' afterwards, so nothing orders the bind before the dial.

What I still cannot show is that this is what happened to g1 in run 35039726767. The harness sends gobgpd's log to the container's shared volume and the job never prints it, so the one line that would settle it is not in the log I can read. This is the only mechanism I found that produces an empty route list instead of a wrong one, and it is unrelated to this pull request either way.

Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.

@fujita
fujita merged commit cf5ad1d into osrg:master Sep 16, 2026
83 of 84 checks passed
@fujita

fujita commented Sep 16, 2026

Copy link
Copy Markdown
Member

Thanks

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants