infra,l2,ip: fix flow hashing for ports without RSS - #701
Conversation
| static struct module module = { | ||
| .name = "flow_hash", | ||
| .init = flow_hash_init, | ||
| }; |
There was a problem hiding this comment.
1. Flow-hash init race 🐞 Bug ☼ Reliability
The new flow-hash mbuf dynfield/dynflag is registered in a module with no dependency ordering, but worker module init starts datapath threads (and loads graphs) during modules_init. If the topo-sorted init order runs worker before flow_hash, gr_mbuf_flow_hash_set/get can execute with gr_mbuf_flow_hash_offset still -1, causing out-of-bounds writes via RTE_MBUF_DYNFIELD().
Agent Prompt
### Issue description
`flow_hash` registers an mbuf dynfield and dynflag in `flow_hash_init()`, but the module definition has no `depends_on`. `worker_init()` starts datapath threads and reloads graphs during `modules_init()`. Because `modules_init()` orders modules only via `depends_on`, `flow_hash_init()` can legally run after `worker_init()`, allowing datapath code paths (including `gr_mbuf_flow_hash_set/get`) to run while `gr_mbuf_flow_hash_offset` is still `-1`.
This can turn `*RTE_MBUF_DYNFIELD(m, -1, ...) = ...` into memory corruption.
### Issue Context
- `gr_mbuf_flow_hash_offset` defaults to `-1` until `flow_hash_init()` runs.
- `worker_init()` calls `worker_queue_distribute()`, which creates pthreads (`gr_datapath_loop`) and calls `worker_graph_reload_all()`, enabling `rte_graph_walk()` in worker threads while `modules_init()` may still be iterating.
### Fix Focus Areas
- Ensure `flow_hash_init()` runs before any worker threads can run graphs:
- modules/infra/datapath/flow_hash.c[211-214]
- modules/infra/control/worker.c[612-616]
- (Optional hardening) add `assert(gr_mbuf_flow_hash_offset >= 0)` in `gr_mbuf_flow_hash_set()` / getters to fail fast if used pre-init:
- modules/infra/datapath/flow_hash.h[26-33]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
rjarry
left a comment
There was a problem hiding this comment.
There is a bit too much code churn in the successive commits. Could you try and make this more incremental?
Did you test if this series introduces any performance change? I am concerned about the re-computation of RSS after decap/encap even with hardware drivers that support L4 UDP ports.
Also, please drop the Assisted-by: <LLM> trailers.
1054ad2 to
87dced9
Compare
|
Thanks for the review - I will re-work and clean this up. This was cherry-picked from other development work on our MH implementation, so it could use some more scrutiny - sit tight. |
87dced9 to
664be0c
Compare
Could you confirm if the commit history is acceptable now? I updated Patch 1 so that it uses the final shape from the beginning to remove most of the churn. This was due to cherry-picking from my upstream MH codebase - apologies.
Fair call - I have reworked so that decap no longer recomputes anything.
Done. |
664be0c to
bc5b23d
Compare
|
Okay, I have thrown together some AI tests and sanity checked them, seems good to me. I benchmarked the series against v0.17.1 on a pair of Xeon D-2143IT boxes (X722 10GbE SFP+, back to back), using the per-node cycle counters from Two topologies: plain routed forwarding (kernel TAP in, 10G wire, TAP out) and a bridged VXLAN overlay mirroring a production EVPN setup. 30 s measured runs after warmup, 3 reps per build per scenario, medians below. X722 RSS was confirmed active (flows spread across both RX queues).
Notes:
Throughput was generator-limited and identical on both builds. The fixed build showed lower loss in the vxlan scenario (<=0.01% vs up to 0.77%): per-flow source ports let the receiving NIC spread flows across both RSS queues instead of piling them onto one. Incidental finding from the same rig: the X722 PF firmware rejects Crux of this: I see about a 62ns (at 2.2ghz) performance decrease with the corrected hashing function in place. However I feel like it could be worthwhile for the correctness aspect of this fix? |
christophefontaine
left a comment
There was a problem hiding this comment.
With the introduction of gr_mbuf_flow_hash_get to get the mbuf->hash.rss, we should also update process_behav_decap in modules/srv6/datapath/srv6_local.c to set the hash.
WDYT ?
And to update modules/ip/datapath/icmp_local_send.c as well as icmp6_local_send.c for consistency.
| eth = rte_pktmbuf_mtod(m, const struct rte_ether_hdr *); | ||
| tuple.l2.mac = eth->dst_addr; | ||
| if (eth->ether_type == RTE_BE16(RTE_ETHER_TYPE_VLAN)) { | ||
| vlan = (const struct rte_vlan_hdr *)(eth + 1); |
There was a problem hiding this comment.
vlan = PAYLOAD(eth);
| l3.ip6 = rte_pktmbuf_mtod_offset(m, const struct rte_ipv6_hdr *, l3_offset); | ||
| tuple.v6.src_addr = l3.ip6->src_addr; | ||
| tuple.v6.dst_addr = l3.ip6->dst_addr; | ||
| switch (l3.ip6->proto) { |
There was a problem hiding this comment.
Outside of this commit, and eligible for a future discussion / PR.
As we only look at ip6->proto, we do not take into account the extension headers which could be before a valid IPPROTO_*.
So, as part of the hash computation, what should we do ?
| } | ||
|
|
||
| rte_pktmbuf_adj(m, sizeof(struct rte_udp_hdr) + sizeof(*vh)); | ||
| gr_mbuf_flow_hash_invalidate(m); |
There was a problem hiding this comment.
I'm not comfortable in always invalidating the hash, as it may have been computed by the hardware on the inner packet.
Also, relying on the PTYPE may not be good either: as you said, even if the hash isn't computed on the inner, the remote VTEP may have spread the source UDP port.
// keep a hardware-provided inner-flow hash; only invalidate an outer one
if (!(m->packet_type & RTE_PTYPE_INNER_L4_MASK))
gr_mbuf_flow_hash_invalidate(m);
And I know that @david-marchand is not fan of using packet_type as well :)
There was a problem hiding this comment.
Hum, I did not look too much in detail at the full PR sorry.
I am trying to go back to the actual problem you are facing.
The issue seems to be that the rss hash is not a function of the inner packet.
This should be the case for routers implementing the recommendation in RFC7348.
Outer UDP Header: This is the outer UDP header with a source port
provided by the VTEP and the destination port being a well-known
UDP port.
...
- Source Port: It is recommended that the UDP source port number
be calculated using a hash of fields from the inner packet --
one example being a hash of the inner Ethernet frame's headers.
This is to enable a level of entropy for the ECMP/load-
balancing of the VM-to-VM traffic across the VXLAN overlay.
When calculating the UDP source port number in this manner, it
is RECOMMENDED that the value be in the dynamic/private port
range 49152-65535 [[RFC6335](https://datatracker.ietf.org/doc/html/rfc6335)].
If you are receiving traffic from another grout instance, the rss hash should have a source port that depends on the inner packet RSS (https://github.com/DPDK/grout/blob/main/modules/l2/datapath/vxlan_output.c#L90).
Is it that in your usecase the inner (meaning, before encapsulation on the transmitter side) packet rss 0 because it is received from a net/virtio (including virtio-user) port?
If this is the case, you may want to compute a RSS when it is not set at the RX side (see rx_virtio_process/rx_bond_virtio_process).
| o->nh = fib4_lookup( | ||
| o->iface->vrf_id, | ||
| ip->dst_addr, | ||
| gr_mbuf_flow_hash_get_l3(m, RTE_BE16(RTE_ETHER_TYPE_IPV4)) | ||
| ); |
There was a problem hiding this comment.
(not introduced by this commit, but to open the discussion for a future PR)
The fib4_lookup gets the nh based on the hash. Yet, this hash is still the one computed by the hardware, pre-nat.
Shouldn't we update the RSS field to reflect the modified dst_ip & dsp_port?
| d->nh = fib4_lookup( | ||
| d->iface->vrf_id, | ||
| ip->dst_addr, | ||
| gr_mbuf_flow_hash_get_l3(mbuf, RTE_BE16(RTE_ETHER_TYPE_IPV4)) | ||
| ); |
There was a problem hiding this comment.
same comment as dnat44_dynamic
Extract the bond Toeplitz hashing logic for reuse by other datapaths. Prefer a hardware RSS value when available and retain a software L3/L4 fallback for TAP and other virtual devices without RSS. Keep the L3/L4 tuple hashing in a separate helper taking an explicit L3 offset and EtherType, so a later patch can also hash packets whose data starts directly at the IP header. Add focused tests for flow stability, UDP differentiation, L2 behaviour and hardware RSS precedence. Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
The fragment check included the DF flag, so ordinary packets with DF set fell back to an L3-only hash. Mask the fragment offset before deciding whether transport ports are available. Fixes: e2953be ("lacp: only use tcp/udp ports for non-fragmented packets") Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
The port availability check only masked the fragment offset, so a first fragment (offset zero, MF set) was hashed with its UDP or TCP ports while the following fragments of the same datagram were hashed on addresses only, steering them onto different paths. Include the MF flag in the check so every fragment of a datagram shares one L3-only hash. Fixes: e2953be ("lacp: only use tcp/udp ports for non-fragmented packets") Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
Several datapaths read m->hash.rss directly for ECMP and load balancing decisions. On ports without RSS that field holds stale or zero data, and every consumer that falls back to a software hash recomputes it from scratch. Store the software Toeplitz hash in m->hash.rss and mark it valid with RTE_MBUF_F_RX_RSS_HASH, exactly as a hardware driver would. gr_mbuf_flow_hash_get() returns any hash already present untouched and only computes the software fallback once per packet. gr_mbuf_flow_hash_get_l3() does the same for packets whose data starts at an IPv4 or IPv6 header. Both getters are inline so packets carrying a valid hash only pay one flag test, never a function call. gr_mbuf_flow_hash_invalidate() drops a hash that no longer describes the packet, for example after tunnel decapsulation; the next consumer recomputes it on demand, so packets that never reach a hash consumer cost nothing. Cover hardware precedence, single computation, L3 entry points, reset and invalidation with unit tests. Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
vxlan encapsulation reads m->hash.rss for the underlay route lookup and the UDP source port even when the port computed no RSS, so flows entering through TAP or other virtual devices load balance on stale or zero data. Switch bond member selection and vxlan output to the cached canonical hash. Decapsulation keeps the outer hash untouched: a VTEP following RFC 7348 derives the outer UDP source port from the inner flow, so an outer RSS value is already a function of the inner flow and stays valid for member selection after decapsulation. Extend the vxlan smoke test with a bridged port injecting distinct UDP flows and check the encapsulated packets no longer share a single source port. Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
The fib4/fib6 ECMP lookups in input, ICMP, error, NAT, IP-in-IP and SRv6 paths read m->hash.rss directly. On ports without RSS the field holds stale or zero data, so multipath selection is either unstable or collapses onto a single nexthop, and locally generated packets never had a meaningful hash at all. Use gr_mbuf_flow_hash_get_l3() in these nodes so every lookup shares the packet's cached canonical hash regardless of how it entered the graph. The ping request nodes already faked a hardware RSS value by writing the mbuf fields directly; store it through the shared helper instead. Extend the load balance smoke test to send distinct UDP flows through an ECMP route and check that both group members carry traffic. Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
The decap behaviors copy the outer flow label into m->hash.usr, which aliases m->hash.rss, without touching RTE_MBUF_F_RX_RSS_HASH. When the port computed RSS on the outer packet the flag stays set, so the fib lookups keep trusting the value; an encapsulating node that leaves the flow label zero (as our own SRH encap does) then collapses every inner flow onto hash 0. Unlike vxlan, the outer headers carry no other inner entropy: there is no UDP source port and RSS cannot parse past the routing header. Keep using a non-zero flow label as the flow entropy (RFC 6438), now stored through the shared helper so the valid flag is set coherently. When the label is zero, invalidate the hash instead so the first consumer computes one from the inner packet. Fixes: a7ede13 ("srv6: update mbuf hash on decap action") Signed-off-by: Harrison Caldicott <harrison@itsfubar.com.au>
bc5b23d to
2b56ebf
Compare
Several datapaths read
m->hash.rssdirectly for ECMP and load-balancingdecisions (fib4/fib6 lookups, vxlan underlay routing and source port
selection). On ports without RSS — TAP and other virtual devices — that
field holds stale or zero data, so every flow shares one value: ECMP
collapses onto a single nexthop and all vxlan flows share one UDP source
port. After SRv6 decapsulation, the outer flow label is copied over the
hash while a hardware valid flag survives; with a zero label (as grout's
own encap emits) every inner flow shares hash 0.
The series first extracts the bond Toeplitz hashing into a shared helper
and fixes two fragment-handling bugs in it: packets with the DF flag set
were treated as fragments and hashed L3-only, and first fragments (offset
zero, MF set) were hashed with their L4 ports unlike the rest of their
datagram. It then caches one canonical per-packet hash by storing the
software result in
m->hash.rssand marking it valid withRTE_MBUF_F_RX_RSS_HASH, exactly as a hardware driver would — no newmbuf metadata. The bond, vxlan and L3 consumers read it through one
helper. vxlan decapsulation leaves the outer hash in place: a VTEP
following RFC 7348 derives the outer UDP source port from the inner
flow, so an outer RSS value is already a function of the inner flow and
stays valid after decapsulation. SRv6 decapsulation keeps using a
non-zero outer flow label as the flow entropy, now stored through the
shared helper so the valid flag is coherent, and invalidates the hash
when the label is zero so the first consumer hashes the inner packet.
For packets whose driver does provide RSS, the datapath is unchanged:
the same flag test and the same
m->hash.rssread as before the series,with no recomputation after encap or decap.
Found while prototyping EVPN all-active multihoming (#698), where bridged
flows must keep per-flow path affinity across bond and vxlan ECMP, but
everything above is reproducible on plain upstream.
Testing
single computation and caching, hardware RSS precedence, hash
invalidation.
hash with the valid flag set; a zero label drops a stale outer RSS
value.
smoke/vxlan_test.sh: a second bridged port injects 32distinct UDP flows and the test asserts the encapsulated packets use
several distinct source ports.
smoke/ip_loadbalance_test.sh: 64 distinct UDP flows throughthe ECMP route, asserting both group members carry traffic.
main): the vxlan test failswith "32 flows shared 1 vxlan source ports", the load-balance test
fails with "member p1 carried 2 of 64 distinct flows", the MF fragment
unit test fails, and the srv6 decap test fails with the stale outer
hash still marked valid.
ip_loadbalance smoke tests pass (AlmaLinux 9 container, arm64).
Related: #698