Any malformed UPDATE used to reset the session, withdrawing every route
learned over it. One bad attribute, typically originated ASes away and
passed on by a peer that did not look at it, could take down a session
carrying a full table.
UpdateMessage::decode now applies RFC 7606 and returns the UPDATE as it is
to be acted on, plus a report of what was wrong:
- Treat-as-withdraw: a malformed or misflagged attribute, a missing
mandatory one, or an attribute that overruns the attribute list. The
routes the UPDATE announced (in the body and in MP_REACH_NLRI) are
withdrawn.
- Attribute discard: malformed ATOMIC_AGGREGATE, AGGREGATOR, AS4_PATH and
AS4_AGGREGATOR; an attribute sent twice (the first is kept); LOCAL_PREF,
ORIGINATOR_ID and CLUSTER_LIST from an external peer; AS4_PATH between
four-octet speakers.
- Session reset remains where the prefixes are in doubt: the lengths that
frame the message, an unreadable prefix, a malformed or repeated
MP_REACH_NLRI or MP_UNREACH_NLRI, an unrecognised well-known attribute.
Each now sends its own subcode instead of always Malformed Attribute
List.
The session logs each handled error and counts it in
bgp_update_errors_total{handling}. The parser context says whether the
peer is internal (replacing a field nothing read).
Stricter than before, because the checks did not exist:
- An announcement must carry ORIGIN and AS_PATH, NEXT_HOP for routes in the
body, and LOCAL_PREF from an internal peer.
- An AS_PATH segment must be an AS_SET or AS_SEQUENCE and not empty.
- An attribute value must be used up entirely by its parser; a NEXT_HOP of
six octets, say, was accepted before.
Bugs found on the way: we sent ATOMIC_AGGREGATE flagged optional and
AGGREGATOR flagged non-transitive. The flags now come from one table used
for both writing and checking.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fuzz targets
Coverage-guided fuzzing of everything that reads input we do not control.
The property tests (cargo test, e.g. crates/packet/tests/properties.rs)
check the same things on generated input and run on every build; these
targets go further by letting libFuzzer follow code coverage for as long as
you leave them running.
| Target | Input |
|---|---|
message |
Raw bytes as they come off a TCP connection |
message_body |
A message body behind a correct header (reaches the body parsers) |
prefix_text |
A prefix as text |
config |
The configuration file |
For the two message targets a finding is either a panic, or a message the decoder accepted that cannot be written and read back as itself.
Running
libFuzzer needs a nightly toolchain and cargo-fuzz:
rustup toolchain install nightly
cargo install cargo-fuzz
Then, from this directory:
cargo +nightly fuzz run message_body corpus/message_body seeds/message_body -- -max_total_time=300
The first directory is the working corpus, which the fuzzer adds to and
which is not checked in. seeds/ holds a few valid inputs to start from.
Leave out -max_total_time to run until interrupted.
When it finds something
The input is saved under artifacts/<target>/. Reproduce it with
cargo +nightly fuzz run message_body artifacts/message_body/crash-...
Fix the bug, and add a unit test with the input next to the code it broke,
so that cargo test keeps it fixed without anyone having to run the fuzzer.