* [PATCH net] net: gianfar: handle startup_gfar failure and fix double free
@ 2026-08-28 22:20 Rosen Penev
2026-09-02 20:18 ` Simon Horman
0 siblings, 1 reply; 2+ messages in thread
From: Rosen Penev @ 2026-08-28 22:20 UTC (permalink / raw)
To: netdev
Cc: Claudiu Manoil, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, open list
reset_gfar() ignores the return value of startup_gfar(), so a reset
that fails to reallocate resources (e.g. -ENOMEM) leaves the interface
logically UP with stale descriptor-ring pointers. If the device is
closed afterwards, gfar_close() -> stop_gfar() -> free_skb_resources()
frees the same coherent DMA ring a second time, causing a double free
of the TX/RX buffer descriptors.
Make reset_gfar() and gfar_change_mtu() propagate the startup_gfar()
error instead of silently continuing. Make free_skb_resources() idempotent
by NULLing the descriptor-ring base pointers after freeing them, so a
second free cannot operate on a stale pointer regardless of which error
path is taken.
Fixes: 0851133bb5ad ("gianfar: Fix device reset races (oops) for Tx")
Assisted-by: opencode:big-pickle
Signed-off-by: Rosen Penev <rosenp@gmail.com>
---
drivers/net/ethernet/freescale/gianfar.c | 42 +++++++++++++++++-------
1 file changed, 30 insertions(+), 12 deletions(-)
diff --git a/drivers/net/ethernet/freescale/gianfar.c b/drivers/net/ethernet/freescale/gianfar.c
index cf636fc5aafa..d0ac8a958696 100644
--- a/drivers/net/ethernet/freescale/gianfar.c
+++ b/drivers/net/ethernet/freescale/gianfar.c
@@ -1140,11 +1140,24 @@ static void free_skb_resources(struct gfar_private *priv)
free_skb_rx_queue(rx_queue);
}
- dma_free_coherent(priv->dev,
- sizeof(struct txbd8) * priv->total_tx_ring_size +
- sizeof(struct rxbd8) * priv->total_rx_ring_size,
- priv->tx_queue[0]->tx_bd_base,
- priv->tx_queue[0]->tx_bd_dma_base);
+ if (priv->tx_queue[0]->tx_bd_base) {
+ dma_free_coherent(priv->dev,
+ sizeof(struct txbd8) *
+ priv->total_tx_ring_size +
+ sizeof(struct rxbd8) *
+ priv->total_rx_ring_size,
+ priv->tx_queue[0]->tx_bd_base,
+ priv->tx_queue[0]->tx_bd_dma_base);
+
+ for (i = 0; i < priv->num_tx_queues; i++) {
+ priv->tx_queue[i]->tx_bd_base = NULL;
+ priv->tx_queue[i]->tx_bd_dma_base = 0;
+ }
+ for (i = 0; i < priv->num_rx_queues; i++) {
+ priv->rx_queue[i]->rx_bd_base = NULL;
+ priv->rx_queue[i]->rx_bd_dma_base = 0;
+ }
+ }
}
void stop_gfar(struct net_device *dev)
@@ -2003,6 +2016,7 @@ static int gfar_set_mac_address(struct net_device *dev)
static int gfar_change_mtu(struct net_device *dev, int new_mtu)
{
struct gfar_private *priv = netdev_priv(dev);
+ int err = 0;
while (test_and_set_bit_lock(GFAR_RESETTING, &priv->state))
cpu_relax();
@@ -2013,24 +2027,27 @@ static int gfar_change_mtu(struct net_device *dev, int new_mtu)
WRITE_ONCE(dev->mtu, new_mtu);
if (dev->flags & IFF_UP)
- startup_gfar(dev);
+ err = startup_gfar(dev);
clear_bit_unlock(GFAR_RESETTING, &priv->state);
- return 0;
+ return err;
}
-static void reset_gfar(struct net_device *ndev)
+static int reset_gfar(struct net_device *ndev)
{
struct gfar_private *priv = netdev_priv(ndev);
+ int ret;
while (test_and_set_bit_lock(GFAR_RESETTING, &priv->state))
cpu_relax();
stop_gfar(ndev);
- startup_gfar(ndev);
+ ret = startup_gfar(ndev);
clear_bit_unlock(GFAR_RESETTING, &priv->state);
+
+ return ret;
}
/* gfar_reset_task gets scheduled when a packet has not been
@@ -2058,6 +2075,7 @@ static int gfar_hwtstamp_set(struct net_device *netdev,
struct netlink_ext_ack *extack)
{
struct gfar_private *priv = netdev_priv(netdev);
+ int ret = 0;
switch (config->tx_type) {
case HWTSTAMP_TX_OFF:
@@ -2076,7 +2094,7 @@ static int gfar_hwtstamp_set(struct net_device *netdev,
case HWTSTAMP_FILTER_NONE:
if (priv->hwts_rx_en) {
priv->hwts_rx_en = 0;
- reset_gfar(netdev);
+ ret = reset_gfar(netdev);
}
break;
default:
@@ -2084,13 +2102,13 @@ static int gfar_hwtstamp_set(struct net_device *netdev,
return -ERANGE;
if (!priv->hwts_rx_en) {
priv->hwts_rx_en = 1;
- reset_gfar(netdev);
+ ret = reset_gfar(netdev);
}
config->rx_filter = HWTSTAMP_FILTER_ALL;
break;
}
- return 0;
+ return ret;
}
static int gfar_hwtstamp_get(struct net_device *netdev,
--
2.55.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH net] net: gianfar: handle startup_gfar failure and fix double free
2026-08-28 22:20 [PATCH net] net: gianfar: handle startup_gfar failure and fix double free Rosen Penev
@ 2026-09-02 20:18 ` Simon Horman
0 siblings, 0 replies; 2+ messages in thread
From: Simon Horman @ 2026-09-02 20:18 UTC (permalink / raw)
To: rosenp
Cc: Simon Horman, netdev, claudiu.manoil, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260828222051.77604-1-rosenp@gmail.com?part=1
---
net: gianfar: handle startup_gfar failure and fix double free
This makes reset_gfar() return an int and propagates the startup_gfar()
error out of reset_gfar() and gfar_change_mtu(), and adds a
tx_bd_base test plus NULLing of the descriptor-ring base and dma-base
pointers in free_skb_resources(). A few questions on the resulting
behaviour below.
> diff --git a/drivers/net/ethernet/freescale/gianfar.c b/drivers/net/ethernet/freescale/gianfar.c
> index cf636fc5aafa..d0ac8a958696 100644
> --- a/drivers/net/ethernet/freescale/gianfar.c
> +++ b/drivers/net/ethernet/freescale/gianfar.c
> @@ -1140,11 +1140,24 @@ static void free_skb_resources(struct gfar_private *priv)
> free_skb_rx_queue(rx_queue);
> }
>
> - dma_free_coherent(priv->dev,
> - sizeof(struct txbd8) * priv->total_tx_ring_size +
> - sizeof(struct rxbd8) * priv->total_rx_ring_size,
> - priv->tx_queue[0]->tx_bd_base,
> - priv->tx_queue[0]->tx_bd_dma_base);
> + if (priv->tx_queue[0]->tx_bd_base) {
> + dma_free_coherent(priv->dev,
> + sizeof(struct txbd8) *
> + priv->total_tx_ring_size +
> + sizeof(struct rxbd8) *
> + priv->total_rx_ring_size,
> + priv->tx_queue[0]->tx_bd_base,
> + priv->tx_queue[0]->tx_bd_dma_base);
[Severity: High]
The changelog says that after this change "a second free cannot operate on
a stale pointer regardless of which error path is taken". Is that true for
the receive skb as well?
free_skb_resources() -> free_skb_rx_queue() frees it but leaves the field
set:
drivers/net/ethernet/freescale/gianfar.c:free_skb_rx_queue() {
dev_kfree_skb(rx_queue->skb);
...
kfree(rx_queue->rx_buff);
rx_queue->rx_buff = NULL;
}
rx_buff is cleared, rx_queue->skb is not, and gfar_init_bds() only resets
next_to_clean/next_to_use/next_to_alloc for the rx rings on the following
startup_gfar().
rx_queue->skb is non-NULL whenever a frame spanning several descriptors was
only partially received:
drivers/net/ethernet/freescale/gianfar.c:gfar_clean_rx_ring() {
/* Store incomplete frames for completion */
rx_queue->skb = skb;
}
So after any stop/start cycle (gfar_change_mtu(), reset_gfar() from
gfar_hwtstamp_set() or gfar_reset_task(), or the ethtool ring/coalesce/
features paths) the field is a dangling pointer, and the next poll picks it
up:
drivers/net/ethernet/freescale/gianfar.c:gfar_clean_rx_ring() {
struct sk_buff *skb = rx_queue->skb;
...
}
Can that append fragments to the freed skb via gfar_add_rx_frag()? And if
no packet arrives before the next teardown, does the next stop_gfar() ->
free_skb_resources() -> free_skb_rx_queue() call dev_kfree_skb() on the
same skb a second time?
Would adding rx_queue->skb = NULL in free_skb_rx_queue() fit here?
> +
> + for (i = 0; i < priv->num_tx_queues; i++) {
> + priv->tx_queue[i]->tx_bd_base = NULL;
> + priv->tx_queue[i]->tx_bd_dma_base = 0;
> + }
> + for (i = 0; i < priv->num_rx_queues; i++) {
> + priv->rx_queue[i]->rx_bd_base = NULL;
> + priv->rx_queue[i]->rx_bd_dma_base = 0;
> + }
> + }
> }
>
> void stop_gfar(struct net_device *dev)
[Severity: Medium]
What serializes these stores against the transmit path?
gfar_start_xmit() reads the base with only the txq _xmit_lock held, and
without testing GFAR_DOWN:
base = tx_queue->tx_bd_base;
stop_gfar() only marks the queues stopped:
drivers/net/ethernet/freescale/gianfar.c:stop_gfar() {
netif_tx_stop_all_queues(dev);
...
disable_napi(priv);
gfar_halt(priv);
phy_stop(dev->phydev);
free_skb_resources(priv);
}
It takes no netif_tx_lock() (gfar_suspend() does) and no synchronize_net(),
and it sleeps in napi_disable() and phy_stop(), so a transmit that already
passed netif_xmit_frozen_or_stopped() inside HARD_TX_LOCK can still be
running here.
With base == NULL, does the wrap arithmetic move the descriptor pointer
below the former ring?
drivers/net/ethernet/freescale/gianfar.c:skip_txbd() {
return (new_bd >= (base + ring_size)) ? (new_bd - ring_size) : new_bd;
}
For a stale cur_tx the comparison against (NULL + ring_size) is always
true, so the result is new_bd - ring_size. Before the patch the same race
scribbled inside the freed ring; can it now write up to
(ring_size - 1) * sizeof(struct txbd8) bytes below it?
The zeroed rx_bd_dma_base feeds gfar_rxbd_dma_lastfree(), used by
gfar_clean_rx_ring() and gfar_update_link_state() to program rfbptr — would
that publish a bogus DMA address? If the NULLing is kept, should it happen
under the same exclusion the readers use, i.e. netif_tx_disable() plus a
GFAR_DOWN check in gfar_start_xmit()?
> @@ -2003,6 +2016,7 @@ static int gfar_set_mac_address(struct net_device *dev)
> static int gfar_change_mtu(struct net_device *dev, int new_mtu)
> {
> struct gfar_private *priv = netdev_priv(dev);
> + int err = 0;
>
> while (test_and_set_bit_lock(GFAR_RESETTING, &priv->state))
> cpu_relax();
> @@ -2013,24 +2027,27 @@ static int gfar_change_mtu(struct net_device *dev, int new_mtu)
> WRITE_ONCE(dev->mtu, new_mtu);
>
> if (dev->flags & IFF_UP)
> - startup_gfar(dev);
> + err = startup_gfar(dev);
>
> clear_bit_unlock(GFAR_RESETTING, &priv->state);
>
> - return 0;
> + return err;
> }
[Severity: Medium]
Who restores the configuration that was already committed before the new
error return?
dev->mtu is written with WRITE_ONCE() before startup_gfar() runs, and the
core does not undo it when the driver callback fails:
net/core/dev.c:__netif_set_mtu() {
if (ops->ndo_change_mtu)
return ops->ndo_change_mtu(dev, new_mtu);
...
}
net/core/dev.c:netif_set_mtu_ext() {
if (new_mtu == dev->mtu)
return 0;
...
orig_mtu = dev->mtu;
err = __netif_set_mtu(dev, new_mtu);
if (!err) {
err = call_netdevice_notifiers_mtu(NETDEV_CHANGEMTU, dev, orig_mtu);
...
}
So on -ENOMEM userspace is told the request failed, but ip link reports the
new MTU, no NETDEV_CHANGEMTU is sent, and a retry with the same value hits
the new_mtu == dev->mtu early return and reports success without ever
restarting the stopped device. Should dev->mtu be put back on the failure
path?
gfar_hwtstamp_set() has the same shape: priv->hwts_rx_en is assigned before
reset_gfar() and is not restored when the new error is returned.
dev_set_hwtstamp_phylib() returns the ndo error directly with no rollback,
so gfar_hwtstamp_get() then reports the filter the setter said it could not
apply, and repeating the identical request returns 0 because the flag
already matches while the controller stays halted.
>
> -static void reset_gfar(struct net_device *ndev)
> +static int reset_gfar(struct net_device *ndev)
> {
> struct gfar_private *priv = netdev_priv(ndev);
> + int ret;
>
> while (test_and_set_bit_lock(GFAR_RESETTING, &priv->state))
> cpu_relax();
[Severity: Medium]
This isn't a bug introduced by this patch, but since both users of the
GFAR_RESETTING guard are being touched here: the set-on-entry /
clear-on-exit bit pair in reset_gfar() and gfar_change_mtu() is an
open-coded, lockdep-invisible spinlock whose holder sleeps, in
dma_alloc_coherent(GFP_KERNEL) inside gfar_alloc_skb_resources(), in
napi_disable()'s usleep_range() and in phy_stop(), while another CPU
busy-waits in cpu_relax(). Two concurrent reconfiguration requests, for
example ip link set mtu racing hwstamp_ctl or a TX-timeout reset racing
ethtool -G, will burn a CPU for the duration.
It also cannot express the state left behind by the new error path: once
the bit is cleared after a failed startup_gfar(), code whose only guard is
the bit runs against the freed and now NULLed rings:
drivers/net/ethernet/freescale/gianfar.c:gfar_update_link_state() {
if (unlikely(test_bit(GFAR_RESETTING, &priv->state)))
return;
...
}
Would a mutex plus an explicit "rings gone" state be a better fit?
>
> stop_gfar(ndev);
> - startup_gfar(ndev);
> + ret = startup_gfar(ndev);
>
> clear_bit_unlock(GFAR_RESETTING, &priv->state);
> +
> + return ret;
> }
>
> /* gfar_reset_task gets scheduled when a packet has not been
[Severity: High]
Is the failure sequence in the changelog reachable? It says
"gfar_close() -> stop_gfar() -> free_skb_resources() frees the same
coherent DMA ring a second time".
When startup_gfar() fails it returns before enable_napi():
drivers/net/ethernet/freescale/gianfar.c:startup_gfar() {
gfar_mac_reset(priv);
err = gfar_alloc_skb_resources(ndev);
if (err)
return err;
...
phy_start(ndev->phydev);
enable_napi(priv);
netif_tx_wake_all_queues(ndev);
...
}
reset_gfar() and gfar_change_mtu() then just forward the error, so NAPI
stays in the state left by the preceding stop_gfar() -> disable_napi() and
the netdev stays IFF_UP. The next teardown disables NAPI again before it
ever reaches free_skb_resources():
drivers/net/ethernet/freescale/gianfar.c:stop_gfar() {
netif_tx_stop_all_queues(dev);
...
disable_napi(priv);
gfar_halt(priv);
phy_stop(dev->phydev);
free_skb_resources(priv);
}
and the second disable has nothing to wait for:
net/core/dev.c:napi_disable_locked() {
while (val & (NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC)) {
usleep_range(20, 200);
val = READ_ONCE(n->state);
}
new = val | NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC;
...
}
Those bits are left set on exit and only napi_enable() clears them, so does
gfar_close() -> cancel_work_sync() -> stop_gfar() spin here forever,
unkillable, holding rtnl_lock and the netdev instance lock? If so the
double free described in the changelog never happens, because the close
never gets past disable_napi(), and the hunk in free_skb_resources() is not
what breaks.
Would it make more sense to restore the NAPI and PHY balance, or to force
the interface fully down, when startup_gfar() fails, rather than only
returning the error?
[Severity: Medium]
The changelog says reset_gfar() now propagates the error "instead of
silently continuing", but the reset path that runs on TX watchdog expiry
and on IEVENT_XFUN still drops it:
drivers/net/ethernet/freescale/gianfar.c:gfar_reset_task() {
struct gfar_private *priv = container_of(work, struct gfar_private,
reset_task);
reset_gfar(priv->ndev);
}
gfar_timeout() -> schedule_work(&priv->reset_task) -> gfar_reset_task()
ignores the return value, with no netdev_err() and no recovery, so after a
failed reset the netdev remains IFF_UP with the MAC halted, PHY stopped,
NAPI disabled, all TX queues stopped and GFAR_DOWN set, and nothing is
logged. Should this caller at least report the failure?
Note the ethtool re-init sites in gianfar_ethtool.c (gfar_scoalesce,
gfar_sringparam, gfar_set_features) already propagated the error before
this patch and end up in the same half-down state, which suggests
propagation alone does not address the "leaves the interface logically UP"
half of the problem the changelog describes.
> @@ -2058,6 +2075,7 @@ static int gfar_hwtstamp_set(struct net_device *netdev,
> struct netlink_ext_ack *extack)
> {
> struct gfar_private *priv = netdev_priv(netdev);
> + int ret = 0;
>
> switch (config->tx_type) {
> case HWTSTAMP_TX_OFF:
[ ... ]
> @@ -2084,13 +2102,13 @@ static int gfar_hwtstamp_set(struct net_device *netdev,
> return -ERANGE;
> if (!priv->hwts_rx_en) {
> priv->hwts_rx_en = 1;
> - reset_gfar(netdev);
> + ret = reset_gfar(netdev);
> }
> config->rx_filter = HWTSTAMP_FILTER_ALL;
> break;
> }
>
> - return 0;
> + return ret;
> }
[Severity: High]
This is a pre-existing issue rather than something the patch introduces,
but it is in the function being modified: gfar_hwtstamp_set() calls
reset_gfar() with no netif_running() or IFF_UP test, and reset_gfar()
unconditionally runs stop_gfar() followed by startup_gfar().
The generic caller only requires the device to be present:
net/core/dev_ioctl.c:dev_set_hwtstamp() {
if (!netif_device_present(dev))
return -ENODEV;
...
}
So SIOCSHWTSTAMP on an administratively down gianfar interface with
FSL_GIANFAR_DEV_HAS_TIMER set reaches stop_gfar() -> disable_napi() ->
napi_disable() on contexts that were only added by netif_napi_add() in
gfar_probe() and never enabled, since napi_enable() runs only in
startup_gfar(). Does that loop forever in napi_disable_locked()'s
usleep_range() with rtnl_lock held? And if it did return, would
phy_stop(dev->phydev) run with dev->phydev already cleared by
phy_disconnect() in gfar_close()?
Should reset_gfar() (or its callers) skip the stop/start when the interface
is not running, the way gfar_change_mtu() checks dev->flags & IFF_UP?
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-09-02 20:18 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28 22:20 [PATCH net] net: gianfar: handle startup_gfar failure and fix double free Rosen Penev
2026-09-02 20:18 ` Simon Horman
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox