* [PATCH net-next 0/4] net: rmnet: optimize hot paths to alleviate CPU/memory bottlenecks
@ 2026-10-02 14:35 Koen Vandeputte
2026-10-02 14:35 ` [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation Koen Vandeputte
` (3 more replies)
0 siblings, 4 replies; 11+ messages in thread
From: Koen Vandeputte @ 2026-10-02 14:35 UTC (permalink / raw)
To: netdev
Cc: quic_subashab, quic_stranche, andrew+netdev, davem, edumazet,
kuba, pabeni, dnlplm, linux-kernel, Koen Vandeputte
While testing embedded ARM platforms over a high-speed
private 5G SA network, we observed severe CPU and memory
bottlenecks limiting overall throughput.
Examining the rmnet driver revealed several opportunities
to significantly increase efficiency,
particularly on the RX deaggregation hot path.
This patch series addresses these bottlenecks by optimizing
memory allocations, reducing unnecessary payload copies,
and improving timestamping efficiency.
Test Environment
Hardware: IPQ4019 (Quad ARM Cortex-A7 @ 717MHz), 512MB DDR3, USB 3.0
Modem: Telit FN990A40
OS: OpenWRT-based
Network: Private 5G SA (n78, 50MHz band, 4x4 MIMO, 1Gbps core uplink)
Methodology
Results are based on the average of 10 consecutive speedtests against
a local speedtest server located in the same datacenter.
Performance Results (Download)
Before: ~318 Mbps average (Peak: ~353 Mbps)
After: ~389 Mbps average (Peak: ~458 Mbps)
Koen Vandeputte (4):
net: rmnet: use fast monotonic time for tx aggregation
net: rmnet: optimize rx deaggregation memory allocation
net: rmnet: conditionally expand skb headroom in ingress handler
net: rmnet: optimize rx handler by replacing skb_linearize with
pskb_may_pull
.../ethernet/qualcomm/rmnet/rmnet_config.h | 4 ++--
.../ethernet/qualcomm/rmnet/rmnet_handlers.c | 10 ++++----
.../ethernet/qualcomm/rmnet/rmnet_map_data.c | 24 ++++++++++---------
3 files changed, 21 insertions(+), 17 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation 2026-10-02 14:35 [PATCH net-next 0/4] net: rmnet: optimize hot paths to alleviate CPU/memory bottlenecks Koen Vandeputte @ 2026-10-02 14:35 ` Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 2026-10-02 14:35 ` [PATCH net-next 2/4] net: rmnet: optimize rx deaggregation memory allocation Koen Vandeputte ` (2 subsequent siblings) 3 siblings, 1 reply; 11+ messages in thread From: Koen Vandeputte @ 2026-10-02 14:35 UTC (permalink / raw) To: netdev Cc: quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel, Koen Vandeputte The rmnet egress aggregation logic currently relies on ktime_get_real_ts64() to determine if the aggregation bypass time threshold has been reached. Calling a wall-clock time function on the transmit hot path introduces significant performance bottlenecks, causing cacheline bouncing and pipeline stalls when processing high packet volumes. Furthermore, manipulating and comparing struct timespec64 fields adds unnecessary branching overhead. Since the driver only needs to measure elapsed time between consecutive packets to evaluate the aggregation bypass condition, absolute wall-clock time is not required. Replace ktime_get_real_ts64() with ktime_get_mono_fast_ns(). This reads a lockless, per-CPU timestamp directly in nanoseconds, returning a simple u64. Update the aggregation state variables (agg_time and agg_last) in struct rmnet_port to u64 accordingly. This optimization significantly reduces CPU overhead, eliminates struct timespec64 math, and provides a much faster, cache-friendly timekeeping mechanism for high-throughput egress traffic. Signed-off-by: Koen Vandeputte <koen.vandeputte@citymesh.com> --- .../ethernet/qualcomm/rmnet/rmnet_config.h | 4 ++-- .../ethernet/qualcomm/rmnet/rmnet_map_data.c | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h index 5adda0323dda..78c0289b6583 100644 --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h @@ -47,8 +47,8 @@ struct rmnet_port { struct sk_buff *skbagg_tail; int agg_state; u8 agg_count; - struct timespec64 agg_time; - struct timespec64 agg_last; + u64 agg_time; + u64 agg_last; struct hrtimer hrtimer; struct work_struct agg_wq; }; diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c index 39d6d084e73f..2eafb1d969c1 100644 --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c @@ -536,7 +536,7 @@ static void reset_aggr_params(struct rmnet_port *port) port->skbagg_head = NULL; port->agg_count = 0; port->agg_state = 0; - memset(&port->agg_time, 0, sizeof(struct timespec64)); + port->agg_time = 0; } static void rmnet_send_skb(struct rmnet_port *port, struct sk_buff *skb) @@ -591,21 +591,23 @@ static enum hrtimer_restart rmnet_map_flush_tx_packet_queue(struct hrtimer *t) unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port, struct net_device *orig_dev) { - struct timespec64 diff, last; + u64 diff, last, now; unsigned int len = skb->len; struct sk_buff *agg_skb; int size; spin_lock_bh(&port->agg_lock); - memcpy(&last, &port->agg_last, sizeof(struct timespec64)); - ktime_get_real_ts64(&port->agg_last); + last = port->agg_last; + + now = ktime_get_mono_fast_ns(); + port->agg_last = now; if (!port->skbagg_head) { /* Check to see if we should agg first. If the traffic is very * sparse, don't aggregate. */ new_packet: - diff = timespec64_sub(port->agg_last, last); + diff = now - last; size = port->egress_agg_params.bytes - skb->len; if (size < 0) { @@ -614,8 +616,7 @@ unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port return 0; } - if (diff.tv_sec > 0 || diff.tv_nsec > RMNET_AGG_BYPASS_TIME_NSEC || - size == 0) + if (diff > RMNET_AGG_BYPASS_TIME_NSEC || size == 0) goto no_aggr; port->skbagg_head = skb_copy_expand(skb, 0, size, GFP_ATOMIC); @@ -625,11 +626,12 @@ unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port dev_kfree_skb_any(skb); port->skbagg_head->protocol = htons(ETH_P_MAP); port->agg_count = 1; - ktime_get_real_ts64(&port->agg_time); + port->agg_time = now; skb_frag_list_init(port->skbagg_head); goto schedule; } - diff = timespec64_sub(port->agg_last, port->agg_time); + + diff = now - port->agg_time; size = port->egress_agg_params.bytes - port->skbagg_head->len; if (skb->len > size) { @@ -653,7 +655,7 @@ unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port port->skbagg_tail = skb; port->agg_count++; - if (diff.tv_sec > 0 || diff.tv_nsec > port->egress_agg_params.time_nsec || + if (diff > port->egress_agg_params.time_nsec || port->agg_count >= port->egress_agg_params.count || port->skbagg_head->len == port->egress_agg_params.bytes) { agg_skb = port->skbagg_head; -- 2.43.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation 2026-10-02 14:35 ` [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation Koen Vandeputte @ 2026-10-06 15:00 ` netdev-bot+sashiko 0 siblings, 0 replies; 11+ messages in thread From: netdev-bot+sashiko @ 2026-10-06 15:00 UTC (permalink / raw) To: koen.vandeputte Cc: netdev, quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider. Critical: 0 · High: 0 (1) · Medium: 3 (2) · Low: 1 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] The commit message describes the new clock source wrongly and gives no measurements for its performance claims. - [Medium] The patch calls ktime_get_mono_fast_ns() from a driver and describes it as monotonic. - [Medium] rmnet: stale timestamp assigned to agg_time causes premature tx flush - [Low] Dropping the `diff.tv_sec > 0` term quietly changes the in-path flush behaviour of rmnet_map_tx_aggregate() for tx_aggr_time_usecs of… Pre-existing issues: - [High] This problem wasn't introduced by this patch, but there is a time-of-check/time-of-use race in rmnet_map_tx_aggregate() that leaks… - [Medium] This problem wasn't introduced by this patch, but rmnet_map_update_ul_agg_config() computes… - [Medium] This problem wasn't introduced by this patch, but both flush branches of rmnet_map_tx_aggregate() (lines 637-645 and 658-667) call… -- net: rmnet: use fast monotonic time for tx aggregation This commit replaces ktime_get_real_ts64() with ktime_get_mono_fast_ns() in rmnet_map_tx_aggregate(). It also changes agg_time and agg_last in struct rmnet_port from struct timespec64 to u64 nanoseconds, so the bypass and flush age checks become plain u64 subtractions. > Calling a wall-clock time function on the transmit hot path introduces > significant performance bottlenecks, causing cacheline bouncing and pipeline > stalls when processing high packet volumes. [ ... ] > Replace ktime_get_real_ts64() with ktime_get_mono_fast_ns(). This reads a > lockless, per-CPU timestamp directly in nanoseconds, returning a simple u64. [ ... ] > This optimization significantly reduces CPU overhead, eliminates struct > timespec64 math, and provides a much faster, cache-friendly timekeeping > mechanism for high-throughput egress traffic. [Severity: Medium] Is this description of ktime_get_mono_fast_ns() accurate? In kernel/time/timekeeping.c it reads from a single global object: static struct tk_fast tk_fast_mono ____cacheline_aligned = { u64 notrace ktime_get_mono_fast_ns(void) { return __ktime_get_fast_ns(&tk_fast_mono); } __ktime_get_fast_ns() then runs a latch seqcount retry loop and reads the hardware clocksource. ktime_get_real_ts64() does the same: do { seq = read_seqcount_latch(&tkf->seq); tkr = tkf->base + (seq & 0x01); now = ktime_to_ns(tkr->base); now += timekeeping_get_ns(tkr); } while (read_seqcount_latch_retry(&tkf->seq, seq)); Nothing in this path is per-CPU. Seqcount readers also never write the shared cacheline, so the old ktime_get_real_ts64() reader path wouldn't cause cacheline bouncing either. The real savings appear to be a latch seqcount in place of a plain seqcount, plus no timespec64 normalisation and no two-field compare. Could the commit message describe that instead? Could it also include measurements to support "significantly reduces CPU overhead"? > diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c > index 39d6d084e73f0..2eafb1d969c1a 100644 > --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c > +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c [ ... ] > @@ -591,21 +591,23 @@ static enum hrtimer_restart rmnet_map_flush_tx_packet_queue(struct hrtimer *t) > unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port, > struct net_device *orig_dev) > { [ ... ] > spin_lock_bh(&port->agg_lock); > - memcpy(&last, &port->agg_last, sizeof(struct timespec64)); > - ktime_get_real_ts64(&port->agg_last); > + last = port->agg_last; > + > + now = ktime_get_mono_fast_ns(); > + port->agg_last = now; [Severity: Medium] Is ktime_get_mono_fast_ns() a good choice for a driver? Its kernel-doc says it is not strictly monotonic: * This timestamp is not guaranteed to be monotonic across an update. ... * So reader 6 will observe time going backwards versus reader 5. ... * Callers need to be aware of that and deal with it. Documentation/core-api/timekeeping.rst also says: machine check reporting, but most drivers should never call them, since the time is allowed to jump under certain conditions. port->agg_last and port->agg_time are shared across CPUs. rmnet_vnd_setup() sets rmnet_dev->lltx = true, and several muxed rmnet devices can share one rmnet_port. As a result, rmnet_map_tx_aggregate() runs on several CPUs at once. agg_lock serialises the transmitters, but it does not serialise them against update_fast_timekeeper(). Suppose CPU A reads now from the old base and stores it in agg_last. CPU B then reads from the new base after the latch switch. B's now can be smaller than last. Neither unsigned subtraction guards against that: diff = now - last; ... diff = now - port->agg_time; In that case diff wraps to roughly U64_MAX. That either forces the sparse traffic bypass (diff > RMNET_AGG_BYPASS_TIME_NSEC) or flushes a partial aggregate early (diff > egress_agg_params.time_nsec). The effect is small: one packet skips aggregation, or one aggregate is flushed early. The old timespec64_sub() code took the same paths on a negative delta. Even so, the commit message calls this clock monotonic. Would ktime_get_ns() fit better here? It is monotonic, returns a u64, and still avoids the timespec64 math. > > if (!port->skbagg_head) { > /* Check to see if we should agg first. If the traffic is very > * sparse, don't aggregate. > */ > new_packet: > - diff = timespec64_sub(port->agg_last, last); > + diff = now - last; > size = port->egress_agg_params.bytes - skb->len; [ ... ] > @@ -625,11 +626,12 @@ unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port > dev_kfree_skb_any(skb); > port->skbagg_head->protocol = htons(ETH_P_MAP); > port->agg_count = 1; > - ktime_get_real_ts64(&port->agg_time); > + port->agg_time = now; > skb_frag_list_init(port->skbagg_head); > goto schedule; > } > - diff = timespec64_sub(port->agg_last, port->agg_time); > + > + diff = now - port->agg_time; > size = port->egress_agg_params.bytes - port->skbagg_head->len; > > if (skb->len > size) { [Severity: Medium] Does this change when a new aggregate's age starts counting on the goto new_packet path? now is read once, at the top of rmnet_map_tx_aggregate(). Sometimes the incoming skb does not fit in the current aggregate. The function then drops agg_lock, calls hrtimer_cancel() and rmnet_send_skb() for the old aggregate, re-takes agg_lock and jumps back to new_packet. A new aggregate started there gets agg_time set to the timestamp read before all of that. The old code called ktime_get_real_ts64(&port->agg_time) at this point, so agg_time was a fresh reading taken after the flush. With this patch, agg_time is backdated by however long hrtimer_cancel() and the nested dev_queue_xmit() took. hrtimer_cancel() can spin until a running rmnet_map_flush_tx_packet_queue() finishes. dev_queue_xmit() on the real device can contend on the qdisc lock. The next packet computes now - port->agg_time against that stale value. It is therefore more likely to exceed egress_agg_params.time_nsec, so the new aggregate is flushed earlier than configured. The hrtimer armed at schedule: uses an expiry relative to the current time. The timer and the age check therefore no longer agree on when the aggregate started. This path runs on every size-triggered flush. Under sustained traffic that overflows aggregates, it seems to work against the throughput goal of this patch. Could agg_time take a fresh ktime_get_mono_fast_ns() reading after agg_lock is re-acquired on this path, as the old code effectively did? The bypass check at new_packet compares against last. It used the entry-time agg_last in the old code too, so only agg_time seems to need the fresh value. [Severity: High] This isn't a bug introduced by this patch, but can this branch leak an aggregate that another CPU built? if (skb->len > size) { agg_skb = port->skbagg_head; reset_aggr_params(port); spin_unlock_bh(&port->agg_lock); hrtimer_cancel(&port->hrtimer); rmnet_send_skb(port, agg_skb); spin_lock_bh(&port->agg_lock); goto new_packet; } The new_packet label is inside the if (!port->skbagg_head) block. After agg_lock is retaken, the goto skips that check. With lltx = true and several muxed devices sharing one rmnet_port, another CPU can run while the lock is dropped: CPU A CPU B reset_aggr_params() skbagg_head = NULL spin_unlock_bh() hrtimer_cancel() spin_lock_bh() rmnet_send_skb() sees skbagg_head == NULL small diff, A just set agg_last skbagg_head = skb_copy_expand(...) spin_unlock_bh() spin_lock_bh() goto new_packet If A's diff is small when it resumes, A overwrites the head without checking it first: port->skbagg_head = skb_copy_expand(skb, 0, size, GFP_ATOMIC); Nothing references B's head skb or any frag_list entries appended to it after that, so they leak. Those packets were already counted as transmitted in rmnet_vnd_tx_fixup_len(), but they are never sent. If A's diff exceeds the bypass threshold instead, A sends its skb directly ahead of B's pending aggregate, so packets are reordered. Could port->skbagg_head be re-checked after the lock is re-acquired? For example, the goto could target a label placed before the if (!port->skbagg_head) test. [ ... ] > @@ -653,7 +655,7 @@ unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port > port->skbagg_tail = skb; > port->agg_count++; > > - if (diff.tv_sec > 0 || diff.tv_nsec > port->egress_agg_params.time_nsec || > + if (diff > port->egress_agg_params.time_nsec || [Severity: Low] Is it intended that removing the diff.tv_sec > 0 term changes flush behaviour for timeouts of one second or more? tv_nsec used to be normalised below 1e9, so the next packet flushed any aggregate that was at least 1s old. In effect, a configured time_nsec of 1s or more was capped at 1s when a packet arrived. The full configured timeout now applies. rmnet_set_coalesce() passes tx_aggr_time_usecs through without an upper bound, and the netlink policy for ETHTOOL_A_COALESCE_TX_AGGR_TIME_USECS is plain NLA_U32. With a 2s timeout, a packet that arrives when the aggregate is 1.1s old used to trigger a flush and no longer does. The new behaviour matches the hrtimer, which was always armed with the full time_nsec. Could the commit message mention this change? [Severity: Medium] This isn't a bug introduced by this patch, but on 32-bit builds the time_nsec value compared here can be wrong. rmnet_map_update_ul_agg_config() does: port->egress_agg_params.time_nsec = time * NSEC_PER_USEC; time is u32 and NSEC_PER_USEC is 1000L. On 32-bit, long is 32 bits, so the product is computed as a 32-bit unsigned long. It wraps before it is stored in the u64 time_nsec. rmnet_set_coalesce() bounds tx_aggr_max_frames and tx_aggr_max_bytes, but not tx_aggr_time_usecs. Any value above about 4294967 usec wraps. For example, 5000000 usec is stored as 705032704 ns, and rmnet_get_coalesce() reports 705032 usec back. Both the hrtimer and this age check use the wrapped value. > port->agg_count >= port->egress_agg_params.count || > port->skbagg_head->len == port->egress_agg_params.bytes) { > agg_skb = port->skbagg_head; [Severity: Medium] This is a pre-existing issue, but can a new aggregate end up with no timer armed? Both flush branches do this: reset_aggr_params(port); spin_unlock_bh(&port->agg_lock); hrtimer_cancel(&port->hrtimer); reset_aggr_params() sets agg_state to 0, and the lock is released while the old hrtimer is still queued. Suppose CPU B creates a new aggregate in that window and reaches schedule:. The old timer is still active, so this check fails: if (!hrtimer_active(&port->hrtimer) && port->agg_state != -EINPROGRESS) { B then neither arms a timer nor sets agg_state. CPU A cancels the old timer next, which leaves B's aggregate with no pending hrtimer and no queued work. It stays buffered until the next egress packet on the port. If the port is torn down first, rmnet_map_tx_aggregate_exit() only frees skbagg_head when agg_state is -EINPROGRESS: if (port->agg_state == -EINPROGRESS) { if (port->skbagg_head) { dev_kfree_skb_any(port->skbagg_head); In that case the stranded aggregate also leaks. -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261002143529.3217189-1-koen.vandeputte%40citymesh.com ^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net-next 2/4] net: rmnet: optimize rx deaggregation memory allocation 2026-10-02 14:35 [PATCH net-next 0/4] net: rmnet: optimize hot paths to alleviate CPU/memory bottlenecks Koen Vandeputte 2026-10-02 14:35 ` [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation Koen Vandeputte @ 2026-10-02 14:35 ` Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 2026-10-02 14:35 ` [PATCH net-next 3/4] net: rmnet: conditionally expand skb headroom in ingress handler Koen Vandeputte 2026-10-02 14:35 ` [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull Koen Vandeputte 3 siblings, 1 reply; 11+ messages in thread From: Koen Vandeputte @ 2026-10-02 14:35 UTC (permalink / raw) To: netdev Cc: quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel, Koen Vandeputte Currently, rmnet_map_deaggregate() uses alloc_skb() with GFP_ATOMIC to allocate new socket buffers for individual packets extracted from an aggregated frame. Under high-speed receive workloads, such as processing large 16KB frames from a USB modem, repeatedly hitting the generic kernel slab allocator introduces unnecessary lock contention and high CPU overhead. Replace alloc_skb() with netdev_alloc_skb() on the deaggregation path. netdev_alloc_skb() is specifically designed for the network receive hot path, utilizing a fast, lockless per-CPU NAPI cache. Furthermore, it automatically provisions standard network padding (NET_SKB_PAD), ensuring that the newly extracted IP headers are optimally cache-aligned for efficient processing by the upper networking stack. This change significantly reduces memory allocation overhead, prevents slab allocator contention, and improves overall throughput when receiving large bursts of aggregated traffic. Signed-off-by: Koen Vandeputte <koen.vandeputte@citymesh.com> --- drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c index 2eafb1d969c1..ccdfde3711db 100644 --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c @@ -390,7 +390,7 @@ struct sk_buff *rmnet_map_deaggregate(struct sk_buff *skb, if (!packet_len) return NULL; - skbn = alloc_skb(packet_len + RMNET_MAP_DEAGGR_SPACING, GFP_ATOMIC); + skbn = netdev_alloc_skb(skb->dev, packet_len + RMNET_MAP_DEAGGR_SPACING); if (!skbn) return NULL; -- 2.43.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 2/4] net: rmnet: optimize rx deaggregation memory allocation 2026-10-02 14:35 ` [PATCH net-next 2/4] net: rmnet: optimize rx deaggregation memory allocation Koen Vandeputte @ 2026-10-06 15:00 ` netdev-bot+sashiko 0 siblings, 0 replies; 11+ messages in thread From: netdev-bot+sashiko @ 2026-10-06 15:00 UTC (permalink / raw) To: koen.vandeputte Cc: netdev, quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider. Critical: 0 · High: 0 · Medium: 1 · Low: 2 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] The commit message describes the allocator change incorrectly. - [Low] The commit message says netdev_alloc_skb()'s built-in NET_SKB_PAD is 'ensuring that the newly extracted IP headers are optimally cache-… - [Low] In rmnet_map_deaggregate() (drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c:397), `skbn->dev… -- net: rmnet: optimize rx deaggregation memory allocation This commit changes rmnet_map_deaggregate() to allocate the per-packet skbs with netdev_alloc_skb() instead of alloc_skb(GFP_ATOMIC). The commit message says this uses a lockless per-CPU NAPI cache, avoids slab allocator contention, and leaves the IP headers cache-aligned through NET_SKB_PAD. > Replace alloc_skb() with netdev_alloc_skb() on the deaggregation path. > netdev_alloc_skb() is specifically designed for the network receive hot > path, utilizing a fast, lockless per-CPU NAPI cache. Furthermore, it [Severity: Medium] Is this an accurate description of netdev_alloc_skb()? In __netdev_alloc_skb(), small heads and heads larger than a page still use the same kmalloc path as before: net/core/skbuff.c:__netdev_alloc_skb() { ... if (len <= SKB_WITH_OVERHEAD(SKB_SMALL_HEAD_CACHE_SIZE) || len > SKB_WITH_OVERHEAD(PAGE_SIZE) || (gfp_mask & (__GFP_DIRECT_RECLAIM | GFP_DMA))) { skb = __alloc_skb(len, gfp_mask, SKB_ALLOC_RX, NUMA_NO_NODE); ... } RMNET_MAX_PACKET_SIZE is 16384. On 4KB pages, packets above roughly 3.7KB take that branch, and so do small packets. Only mid-size packets take the page_frag path. On that path only the head comes from napi_alloc_cache.page, under local_bh_disable() and local_lock_nested_bh(). The struct sk_buff still comes from the slab: net/core/skbuff.c:__build_skb() { ... skb = kmem_cache_alloc(net_hotdata.skbuff_cache, GFP_ATOMIC | __GFP_NOWARN); ... } The old alloc_skb(..., GFP_ATOMIC) path could already take the struct sk_buff from the per-CPU NAPI skb cache when it had entries: net/core/skbuff.c:__alloc_skb() { ... } else if (!in_hardirq() && !irqs_disabled()) { local_bh_disable(); skb = napi_skb_cache_get(false); local_bh_enable(); } ... } The page_frag path through __build_skb() never uses that cache. Every packet still makes at least one slab allocation. So is it correct to say this change "prevents slab allocator contention"? The description seems to fit napi_alloc_skb() better than netdev_alloc_skb(). The commit message also says the change "significantly reduces memory allocation overhead" and "improves overall throughput". Could some measurements be added to support that? > automatically provisions standard network padding (NET_SKB_PAD), ensuring > that the newly extracted IP headers are optimally cache-aligned for > efficient processing by the upper networking stack. [Severity: Low] Can the IP header actually end up cache-aligned here? __netdev_alloc_skb() already does skb_reserve(skb, NET_SKB_PAD). rmnet_map_deaggregate() then reserves its own headroom on top of that: skbn->dev = skb->dev; skb_reserve(skbn, RMNET_MAP_DEAGGR_HEADROOM); skb_put(skbn, packet_len); Here RMNET_MAP_DEAGGR_HEADROOM is 32. After that, __rmnet_map_ingress_handler() pulls the 4 byte MAP header: skb_pull(skb, sizeof(*map_header)); That puts the IP header at NET_SKB_PAD + 32 + 4 bytes from the head. For an L1_CACHE_BYTES of 32, 64 or 128 that is 68, 100 or 164 bytes, and none of these is cache line aligned. The old offset of 36 was not aligned either. The netdev_alloc_skb() kerneldoc says users should allocate the headroom they need without counting the built in space. RMNET_MAP_DEAGGR_SPACING and RMNET_MAP_DEAGGR_HEADROOM were left as they were, so each deaggregated skb now carries NET_SKB_PAD extra bytes of headroom that nothing uses. Could the commit message be corrected, or the headroom handling changed so that the alignment claim holds? > diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c > index 2eafb1d969c1a..ccdfde3711db8 100644 > --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c > +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c > @@ -390,7 +390,7 @@ struct sk_buff *rmnet_map_deaggregate(struct sk_buff *skb, > if (!packet_len) > return NULL; > > - skbn = alloc_skb(packet_len + RMNET_MAP_DEAGGR_SPACING, GFP_ATOMIC); > + skbn = netdev_alloc_skb(skb->dev, packet_len + RMNET_MAP_DEAGGR_SPACING); > if (!skbn) > return NULL; > [Severity: Low] This isn't a bug, but the skbn->dev = skb->dev assignment that follows in rmnet_map_deaggregate() is now redundant. __netdev_alloc_skb() sets skb->dev on every success path, including the __alloc_skb() fallback: skb_success: skb_reserve(skb, NET_SKB_PAD); skb->dev = dev; Could that assignment be removed as part of this conversion? -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261002143529.3217189-1-koen.vandeputte%40citymesh.com ^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net-next 3/4] net: rmnet: conditionally expand skb headroom in ingress handler 2026-10-02 14:35 [PATCH net-next 0/4] net: rmnet: optimize hot paths to alleviate CPU/memory bottlenecks Koen Vandeputte 2026-10-02 14:35 ` [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation Koen Vandeputte 2026-10-02 14:35 ` [PATCH net-next 2/4] net: rmnet: optimize rx deaggregation memory allocation Koen Vandeputte @ 2026-10-02 14:35 ` Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 2026-10-02 14:35 ` [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull Koen Vandeputte 3 siblings, 1 reply; 11+ messages in thread From: Koen Vandeputte @ 2026-10-02 14:35 UTC (permalink / raw) To: netdev Cc: quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel, Koen Vandeputte Currently, when the rmnet driver processes ingress packets for an Ethernet device (ARPHRD_ETHER), it unconditionally calls pskb_expand_head() to ensure there is enough room to push the MAC header. This forces a costly memory reallocation and payload copy for every single packet on the RX hotpath, severely degrading throughput and increasing CPU overhead. In many modern architectures, the underlying physical driver (e.g., USB) can be configured to pre-allocate this extra ETH_HLEN headroom when minting the initial SKB. Optimize the ingress path by checking if skb_headroom(skb) < ETH_HLEN before triggering the expansion. If the packet arrives with sufficient headroom, the driver now skips the reallocation entirely and safely pushes the header. The expensive pskb_expand_head() operation is now strictly a fallback, allowing properly configured hardware to achieve zero-copy MAC header insertion. Testing this using a temporary print in the new condition showed that the expansion is not triggered as enough space is already available. Signed-off-by: Koen Vandeputte <koen.vandeputte@citymesh.com> --- drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c index aa5523f4618e..95c3e3934fd3 100644 --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c @@ -114,9 +114,11 @@ rmnet_map_ingress_handler(struct sk_buff *skb, u32 data_format; if (skb->dev->type == ARPHRD_ETHER) { - if (pskb_expand_head(skb, ETH_HLEN, 0, GFP_ATOMIC)) { - kfree_skb(skb); - return; + if (skb_headroom(skb) < ETH_HLEN) { + if (pskb_expand_head(skb, ETH_HLEN, 0, GFP_ATOMIC)) { + kfree_skb(skb); + return; + } } skb_push(skb, ETH_HLEN); -- 2.43.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 3/4] net: rmnet: conditionally expand skb headroom in ingress handler 2026-10-02 14:35 ` [PATCH net-next 3/4] net: rmnet: conditionally expand skb headroom in ingress handler Koen Vandeputte @ 2026-10-06 15:00 ` netdev-bot+sashiko 0 siblings, 0 replies; 11+ messages in thread From: netdev-bot+sashiko @ 2026-10-06 15:00 UTC (permalink / raw) To: koen.vandeputte Cc: netdev, quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider. Critical: 0 · High: 0 · Medium: 2 · Low: 1 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] The patch drops the implicit unclone that the old code did for every ARPHRD_ETHER skb. - [Medium] On architectures where NET_IP_ALIGN is 2, removing the per- packet pskb_expand_head() leaves the inner IP header 2 bytes off… - [Low] The commit message misdescribes what the patch changes. -- net: rmnet: conditionally expand skb headroom in ingress handler This patch changes rmnet_map_ingress_handler() so that pskb_expand_head() is called only when skb_headroom() is less than ETH_HLEN. Before, it was called for every ARPHRD_ETHER skb. The goal is to avoid a reallocation and copy on the RX hot path when the lower driver already provides enough headroom. > In many modern architectures, the underlying physical driver (e.g., USB) > can be configured to pre-allocate this extra ETH_HLEN headroom when > minting the initial SKB. [ ... ] > The expensive pskb_expand_head() operation is now strictly a > fallback, allowing properly configured hardware to achieve zero-copy MAC > header insertion. [Severity: Low] Is this description accurate? For an ARPHRD_ETHER lower device, the lower driver has already called eth_type_trans() before rmnet_rx_handler() runs: net/ethernet/eth.c:eth_type_trans() { ... eth = eth_skb_pull_mac(skb); ... } That pulls ETH_HLEN bytes, and those bytes are really the start of the MAP frame. The skb_push(skb, ETH_HLEN) in rmnet_map_ingress_handler() just gives them back, so skb->data points at the MAP header again. No MAC header is inserted. Because those bytes were already pulled, skb_headroom(skb) >= ETH_HLEN always holds at this point. That means the new branch can't be taken, which matches the test result in the commit message: "the expansion is not triggered as enough space is already available" If headroom ever were below ETH_HLEN, expanding and then pushing would leave 14 uninitialized bytes where the MAP header is expected. So the fallback wouldn't give a usable frame either. In practice, does this patch remove the pskb_expand_head() call for every ARPHRD_ETHER skb, whatever the lower driver's configuration? If so, could the commit message say that directly? It would also help to describe what else the call was doing: it made the skb data private and shifted the payload by ETH_HLEN. Commit ec405641e2b7 ("net: qualcomm: rmnet: Fix incorrect allocation flag in receive path") describes the call as needed only when headroom is insufficient. The "severely degrading throughput" claim also has no numbers. Could some measurements be added? > diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c > index aa5523f4618eb..95c3e3934fd3c 100644 > --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c > +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c > @@ -114,9 +114,11 @@ rmnet_map_ingress_handler(struct sk_buff *skb, > u32 data_format; > > if (skb->dev->type == ARPHRD_ETHER) { > - if (pskb_expand_head(skb, ETH_HLEN, 0, GFP_ATOMIC)) { > - kfree_skb(skb); > - return; > + if (skb_headroom(skb) < ETH_HLEN) { > + if (pskb_expand_head(skb, ETH_HLEN, 0, GFP_ATOMIC)) { > + kfree_skb(skb); > + return; > + } > } [Severity: Medium] Can this let rmnet_map_send_ack() write into a data buffer that another skb also uses? Before this patch, pskb_expand_head() ran for every ARPHRD_ETHER skb. As a side effect it gave the skb a private head (skb->cloned = 0, dataref = 1). Headroom is now always >= ETH_HLEN here, so that no longer happens. Nothing earlier in rmnet_rx_handler() unclones the skb either. skb_linearize() does nothing for a linear skb, and the pskb_may_pull() that replaces it later in the series doesn't unclone. The skb can arrive cloned when a ptype_all tap or a tc mirred mirror is active on the lower device: __netif_receive_skb_core() deliver_skb() packet_rcv() skb_clone(), clone queued to the socket rmnet_rx_handler() rmnet_map_ingress_handler() __rmnet_map_ingress_handler() rmnet_map_command() rmnet_map_send_ack() With RMNET_FLAGS_INGRESS_DEAGGREGATION clear and RMNET_FLAGS_INGRESS_MAP_COMMANDS set, an acknowledged FLOW_ENABLE or FLOW_DISABLE command ends up here: rmnet_map_command.c:rmnet_map_send_ack() { ... cmd = (struct rmnet_map_control_command *)(map_header + 1); cmd->cmd_type = type & 0x03; netif_tx_lock(dev); dev->netdev_ops->ndo_start_xmit(skb, dev); ... } This write changes the packet already queued to the AF_PACKET socket, after the socket filter has accepted it. It can also race with a recvmsg() copying the same buffer on another CPU. Raw-IP lower devices never had the implicit unclone, but for ARPHRD_ETHER this looks new. Would skb_cow_head(skb, ETH_HLEN) work here instead? It reallocates only when headroom is short or the header is cloned, so the fast path stays. Another option is for rmnet_map_send_ack() to make the skb writable, for example with skb_ensure_writable() or skb_unclone(), before the write. > > skb_push(skb, ETH_HLEN); [Severity: Medium] Could skipping the copy leave the inner IP header misaligned on architectures where NET_IP_ALIGN is 2? The generic NET_IP_ALIGN in include/linux/skbuff.h is 2. Only x86, arm64 and powerpc set it to 0. A lower Ethernet driver that uses NET_IP_ALIGN lays out its RX buffer for a 14 byte Ethernet header before L3. Here, though, the frame starts with a 4 byte MAP header, plus 4 more bytes for a MAPv5 csum header. With NET_SKB_PAD of 64 and NET_IP_ALIGN of 2: before: pskb_expand_head() copied into a fresh head shifted by ETH_HLEN MAP header at head+80, IP header at head+84 (4-byte aligned) after: frame is parsed in place MAP header at head+66, IP header at head+70 or +74 (2 mod 4) In non-deaggregation mode, the checksum code then does word loads on that header: rmnet_map_data.c:rmnet_map_ipv4_dl_csum_trailer() { ... if (ip_fast_csum(ip4h, ip4h->ihl)) { ... } The skb then goes to the IP stack. On architectures without CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS, the stack expects an aligned iphdr. On strict-alignment CPUs such as MIPS, wouldn't these loads trap and be emulated on every packet? That would make this change a slowdown rather than a speedup. The deaggregation path is not affected, because rmnet_map_deaggregate() copies each packet into a new skb. Which architecture and ingress data format was this tested with? -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261002143529.3217189-1-koen.vandeputte%40citymesh.com ^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull 2026-10-02 14:35 [PATCH net-next 0/4] net: rmnet: optimize hot paths to alleviate CPU/memory bottlenecks Koen Vandeputte ` (2 preceding siblings ...) 2026-10-02 14:35 ` [PATCH net-next 3/4] net: rmnet: conditionally expand skb headroom in ingress handler Koen Vandeputte @ 2026-10-02 14:35 ` Koen Vandeputte 2026-10-02 20:48 ` Sean Tranchetti 2026-10-06 15:00 ` netdev-bot+sashiko 3 siblings, 2 replies; 11+ messages in thread From: Koen Vandeputte @ 2026-10-02 14:35 UTC (permalink / raw) To: netdev Cc: quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel, Koen Vandeputte The rmnet RX handler currently calls skb_linearize() unconditionally on every incoming packet. When receiving large, hardware-aggregated frames (e.g., 16KB or 32KB from USB modems via qmi_wwan), this forces the network stack to allocate a massive contiguous buffer and memcpy the entire scattered payload out of paged memory. This severe performance bottleneck is completely unnecessary because the RX handler only needs to inspect the initial 4-byte MAP header to determine the logical endpoint and route the packet. Replace skb_linearize() with pskb_may_pull(). This targeted check ensures the MAP header is safely accessible in the linear data area while allowing the bulky payload to remain untouched in paged memory fragments. This preserves the zero-copy architecture for aggregated frames, drastically reducing CPU overhead, cache thrashing, and memory allocations on the RX hotpath. On a slower embedded board, this improves throughput by ~10%. Signed-off-by: Koen Vandeputte <koen.vandeputte@citymesh.com> --- drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c index 95c3e3934fd3..a2379df69a6d 100644 --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c @@ -217,7 +217,7 @@ rx_handler_result_t rmnet_rx_handler(struct sk_buff **pskb) if (!skb) goto done; - if (skb_linearize(skb)) { + if (!pskb_may_pull(skb, sizeof(struct rmnet_map_header))) { kfree_skb(skb); goto done; } -- 2.43.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull 2026-10-02 14:35 ` [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull Koen Vandeputte @ 2026-10-02 20:48 ` Sean Tranchetti 2026-10-06 9:58 ` Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 1 sibling, 1 reply; 11+ messages in thread From: Sean Tranchetti @ 2026-10-02 20:48 UTC (permalink / raw) To: netdev Cc: andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel, subash.a.kasiviswanathan, koen.vandeputte Hi Koen, Unfortunately this patch only works in MAP v1 configuration. In either MAPv4 or MAPv5 with checksum offload support, the driver needs to read more than just the standard 4 byte MAP header. ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull 2026-10-02 20:48 ` Sean Tranchetti @ 2026-10-06 9:58 ` Koen Vandeputte 0 siblings, 0 replies; 11+ messages in thread From: Koen Vandeputte @ 2026-10-06 9:58 UTC (permalink / raw) To: Sean Tranchetti Cc: netdev, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel, subash.a.kasiviswanathan On Fri, Oct 2, 2026 at 10:48 PM Sean Tranchetti <sean.tranchetti@oss.qualcomm.com> wrote: > > Hi Koen, > > Unfortunately this patch only works in MAP v1 configuration. > In either MAPv4 or MAPv5 with checksum offload support, the driver needs > to read more than just the standard 4 byte MAP header. Hi Sean, Thanks a lot for checking this. You are right .. I did test it on V5 also, but got lucky here that data was already aligned by nature. I'll check if this can be fixed and submit a v2 Appreciated! Koen ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull 2026-10-02 14:35 ` [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull Koen Vandeputte 2026-10-02 20:48 ` Sean Tranchetti @ 2026-10-06 15:00 ` netdev-bot+sashiko 1 sibling, 0 replies; 11+ messages in thread From: netdev-bot+sashiko @ 2026-10-06 15:00 UTC (permalink / raw) To: koen.vandeputte Cc: netdev, quic_subashab, quic_stranche, andrew+netdev, davem, edumazet, kuba, pabeni, dnlplm, linux-kernel Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider. Critical: 1 · High: 0 · Medium: 2 · Low: 0 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Critical] Regression in rmnet_rx_handler() (drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c:220): the patch… - [Medium] The commit message misstates what the RX path needs and what the change does, and it hides that the patch undoes a crash fix. - [Medium] Shared skb data modified without copy-on-write for non-linear cloned MAP command packets. -- net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull This patch replaces the unconditional skb_linearize() in rmnet_rx_handler() with pskb_may_pull() of sizeof(struct rmnet_map_header). After the change, only the 4-byte MAP header is guaranteed to be in the linear area. The goal is to avoid copying large aggregated frames on the RX path. > When receiving large, hardware-aggregated frames > (e.g., 16KB or 32KB from USB modems via qmi_wwan), > this forces the network stack to allocate a massive > contiguous buffer and memcpy the entire scattered payload > out of paged memory. [ ... ] > On a slower embedded board, this improves throughput by ~10%. [Severity: Medium] Can the throughput gain come from this change in the qmi_wwan setup? skb_linearize() is: return skb_is_nonlinear(skb) ? __skb_linearize(skb) : 0; That makes it a no-op for linear skbs. The usbnet rx_submit() path used by qmi_wwan allocates linear skbs with __netdev_alloc_skb(). The only skbs this change affects are non-linear ones. Those are the skbs that then reach the linear-only parsers described below. > This severe performance bottleneck is completely unnecessary > because the RX handler only needs to inspect the initial > 4-byte MAP header to determine the logical endpoint > and route the packet. Is this description accurate? The same RX path goes on into rmnet_map_deaggregate(), rmnet_map_validate_packet_len(), rmnet_map_process_next_hdr_packet(), rmnet_map_checksum_downlink_packet() and rmnet_map_command(). All of them access the full frame through skb->data. > This preserves the zero-copy architecture for aggregated frames, > drastically reducing CPU overhead, cache thrashing, > and memory allocations on the RX hotpath. The aggregated path doesn't look zero-copy. rmnet_map_deaggregate() allocates a new skb for each record and copies into it: drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c:rmnet_map_deaggregate() { ... skbn = netdev_alloc_skb(skb->dev, packet_len + RMNET_MAP_DEAGGR_SPACING); ... memcpy(skbn->data, skb->data, packet_len); skb_pull(skb, packet_len); ... } The commit message also leaves out commit d698e6a00a60 ("net: qualcomm: rmnet: Fix rx_handler for non-linear skbs"). That commit added this skb_linearize() to fix crashes with non-linear skbs. Should it be mentioned here? > diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c > index 95c3e3934fd3c..a2379df69a6d0 100644 > --- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c > +++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c > @@ -217,7 +217,7 @@ rx_handler_result_t rmnet_rx_handler(struct sk_buff **pskb) > if (!skb) > goto done; > > - if (skb_linearize(skb)) { > + if (!pskb_may_pull(skb, sizeof(struct rmnet_map_header))) { [Severity: Critical] Does this bring back the crash fixed by commit d698e6a00a60 ("net: qualcomm: rmnet: Fix rx_handler for non-linear skbs")? That commit says: There is no guarantee that rmnet rx_handler is only fed with linear skbs, but current rmnet implementation does not check that, leading to crash in case of non linear skbs processed as linear ones. Now only 4 bytes are guaranteed to be linear. The downstream parsers still treat skb->data as covering the whole frame. They check lengths only against skb->len, which includes paged and frag_list data. Non-linear input does happen in-tree. For example, mhi_net_skb_agg() in drivers/net/mhi_net.c builds frag_list skbs that end up in rmnet. With RMNET_FLAGS_INGRESS_DEAGGREGATION: rmnet_rx_handler() rmnet_map_ingress_handler() rmnet_map_deaggregate() rmnet_map_validate_packet_len() if (skb->len < packet_len) return 0; memcpy(skbn->data, skb->data, packet_len); skb_pull(skb, packet_len); Won't the memcpy() read past skb->tail into tailroom and skb_shared_info? That memory would then be copied into the packet delivered to the stack. When packet_len > skb_headlen(skb), __skb_pull() would also hit: skb->len -= len; if (unlikely(skb->len < skb->data_len)) { ... BUG(); The MAPv5 next_hdr at data + sizeof(*maph) in rmnet_map_validate_packet_len() is also read without a pull. Without deaggregation, __rmnet_map_ingress_handler() has similar problems: - MAPv5: rmnet_map_process_next_hdr_packet() reads next_hdr at skb->data + sizeof(struct rmnet_map_header). Then 4 + 4 bytes are pulled in total, so BUG() fires when the linear head is shorter than 8 bytes. - MAPv4: rmnet_set_skb_proto() reads skb->data[0] after the MAP header has been pulled. rmnet_map_checksum_downlink_packet() dereferences skb->data + len, and len comes from the device-supplied pkt_len. The IPv4/IPv6 helpers also read IP and L4 headers at raw skb->data offsets. Could the CHECKSUM_UNNECESSARY verdict end up computed from out-of-bounds memory? - skb_trim(skb, len) goes through __skb_trim()->__skb_set_length(). That does WARN_ON(skb_is_nonlinear(skb)) and returns without trimming, so MAP padding and the checksum trailer would reach the IP stack. With RMNET_FLAGS_INGRESS_MAP_COMMANDS, rmnet_map_command() reads cmd->command_name at map_header + 1, and rmnet_map_send_ack() writes: cmd = (struct rmnet_map_control_command *)(map_header + 1); cmd->cmd_type = type & 0x03; Is this an out-of-bounds write when only the MAP header is linear? The skb_trim() in rmnet_map_send_ack() would also hit the nonlinear WARN. The aggregation section of Documentation/networking/device_drivers/cellular/qualcomm/rmnet.rst also says the MAP packets are "delivered to rmnet in a single linear skb". Would it be better to keep the linearization? The alternative is to make every parser frag-aware: - per-record pskb_may_pull() or skb_copy_bits(), or clone plus pskb_trim(), in deaggregation - pulls for the csum header, the trailer and the command - pskb_trim() instead of skb_trim() A single 4-byte pull at the top doesn't seem to be enough. [Severity: Medium] Can this leave the head of a cloned non-linear skb shared when rmnet later writes to it? If a packet tap is active on the real device, packet_rcv() in net/packet/af_packet.c sees skb_shared() and queues a skb_clone(). The skb that rmnet_rx_handler() then receives is skb_cloned(). Before this patch, skb_linearize() on a non-linear skb went through __pskb_pull_tail(), which does: if (eat > 0 || skb_cloned(skb)) { if (pskb_expand_head(skb, 0, eat > 0 ? eat + 128 : 0, GFP_ATOMIC)) That gave rmnet a private head. pskb_may_pull(skb, 4) returns early when the MAP header is already linear, so the head stays shared. With RMNET_FLAGS_INGRESS_MAP_COMMANDS and without deaggregation, rmnet_map_command()->rmnet_map_send_ack() then does: cmd->cmd_type = type & 0x03; There is no skb_cow() or skb_ensure_writable() before this write. Would the packet socket reader then see the captured command request changed into an ACK? The netif_tx_lock() taken afterwards doesn't serialize against that reader. The same write already happened for linear cloned skbs before this patch. The non-linear cloned case is what this patch adds. -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261002143529.3217189-1-koen.vandeputte%40citymesh.com ^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-06 15:00 UTC | newest] Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-02 14:35 [PATCH net-next 0/4] net: rmnet: optimize hot paths to alleviate CPU/memory bottlenecks Koen Vandeputte 2026-10-02 14:35 ` [PATCH net-next 1/4] net: rmnet: use fast monotonic time for tx aggregation Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 2026-10-02 14:35 ` [PATCH net-next 2/4] net: rmnet: optimize rx deaggregation memory allocation Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 2026-10-02 14:35 ` [PATCH net-next 3/4] net: rmnet: conditionally expand skb headroom in ingress handler Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko 2026-10-02 14:35 ` [PATCH net-next 4/4] net: rmnet: optimize rx handler by replacing skb_linearize with pskb_may_pull Koen Vandeputte 2026-10-02 20:48 ` Sean Tranchetti 2026-10-06 9:58 ` Koen Vandeputte 2026-10-06 15:00 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®