* [PATCH net v2] ice: fix Tx hang dump using stale tx_buf[0].skb
@ 2026-10-26 13:30 ` Vincent Choiniere
0 siblings, 0 replies; 4+ messages in thread
From: Vincent Choiniere via B4 Relay @ 2026-10-26 13:30 UTC (permalink / raw)
To: Tony Nguyen, Przemek Kitszel, intel-wired-lan
Cc: Aleksandr Loktionov, Michal Kubiak, Wojciech Drewek,
Mateusz Polchlopek, Paul Menzel, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, netdev, linux-kernel,
stable, Vincent Choiniere
From: Vincent Choiniere <choiniere.vincent@hydroquebec.com>
ice_tx_hang_reporter_dump() always reads tx_ring->tx_buf[0].skb,
regardless of next_to_clean. The Tx cleanup path frees the skb and
marks the buffer empty without clearing the skb pointer, so slot 0 can
hold a stale pointer. A NULL check alone does not cover this, and
devlink_fmsg_dump_skb() can fault on the freed skb.
Using tx_buf[next_to_clean] alone does not fix the lifetime issues.
The dump runs from the service task without synchronization with Tx
cleanup or ring replacement:
- ice_clean_tx_irq() can free the skb at next_to_clean while the
report is being prepared. It calls napi_consume_skb() before
marking the buffer empty.
- The ring saved by ice_tx_timeout() can be replaced or freed before
the service task runs. ethtool -G replaces the ring contents and
frees tx_buf and desc, while ethtool -L can free whole rings.
Record the queue index, next_to_clean and next_to_use at timeout time.
Before reporting, take RTNL and check that the saved ring is still
one of the PF VSI Tx rings and that the VSI is up. Copy the
descriptors and IRQ name while the ring storage is protected against
replacement. If the ring cannot be used or the copy cannot be made,
report only the values recorded at timeout time.
Tx cleanup can still run while the ring state is copied, so do not
dereference the skb. Retain only its pointer value in the dump and
remove the skb contents.
Release RTNL before calling devlink_health_report(), which takes the
devlink instance lock. Build the dump from the copied data and free
the descriptor copy after reporting.
Observed on an E810 PF passed through to a KVM guest with Ubuntu
kernel 7.0.0-14 (PREEMPT_RT and a local scheduler patch): a delayed
queue IRQ led to a Tx timeout and a page fault in
devlink_fmsg_dump_skb():
BUG: unable to handle page fault for address: 0000000004cd0c11
Workqueue: ice ice_service_task [ice]
RIP: devlink_fmsg_dump_skb+0x2d8/0x5d0
Call Trace:
ice_tx_hang_reporter_dump+0x1ce/0x210 [ice]
devlink_health_do_dump+0xcc/0x190
devlink_health_report+0x1d7/0x260
ice_report_tx_hang+0x7d/0xa0 [ice]
Fixes: 2a82874a3b7b ("ice: add Tx hang devlink health reporter")
Link: https://lore.kernel.org/all/20260327072332.130320-1-aleksandr.loktionov@intel.com/
Cc: stable@vger.kernel.org
Signed-off-by: Vincent Choiniere <choiniere.vincent@hydroquebec.com>
---
Changes in v2:
- Validate the saved ring under RTNL before dereferencing it from
the service task, and restrict snapshots to the PF VSI while it
is up (Sashiko).
- Check next_to_clean against count before indexing tx_buf, and
copy the descriptors and IRQ name while ring replacement is
excluded (Sashiko).
- Stop dereferencing the skb and remove its contents from the dump.
Keep only its pointer value (Sashiko).
- Release RTNL before reporting to preserve the devlink instance
lock ordering.
- Record the queue index and ring indices at timeout time as a
fallback when a snapshot cannot be made.
- Drop Paul's Reviewed-by because the patch changed substantially.
v1:
https://lore.kernel.org/netdev/20261016-ice-tx-hang-dump-v1-1-3df0f5979a51@hydroquebec.com/
Testing:
- Built ice.ko with x86_64 defconfig, CONFIG_ICE=m, GCC 12.2 and W=1.
Ran sparse with C=2 (sparse git c464045). Neither reported warnings
in the changed files.
- checkpatch --strict reported only an unknown Fixes commit because
the local tree is a shallow clone.
- Runtime testing used an E810 PF passed through to a KVM guest with
Ubuntu kernel 7.0.0-38 (PREEMPT_RT), KASAN, PROVE_LOCKING and
DEBUG_ATOMIC_SLEEP.
- Forced Tx timeouts by starving one queue's IRQ thread. The watchdog
fired after five seconds, the PF recovered, and the dump retained
the v1 fields except for the skb contents.
- Used a test-only kernel patch to insert configurable delays before
ice_report_tx_hang() or the dump callback. Racing ethtool commands
were started when the instrumentation logged the delay.
- With v1 and the same instrumentation, shrinking the ring with
ethtool -G tx 64 while next_to_clean was above 64 triggered a KASAN
slab-out-of-bounds report. This was also reproduced on a non-RT
kernel built from net. Racing ethtool -L combined 1 triggered a
use-after-free followed by a general protection fault.
- With v2, ran 168 forced Tx timeouts, including 149 with concurrent
ethtool -G (tx 64, 128 or 512) or ethtool -L combined 1, before and
after the ring state was copied. No KASAN, lockdep or
sleeping-in-atomic reports were observed. When the saved ring was
no longer present, the dump contained only the timeout-time values.
Limitations:
- The same stress testing was not completed on a non-RT kernel,
where Tx timeouts could not be forced reliably.
- Subfunction VSIs and XDP were not tested. Subfunction VSI rings
are excluded from the snapshot because ice_sf_dev_remove() frees
them without RTNL.
- This patch does not synchronize publication and consumption of
tx_hang_buf between ice_tx_timeout() and the service task. A newer
timeout may still be dropped or its fields mixed with an older
event.
The original devlink_fmsg_dump_skb() crash was reproduced on Ubuntu
kernel 7.0.0-14 with PREEMPT_RT and a local scheduler patch. The
additional race reproductions above used test-only instrumentation.
The affected reporter code is also present in upstream net at
191aa4154.
---
drivers/net/ethernet/intel/ice/devlink/health.c | 117 ++++++++++++++++++++----
drivers/net/ethernet/intel/ice/devlink/health.h | 6 ++
2 files changed, 107 insertions(+), 16 deletions(-)
diff --git a/drivers/net/ethernet/intel/ice/devlink/health.c b/drivers/net/ethernet/intel/ice/devlink/health.c
index 8e9a8a817..b9493ab47 100644
--- a/drivers/net/ethernet/intel/ice/devlink/health.c
+++ b/drivers/net/ethernet/intel/ice/devlink/health.c
@@ -382,7 +382,13 @@ struct ice_tx_hang_event {
u16 queue;
u16 next_to_clean;
u16 next_to_use;
- struct ice_tx_ring *tx_ring;
+ /* the fields below are only valid if @desc is set */
+ char irq_name[ICE_INT_NAME_STR_LEN];
+ void *desc_ptr;
+ dma_addr_t dma;
+ void *desc;
+ u32 desc_size;
+ void *skb_ptr; /* pointer value only, never dereferenced */
};
static int ice_tx_hang_reporter_dump(struct devlink_health_reporter *reporter,
@@ -390,12 +396,10 @@ static int ice_tx_hang_reporter_dump(struct devlink_health_reporter *reporter,
struct netlink_ext_ack *extack)
{
struct ice_tx_hang_event *event = priv_ctx;
- struct sk_buff *skb;
if (!event)
return 0;
- skb = event->tx_ring->tx_buf->skb;
devlink_fmsg_obj_nest_start(fmsg);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, head);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, intr);
@@ -403,13 +407,14 @@ static int ice_tx_hang_reporter_dump(struct devlink_health_reporter *reporter,
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, queue);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, next_to_clean);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, next_to_use);
- devlink_fmsg_put(fmsg, "irq-mapping", event->tx_ring->q_vector->name);
- ice_fmsg_put_ptr(fmsg, "desc-ptr", event->tx_ring->desc);
- ice_fmsg_put_ptr(fmsg, "dma-ptr", (void *)(long)event->tx_ring->dma);
- ice_fmsg_put_ptr(fmsg, "skb-ptr", skb);
- devlink_fmsg_binary_pair_put(fmsg, "desc", event->tx_ring->desc,
- event->tx_ring->count * sizeof(struct ice_tx_desc));
- devlink_fmsg_dump_skb(fmsg, skb);
+ if (event->desc) {
+ devlink_fmsg_put(fmsg, "irq-mapping", event->irq_name);
+ ice_fmsg_put_ptr(fmsg, "desc-ptr", event->desc_ptr);
+ ice_fmsg_put_ptr(fmsg, "dma-ptr", (void *)(long)event->dma);
+ ice_fmsg_put_ptr(fmsg, "skb-ptr", event->skb_ptr);
+ devlink_fmsg_binary_pair_put(fmsg, "desc", event->desc,
+ event->desc_size);
+ }
devlink_fmsg_obj_nest_end(fmsg);
return 0;
@@ -424,24 +429,104 @@ void ice_prep_tx_hang_report(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
buf->vsi_num = vsi_num;
buf->head = head;
buf->intr = intr;
+ buf->queue = tx_ring->q_index;
+ buf->next_to_clean = tx_ring->next_to_clean;
+ buf->next_to_use = tx_ring->next_to_use;
+}
+
+/**
+ * ice_tx_hang_snapshot - copy the hung Tx ring state for the dump
+ * @pf: PF on which the Tx hang was detected
+ * @tx_ring: ring saved by ice_tx_timeout(), possibly stale
+ * @ev: event to fill in
+ *
+ * The saved ring may have been freed or replaced since the timeout (e.g. by
+ * ethtool -G or -L). Use it only if it is still one of the PF VSI Tx rings
+ * and the VSI is up: those rings are only replaced or freed under RTNL or
+ * from the service task, so with RTNL held every access below is to live
+ * memory. ice_clean_tx_irq() may still run and free the skb at
+ * next_to_clean at any time, so only the pointer value is recorded and the
+ * skb itself is never dereferenced.
+ */
+static void ice_tx_hang_snapshot(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
+ struct ice_tx_hang_event *ev)
+{
+ struct ice_vsi *vsi = ice_get_main_vsi(pf);
+ struct ice_q_vector *q_vector;
+ struct ice_tx_buf *tx_buf;
+ unsigned int i;
+ u16 ntc, ntu;
+ void *desc;
+ u32 size;
+
+ ASSERT_RTNL();
+
+ if (!tx_ring || !vsi || !vsi->tx_rings ||
+ test_bit(ICE_VSI_DOWN, vsi->state))
+ return;
+
+ ice_for_each_txq(vsi, i)
+ if (vsi->tx_rings[i] == tx_ring)
+ break;
+ if (i == vsi->num_txq)
+ return;
+
+ q_vector = tx_ring->q_vector;
+ if (!q_vector || !tx_ring->desc || !tx_ring->tx_buf || !tx_ring->count)
+ return;
+
+ size = tx_ring->count * sizeof(struct ice_tx_desc);
+ desc = kvmalloc(size, GFP_KERNEL);
+ if (!desc)
+ return;
+
+ ntc = READ_ONCE(tx_ring->next_to_clean);
+ ntu = READ_ONCE(tx_ring->next_to_use);
+
+ if (ntc < tx_ring->count) {
+ tx_buf = &tx_ring->tx_buf[ntc];
+ if (tx_buf->type == ICE_TX_BUF_SKB)
+ ev->skb_ptr = READ_ONCE(tx_buf->skb);
+
+ memcpy(desc, tx_ring->desc, size);
+ ev->next_to_clean = ntc;
+ ev->next_to_use = ntu;
+ strscpy(ev->irq_name, q_vector->name);
+ ev->desc_ptr = tx_ring->desc;
+ ev->dma = tx_ring->dma;
+ ev->desc_size = size;
+ ev->desc = desc;
+ }
+
+ if (!ev->desc)
+ kvfree(desc);
}
void ice_report_tx_hang(struct ice_pf *pf)
{
struct ice_health_tx_hang_buf *buf = &pf->health_reporters.tx_hang_buf;
- struct ice_tx_ring *tx_ring = buf->tx_ring;
-
struct ice_tx_hang_event ev = {
.head = buf->head,
.intr = buf->intr,
.vsi_num = buf->vsi_num,
- .queue = tx_ring->q_index,
- .next_to_clean = tx_ring->next_to_clean,
- .next_to_use = tx_ring->next_to_use,
- .tx_ring = tx_ring,
+ .queue = buf->queue,
+ .next_to_clean = buf->next_to_clean,
+ .next_to_use = buf->next_to_use,
};
+ if (!pf->health_reporters.tx_hang)
+ return;
+
+ rtnl_lock();
+ ice_tx_hang_snapshot(pf, buf->tx_ring, &ev);
+ rtnl_unlock();
+
+ /* devlink_health_report() takes the devlink instance lock, which nests
+ * outside RTNL, so only report once RTNL has been released.
+ */
ice_devlink_health_report(pf->health_reporters.tx_hang, "Tx hang", &ev);
+
+ kvfree(ev.desc);
}
static struct devlink_health_reporter *
diff --git a/drivers/net/ethernet/intel/ice/devlink/health.h b/drivers/net/ethernet/intel/ice/devlink/health.h
index 5edfc4d2a..508e4115d 100644
--- a/drivers/net/ethernet/intel/ice/devlink/health.h
+++ b/drivers/net/ethernet/intel/ice/devlink/health.h
@@ -37,6 +37,9 @@ enum ice_mdd_src {
* @head: descriptor head
* @intr: interrupt register value
* @vsi_num: VSI owning the queue that the hang occurred on
+ * @queue: index of the queue that the hang occurred on
+ * @next_to_clean: next_to_clean of the ring when the hang was detected
+ * @next_to_use: next_to_use of the ring when the hang was detected
* @fw_status: buffer for last received FW Status event
* @port_status: buffer for last received Port Status event
*/
@@ -50,6 +53,9 @@ struct ice_health {
u32 head;
u32 intr;
u16 vsi_num;
+ u16 queue;
+ u16 next_to_clean;
+ u16 next_to_use;
);
struct ice_aqc_health_status_elem fw_status;
struct ice_aqc_health_status_elem port_status;
---
base-commit: 7076f684d5132e6a6145e1da2fa74771ecd052e5
change-id: 20261016-ice-tx-hang-dump-e46b4ed483d6
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH net v2] ice: fix Tx hang dump using stale tx_buf[0].skb
@ 2026-10-26 13:30 ` Vincent Choiniere
0 siblings, 0 replies; 4+ messages in thread
From: Vincent Choiniere @ 2026-10-26 13:30 UTC (permalink / raw)
To: Tony Nguyen, Przemek Kitszel, intel-wired-lan
Cc: Aleksandr Loktionov, Michal Kubiak, Wojciech Drewek,
Mateusz Polchlopek, Paul Menzel, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, netdev, linux-kernel,
stable, Vincent Choiniere
ice_tx_hang_reporter_dump() always reads tx_ring->tx_buf[0].skb,
regardless of next_to_clean. The Tx cleanup path frees the skb and
marks the buffer empty without clearing the skb pointer, so slot 0 can
hold a stale pointer. A NULL check alone does not cover this, and
devlink_fmsg_dump_skb() can fault on the freed skb.
Using tx_buf[next_to_clean] alone does not fix the lifetime issues.
The dump runs from the service task without synchronization with Tx
cleanup or ring replacement:
- ice_clean_tx_irq() can free the skb at next_to_clean while the
report is being prepared. It calls napi_consume_skb() before
marking the buffer empty.
- The ring saved by ice_tx_timeout() can be replaced or freed before
the service task runs. ethtool -G replaces the ring contents and
frees tx_buf and desc, while ethtool -L can free whole rings.
Record the queue index, next_to_clean and next_to_use at timeout time.
Before reporting, take RTNL and check that the saved ring is still
one of the PF VSI Tx rings and that the VSI is up. Copy the
descriptors and IRQ name while the ring storage is protected against
replacement. If the ring cannot be used or the copy cannot be made,
report only the values recorded at timeout time.
Tx cleanup can still run while the ring state is copied, so do not
dereference the skb. Retain only its pointer value in the dump and
remove the skb contents.
Release RTNL before calling devlink_health_report(), which takes the
devlink instance lock. Build the dump from the copied data and free
the descriptor copy after reporting.
Observed on an E810 PF passed through to a KVM guest with Ubuntu
kernel 7.0.0-14 (PREEMPT_RT and a local scheduler patch): a delayed
queue IRQ led to a Tx timeout and a page fault in
devlink_fmsg_dump_skb():
BUG: unable to handle page fault for address: 0000000004cd0c11
Workqueue: ice ice_service_task [ice]
RIP: devlink_fmsg_dump_skb+0x2d8/0x5d0
Call Trace:
ice_tx_hang_reporter_dump+0x1ce/0x210 [ice]
devlink_health_do_dump+0xcc/0x190
devlink_health_report+0x1d7/0x260
ice_report_tx_hang+0x7d/0xa0 [ice]
Fixes: 2a82874a3b7b ("ice: add Tx hang devlink health reporter")
Link: https://lore.kernel.org/all/20260327072332.130320-1-aleksandr.loktionov@intel.com/
Cc: stable@vger.kernel.org
Signed-off-by: Vincent Choiniere <choiniere.vincent@hydroquebec.com>
---
Changes in v2:
- Validate the saved ring under RTNL before dereferencing it from
the service task, and restrict snapshots to the PF VSI while it
is up (Sashiko).
- Check next_to_clean against count before indexing tx_buf, and
copy the descriptors and IRQ name while ring replacement is
excluded (Sashiko).
- Stop dereferencing the skb and remove its contents from the dump.
Keep only its pointer value (Sashiko).
- Release RTNL before reporting to preserve the devlink instance
lock ordering.
- Record the queue index and ring indices at timeout time as a
fallback when a snapshot cannot be made.
- Drop Paul's Reviewed-by because the patch changed substantially.
v1:
https://lore.kernel.org/netdev/20261016-ice-tx-hang-dump-v1-1-3df0f5979a51@hydroquebec.com/
Testing:
- Built ice.ko with x86_64 defconfig, CONFIG_ICE=m, GCC 12.2 and W=1.
Ran sparse with C=2 (sparse git c464045). Neither reported warnings
in the changed files.
- checkpatch --strict reported only an unknown Fixes commit because
the local tree is a shallow clone.
- Runtime testing used an E810 PF passed through to a KVM guest with
Ubuntu kernel 7.0.0-38 (PREEMPT_RT), KASAN, PROVE_LOCKING and
DEBUG_ATOMIC_SLEEP.
- Forced Tx timeouts by starving one queue's IRQ thread. The watchdog
fired after five seconds, the PF recovered, and the dump retained
the v1 fields except for the skb contents.
- Used a test-only kernel patch to insert configurable delays before
ice_report_tx_hang() or the dump callback. Racing ethtool commands
were started when the instrumentation logged the delay.
- With v1 and the same instrumentation, shrinking the ring with
ethtool -G tx 64 while next_to_clean was above 64 triggered a KASAN
slab-out-of-bounds report. This was also reproduced on a non-RT
kernel built from net. Racing ethtool -L combined 1 triggered a
use-after-free followed by a general protection fault.
- With v2, ran 168 forced Tx timeouts, including 149 with concurrent
ethtool -G (tx 64, 128 or 512) or ethtool -L combined 1, before and
after the ring state was copied. No KASAN, lockdep or
sleeping-in-atomic reports were observed. When the saved ring was
no longer present, the dump contained only the timeout-time values.
Limitations:
- The same stress testing was not completed on a non-RT kernel,
where Tx timeouts could not be forced reliably.
- Subfunction VSIs and XDP were not tested. Subfunction VSI rings
are excluded from the snapshot because ice_sf_dev_remove() frees
them without RTNL.
- This patch does not synchronize publication and consumption of
tx_hang_buf between ice_tx_timeout() and the service task. A newer
timeout may still be dropped or its fields mixed with an older
event.
The original devlink_fmsg_dump_skb() crash was reproduced on Ubuntu
kernel 7.0.0-14 with PREEMPT_RT and a local scheduler patch. The
additional race reproductions above used test-only instrumentation.
The affected reporter code is also present in upstream net at
191aa4154.
---
drivers/net/ethernet/intel/ice/devlink/health.c | 117 ++++++++++++++++++++----
drivers/net/ethernet/intel/ice/devlink/health.h | 6 ++
2 files changed, 107 insertions(+), 16 deletions(-)
diff --git a/drivers/net/ethernet/intel/ice/devlink/health.c b/drivers/net/ethernet/intel/ice/devlink/health.c
index 8e9a8a817..b9493ab47 100644
--- a/drivers/net/ethernet/intel/ice/devlink/health.c
+++ b/drivers/net/ethernet/intel/ice/devlink/health.c
@@ -382,7 +382,13 @@ struct ice_tx_hang_event {
u16 queue;
u16 next_to_clean;
u16 next_to_use;
- struct ice_tx_ring *tx_ring;
+ /* the fields below are only valid if @desc is set */
+ char irq_name[ICE_INT_NAME_STR_LEN];
+ void *desc_ptr;
+ dma_addr_t dma;
+ void *desc;
+ u32 desc_size;
+ void *skb_ptr; /* pointer value only, never dereferenced */
};
static int ice_tx_hang_reporter_dump(struct devlink_health_reporter *reporter,
@@ -390,12 +396,10 @@ static int ice_tx_hang_reporter_dump(struct devlink_health_reporter *reporter,
struct netlink_ext_ack *extack)
{
struct ice_tx_hang_event *event = priv_ctx;
- struct sk_buff *skb;
if (!event)
return 0;
- skb = event->tx_ring->tx_buf->skb;
devlink_fmsg_obj_nest_start(fmsg);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, head);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, intr);
@@ -403,13 +407,14 @@ static int ice_tx_hang_reporter_dump(struct devlink_health_reporter *reporter,
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, queue);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, next_to_clean);
ICE_DEVLINK_FMSG_PUT_FIELD(fmsg, event, next_to_use);
- devlink_fmsg_put(fmsg, "irq-mapping", event->tx_ring->q_vector->name);
- ice_fmsg_put_ptr(fmsg, "desc-ptr", event->tx_ring->desc);
- ice_fmsg_put_ptr(fmsg, "dma-ptr", (void *)(long)event->tx_ring->dma);
- ice_fmsg_put_ptr(fmsg, "skb-ptr", skb);
- devlink_fmsg_binary_pair_put(fmsg, "desc", event->tx_ring->desc,
- event->tx_ring->count * sizeof(struct ice_tx_desc));
- devlink_fmsg_dump_skb(fmsg, skb);
+ if (event->desc) {
+ devlink_fmsg_put(fmsg, "irq-mapping", event->irq_name);
+ ice_fmsg_put_ptr(fmsg, "desc-ptr", event->desc_ptr);
+ ice_fmsg_put_ptr(fmsg, "dma-ptr", (void *)(long)event->dma);
+ ice_fmsg_put_ptr(fmsg, "skb-ptr", event->skb_ptr);
+ devlink_fmsg_binary_pair_put(fmsg, "desc", event->desc,
+ event->desc_size);
+ }
devlink_fmsg_obj_nest_end(fmsg);
return 0;
@@ -424,24 +429,104 @@ void ice_prep_tx_hang_report(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
buf->vsi_num = vsi_num;
buf->head = head;
buf->intr = intr;
+ buf->queue = tx_ring->q_index;
+ buf->next_to_clean = tx_ring->next_to_clean;
+ buf->next_to_use = tx_ring->next_to_use;
+}
+
+/**
+ * ice_tx_hang_snapshot - copy the hung Tx ring state for the dump
+ * @pf: PF on which the Tx hang was detected
+ * @tx_ring: ring saved by ice_tx_timeout(), possibly stale
+ * @ev: event to fill in
+ *
+ * The saved ring may have been freed or replaced since the timeout (e.g. by
+ * ethtool -G or -L). Use it only if it is still one of the PF VSI Tx rings
+ * and the VSI is up: those rings are only replaced or freed under RTNL or
+ * from the service task, so with RTNL held every access below is to live
+ * memory. ice_clean_tx_irq() may still run and free the skb at
+ * next_to_clean at any time, so only the pointer value is recorded and the
+ * skb itself is never dereferenced.
+ */
+static void ice_tx_hang_snapshot(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
+ struct ice_tx_hang_event *ev)
+{
+ struct ice_vsi *vsi = ice_get_main_vsi(pf);
+ struct ice_q_vector *q_vector;
+ struct ice_tx_buf *tx_buf;
+ unsigned int i;
+ u16 ntc, ntu;
+ void *desc;
+ u32 size;
+
+ ASSERT_RTNL();
+
+ if (!tx_ring || !vsi || !vsi->tx_rings ||
+ test_bit(ICE_VSI_DOWN, vsi->state))
+ return;
+
+ ice_for_each_txq(vsi, i)
+ if (vsi->tx_rings[i] == tx_ring)
+ break;
+ if (i == vsi->num_txq)
+ return;
+
+ q_vector = tx_ring->q_vector;
+ if (!q_vector || !tx_ring->desc || !tx_ring->tx_buf || !tx_ring->count)
+ return;
+
+ size = tx_ring->count * sizeof(struct ice_tx_desc);
+ desc = kvmalloc(size, GFP_KERNEL);
+ if (!desc)
+ return;
+
+ ntc = READ_ONCE(tx_ring->next_to_clean);
+ ntu = READ_ONCE(tx_ring->next_to_use);
+
+ if (ntc < tx_ring->count) {
+ tx_buf = &tx_ring->tx_buf[ntc];
+ if (tx_buf->type == ICE_TX_BUF_SKB)
+ ev->skb_ptr = READ_ONCE(tx_buf->skb);
+
+ memcpy(desc, tx_ring->desc, size);
+ ev->next_to_clean = ntc;
+ ev->next_to_use = ntu;
+ strscpy(ev->irq_name, q_vector->name);
+ ev->desc_ptr = tx_ring->desc;
+ ev->dma = tx_ring->dma;
+ ev->desc_size = size;
+ ev->desc = desc;
+ }
+
+ if (!ev->desc)
+ kvfree(desc);
}
void ice_report_tx_hang(struct ice_pf *pf)
{
struct ice_health_tx_hang_buf *buf = &pf->health_reporters.tx_hang_buf;
- struct ice_tx_ring *tx_ring = buf->tx_ring;
-
struct ice_tx_hang_event ev = {
.head = buf->head,
.intr = buf->intr,
.vsi_num = buf->vsi_num,
- .queue = tx_ring->q_index,
- .next_to_clean = tx_ring->next_to_clean,
- .next_to_use = tx_ring->next_to_use,
- .tx_ring = tx_ring,
+ .queue = buf->queue,
+ .next_to_clean = buf->next_to_clean,
+ .next_to_use = buf->next_to_use,
};
+ if (!pf->health_reporters.tx_hang)
+ return;
+
+ rtnl_lock();
+ ice_tx_hang_snapshot(pf, buf->tx_ring, &ev);
+ rtnl_unlock();
+
+ /* devlink_health_report() takes the devlink instance lock, which nests
+ * outside RTNL, so only report once RTNL has been released.
+ */
ice_devlink_health_report(pf->health_reporters.tx_hang, "Tx hang", &ev);
+
+ kvfree(ev.desc);
}
static struct devlink_health_reporter *
diff --git a/drivers/net/ethernet/intel/ice/devlink/health.h b/drivers/net/ethernet/intel/ice/devlink/health.h
index 5edfc4d2a..508e4115d 100644
--- a/drivers/net/ethernet/intel/ice/devlink/health.h
+++ b/drivers/net/ethernet/intel/ice/devlink/health.h
@@ -37,6 +37,9 @@ enum ice_mdd_src {
* @head: descriptor head
* @intr: interrupt register value
* @vsi_num: VSI owning the queue that the hang occurred on
+ * @queue: index of the queue that the hang occurred on
+ * @next_to_clean: next_to_clean of the ring when the hang was detected
+ * @next_to_use: next_to_use of the ring when the hang was detected
* @fw_status: buffer for last received FW Status event
* @port_status: buffer for last received Port Status event
*/
@@ -50,6 +53,9 @@ struct ice_health {
u32 head;
u32 intr;
u16 vsi_num;
+ u16 queue;
+ u16 next_to_clean;
+ u16 next_to_use;
);
struct ice_aqc_health_status_elem fw_status;
struct ice_aqc_health_status_elem port_status;
---
base-commit: 7076f684d5132e6a6145e1da2fa74771ecd052e5
change-id: 20261016-ice-tx-hang-dump-e46b4ed483d6
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] ice: fix Tx hang dump using stale tx_buf[0].skb
2026-10-26 13:30 ` Vincent Choiniere
(?)
@ 2026-10-28 14:41 ` Przemek Kitszel
-1 siblings, 0 replies; 4+ messages in thread
From: Przemek Kitszel @ 2026-10-28 14:41 UTC (permalink / raw)
To: choiniere.vincent, Tony Nguyen, intel-wired-lan
Cc: Aleksandr Loktionov, Wojciech Drewek, Paul Menzel, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
netdev, linux-kernel, stable
On 10/26/26 2:30 PM, Vincent Choiniere via B4 Relay wrote:
> From: Vincent Choiniere <choiniere.vincent@hydroquebec.com>
>
> ice_tx_hang_reporter_dump() always reads tx_ring->tx_buf[0].skb,
> regardless of next_to_clean. The Tx cleanup path frees the skb and
> marks the buffer empty without clearing the skb pointer, so slot 0 can
> hold a stale pointer. A NULL check alone does not cover this, and
> devlink_fmsg_dump_skb() can fault on the freed skb.
>
> Using tx_buf[next_to_clean] alone does not fix the lifetime issues.
> The dump runs from the service task without synchronization with Tx
> cleanup or ring replacement:
>
> - ice_clean_tx_irq() can free the skb at next_to_clean while the
> report is being prepared. It calls napi_consume_skb() before
> marking the buffer empty.
> - The ring saved by ice_tx_timeout() can be replaced or freed before
> the service task runs. ethtool -G replaces the ring contents and
> frees tx_buf and desc, while ethtool -L can free whole rings.
>
> Record the queue index, next_to_clean and next_to_use at timeout time.
> Before reporting, take RTNL and check that the saved ring is still
> one of the PF VSI Tx rings and that the VSI is up. Copy the
> descriptors and IRQ name while the ring storage is protected against
> replacement. If the ring cannot be used or the copy cannot be made,
> report only the values recorded at timeout time.
>
> Tx cleanup can still run while the ring state is copied, so do not
> dereference the skb. Retain only its pointer value in the dump and
> remove the skb contents.
>
> Release RTNL before calling devlink_health_report(), which takes the
> devlink instance lock. Build the dump from the copied data and free
> the descriptor copy after reporting.
>
> Observed on an E810 PF passed through to a KVM guest with Ubuntu
> kernel 7.0.0-14 (PREEMPT_RT and a local scheduler patch): a delayed
> queue IRQ led to a Tx timeout and a page fault in
> devlink_fmsg_dump_skb():
>
> BUG: unable to handle page fault for address: 0000000004cd0c11
> Workqueue: ice ice_service_task [ice]
> RIP: devlink_fmsg_dump_skb+0x2d8/0x5d0
> Call Trace:
> ice_tx_hang_reporter_dump+0x1ce/0x210 [ice]
> devlink_health_do_dump+0xcc/0x190
> devlink_health_report+0x1d7/0x260
> ice_report_tx_hang+0x7d/0xa0 [ice]
>
> Fixes: 2a82874a3b7b ("ice: add Tx hang devlink health reporter")
> Link: https://lore.kernel.org/all/20260327072332.130320-1-aleksandr.loktionov@intel.com/
> Cc: stable@vger.kernel.org
> Signed-off-by: Vincent Choiniere <choiniere.vincent@hydroquebec.com>
> ---
> Changes in v2:
> - Validate the saved ring under RTNL before dereferencing it from
> the service task, and restrict snapshots to the PF VSI while it
> is up (Sashiko).
> - Check next_to_clean against count before indexing tx_buf, and
> copy the descriptors and IRQ name while ring replacement is
> excluded (Sashiko).
> - Stop dereferencing the skb and remove its contents from the dump.
> Keep only its pointer value (Sashiko).
> - Release RTNL before reporting to preserve the devlink instance
> lock ordering.
> - Record the queue index and ring indices at timeout time as a
> fallback when a snapshot cannot be made.
> - Drop Paul's Reviewed-by because the patch changed substantially.
lot's of changes and code, but rather nothing controversial, I'm
generally happy with it, one nitpick from me inline, and otherwise:
Reviewed-by: Przemek Kitszel <przemyslaw.kitszel@intel.com>
> +static void ice_tx_hang_snapshot(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
> + struct ice_tx_hang_event *ev)
> +{
> + struct ice_vsi *vsi = ice_get_main_vsi(pf);
> + struct ice_q_vector *q_vector;
> + struct ice_tx_buf *tx_buf;
> + unsigned int i;
> + u16 ntc, ntu;
> + void *desc;
> + u32 size;
> +
> + ASSERT_RTNL();
> +
> + if (!tx_ring || !vsi || !vsi->tx_rings ||
> + test_bit(ICE_VSI_DOWN, vsi->state))
> + return;
> +
> + ice_for_each_txq(vsi, i)
> + if (vsi->tx_rings[i] == tx_ring)
> + break;
> + if (i == vsi->num_txq)
> + return;
> +
> + q_vector = tx_ring->q_vector;
> + if (!q_vector || !tx_ring->desc || !tx_ring->tx_buf || !tx_ring->count)
> + return;
> +
> + size = tx_ring->count * sizeof(struct ice_tx_desc);
> + desc = kvmalloc(size, GFP_KERNEL);
> + if (!desc)
> + return;
> +
> + ntc = READ_ONCE(tx_ring->next_to_clean);
> + ntu = READ_ONCE(tx_ring->next_to_use);
> +
> + if (ntc < tx_ring->count) {
> + tx_buf = &tx_ring->tx_buf[ntc];
> + if (tx_buf->type == ICE_TX_BUF_SKB)
> + ev->skb_ptr = READ_ONCE(tx_buf->skb);
> +
> + memcpy(desc, tx_ring->desc, size);
> + ev->next_to_clean = ntc;
> + ev->next_to_use = ntu;
> + strscpy(ev->irq_name, q_vector->name);
> + ev->desc_ptr = tx_ring->desc;
> + ev->dma = tx_ring->dma;
> + ev->desc_size = size;
> + ev->desc = desc;
> + }
> +
> + if (!ev->desc)
this is abuse of ev and bad style, instead just glue an else to the
previous if, basically:
if (ntc < tx_ring->count) { .... } else { kvfree(desc); }
> + kvfree(desc);
> }
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] ice: fix Tx hang dump using stale tx_buf[0].skb
2026-10-26 13:30 ` Vincent Choiniere
(?)
(?)
@ 2026-10-29 2:37 ` netdev-bot+sashiko
-1 siblings, 0 replies; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-10-29 2:37 UTC (permalink / raw)
To: choiniere.vincent
Cc: anthony.l.nguyen, przemyslaw.kitszel, intel-wired-lan,
aleksandr.loktionov, michal.kubiak, wojciech.drewek,
mateusz.polchlopek, pmenzel, andrew+netdev, davem, edumazet, kuba,
pabeni, netdev, linux-kernel, stable
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 1 (2) · Medium: 0 · Low: 0 (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).
- [High] ice_tx_hang_snapshot()'s safety argument is that PF VSI rings
'are only replaced or freed under RTNL or from the service task, so…
Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the patch's
new guard makes it look handled.
- [High] This problem wasn't introduced by this patch, but it is a
deadlock between devlink DRIVER_REINIT reload and the Tx-hang…
- [Low] tx_hang_buf is published and consumed without synchronization.
--
ice: fix Tx hang dump using stale tx_buf[0].skb
This patch changes the ice Tx hang devlink dump so it no longer
dereferences tx_buf[0].skb or the saved tx_ring when the dump runs. It
records the queue and ring indices at timeout time. Under RTNL, it checks
that the saved ring is still one of the PF VSI Tx rings and that the VSI
is up, then copies the descriptors and IRQ name. It keeps only the skb
pointer value and drops RTNL before calling devlink_health_report().
Link: https://lore.kernel.org/all/20260327072332.130320-1-aleksandr.loktionov@intel.com/
> diff --git a/drivers/net/ethernet/intel/ice/devlink/health.c b/drivers/net/ethernet/intel/ice/devlink/health.c
> index 8e9a8a8178d4..b9493ab4766e 100644
> --- a/drivers/net/ethernet/intel/ice/devlink/health.c
> +++ b/drivers/net/ethernet/intel/ice/devlink/health.c
[ ... ]
> @@ -424,24 +429,104 @@ void ice_prep_tx_hang_report(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
> buf->vsi_num = vsi_num;
> buf->head = head;
> buf->intr = intr;
> + buf->queue = tx_ring->q_index;
> + buf->next_to_clean = tx_ring->next_to_clean;
> + buf->next_to_use = tx_ring->next_to_use;
> +}
> +
> +/**
> + * ice_tx_hang_snapshot - copy the hung Tx ring state for the dump
> + * @pf: PF on which the Tx hang was detected
> + * @tx_ring: ring saved by ice_tx_timeout(), possibly stale
> + * @ev: event to fill in
> + *
> + * The saved ring may have been freed or replaced since the timeout (e.g. by
> + * ethtool -G or -L). Use it only if it is still one of the PF VSI Tx rings
> + * and the VSI is up: those rings are only replaced or freed under RTNL or
> + * from the service task, so with RTNL held every access below is to live
> + * memory. ice_clean_tx_irq() may still run and free the skb at
> + * next_to_clean at any time, so only the pointer value is recorded and the
> + * skb itself is never dereferenced.
> + */
> +static void ice_tx_hang_snapshot(struct ice_pf *pf, struct ice_tx_ring *tx_ring,
> + struct ice_tx_hang_event *ev)
> +{
> + struct ice_vsi *vsi = ice_get_main_vsi(pf);
[ ... ]
> + ASSERT_RTNL();
> +
> + if (!tx_ring || !vsi || !vsi->tx_rings ||
> + test_bit(ICE_VSI_DOWN, vsi->state))
> + return;
[Severity: High]
Does the lifetime claim in the kernel-doc above hold for the ice_remove()
path?
In ice_remove(), RTNL is dropped inside ice_unload(). ice_deinit() then
tears down the VSIs without RTNL. The service work is only cancelled
later, in ice_deinit_dev():
ice_remove()
ice_unload(pf); <- unregister_netdev() takes/drops RTNL
devl_unlock(...);
ice_deinit(pf);
set_bit(ICE_SERVICE_DIS, pf->state);
set_bit(ICE_DOWN, pf->state);
ice_deinit_pf_sw(pf); <- ice_vsi_release() frees rings,
tx_rings and the vsi
ice_dealloc_vsis(pf); <- devm_kfree(..., pf->vsi)
...
ice_deinit_dev(pf); <- ice_service_task_stop()
-> cancel_work_sync(&pf->serv_task)
ICE_SERVICE_DIS stops new scheduling, but it does not stop a work item
that is already queued or running. ice_vsi_free() holds only
pf->sw_mutex:
pf->vsi[vsi->idx] = NULL;
...
mutex_unlock(&pf->sw_mutex);
devm_kfree(dev, vsi);
Suppose a Tx timeout fires shortly before removal. ice_service_task()
then sees tx_hang_buf.tx_ring set and calls ice_report_tx_hang() without
checking ICE_SERVICE_DIS. The work can block in rtnl_lock() while
unregister_netdev() holds RTNL. It then gets RTNL while ice_deinit() is
freeing the VSIs.
Could ice_get_main_vsi(pf), vsi->tx_rings and test_bit(ICE_VSI_DOWN,
vsi->state) then read the freed pf->vsi array or the freed struct ice_vsi?
The ICE_VSI_DOWN check only helps while the vsi itself is still
allocated. Does the service task need to be stopped or flushed, or
tx_hang_buf.tx_ring cleared, before the VSI teardown in ice_remove()?
[ ... ]
> void ice_report_tx_hang(struct ice_pf *pf)
> {
> struct ice_health_tx_hang_buf *buf = &pf->health_reporters.tx_hang_buf;
> - struct ice_tx_ring *tx_ring = buf->tx_ring;
> -
> struct ice_tx_hang_event ev = {
> .head = buf->head,
> .intr = buf->intr,
> .vsi_num = buf->vsi_num,
> - .queue = tx_ring->q_index,
> - .next_to_clean = tx_ring->next_to_clean,
> - .next_to_use = tx_ring->next_to_use,
> - .tx_ring = tx_ring,
> + .queue = buf->queue,
> + .next_to_clean = buf->next_to_clean,
> + .next_to_use = buf->next_to_use,
> };
>
> + if (!pf->health_reporters.tx_hang)
> + return;
[Severity: High]
This isn't a bug introduced by this patch, but can this check pass with
a reporter that has already been freed?
In ice_remove(), the reporter is destroyed before ice_unload() and
ice_deinit(). The service work is still live at that point:
ice_remove()
ice_deinit_devlink(pf);
ice_health_deinit(pf);
ice_deinit_devl_reporter(pf->health_reporters.tx_hang);
devl_health_reporter_destroy() <- kfree(reporter)
ice_unload(pf);
...
ice_deinit_dev(pf); <- cancel_work_sync(&pf->serv_task)
ice_health_deinit() never sets pf->health_reporters.tx_hang to NULL, so
the stale pointer passes this check.
With a Tx timeout just before or during removal, the service task can
reach ice_devlink_health_report() after the kfree(). devlink_health_report()
then writes reporter->error_count and reporter->health_state, and it
dereferences reporter->devlink and reporter->ops.
Waiting in rtnl_lock() while unregister_netdev() holds RTNL also makes
it more likely that the report runs after the reporter has been freed.
> +
> + rtnl_lock();
> + ice_tx_hang_snapshot(pf, buf->tx_ring, &ev);
> + rtnl_unlock();
[Severity: Low]
This is a pre-existing issue, and the changelog's limitations section
already covers it. The window looks wider now, though. ev.head, ev.intr,
ev.queue and the ring indices are read from tx_hang_buf before
rtnl_lock(). buf->tx_ring is only read after rtnl_lock() returns.
ice_prep_tx_hang_report() stores buf->tx_ring before the other fields,
with no ordering. ice_service_task() then clears the pointer with a
plain store:
if (pf->health_reporters.tx_hang_buf.tx_ring) {
ice_report_tx_hang(pf);
pf->health_reporters.tx_hang_buf.tx_ring = NULL;
}
Suppose a second timeout arrives while rtnl_lock() is blocked. Could the
dump then combine the first event's queue/head/intr with the second
event's ring snapshot? Could the clear above also drop the second event?
> ice_devlink_health_report(pf->health_reporters.tx_hang, "Tx hang", &ev);
[Severity: High]
This isn't a bug introduced by this patch, but can this deadlock against
a devlink driver_reinit reload?
ice_devlink_reinit_down() runs with the devlink instance lock held. It
ends up in cancel_work_sync() on the service work:
ice_devlink_reload_down()
ice_devlink_reinit_down() <- devl_lock held by devlink core
ice_unload(pf);
...
ice_deinit_dev(pf);
ice_service_task_stop()
cancel_work_sync(&pf->serv_task);
The tx_hang reporter has a dump op, so auto_dump is set. In that case
devlink_health_report() takes the same lock from the service work:
if (reporter->auto_dump) {
devl_lock(devlink);
Suppose a Tx timeout fires just before the reload. The service task can
then be inside ice_report_tx_hang(), possibly waiting for RTNL during
unregister_netdev(). It returns early from the snapshot because the VSI
is down and then blocks in devl_lock(). Meanwhile the reload thread waits
for that same work in cancel_work_sync().
Dropping RTNL before reporting avoids the RTNL/devlink lock ordering
problem. Does it do anything for this dependency between the reload and
the service work?
> +
> + kvfree(ev.desc);
> }
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261026-ice-tx-hang-dump-v2-1-e2136ef2c5dc%40hydroquebec.com
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-29 2:37 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-26 13:30 [PATCH net v2] ice: fix Tx hang dump using stale tx_buf[0].skb Vincent Choiniere via B4 Relay
2026-10-26 13:30 ` Vincent Choiniere
2026-10-28 14:41 ` Przemek Kitszel
2026-10-29 2:37 ` netdev-bot+sashiko
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.