From: netdev-bot+sashiko@kernel.org
To: daniel@makrotopia.org
Cc: jiri@resnulli.us, davem@davemloft.net, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, horms@kernel.org,
corbet@lwn.net, skhan@linuxfoundation.org, rdunlap@infradead.org,
gregkh@linuxfoundation.org, rafael@kernel.org, dakr@kernel.org,
andrew@lunn.ch, olteanv@gmail.com, netdev@vger.kernel.org,
linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
driver-core@lists.linux.dev
Subject: Re: [PATCH net-next v15 5/6] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode
Date: Mon, 14 Sep 2026 18:34:56 +0000 [thread overview]
Message-ID: <178941089675.22033.4191173896423195861@kernel.org> (raw)
In-Reply-To: =?utf-8?q?=3C76f2d4150cffd8188a5b945bb0bd227fa453b99e=2E1789175?= =?utf-8?q?618=2Egit=2Edaniel=40makrotopia=2Eorg=3E?=
Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider:
Critical: 0 · High: 2 · Medium: 3 · Low: 1
- [High] ABBA deadlock between device_lock(dev) and
cancel_work_sync(&priv->rescue_heal_work).
- [High] Rescue detection cannot tell a loader that is still erasing or
programming from a running firmware, because the 'settle' window is…
- [Medium] mxl862xx_phylink_get_caps() still derives the SerDes interface
capabilities from priv->fw_version, which rescue mode never populates.
- [Medium] priv->rescue_failed is latched for the STAT==START/START+1
handshake state and is never cleared for the life of the binding, so…
- [Medium] In mxl862xx_devlink_flash_update() the failure of
device_schedule_reprobe() (e.g. -ENOMEM from its internal allocation)…
- [Low] Kernel-doc return sets do not match the code for the new
-ECANCELED path.
--
net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode
This adds SB PDI (clause-22) probing at setup time so a switch left in its
MCUboot loader is detected before any clause-45 access, and lets the driver
bind in a reduced "rescue mode" where devlink can reflash it. A wedged
mid-download loader is drained back to a ready state by a background work
item, which then hands off to device_schedule_reprobe() so probe-time
detection can re-classify the switch.
A few questions below about the new detection and self-heal paths.
> diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
> index 119792eb7bd58..457830c9f51f4 100644
> --- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
> +++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
[ ... ]
> @@ -219,6 +266,371 @@ static void mxl862xx_flash_notify(struct devlink *dl, const char *status,
> devlink_flash_update_status_notify(dl, status, NULL, done, total);
> }
>
[ ... ]
> +/* Wait for the loader to ask for the next chunk (STAT 0) or to come back to its
> + * command loop (STAT READY), and return the STAT value either way. On timeout
> + * that is whatever STAT still holds, which carries no further information: the
> + * loader keeps the count we wrote visible while it programs the chunk, and that
> + * is the same value it publishes as the "image rejected" verdict once the
> + * counter reaches zero.
[ ... ]
> +static int mxl862xx_sb_pdi_poll_drain(struct mxl862xx_priv *priv,
> + unsigned long timeout_ms)
> +{
> + int val;
> +
> + read_poll_timeout(mxl862xx_smdio_read, val,
> + val < 0 || (u16)val == MXL862XX_SB_PDI_READY ||
> + (u16)val == 0 ||
> + test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags),
> + 50, timeout_ms * 1000, false,
> + priv, MXL862XX_SB_PDI_STAT);
> + if (val < 0)
> + return val;
> + if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
> + return -ECANCELED;
> + return (u16)val;
> +}
> +
> +/* The loader is not asking for a chunk: it may still be programming the last
> + * one, or the counter has reached zero and it is verifying the image and
> + * resetting into READY. Wait that out -- STAT cannot tell the two apart, and
> + * guessing would mean writing END into a live receive loop.
> + *
> + * Return: 0 once the loader has left the loop, -EAGAIN if it asks for another
> + * chunk after all, -EIO for a loader still holding the count when the verify
> + * window expires, or an SMDIO bus error.
> + */
[Severity: Low]
The comment on mxl862xx_sb_pdi_poll_drain() says it returns "the STAT value
either way", but there is a fourth outcome:
if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
return -ECANCELED;
Should the documented return sets mention -ECANCELED?
mxl862xx_rescue_drain_finish() forwards it through its "if (stat < 0) return
stat;" while its Return: list names only 0, -EAGAIN, -EIO and an SMDIO bus
error, and the Return: list of mxl862xx_rescue_mode_detect() (IN_RESCUE,
NOT_RESCUE, -ENODEV, -EOPNOTSUPP, -ENXIO, SMDIO bus error) leaves it out too,
even though both of its mxl862xx_sb_pdi_poll_drain() calls can produce it.
> +static int mxl862xx_rescue_drain_finish(struct mxl862xx_priv *priv, u32 chunk)
> +{
[ ... ]
> +/* Background self-heal: drain a wedged download off the devlink flash path, so
> + * the long recovery never holds the devlink lock. Scheduled from probe;
> + * reprobes on success so the probe-time detection re-classifies the switch.
> + */
> +void mxl862xx_rescue_heal_work_fn(struct work_struct *work)
> +{
> + struct mxl862xx_priv *priv =
> + container_of(work, struct mxl862xx_priv, rescue_heal_work);
> + struct device *dev = &priv->mdiodev->dev;
> + int ret;
> +
> + ret = mxl862xx_rescue_drain(priv);
> + if (ret == -ECANCELED)
> + return;
[ ... ]
> + if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
> + return;
> +
> + if (device_schedule_reprobe(dev, MXL862XX_FW_REPROBE_DELAY_MS))
> + WRITE_ONCE(priv->rescue_failed, true);
> +}
[Severity: High]
Can this work item deadlock against teardown?
device_schedule_reprobe() takes the device lock of the same device:
drivers/base/dd.c:device_schedule_reprobe() {
...
device_lock(dev);
parent = dev->parent;
if (!dev->bus || !dev->p || dev->p->dead ||
!device_is_registered(dev) || !dev->driver) {
...
}
and this patch adds a synchronous cancel of the same work to the teardown
path:
drivers/net/dsa/mxl862xx/mxl862xx-host.c:mxl862xx_host_shutdown() {
cancel_work_sync(&priv->crc_err_work);
cancel_work_sync(&priv->rescue_heal_work);
}
mxl862xx_host_shutdown() is reached from mxl862xx_remove(),
mxl862xx_shutdown() and the dsa_register_switch() failure path of
mxl862xx_probe(), all of which the driver core invokes with device_lock(dev)
already held, e.g.:
drivers/base/core.c:device_shutdown() {
...
device_lock(dev);
...
} else if (dev->driver && dev->driver->shutdown) {
dev->driver->shutdown(dev);
...
}
So is this interleaving possible?
heal work teardown thread
mxl862xx_rescue_drain() == 0
test_bit(WORK_STOPPED) == false
device_lock(dev) (core)
mxl862xx_host_shutdown()
device_schedule_reprobe()
device_lock(dev) -> blocks
cancel_work_sync(&rescue_heal_work)
-> waits for the work
The test_bit(MXL862XX_FLAG_WORK_STOPPED) check above is a check, not mutual
exclusion, and in mxl862xx_remove() the flag is set well before the cancel
(dsa_unregister_switch() runs in between), so the work has time to reach the
blocking device_lock(). If that happens, does unbind ever return, and does a
reboot hang inside device_shutdown() holding the device lock? The probe error
path looks similar: setup() queues the work, the drain can return quickly when
the loader is already at READY, and probe still holds the device lock for the
rest of dsa_register_switch().
Would deferring the reprobe hand-off to something that does not take
device_lock() from the work, or dropping the sync cancel in favour of the
core's own "device is going away" handling, avoid this?
> +
> +/* Detect MCUboot rescue mode over clause-22 SMDIO alone, so the caller can rule
> + * the loader out before any C45 API request (which spews CRC errors when no WSP
> + * firmware answers). A scratch write to ADDR/DATA must latch or the chip is
> + * absent (-ENODEV); the mailbox is reset first, or a transfer interrupted with
> + * CTRL=WR would take that write as a payload word instead of latching it. STAT
> + * then classifies the state, poked destructively only when 0, the one value a
> + * running firmware never holds:
[ ... ]
> +int mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv, bool settle)
> +{
[ ... ]
> + /* The download handshake lives in STAT too and outlives a mailbox
> + * reset, so an aborted transfer leaves the loader waiting for a
> + * header no later session can supply: only a power cycle clears it.
> + */
> + if ((u16)stat == MXL862XX_SB_PDI_START ||
> + (u16)stat == MXL862XX_SB_PDI_START + 1) {
> + WRITE_ONCE(priv->rescue_failed, true);
> + return MXL862XX_IN_RESCUE;
> + }
[Severity: Medium]
Is this state reachable from the driver's own flash path, and is the resulting
refusal permanent?
mxl862xx_flash_firmware() publishes START and then waits for the ACK:
ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
MXL862XX_SB_PDI_START);
...
ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_START + 1,
MXL862XX_FW_ACK_TIMEOUT_MS);
if (ret) {
...
goto no_end;
}
An SMDIO error, an ACK timeout or a host interruption in that window leaves
STAT at START/ACK and the loader is not returned to READY, so the post-flash
reprobe lands in the branch above and latches rescue_failed. Nothing clears
rescue_failed for the life of the binding, and mxl862xx_devlink_flash_update()
tests it first:
if (READ_ONCE(priv->rescue_failed)) {
NL_SET_ERR_MSG_MOD(extack,
"download recovery failed, power cycle the switch");
return -EIO;
}
That looks at odds with the commit message:
after a failed one the switch is still in MCUboot, rescue mode
is detected again, and the user can retry.
Also, does a power cycle alone (as the extack suggests) restore flashability?
The still-bound driver keeps rescue_mode and rescue_failed set, so
mxl862xx_api_wrap() keeps short-circuiting to -ENODEV and user ports stay
unused until an unbind/rebind. Should the extack and the commit message
mention the rebind, and should the START/ACK case be listed among the
detection outcomes the commit message enumerates?
> +
> + /* Any other non-zero value is a running firmware, not a loader -- but
> + * the loader also holds the count of a chunk it is still programming,
> + * so let a caller that has ruled the firmware out wait that out.
> + */
> + if (stat) {
> + if (!settle)
> + return MXL862XX_NOT_RESCUE;
> +
> + stat = mxl862xx_sb_pdi_poll_drain(priv,
> + MXL862XX_SB_PDI_STEP_MS);
> + if (stat < 0)
> + return stat;
> + if (stat == MXL862XX_SB_PDI_READY) {
> + WRITE_ONCE(priv->rescue_ready, true);
> + return MXL862XX_IN_RESCUE;
> + }
> + if (stat)
> + return MXL862XX_NOT_RESCUE;
> + }
[Severity: High]
Is a 2 s settle window (MXL862XX_SB_PDI_STEP_MS) long enough to tell a busy
loader from a running firmware?
The driver's own bounds for those loader phases are much longer:
#define MXL862XX_FW_ERASE_TIMEOUT_MS 300000
#define MXL862XX_FW_WRITE_TIMEOUT_MS 60000
During an erase the loader keeps the count it was given in STAT
(mxl862xx_flash_firmware() writes STAT=MXL862XX_FW_HDR_SIZE, polls for
HDR_SIZE + 1, then polls for STAT == 0 with MXL862XX_FW_ERASE_TIMEOUT_MS), and
while a slice is being programmed STAT holds data_written. Both are non-zero
and neither is a magic value.
So if the host dies during the erase or a slice write, does the next probe take
this "other non-zero => running firmware" branch? mxl862xx_setup() first calls
mxl862xx_rescue_mode_detect(priv, false), which returns NOT_RESCUE, then runs
mxl862xx_reset() and mxl862xx_wait_ready() over clause-45, and the follow-up
mxl862xx_rescue_mode_detect(priv, true) waits only 2 s before returning
NOT_RESCUE again, leaving probe to fail with the -ETIMEDOUT from
mxl862xx_wait_ready().
That is the outcome the commit message says is removed:
The clause-45 MMD API never comes up, so probe
would fail with -ETIMEDOUT
The first
three enter rescue mode or normal operation, the last two fail probe
cleanly with -EOPNOTSUPP and -ENODEV instead of a CRC-error storm.
Should the settle window be sized against MXL862XX_FW_ERASE_TIMEOUT_MS /
MXL862XX_FW_WRITE_TIMEOUT_MS instead, or should the erase/program counts be
classified explicitly?
> +
> + /* STAT == 0: a wedged receive loop takes a 1-byte slice-advance (feed
> + * one DATA word first, like a drain chunk) and asks for the next chunk
> + * by publishing 0 again.
[ ... ]
> @@ -559,9 +992,28 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
> return ret;
> }
>
> - dev_info(ds->dev, "flash: running firmware %u.%u.%u\n",
> - priv->fw_version.major, priv->fw_version.minor,
> - priv->fw_version.revision);
> + /* Refuse to flash while the background self-heal is still draining, and
> + * for good once it has given up on the loader.
> + */
> + if (READ_ONCE(priv->rescue_failed)) {
> + NL_SET_ERR_MSG_MOD(extack,
> + "download recovery failed, power cycle the switch");
> + return -EIO;
> + }
> +
> + if (READ_ONCE(priv->rescue_mode) && !READ_ONCE(priv->rescue_ready)) {
> + NL_SET_ERR_MSG_MOD(extack,
> + "switch is recovering an interrupted download, retry shortly");
> + return -EBUSY;
> + }
[ ... ]
> @@ -608,6 +1060,7 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
> * readiness poll below read the freshly booted firmware.
> */
> priv->flash_owner = current;
> + WRITE_ONCE(priv->rescue_mode, false);
> mutex_unlock(&priv->mdiodev->bus->mdio_lock);
>
> /* Refresh the cached versions so the flash update only
[Severity: Medium]
This isn't introduced here alone, but the rescue-mode flash path makes it
reachable in one more way: what happens at the tail of
mxl862xx_devlink_flash_update() when the reprobe hand-off fails?
priv->skip_teardown = true;
mutex_unlock(&priv->mdiodev->bus->mdio_lock);
...
err = device_schedule_reprobe(ds->dev, MXL862XX_FW_REPROBE_DELAY_MS);
return ret ? ret : err;
device_schedule_reprobe() returns -ENOMEM when its allocation fails, and the
function returns that without restoring skip_teardown, clearing
MXL862XX_FLAG_WORK_STOPPED, re-arming the stats/CRC/flood workers or reopening
the ports, and with no reprobe queued to rebuild any of it. Every later flash
attempt then hits the earlier guard:
if (priv->skip_teardown) {
NL_SET_ERR_MSG_MOD(extack,
"device is reinitializing, retry later");
return -EBUSY;
}
Does that leave the instance quiesced and unflashable until a manual
unbind/rebind?
> diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
> index b689652aa9b92..6fde3a58939d0 100644
> --- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
> +++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
> @@ -406,6 +406,8 @@ mxl862xx_phylink_mac_select_pcs(struct phylink_config *config,
>
> switch (port) {
> case 9 ... 16:
> + if (READ_ONCE(priv->rescue_mode))
> + return NULL;
> if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 84)) {
> dev_warn_once(dp->ds->dev,
> "SerDes PCS unsupported on old firmware.\n");
[Severity: Medium]
mac_select_pcs() gets a rescue_mode escape here, but should
mxl862xx_phylink_get_caps() get one too? It still derives the interface mask
from the cached firmware version:
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c:mxl862xx_phylink_get_caps() {
...
case 10 ... 12:
case 14 ... 16:
if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 84))
break;
__set_bit(PHY_INTERFACE_MODE_QSGMII, config->supported_interfaces);
__set_bit(PHY_INTERFACE_MODE_10G_QXGMII, config->supported_interfaces);
...
}
In rescue mode mxl862xx_setup() returns before mxl862xx_wait_ready(), which is
the only writer of priv->fw_version, so the version stays 0.0.0 and
MXL862XX_FW_VER_MIN(priv, 1, 0, 84) is false. For ports 10-12 and 14-16 that
leaves supported_interfaces empty, and phylink rejects that:
drivers/net/phy/phylink.c:phylink_create() {
...
if (phy_interface_empty(config->supported_interfaces)) {
dev_err(config->dev, "phylink: error: empty supported_interfaces\n");
return ERR_PTR(-EINVAL);
}
...
}
Since dsa_tree_setup_ports() has no as-unused fallback for CPU/DSA ports:
net/dsa/dsa.c:dsa_tree_setup_ports() {
...
if (dsa_port_is_dsa(dp) || dsa_port_is_cpu(dp)) {
err = dsa_port_setup(dp);
if (err)
goto teardown;
}
...
}
does a shared or CPU port in that index range fail the whole probe instead of
binding in rescue mode? This is the opposite of what the commit message
describes:
user ports fail port_setup with -ENODEV (the DSA core
re-registers them as unused) while shared and CPU ports succeed, and the
CPU port works on its fixed link with mac_select_pcs returning no PCS.
> diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c
> index 2f112d69f7887..3048073d02f98 100644
> --- a/drivers/net/dsa/mxl862xx/mxl862xx.c
> +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
> @@ -674,21 +674,68 @@ static int mxl862xx_setup(struct dsa_switch *ds)
[ ... ]
> + rescue = mxl862xx_rescue_mode_detect(priv, false);
> + if (rescue < 0) {
> + dev_err(ds->dev, "switch state detection failed: %pe\n",
> + ERR_PTR(rescue));
> + return rescue;
> + }
>
> - ret = mxl862xx_wait_ready(ds);
> - if (ret)
> - return ret;
> + if (rescue == MXL862XX_NOT_RESCUE) {
> + ret = mxl862xx_reset(priv);
> + if (ret)
> + return ret;
> +
> + ret = mxl862xx_wait_ready(ds);
> + if (ret) {
> + /* the reset may only now have triggered rescue mode */
> + rescue = mxl862xx_rescue_mode_detect(priv, true);
> + if (rescue < 0) {
> + dev_err(ds->dev,
> + "switch not responding after reset: %pe\n",
> + ERR_PTR(rescue));
> + return rescue;
> + }
> + if (rescue == MXL862XX_NOT_RESCUE)
> + return ret;
> + }
> + }
>
> + priv->rescue_mode = rescue;
> +
> + /* Software-only SerDes state, needed before anything can reach phylink,
> + * including a rescue-mode flash clearing rescue_mode ahead of reprobe.
> + */
> mutex_init(&priv->serdes_lock);
> for (i = 0; i < ARRAY_SIZE(priv->serdes_ports); i++)
> mxl862xx_setup_pcs(priv, &priv->serdes_ports[i],
> i + MXL862XX_FIRST_SERDES_PORT);
>
> + if (priv->rescue_mode) {
> + if (priv->rescue_ready) {
> + dev_warn(ds->dev,
> + "switch in MCUboot rescue mode, use devlink to flash new firmware\n");
> + } else if (priv->rescue_failed) {
> + dev_warn(ds->dev,
> + "switch in MCUboot with an unfinishable download, power cycle it\n");
> + } else {
[ ... ]
> + queue_work(system_long_wq, &priv->rescue_heal_work);
> + }
> + return 0;
> + }
> +
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1789175618.git.daniel%40makrotopia.org
next reply other threads:[~2026-09-14 18:34 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-14 18:34 netdev-bot+sashiko [this message]
-- strict thread matches above, loose matches on Subject: below --
2026-09-13 18:00 [PATCH net-next v15 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
2026-09-13 18:02 ` [PATCH net-next v15 5/6] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178941089675.22033.4191173896423195861@kernel.org \
--to=netdev-bot+sashiko@kernel.org \
--cc=andrew@lunn.ch \
--cc=corbet@lwn.net \
--cc=dakr@kernel.org \
--cc=daniel@makrotopia.org \
--cc=davem@davemloft.net \
--cc=driver-core@lists.linux.dev \
--cc=edumazet@google.com \
--cc=gregkh@linuxfoundation.org \
--cc=horms@kernel.org \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=olteanv@gmail.com \
--cc=pabeni@redhat.com \
--cc=rafael@kernel.org \
--cc=rdunlap@infradead.org \
--cc=skhan@linuxfoundation.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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®