mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue
@ 2026-10-03 15:50 Daniel Golle
  2026-10-03 15:51 ` [PATCH net-next v19 1/6] net: dsa: add devlink flash_update callback to dsa_switch_ops Daniel Golle
                   ` (5 more replies)
  0 siblings, 6 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:50 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

This series adds "devlink dev flash" and "devlink dev info" support to
the MaxLinear MxL862xx DSA driver, and makes a switch stuck in its
MCUboot loader recoverable through the same path.

The switch is flashed over the loader's clause-22 SMDIO download
interface after the firmware API has rebooted it into MCUboot, and the
driver reinitialises through a deferred detach and re-probe once the
new image runs. A switch found in MCUboot at probe registers in a
reduced rescue mode with firmware version 0.0.0, so the same flash flow
recovers it; an interrupted download is drained in the background
first. The deferred re-probe comes from a new driver-core helper,
device_schedule_reprobe(), which the bound driver calls without a
module reference; it also replaces the open-coded self-reprobe that
iwlwifi, hci_h5 and btintel_pcie carry, whose work function frees its
own module text from under a racing rmmod. fwupd's devlink plugin
carries the matching quirks [17].

Patch 3 is a driver-core change and patch 4 does not link without it,
so the series needs a driver-core ack before net-next can take it.

Tested on an MxL86252C switch of the BananaPi R4 Pro 8X as well as
the MxL86282C of Adtran SDG-9000:
 - an upgrade through `fwupdmgr upgrade`
 - an unbind and a reboot issued while a flash was running, which wait
   out the transfer and announce the wait
 - a host crash (`echo c > /proc/sysrq-trigger`) recovered by the
   background drain and by the rescue path on the next boot
 - and a power cut mid-transfer, switch is detected in MCUboot rescue
   mode on the next boot
 - an unbind and a reboot issued while a background drain was running,
   which abort the drain at once and let it resume and complete on the
   rebind or reboot.

Changes since v18 [24]:
 - patch 3: keep one pending re-probe request per device in struct
   device_private and cancel it when the device is deleted or its
   binding ends, so the reference it holds is dropped with the binding
   and a rebind or a module reload within the delay gets no stale
   re-probe; drop the error for a re-probe that left the device unbound,
   since the probe path logs a failed probe itself and a deferred retry
   is off the deferred-probe list while it runs, so no state read tells
   it from a failure; drop a request that fires while probing is blocked
   for a shutdown and re-arm one that fires during a suspend once a
   second; kernel-doc and commit message corrections (found by Sashiko
   AI review and a local review)
 - patch 4: refuse a flash while the DSA tree is still being set up,
   since devlink registers the switch before the core creates the user
   ports, which it creates and frees without rtnl held; read the flash
   gates under the MDIO bus lock in the MDB ops and get_ethtool_stats();
   tell devlink when the new firmware runs but the driver needs
   rebinding; log a rejected image once and name it in the extack;
   message, comment and kernel-doc corrections; the final slice's
   verification window moves here from patch 5 (found by Sashiko AI
   review)
 - patch 5: map the firmware's CRC verdicts where the command is issued
   and keep the host's own verdict outside the firmware's result range;
   confirm READY with the register-read challenge after the settle step
   too, judged by the re-armed ready word alone; return a bus timeout
   during the challenge as the bus error it is; drop the "firmware
   booted while draining" outcome, which the loader's CRC gate makes
   unreachable, so any count outliving the window is a stuck loader;
   log a self-heal re-probe that cannot be scheduled; set WORK_STOPPED
   before waiting out a flash in .remove() and ->shutdown(); drop the
   unreachable rescue-mode disjunct from the MDB ops; commit message
   and comment corrections (found by Sashiko AI review)
 - patch 6: the documentation names every recovery failure with its
   remedy and says the kernel log names the cause, lists every state
   without a firmware version, and says that the conduit is closed too,
   that the reprobe loses the user ports' configuration and that the
   duration depends on the board's flash chip (found by Sashiko AI
   review)
 - patches 1 and 2: commit message corrections, and the comment on the
   SMDIO helpers names the rescue work among their callers (found by
   Sashiko AI review)
 - a local run of the same review pipeline on this version found more,
   acted on as follows. Patch 3: the unbind cancelled the request before
   ->remove(), where this driver's flash and drain wait and then
   schedule, so the cancel moved to the end of the binding, where it
   also covers a probe that fails; a spinlock pairs the slot with the
   queueing and cancelling of its work, which closes the windows between
   publishing and queueing a request and around the re-arm; the slot is
   held until the work has released the driver, so a second request
   during a re-probe is refused. Patch 4: the DSA workqueue is flushed
   after the ports close, so the bridge's host address deletions reach
   the firmware while it still runs; the chip ID is zeroed before it is
   re-read; the unreachable component check is gone. Patch 5: rescue
   mode counts among the gates get_ethtool_stats() keeps quiet for, the
   CRC codes are logged as reported, -EINVAL from the self-heal's
   request is the device deletion it is, and the probe-time messages
   name the state. Patch 6 and the messages of patches 1 and 2 follow.
   A second pass on that result found less: patch 3 drops the driver
   pointer and name copy, which the slot makes redundant, and re-arms
   across a hibernation restore; patch 4 takes -EINVAL from the
   re-probe request as the device deletion it is; patch 5 returns a bus
   error of the challenge's closing reset as such and names the host's
   CRC verdict in the log; the rest is prose
 - Andrew's Reviewed-by is kept on patches 1, 2 and 6, which changed in
   prose only
 - the Sashiko review's remaining findings are not acted on: a refused
   FW_UPDATE command keeps the teardown and re-probe, since the ports
   are closed by then and the re-probe is what reinitialises them, and
   the log and extack name the refusal; the wait of a reboot for a flash
   in flight is announced in the log and ends with the transfer; a
   switch left unbound by a failed re-probe hand-off is a rebind matter,
   as documented; an MDIO bus without clause-22 access cannot carry this
   switch, as no SoC or MAC on a real board exposes one without both
   clauses; a final slice whose byte count equals a status magic holds
   that value only while the loader programs the slice, well before a
   host that died could probe again; the drain stays off a freezable
   workqueue, a suspend during the exceptional recovery path being rare
   and recovered by a rebind; and a running firmware never holds 0 in
   the status register, which reads 0x0003 after boot whatever the
   loader left behind, as observed on the BananaPi BPi-R4 Pro 8X after
   a flash and after a plain rebind; a status word left at 0 by an END
   write that failed is not guarded against either, since a clause-22
   write failing after the hundreds the transfer made is a broken bus,
   which the readiness poll that follows fails on as well.
 - the changes to patches 1 to 6, the commit messages of patches 1 and 2
   included, were written with an LLM coding assistant working from the
   Sashiko findings and a local review, and reviewed by hand; the
   Assisted-by tags on patches 3 to 6 record this

Changes since v17 [23]:
 - patch 3: re-arm the work while probing is blocked, since
   dpm_prepare() blocks it as well and a kexec jump or a kernel without
   the suspend freezer reaches that window with the workqueue running,
   which lost the request for good; log an error for every unbound
   outcome of device_attach() except a deferred probe, a failed probe
   coming back as 0, which the check for a negative value missed;
   kernel-doc and commit message corrections (found by Sashiko AI
   review)
 - patch 4: accept a read refused by the flash gate or the teardown gate
   while either is set in port_mdb_add() and port_mdb_del(), since the
   flash moves from one gate to the other after api_wrap() has dropped
   the MDIO bus lock; wait for the header byte count to change rather
   than for its ACK value, which a short erase retires between two
   polls; log a re-probe that cannot be scheduled after a failed
   transfer as well; return silently from get_ethtool_stats() while the
   gates refuse reads, which unprivileged ethtool -S reaches (found by
   Sashiko AI review)
 - patch 5: after a failed clause-45 wait, a status word that outlives
   the settle step is rescue only when it is the erase count of the
   session that died; any other value is a firmware that answered the
   reset without becoming ready, and probe fails with the wait error as
   it did before the patch; the rescue-mode version is reported as
   running only, since the driver cannot tell what the flash holds; the
   probe-time log names the interrupted handshake and the extack covers
   every failed-recovery cause; the commit message describes the
   detection outcomes again (found by Sashiko AI review); a quiet
   firmware command, as the readiness poll issues, does not arm the
   CRC-error work, since a probe that meets the loader mid-erase fails
   every poll's CRC check and closed the conduit with a warning on each
 - patch 6: the documentation follows the patch 4 and 5 changes
 - Andrew's Reviewed-by is kept on patches 1, 2 and 6, whose only change
   this round is two sentences of documentation
 - the review's remaining findings are not acted on: an MDIO bus without
   clause-22 access cannot carry this switch, as no SoC or MAC on a real
   board exposes one without both clauses, so its absence is not
   handled; a final slice whose byte count equals a status magic holds
   that value only while the loader programs the slice, well before a
   host that died could probe again; the drain's chunk == 0 error was
   raised again and the v16 note below stands, the one path that fed a
   running firmware into the drain being the settle classification
   patch 5 now corrects; the ->remove()-after-->shutdown() window and
   the two patches between which a failed transfer is not yet recovered
   are described in the commit messages of patches 3 and 4; and the
   drain stays off a freezable workqueue as in v17
 - the changes to patches 3 to 6 were written with an LLM coding
   assistant working from the Sashiko findings, and reviewed by hand;
   the Assisted-by tags on patches 3 to 6 record this

Changes since v16 [21]:
 - patch 3: drop the abort_if_blocked flag and the bool return of
   __device_release_driver(), leaving that function unchanged. The flag
   left the device-links state half torn down when it fired and did not
   cover the consumers unbound in the same window; the shutdown-versus-
   release window it targeted is pre-existing to every unbind path
   (driver_detach(), unbind_store(), the consumer recursion), so the
   re-probe, which is device_reprobe() deferred, shares it rather than
   introducing it, and the kernel-doc now says so (found by Sashiko AI
   review)
 - patch 3: record the bound driver's name beside its pointer and
   compare both, so a freed struct device_driver address the allocator
   later hands to a different driver is not mistaken for the original
   binding; this closes the recorded-identity race without a module
   reference (found by Sashiko AI review)
 - patch 4: wait out a flash in flight in .remove() as ->shutdown()
   already did, before dsa_unregister_switch() frees the user netdevs.
   The instance lock a flash holds does not cover that free, which the
   teardown reaches first, so an unbind racing a flash could touch a
   freed netdev; both paths now take the lock up front and announce the
   wait with dev_info() so the up-to-a-minute pause is not mistaken for
   a hang (found by Sashiko AI review)
 - patch 4: report success for a firmware read blocked by a flash
   (-EBUSY), not only the teardown -ENODEV, in port_mdb_add() and
   port_mdb_del(), so an MDB change racing a flash does not fail and
   leave the entry linked; clear the SB PDI ADDR and DATA latches with 0
   rather than a CTRL mode value that only happens to be 0; log that a
   rebind is needed if the re-probe cannot be scheduled after the new
   firmware is already running; correct the block_host/skip_teardown
   kernel-doc to the policy the code implements (all found by Sashiko AI
   review)
 - patch 5: bypass the firmware-version gate in
   mxl862xx_phylink_get_caps() in rescue mode, so a SerDes CPU port does
   not get an empty supported_interfaces mask and fail phylink_create(),
   matching the mac_select_pcs() bypass; the @rescue_failed kernel-doc
   and the setup log message no longer prescribe a power cycle for every
   cause, whose per-cause remedy is in the documentation (found by
   Sashiko AI review)
 - Andrew's Reviewed-by is kept on patches 1, 2 and 6, unchanged this
   round
 - Jakub asked whether the mode transitions could be driven from
   userspace through devlink reload [22]. The deferred re-probe is kept:
   DSA has no reload plumbing, and adding it would need a new DSA-core
   path to reinitialise a switch while its devlink instance stays alive,
   whereas the re-probe reuses the existing unbind/register path, and
   the helper it uses is wanted regardless to replace the open-coded
   self-reprobe in iwlwifi, hci_h5 and btintel_pcie
 - patch 5: the interrupted-download drain's chunk == 0 error, which the
   review reads as mis-reporting a firmware that booted mid-drain, was
   reproduced on hardware and does not arise. The loader verifies the
   received image by CRC, so the zero-filled drain never reconstructs a
   bootable image; it returns the loader to its ready state instead, and
   the chunk == 0 error only fires for a genuinely wedged loader, where
   it is correct (found by Sashiko AI review)
 - the review's remaining findings are not acted on: patch 1 stays on
   the unconditional shared ops table as Andrew asked in v9; the SB PDI
   register offsets and the status words patch 5 classifies the switch
   by are a fixed hardware property configured by MCUboot and a stable
   contract the firmware release QA gates enforce, probe detection being
   part of the gate, so neither a relocated window nor a status value
   that breaks classification is a configuration that ships; and the
   background drain deliberately stays off a freezable workqueue, a
   suspend or bus error there being a rare event on an exceptional path
   that a rebind recovers
 - the changes to patches 3 to 5 were written with an LLM coding
   assistant working from the Sashiko findings and a local review, and
   reviewed by hand; the Assisted-by tags on patches 3 to 5 record this

Changes since v15 [20]:
 - patch 1: the commit message no longer puts a notification pair
   around a missing firmware file; the core fails that before the pair,
   which wraps the call into the trampoline only
 - patch 3: take no lock in the caller's context and drop the parent
   snapshot, refusing buses that need the parent lock instead. The
   caller-context device lock inverted against the devlink instance
   lock, which a flash holds across the call, and against the
   synchronous cancel of the rescue self-heal work; a pinned parent can
   also be freed by device_move(). Only usb_bus_type sets
   need_parent_lock, and the conversions posted separately re-probe PCI
   and serdev devices, so none of them is refused. Patches 4 and 5 need
   no change of their own for either inversion (all found by Sashiko AI
   review)
 - patch 3: abandon the release when probing gets blocked while
   __device_release_driver() has the locks dropped to unbind busy
   consumer links, closing the one window where a ->remove() could
   follow a ->shutdown(). Earlier versions called it pre-existing, which
   it is, but a deferred re-probe is the one unbind that may be
   abandoned, so it takes a flag the other callers do not (found by
   Sashiko AI review)
 - patch 3: the commit message now describes what this patch changes
   rather than bugs in the drivers it does not convert (found by Sashiko
   AI review)
 - patch 3: the commit message no longer claims the detach is
   synchronised with device_shutdown() in every case. Probing is
   blocked only once wait_for_device_probe() has returned, so a
   re-probe already past the check detaches the device, which then runs
   ->remove() in place of ->shutdown()
 - patch 4: a firmware write blocked by a running flash reports success
   instead of -EBUSY. Tearing a bridge down over a flash, as a reboot
   does, made port_vlan_del() fail; the bridge then leaves the VLAN on
   its list and __vlan_group_free() warns and frees the group with the
   entries still linked
 - patch 4: ->shutdown() now waits for a flash in flight and refuses
   one requested after it, so a reboot cannot cut the image in half.
   The devlink core already serialises .remove() through the instance
   lock; ->shutdown() does not go through devlink and takes that lock
   itself
 - patch 4: the -EBUSY extack of a pending reprobe states a fact rather
   than advising a retry, and the comment on mxl862xx_read_chip_id()
   drops the rescue-mode cache that only patch 5 creates
 - patch 4: select CRC32, which nothing else selects for the image
   checksum validation (found by Sashiko AI review)
 - patch 5: treat a byte count outliving the settle step as a busy
   loader and wait out an erase a dead session left running before
   draining, so a host that died during the loader's erase no longer
   fails probe with the -ETIMEDOUT this series exists to avoid; drop the
   loader's clean ready state with the cached identity when a flash
   fails; state facts only in the extack of a failed recovery (all found
   by Sashiko AI review)
 - patch 5: the commit message and the comments no longer say the
   clause-45 API floods the log with CRC errors when no firmware
   answers. The mailbox commands run into their timeouts instead, and
   testing the stuck-in-MCUboot paths produces no such message; what
   probing SB PDI first saves is the ten seconds of polling and the
   -ETIMEDOUT that ends probe
 - patch 5: move the rescue branch of mxl862xx_setup() into a function
   of its own, which brings its messages back inside 100 columns; state
   a fact in the -EBUSY extack of a running recovery; trim the drain
   comment, whose protocol detail is in the file header, and order its
   declarations
 - patch 6: document that the ports come back down, which recovery
   failure needs a power cycle and which a rebind, and what a re-probe
   that cannot be scheduled leaves behind (found by Sashiko AI review)
 - patch 6: document that a reboot waits for a running flash, that a
   flash requested after it is refused, and that a bus error ends the
   download recovery for good; title-case the "Flash Update" heading
   like the other devlink driver documents
 - Andrew's Reviewed-by is kept on patches 1, 2 and 6: patch 1 gained
   commit message text only and patch 6 the documentation sentences
   above. It is dropped on patch 4, which gained the shutdown
   serialisation
 - patch 3 has drawn no driver-core reply here or in its three
   standalone postings [11][14]. The helper is no longer an RFC: Hans
   de Goede reviewed and tested it there [15][16], and the conversions
   of the open-coded users in iwlwifi, hci_h5 and btintel_pcie follow
   once this series is merged
 - the remaining findings of that review are not acted on: patch 1 is
   asked once more to install .flash_update only for drivers
   implementing the callback, as v4 did, and stays unconditional as
   Andrew asked in v9; the empty supported_interfaces a rescue-mode
   probe leaves for the quad-mode sub-interfaces can only reach phylink
   through a CPU port on one of them, which the chip does not support;
   and the driver pointer patch 3 records could in theory match a
   different driver loaded at the same address within the delay, at the
   cost of one spurious re-probe
 - the changes to patches 1 and 3 to 6, the commit message of patch 1
   included, were written with an LLM coding assistant working from the
   Sashiko findings and from a local review of the posted series, and
   reviewed by hand; the Assisted-by tags on patches 3 to 6 record this

Changes since v14 [19]:
 - patch 3: skip the detach while probing is blocked, which
   device_shutdown() does before its walk reaches any device, instead of
   a per-device flag set only once the walk arrives; validate the device
   and snapshot the parent, its locking requirement and the bound driver
   under the device lock; keep -EPROBE_DEFER out of the re-probe error
   path so it cannot overwrite a deferred probe reason (all found by
   Sashiko AI review)
 - patch 4: treat the closing END write as advisory, since the loader
   has verified the image by then, and admit only the flash task's own
   firmware reads past block_host instead of every read from every
   context (found by Sashiko AI review)
 - patch 5: classify a status register left in the download handshake as
   a loader needing a power cycle rather than as running firmware, give
   the loader one step to publish its next state before ruling it out
   after a failed clause-45 wait, abort the drain polls as soon as
   teardown asks for it instead of stalling unbind for up to 17 s, and
   pair the rescue_mode accesses with WRITE_ONCE()/READ_ONCE() (all
   found by Sashiko AI review)
 - patch 6: asic.rev comes from the CHIP ID registers as well, and -EIO
   means the driver gave up on the recovery, which may need a rebind
   rather than a power cycle (found by Sashiko AI review)
 - Andrew's Reviewed-by is kept on patches 1, 2, 4 and 6; the changes to
   4 and 6 are the two small ones above and a documentation reword
 - the same review asks again whether patch 1 should install
   .flash_update only for drivers implementing the callback, as v4 did;
   it stays unconditional as Andrew asked in v9. Its remaining findings
   are not acted on either: the pre-existing window in
   __device_release_driver() where a ->shutdown() can interleave with a
   release, which every unbind path shares; the recorded driver pointer,
   which an unbind and rebind within the delay can match again at the
   cost of one spurious re-probe; and the get_stats64() re-arm race in
   remove(), which predates this series and is fixed separately for net
 - the changes to patches 3 to 6 were written with an LLM coding
   assistant working from the Sashiko findings and reviewed by hand; the
   Assisted-by tags on those patches record this

Changes since v13 [18]:
 - patch 5: initialise the SerDes state once mxl862xx_wait_ready() has
   cached the firmware version, still before the rescue-mode early
   return, so PCS setup can depend on the running firmware
 - picked up Andrew's Reviewed-by on patches 1 and 4; the one on patch 5
   is not carried as that patch changed
 - the Sashiko review of v13 repeats the ABA finding on patch 3 dismissed
   in v13 and marks the two __device_release_driver() windows and the
   get_stats64() re-arm race as pre-existing; no change
 - the change to patch 5 was written with an LLM coding assistant
   working from a report against a downstream tree and reviewed by
   hand; the Assisted-by tag on that patch records this

Changes since v12 [13]:
 - v12 went out just as net-next closed for the 7.3 merge window. The
   helper of patch 3 was then posted on its own, with conversions of the
   existing open-coded users in iwlwifi, hci_h5 and btintel_pcie, most
   recently as v3 [14], which Greg's patch bot deferred past the merge
   window; Hans de Goede reviewed and tested the helper and the hci_h5
   conversion there on RTL8723BS hardware [15][16]. The Sashiko review
   of that posting found the same issues in the helper as the review of
   v12 did, so patch 3 here supersedes the helper patch of that series;
   once this series is merged, the conversions follow as patches for
   bluetooth-next and wireless-next. On the userspace side, fwupd's
   devlink plugin has meanwhile gained the quirks for these switches
   [17]
 - patch 3: queue the re-probe on system_freezable_wq, so one pending
   across system suspend runs after resume instead of detaching a
   suspended device or racing its late suspend callbacks; record at
   scheduling time whether the parent needs locking instead of reading
   dev->bus, which may be gone with its module once the device was
   unregistered; let __device_release_driver() report whether it
   released the driver, so an administrative unbind that wins the race
   inside the device links loop is not undone by the re-attach (all
   found by Sashiko AI review); use dev_err_probe() for the re-probe
   error path (Hans de Goede)
 - patch 4: stop the stats poll with disable_delayed_work_sync() in the
   flash path, so a racing get_stats64() re-arm is a no-op, and drop the
   early return in the work function; the v12 reordering of remove()
   is gone as well, since the get_stats64() race it addressed predates
   this series and needs a fix of its own (found by Sashiko AI review)
 - two further findings of the same review are not acted on: the
   recorded driver pointer could in theory match a different driver
   loaded at the same address within the delay, which would cost that
   driver one spurious detach and re-probe; and the final put_device()
   from the work could call a release() whose module was unloaded in
   the meantime, which is the same hazard every asynchronous device
   reference in the core carries, the async probe helper included
 - the changes to patches 3 and 4 were written with an LLM coding
   assistant working from the Sashiko findings and reviewed by hand; the
   Assisted-by tags on those two patches record this

Changes since v11 [12]:
 - patch 3: pin the parent device across the deferred re-probe and take
   the parent lock across device_attach() on buses that need it; a
   reference on the child alone left a freed dev->parent dereferenced
   under __device_driver_lock() (found by Sashiko AI review)
 - patch 4: cancel the stats poll after dsa_unregister_switch() so a
   racing get_stats64() cannot re-arm it against freed priv; and keep
   the host blocked for writes across the post-flash readiness poll,
   letting only the flash path's own reads reach the new firmware
   (found by Sashiko AI review)

Changes since v10 [10]:
 - new patch 3: driver core: add device_schedule_reprobe(), as posted
   in the RFC [11], used to schedule the post-flash and post-drain re-probe with
   device_schedule_reprobe() instead of a driver-owned work item.
 - the dsa_switch allocation returns to devres. Keeping it out of
   devres only defused the remaining check-vs-detach window, which the
   core helper closes outright
 - scheduling the re-probe is now the one step that can fail after the
   switch was flashed, since the helper allocates its work item
   internally and v10's allocate-up-front dance is no longer possible.
   An -ENOMEM there is a system-wide condition no driver-level message
   or recovery improves, so flash_update just returns it (unbind and
   rebind reinitialises the driver), and a drain whose hand-off fails
   still marks recovery failed so devlink does not keep promising a
   retry

Changes since v9 [9]:
 - Harmonised the SB PDI timeouts. The verify wait is now one constant
   shared by the flash and drain paths (15 s), the 1-byte mailbox step
   is another (2 s) used by the drain and by both detection waits, and
   the per-slice write budget drops from 120 s to 60 s. The last-slice
   flush, which cannot see the boundary between programming and
   verifying, gets the sum of the two.
 - The post-flash and post-drain reprobe no longer detaches a device
   that has been shut down or unbound, and the dsa_switch is allocated
   outside devres so a lost race cannot leave dsa_switch_find() reading
   freed memory. This was a live bug in v9's patch 3 as well, reachable
   by rebooting within 500 ms of a flash.
 - A failed reprobe hand-off after a successful drain now logs and
   fails flashing outright, instead of leaving devlink answering "retry
   shortly" for good. Dropped heal_lock and mxl862xx_stop_work() with
   it: the lock only made the flag test and the queueing atomic, which
   is not the guarantee the comment claimed, and the reprobe's own
   check is what actually decides now.
 - A loader that publishes READY but never services the register-read
   challenge now reports -ENXIO rather than propagating -ETIMEDOUT, and
   the documented return sets of mxl862xx_rescue_mode_detect() and
   mxl862xx_rescue_drain_finish() match what the code returns.
 - Commit message for patch 4 no longer claims a running firmware is
   "left untouched" (the presence probe writes two mailbox scratch
   registers, inert to a firmware that does not read them), says that
   an SB PDI window away from the OTP reset offsets also yields
   -ENODEV, and describes the -ENXIO outcome above.
 - Commented why STAT == 0 during a drain is unambiguous: by the
   r_remain == 0 rule the loader cannot be both inside the receive loop
   asking for a chunk and publishing a verdict.
 - Documented that a switch power cycled on its own needs the driver
   unbound and rebound before a failed recovery is re-examined.

Changes since v8 [8]:
 - install the flash_update devlink op unconditionally and return
   -EOPNOTSUPP from the trampoline for drivers without the callback,
   instead of a second devlink_ops permutation (Andrew Lunn)
 - picked up Andrew's Reviewed-by on patches 2 and 5, given on v5

Changes since v7 [7]:
 - most of the changes below address findings of the Sashiko AI reviews
   of v7
 - the MCUboot loader's transfer completion was reverse-engineered to
   settle two of them: it publishes an image verification verdict in its
   status register, finalises without the END magic once a 2 s timeout
   expires, and keeps the host's byte count visible while it programs a
   chunk. The interrupted-download drain therefore no longer sends END,
   which a loader still in its receive loop consumes as a byte count and
   underflows its receive counter on, and the flash path now reports a
   rejected image instead of a write timeout
 - refuse a second devlink dev flash while the previous one's reprobe is
   still pending, and stop publishing a zeroed firmware version after a
   failed transfer
 - initialise the SerDes state before the rescue-mode early return, so a
   successful rescue-mode flash cannot hand phylink an unconfigured PCS
 - do not fail probe from the wedged-download branch of the rescue
   detection, which a download interrupted with exactly one byte
   outstanding would trigger, and report a failed recovery through
   devlink rather than refusing every flash for good
 - reset the SB PDI mailbox before probing it, log the drain's progress,
   and correct the protocol and register comments throughout

Changes since v6 [6]:
 - reprobe from a single delayed work item instead of a kthread spawned
   by a workqueue kickoff; the kthread existed only to drop the module
   reference from core code, but its creation-failure path did the racy
   module_put() from module text anyway and could strand the driver
   bound with skip_teardown set. The collapsed form matches
   iwl_trans_reprobe_wk(), and a failed reprobe now leaves the device
   unbound like a failed probe
 - only signal END on a successful transfer; a failure leaves the loader
   mid-payload, where END is read as a byte count and can underflow the
   receive counter, so return the error and let the reprobe recover
 - add cond_resched() to the payload loop so a long transfer over a
   bit-banged MDIO bus under CONFIG_PREEMPT_NONE does not trip the
   soft-lockup detector
 - drop the cached firmware version and chip id on a failed flash so
   devlink dev info stops reporting the pre-flash version until the
   reprobe
 - report the firmware version under DEVLINK_INFO_VERSION_GENERIC_FW
   instead of a bare "fw" string
 - correct the SB PDI header comment's SMDIO register map and expand the
   note on why closing the shared conduit is safe

Changes since v5 [5]:
 - run the post-flash reprobe from a kthread that drops the module
   reference with module_put_and_kthread_exit() from core code, fixing
   a use-after-free where a work item's trailing module_put() could
   return into module text a racing rmmod had freed; a workqueue kickoff
   spawns the kthread off the devlink caller where kthread_create() can
   return -EINTR
 - send END on every flash failure from the ready handshake onward so an
   aborted transfer lets MCUboot reboot instead of leaving it waiting
 - after the background drain finalises an interrupted download, reprobe
   and let the probe-time detection re-classify the switch, so a valid
   image a last-moment interruption left bootable comes up as running
   firmware; rescue_drain() no longer inspects or reports the outcome
 - re-read the SB PDI status register once more after a poll timeout
   expires, so a preempted poll cannot report a spurious -ETIMEDOUT
 - bail out of the periodic stats poll when the flash teardown has set
   WORK_STOPPED, closing a get_stats64() re-arm race
 - allocate the reprobe kickoff before disturbing the switch, so an
   -ENOMEM cannot leave it flashed but never reprobed
 - omit asic.id/asic.rev when the CHIP ID read returned 0, instead of
   publishing a bogus "0000" for fwupd to match firmware against
 - treat the flashless-download loop (STAT 0xc33c) as an unsupported
   configuration and fail probe with -ENODEV instead of advertising it
   as flashable

Changes since v4 [4]:
 - report the numeric chip part number and version read from the
   static CHIP ID registers as the "asic.id" and "asic.rev" fixed
   versions instead of a model-name string, which does not belong in
   a devlink version identifier (Jakub Kicinski)
 - report the running firmware version as the "stored" version too,
   since the switch boots it from its own flash, so userspace can
   distinguish a flash-backed part from a flashless one by the
   presence of "stored" without a future API change
 - run the post-flash reprobe from a self-contained work item again
   instead of the v4 kernel thread, which tripped the hung-task
   watchdog while parked across the flash and returned -EINTR from
   kthread_create() when the devlink command was interrupted
 - re-read the new firmware version through the reprobe's fresh probe
   and drop the SYS_MISC_FW_VERSION exemption from the host block
 - raise the firmware command poll timeout so the FW_UPDATE command
   that reboots into MCUboot is not cut short
 - detect the switch state from the value MCUboot publishes in the SB
   PDI STAT register (loader ready, wedged download, or running
   firmware), confirming a live console loader with a register-read
   challenge, instead of trusting a bare SMDIO scratch write
 - fail probe with -ENODEV over SB PDI when the switch does not respond
   at all (absent, unpowered, or misdescribed in the device tree)
   instead of letting the clause-45 API flood the log with CRC errors
 - drain a wedged interrupted download back to a clean ready state
   from a background work item so the multi-minute recovery never
   holds the devlink instance lock, reporting no firmware version and
   refusing flash with -EBUSY until it completes
 - report the rescue-mode null firmware version "0.0.0" as both the
   running and stored version, matching the running/stored reporting
   above
 - split the devlink documentation into its own patch and add
   Documentation/networking/devlink/mxl862xx.rst describing the info
   versions and the flash update behaviour (Jakub Kicinski)
 - include example "devlink dev info" outputs in the commit messages
   of patches 3 and 4 (Jakub Kicinski)

Changes since v3 [3]:
 - only install the flash_update devlink op for switches whose
   driver implements it, so the devlink core rejects unsupported
   requests before fetching the firmware file from userspace
 - run the deferred reprobe from a kernel thread which ends in
   module_put_and_kthread_exit() instead of a work item that
   dropped its module reference while still executing module code
 - fail firmware API read commands with -ENODEV after the update
   has finished instead of faking success with an unfilled buffer,
   which could send port_fdb_dump() into an endless loop
 - keep the host block in place across the post-update version
   query by exempting SYS_MISC_FW_VERSION from block_host instead
   of briefly lifting the block, and write all blocking flags under
   the MDIO bus lock
 - check the return value of all SB PDI control writes; a failed
   address write during the half-bank switch could otherwise place
   the second half of the payload at the wrong flash offset
 - initialise the progress notification deadline from jiffies so
   notifications are not suppressed on 32-bit systems shortly
   after boot
 - log a distinct diagnostic when rescue mode detection fails on an
   SMDIO bus error instead of silently treating it as not being in
   rescue mode
 - flush the switchdev deferred queue after closing the ports so
   the bridge's deferred STP DISABLED transitions reach the
   firmware while it is still running instead of failing against
   the host block with "failed to set STP state" errors
 - treat -ENODEV as successful deletion in port_mdb_del() so the
   post-update teardown no longer leaves host MDB entries behind
   for the DSA core to report when the tree is torn down

Changes since v2 [2]:
 - validate the firmware image, including both CRCs, before taking
   down any ports, so that a malformed file is rejected without
   disturbing the running switch and without the needless flash and
   reprobe cycle it previously triggered
 - reject images whose declared payload sizes overflow when summed
   (check_add_overflow) or sum up to zero; the latter previously
   erased the flash without writing anything back
 - allocate the reprobe work item and take the module and device
   references before starting the update, so scheduling the reprobe
   can no longer fail after the switch has been pushed into MCUboot
 - prevent the stats poll work from being re-armed and cancel the
   CRC error work before starting the transfer
 - check the host-blocking flags in mxl862xx_api_wrap() under the
   MDIO bus lock to close the race window where an API command
   which had already passed the check could reach the bus after the
   switch rebooted into MCUboot
 - check the return value of SB PDI data word writes so a failed
   MDIO transaction aborts the transfer instead of being noticed
   only through a corrupted image
 - report a per-model chip name (e.g. "MaxLinear MxL86252") as the
   devlink "asic.id" fixed version instead of the devicetree
   compatible string, whose comma is awkward for userspace
   consumers such as fwupd (see discussion on v2 patch 3)
 - report the canonical null version "0.0.0" instead of
   "mcuboot-rescue" as the running firmware version in rescue mode,
   so that version-comparing update tools like fwupd treat every
   available release as an upgrade and offer it for recovery

Changes since RFC [1]:
 - detect a switch stuck in MCUboot rescue mode at probe, register
   the switch without any ports and report "mcuboot-rescue" as the
   running firmware version, so devlink flash can recover from a
   failed or interrupted update (Andrew Lunn)
 - clarify in the commit message of patch 2 that the per-transaction
   MDIO bus locking is about other, non-switch devices on the same
   MDIO bus (Andrew Lunn)
 - mention in the commit message of patch 3 that closing the ports
   also stops phylib from polling the switch-internal PHYs during
   the transfer (Andrew Lunn)
 - split up run-on sentence and explain the dynamically allocated
   reprobe work item instead of just pointing at iwlwifi in the
   commit message of patch 3 (Manuel Ebner)
 - use kzalloc_obj() (Manuel Ebner)
 - state the actual duration of a complete flash and reprobe cycle
   (just under a minute) in comments and the commit message, and
   clarify that the timeout values are generous upper bounds
   (Manuel Ebner)

[1] https://lore.kernel.org/all/ak0J-HgzMRea53om@makrotopia.org/
[2] https://lore.kernel.org/all/cover.1783988826.git.daniel@makrotopia.org/
[3] https://lore.kernel.org/all/cover.1784513694.git.daniel@makrotopia.org/
[4] https://lore.kernel.org/all/cover.1784665017.git.daniel@makrotopia.org/
[5] https://lore.kernel.org/all/cover.1784945329.git.daniel@makrotopia.org/
[6] https://lore.kernel.org/all/cover.1785119999.git.daniel@makrotopia.org/
[7] https://lore.kernel.org/all/cover.1785274610.git.daniel@makrotopia.org/
[8] https://lore.kernel.org/all/cover.1785389905.git.daniel@makrotopia.org/
[9] https://lore.kernel.org/all/cover.1785728574.git.daniel@makrotopia.org/
[10] https://lore.kernel.org/all/cover.1786294649.git.daniel@makrotopia.org/
[11] https://lore.kernel.org/all/anpxFdwNxk0XwPjQ@makrotopia.org/
[12] https://lore.kernel.org/all/cover.1786773971.git.daniel@makrotopia.org/
[13] https://lore.kernel.org/all/cover.1786922210.git.daniel@makrotopia.org/
[14] https://lore.kernel.org/all/cover.1787281239.git.daniel@makrotopia.org/
[15] https://lore.kernel.org/all/c461462f-de0b-43e8-ac9e-541013f5f8da@oss.qualcomm.com/
[16] https://lore.kernel.org/all/7ffe0c2e-0742-488a-ab6c-1dc2fabc049c@oss.qualcomm.com/
[17] https://github.com/fwupd/fwupd/commit/e50c9e5ab39d31242e664efbbf441fd46d15a0cd
[18] https://lore.kernel.org/all/cover.1788783126.git.daniel@makrotopia.org/
[19] https://lore.kernel.org/all/cover.1788976064.git.daniel@makrotopia.org/
[20] https://lore.kernel.org/all/cover.1789175618.git.daniel@makrotopia.org/
[21] https://lore.kernel.org/all/cover.1789477568.git.daniel@makrotopia.org/
[22] https://lore.kernel.org/all/aq5QDmuQnqQTudca@makrotopia.org/
[23] https://lore.kernel.org/all/cover.1790130482.git.daniel@makrotopia.org/
[24] https://lore.kernel.org/all/cover.1790426599.git.daniel@makrotopia.org/

Daniel Golle (6):
  net: dsa: add devlink flash_update callback to dsa_switch_ops
  net: dsa: mxl862xx: add SMDIO clause-22 register access
  driver core: add device_schedule_reprobe()
  net: dsa: mxl862xx: add devlink flash_update and info_get
  net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode
  net: dsa: mxl862xx: document devlink flash and info support

 Documentation/networking/devlink/index.rst    |    1 +
 Documentation/networking/devlink/mxl862xx.rst |  118 ++
 MAINTAINERS                                   |    1 +
 drivers/base/base.h                           |    5 +
 drivers/base/core.c                           |    1 +
 drivers/base/dd.c                             |  172 +++
 drivers/net/dsa/mxl862xx/Kconfig              |    1 +
 drivers/net/dsa/mxl862xx/Makefile             |    2 +-
 drivers/net/dsa/mxl862xx/mxl862xx-api.h       |   10 +
 drivers/net/dsa/mxl862xx/mxl862xx-cmd.h       |    2 +
 drivers/net/dsa/mxl862xx/mxl862xx-fw.c        | 1188 +++++++++++++++++
 drivers/net/dsa/mxl862xx/mxl862xx-fw.h        |   21 +
 drivers/net/dsa/mxl862xx/mxl862xx-host.c      |  143 +-
 drivers/net/dsa/mxl862xx/mxl862xx-host.h      |    3 +
 drivers/net/dsa/mxl862xx/mxl862xx-phylink.c   |    9 +-
 drivers/net/dsa/mxl862xx/mxl862xx.c           |  187 ++-
 drivers/net/dsa/mxl862xx/mxl862xx.h           |   56 +-
 include/linux/device.h                        |    2 +
 include/net/dsa.h                             |    3 +
 net/dsa/devlink.c                             |   13 +
 20 files changed, 1894 insertions(+), 44 deletions(-)
 create mode 100644 Documentation/networking/devlink/mxl862xx.rst
 create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.c
 create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.h


base-commit: 42a9fb3382fc2573e92f41d203b095d9a372cfc9
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH net-next v19 1/6] net: dsa: add devlink flash_update callback to dsa_switch_ops
  2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
@ 2026-10-03 15:51 ` Daniel Golle
  2026-10-03 15:51 ` [PATCH net-next v19 2/6] net: dsa: mxl862xx: add SMDIO clause-22 register access Daniel Golle
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:51 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

Add a devlink_flash_update callback to dsa_switch_ops so that DSA
drivers can support devlink dev flash without open-coding the devlink
plumbing. Like the other trampolines in net/dsa/devlink.c, the op
returns -EOPNOTSUPP when the driver does not implement the callback.
The devlink core takes a non-NULL ops->flash_update as the capability
gate, so on a switch without the callback a flash request now fetches
the firmware file, unless it names a component, which the core refuses
after consulting the driver's info versions, or an overwrite mask,
which it refuses outright: a missing file fails with the firmware
loader's errno, -ENOENT, instead of -EOPNOTSUPP, and a file that is
found reaches the trampoline inside a FLASH_UPDATE/FLASH_UPDATE_END
notification pair. The ETHTOOL_FLASHDEV ioctl on a user port reaches
the same trampoline through devlink's compat path, without extack.
Both are acceptable for an operation as infrequent as a firmware
update.

The devlink core calls the op with the devlink instance lock held and
without rtnl_lock. DSA calls its port ops from paths of its own, some
without rtnl_lock either, and registers the devlink instance before
the user ports exist, so a driver serialises a flash against its own
ops and its own setup itself.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
---
v19: commit message: DSA calls some port ops without rtnl and registers
     devlink before the user ports exist, so the driver serialises its
     flash against its setup as well; a request with a component or an
     overwrite mask is refused before the file is fetched, the errno of
     a missing file is the firmware loader's, and the ETHTOOL_FLASHDEV
     ioctl on a user port reaches the trampoline too, without extack
     (found by Sashiko AI review and a local review)
v18: no changes
v17: no changes
v16:
 - commit message: name the errno and the notifications the shared ops
   table changes for switches without the callback (found by Sashiko
   AI review)
 - commit message: a missing firmware file fails before the
   notification pair, which wraps only the call into the trampoline
v15: no changes
v14: no changes, picked up Andrew's v13 Reviewed-by
v13: no changes
v12: no changes
v11: no changes
v10: no changes
v9: install the flash_update op unconditionally and return -EOPNOTSUPP
    from the trampoline like the other DSA devlink trampolines,
    instead of a second devlink_ops permutation (Andrew Lunn)
v8:
 - retitled: this patch adds the callback, its first user is patch 3
 - describe the op's calling context in the commit message
v7: no changes
v6: no changes
v5: no changes
v4: only install the flash_update op for drivers implementing the
    callback so the devlink core keeps rejecting unsupported flash
    requests before fetching the firmware file
v3: no changes
v2: align continuation lines with the open parenthesis

 include/net/dsa.h |  3 +++
 net/dsa/devlink.c | 13 +++++++++++++
 2 files changed, 16 insertions(+)

diff --git a/include/net/dsa.h b/include/net/dsa.h
index 5d12191b6f6f5..1fcf4af6c5064 100644
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -1176,6 +1176,9 @@ struct dsa_switch_ops {
 	int	(*devlink_info_get)(struct dsa_switch *ds,
 				    struct devlink_info_req *req,
 				    struct netlink_ext_ack *extack);
+	int	(*devlink_flash_update)(struct dsa_switch *ds,
+					struct devlink_flash_update_params *params,
+					struct netlink_ext_ack *extack);
 	int	(*devlink_sb_pool_get)(struct dsa_switch *ds,
 				       unsigned int sb_index, u16 pool_index,
 				       struct devlink_sb_pool_info *pool_info);
diff --git a/net/dsa/devlink.c b/net/dsa/devlink.c
index ed342f345692b..25311a87cbc56 100644
--- a/net/dsa/devlink.c
+++ b/net/dsa/devlink.c
@@ -20,6 +20,18 @@ static int dsa_devlink_info_get(struct devlink *dl,
 	return -EOPNOTSUPP;
 }
 
+static int dsa_devlink_flash_update(struct devlink *dl,
+				    struct devlink_flash_update_params *params,
+				    struct netlink_ext_ack *extack)
+{
+	struct dsa_switch *ds = dsa_devlink_to_ds(dl);
+
+	if (!ds->ops->devlink_flash_update)
+		return -EOPNOTSUPP;
+
+	return ds->ops->devlink_flash_update(ds, params, extack);
+}
+
 static int dsa_devlink_sb_pool_get(struct devlink *dl,
 				   unsigned int sb_index, u16 pool_index,
 				   struct devlink_sb_pool_info *pool_info)
@@ -169,6 +181,7 @@ dsa_devlink_sb_occ_tc_port_bind_get(struct devlink_port *dlp,
 
 static const struct devlink_ops dsa_devlink_ops = {
 	.info_get			= dsa_devlink_info_get,
+	.flash_update			= dsa_devlink_flash_update,
 	.sb_pool_get			= dsa_devlink_sb_pool_get,
 	.sb_pool_set			= dsa_devlink_sb_pool_set,
 	.sb_port_pool_get		= dsa_devlink_sb_port_pool_get,
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH net-next v19 2/6] net: dsa: mxl862xx: add SMDIO clause-22 register access
  2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
  2026-10-03 15:51 ` [PATCH net-next v19 1/6] net: dsa: add devlink flash_update callback to dsa_switch_ops Daniel Golle
@ 2026-10-03 15:51 ` Daniel Golle
  2026-10-03 15:52 ` [PATCH net-next v19 3/6] driver core: add device_schedule_reprobe() Daniel Golle
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:51 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

Add mxl862xx_smdio_read() and mxl862xx_smdio_write() for clause-22
SMDIO register access. The switch's MCUboot bootloader exposes only
clause-22 registers; the clause-45 MMD interface needs the running
firmware and is unavailable while the switch is in MCUboot. The MDIO
bus lock is held per-transaction (not across polls) so that SB PDI
polling during flash erase does not starve other non-switch users of
the same MDIO bus, such as separate PHYs providing WAN or management
interfaces.

mxl862xx_api_wrap() takes the bus lock with MDIO_MUTEX_NESTED because
it can be entered from the accessors of the switch-internal MDIO bus
while that bus's lock of the same lock class is already held. The
SMDIO helpers take it with a plain mutex_lock(): they run where no
other MDIO bus lock is held, from probe, the devlink flash path and
the rescue recovery work.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
---
v19: commit message and comment: name the rescue recovery work among the
     callers, state the locking of each path on its own, and say that no
     MDIO bus lock at all may be held, as every bus shares the lock class
     (found by Sashiko AI review and a local review)
v18: no changes
v17: no changes
v16: no changes
v15: no changes
v14: no changes
v13: no changes
v12: no changes
v11: no changes
v10: no changes
v9: no changes, picked up Andrew's v5 Reviewed-by
v8: document the paged register window and the per-transaction bus
    locking next to the helpers, rather than only in this changelog
    (found by Sashiko AI review)
v7: no changes
v6: no changes
v5: no changes
v4: no changes
v3: explain the plain mutex_lock() vs MDIO_MUTEX_NESTED choice in
    the commit message
v2: clarify in the commit message that the per-transaction bus
    locking is about unrelated non-switch devices on the same MDIO
    bus (Andrew Lunn)

 drivers/net/dsa/mxl862xx/mxl862xx-host.c | 41 ++++++++++++++++++++++++
 drivers/net/dsa/mxl862xx/mxl862xx-host.h |  2 ++
 2 files changed, 43 insertions(+)

diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
index 4acd216f7cc00..6fc04a38675ae 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -495,6 +495,47 @@ int mxl862xx_reset(struct mxl862xx_priv *priv)
 	return ret;
 }
 
+#define MXL862XX_SMDIO_ADDR_REG		0x1f
+#define MXL862XX_SMDIO_PAGE_MASK	0xfff0
+#define MXL862XX_SMDIO_OFF_MASK		0x000f
+
+/* Paged clause-22 window: the page goes into MII register 0x1f, the low nibble
+ * of addr selects one of the 16 registers within it. Both helpers take the MDIO
+ * bus lock per transaction with a plain mutex_lock(), so the caller may hold
+ * no MDIO bus lock at all, every bus sharing the lock class;
+ * mxl862xx_api_wrap() holds this one across a whole firmware command.
+ */
+int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr)
+{
+	struct mii_bus *bus = priv->mdiodev->bus;
+	int phy = priv->mdiodev->addr;
+	int ret;
+
+	mutex_lock(&bus->mdio_lock);
+	ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
+			      addr & MXL862XX_SMDIO_PAGE_MASK);
+	if (ret >= 0)
+		ret = __mdiobus_read(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK);
+	mutex_unlock(&bus->mdio_lock);
+	return ret;
+}
+
+int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val)
+{
+	struct mii_bus *bus = priv->mdiodev->bus;
+	int phy = priv->mdiodev->addr;
+	int ret;
+
+	mutex_lock(&bus->mdio_lock);
+	ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
+			      addr & MXL862XX_SMDIO_PAGE_MASK);
+	if (ret >= 0)
+		ret = __mdiobus_write(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK,
+				      val);
+	mutex_unlock(&bus->mdio_lock);
+	return ret;
+}
+
 void mxl862xx_host_init(struct mxl862xx_priv *priv)
 {
 	INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.h b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
index 66d6ae198aff4..4e054c6e4c0e4 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
@@ -18,5 +18,7 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
 	mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
 
 int mxl862xx_reset(struct mxl862xx_priv *priv);
+int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr);
+int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val);
 
 #endif /* __MXL862XX_HOST_H */
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH net-next v19 3/6] driver core: add device_schedule_reprobe()
  2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
  2026-10-03 15:51 ` [PATCH net-next v19 1/6] net: dsa: add devlink flash_update callback to dsa_switch_ops Daniel Golle
  2026-10-03 15:51 ` [PATCH net-next v19 2/6] net: dsa: mxl862xx: add SMDIO clause-22 register access Daniel Golle
@ 2026-10-03 15:52 ` Daniel Golle
  2026-10-03 15:52 ` [PATCH net-next v19 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:52 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

Drivers that need a deferred re-probe of their own device open-code a
work item in module text. iwlwifi (iwl_trans_schedule_reprobe(), for a
firmware crash a lighter restart cannot fix) and hci_h5
(h5_btrtl_resume(), RTL devices lose their firmware state over suspend)
both end that work function with put_device(); kfree();
module_put(THIS_MODULE);, where a concurrent rmmod can free the module
text the epilogue is still executing. Neither gates the re-probe on the
device still being bound to the driver that scheduled it, which a driver
cannot do from outside because device_reprobe() takes the device lock
internally. Their conversion is left to follow-up patches.

Add device_schedule_reprobe(), which detaches and re-probes a device
after a caller-specified delay. The work function is built-in text, so a
caller needs no module reference. The first user is the mxl862xx
devlink flash path added later in this series.

At most one request exists per device, recorded in struct
device_private from its scheduling until its work has released the
driver. The slot identifies the binding: device_del() and
device_unbind_cleanup() clear it, the latter after ->remove() has
returned and the driver pointer is cleared, so a request the driver
schedules from a context its ->remove() waits for is cancelled too, a
failed probe leaves none behind, and neither an unbind followed by a
rebind nor a module reload within the delay gets a re-probe it did not
ask for. A spinlock ties the slot to the queueing and cancelling of its
work, so a cancel either removes a pending work and frees the request
or leaves it to the running work, which frees itself once the slot is
no longer its own; the reference the request holds on the device goes
with it.

A failed re-probe leaves the device unbound, as a failed initial probe
would, and really_probe() logs it like one. The work does not judge the
outcome itself: a deferred retry is off the deferred-probe list while
it runs, so no read of the device state tells one from a failure.

No sleeping lock is taken in the caller's context, only the leaf
spinlock of the slot, and the checks are snapshots the work repeats
under the device lock, so the helper may be called with the device
lock held, as the prepare, suspend, resume and complete callbacks,
->remove() and ->shutdown() hold it. The caller is the bound driver, in
a context its ->remove() waits for. Buses that take the parent lock to
bind are refused with -EINVAL: that lock has to be taken before @dev's
own, so the parent would have to be recorded before either is held,
where device_move() can replace it without taking any device lock.
usb_bus_type is the only such bus and no caller needs it today.

While probing is blocked, which device_shutdown() and dpm_prepare() both
set before they touch any device, a request that fires is dropped once
a halt, power-off or restart has begun and otherwise re-arms itself
once a second, so one pending across a system suspend or a hibernation
restore runs once the system has resumed. Beyond
that gate this is device_reprobe() deferred, with the release path of
__device_release_driver() untouched, so it carries device_reprobe()'s
pre-existing limitations: the detach and the re-attach are not one
locked operation, so an administrative unbind between them may be
undone, and detaching a device that has managed consumers unbinds them
as any release does, so a re-probe a concurrent device_shutdown()
overtakes may run ->remove() after ->shutdown(). None of this is
specific to the helper.

Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v19:
 - keep one request per device in struct device_private, held until
   its work has released the driver, and cancel it from device_del()
   and device_unbind_cleanup(), so the reference it holds is dropped
   with the binding or by a work already running, a rebind or a module
   reload within the delay no longer gets a stale re-probe, and a
   request scheduled from a context ->remove() waits for, or from a
   probe that then fails, is cancelled with the binding; a spinlock
   pairs the slot with the queueing and cancelling of its work, and the
   slot identifies the binding, which makes the driver pointer and name
   copy redundant (found by Sashiko AI review and a local review)
 - re-arm while probing is blocked unless a halt, power-off or restart
   has begun, so a request pending across a hibernation restore
   survives it (found by a local review)
 - drop the error for a re-probe that left the device unbound: the probe
   path logs a failed probe itself, device_attach() reports a probe the
   driver deferred as 0, and a deferred retry is off the deferred-probe
   list while it runs, so no state read tells it from a failure (found
   by Sashiko AI review and a local review)
 - drop a request that fires once a shutdown has begun and re-arm one
   that fires during a suspend once a second, since a zero delay spun a
   kworker while probing was blocked (found by Sashiko AI review)
 - kernel-doc and commit message: a re-probe overtaken by a shutdown can
   run ->remove() after ->shutdown(), only the prepare, suspend, resume
   and complete PM callbacks hold the device lock, a request from
   ->probe() runs once the probe has returned, and the error is also
   withheld for a device bound or gone after all (found by Sashiko AI
   review and a local review)
v18:
 - re-arm the work while probing is blocked instead of dropping the
   request: dpm_prepare() blocks probing as well, and a kexec jump or a
   kernel without the suspend freezer reaches that window with the
   workqueue running, which lost the request for good (found by Sashiko
   AI review)
 - log an error for every unbound outcome of device_attach() except a
   deferred probe: a failed probe comes back as 0, which the check for a
   negative value missed (found by Sashiko AI review)
 - kernel-doc: the caller is the bound driver in a context its ->remove()
   waits for, and a rebind of the same driver within the delay still
   gets the re-probe (found by Sashiko AI review)
 - commit message: the caller's checks are unlocked snapshots, only the
   stored driver pointer is never dereferenced, and the drivers named as
   motivation are converted later (found by Sashiko AI review)
v17:
 - drop the abort_if_blocked flag and the bool return of
   __device_release_driver(), leaving that function unchanged: the flag
   left the device-links state half torn down when it fired and did not
   cover the consumers unbound in the same window, so the shutdown-vs-
   release window it targeted is documented as pre-existing to every
   unbind path instead (found by Sashiko AI review)
 - record the bound driver's name beside the pointer and compare both,
   so a freed struct device_driver address reused by another driver is
   not mistaken for the original binding (found by Sashiko AI review)
 - kernel-doc: add a Context line and state the pre-existing limitations
   shared with device_reprobe() (found by Sashiko AI review)
v16:
 - commit message: device_shutdown() blocks probing only once
   wait_for_device_probe() has returned, so a re-probe already past the
   test detaches the device instead of leaving it bound for its
   ->shutdown()
 - take no lock in the caller's context and drop the parent snapshot,
   refusing buses that need the parent lock instead: the caller-context
   device lock inverted against the devlink instance lock on the flash
   path and against a synchronous work cancel on the rescue path, and a
   pinned parent can be freed by device_move() (found by Sashiko AI
   review)
 - abandon the release when probing is blocked while the device links
   loop has the locks dropped, rather than calling that window
   pre-existing: a deferred re-probe is the one unbind that may be
   abandoned, so it is the one that can close it (found by Sashiko AI
   review)
 - commit message: describe what this patch changes rather than bugs in
   drivers it does not convert, and name the first user (found by
   Sashiko AI review)
 - kernel-doc: drop the promise that an administrative unbind always
   wins, which unbind_store() does not guarantee (found by Sashiko AI
   review)
v15:
 - skip the detach while probing is blocked instead of adding a
   per-device shutdown_done flag: device_shutdown() blocks probing
   before its walk starts, so the flag left a window where the work
   detached a device that then neither re-attached nor got its
   ->shutdown() call (found by Sashiko AI review)
 - validate the device and snapshot the parent, its locking requirement
   and the bound driver under the device lock, so an unregister racing
   the allocation can neither leave a freed parent pinned nor pair a
   NULL parent with a request to lock it (found by Sashiko AI review)
 - keep -EPROBE_DEFER out of the re-probe error path, where
   dev_err_probe() would record the message as the device's deferred
   probe reason (found by Sashiko AI review)
 - kernel-doc: a stale re-probe leaves an unbound device unbound, which
   an unbind followed by a rebind within the delay does not (found by
   Sashiko AI review)
v14: no changes
v13:
 - queue the work on system_freezable_wq, so a re-probe pending across
   system suspend can neither detach a device the PM core has suspended
   nor race its late suspend callbacks; it runs after resume instead
   (found by Sashiko AI review)
 - record at scheduling time whether the parent needs locking, instead
   of reading dev->bus in the work, which may be gone with its module
   once the device has been unregistered (found by Sashiko AI review)
 - let __device_release_driver() report whether it released the driver,
   so an administrative unbind that wins the race inside the device
   links loop is not undone by the re-attach (found by Sashiko AI
   review)
 - use dev_err_probe() for the re-probe error path, so a re-probe
   deferred at resume no longer logs a spurious error (Hans de Goede,
   on the standalone posting of this helper)
 - describe the parent pinning and locking in the commit message, as in
   the standalone posting

v12:
 - pin the parent device across the deferred work; a reference on the
   child alone left device_reprobe_work_fn() dereferencing a freed
   dev->parent under __device_driver_lock() when the device was
   unregistered before the work ran (found by Sashiko AI review)
 - take the parent lock across device_attach() on buses that require
   it, matching bus_rescan_devices_helper() (found by Sashiko AI review)

v11: new patch: add device_schedule_reprobe() to the driver core (posted
     earlier as an RFC) so mxl862xx can schedule its post-flash and
     post-drain re-probe through the core instead of open-coding a work
     item
---
 drivers/base/base.h    |   5 ++
 drivers/base/core.c    |   1 +
 drivers/base/dd.c      | 172 +++++++++++++++++++++++++++++++++++++++++
 include/linux/device.h |   2 +
 4 files changed, 180 insertions(+)

diff --git a/drivers/base/base.h b/drivers/base/base.h
index a5b7abc10ff02..e85a84129eb48 100644
--- a/drivers/base/base.h
+++ b/drivers/base/base.h
@@ -106,6 +106,9 @@ struct driver_private {
  * @dead: This device is currently either in the process of or has been
  *	  removed from the system. Any asynchronous events scheduled for this
  *	  device should exit without taking any action.
+ * @reprobe: request scheduled with device_schedule_reprobe(), held until
+ *	     its work has released the driver; cancelled when the device is
+ *	     deleted or the binding that scheduled it ends
  *
  * Nothing outside of the driver core should ever touch these fields.
  */
@@ -119,6 +122,7 @@ struct device_private {
 	const struct device_driver *async_driver;
 	char *deferred_probe_reason;
 	struct device *device;
+	struct device_reprobe *reprobe;
 	u8 dead:1;
 };
 #define to_device_private_parent(obj)	\
@@ -241,6 +245,7 @@ void devres_for_each_res(struct device *dev, dr_release_t release,
 int devres_release_all(struct device *dev);
 void device_block_probing(void);
 void device_unblock_probing(void);
+void device_reprobe_cancel(struct device *dev);
 void deferred_probe_extend_timeout(void);
 void driver_deferred_probe_trigger(void);
 const char *device_get_devnode(const struct device *dev, umode_t *mode,
diff --git a/drivers/base/core.c b/drivers/base/core.c
index 4c0c373998a19..bcd0f821f8802 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -3927,6 +3927,7 @@ void device_del(struct device *dev)
 	device_lock(dev);
 	kill_device(dev);
 	device_unlock(dev);
+	device_reprobe_cancel(dev);
 
 	if (dev->fwnode && dev->fwnode->dev == dev)
 		dev->fwnode->dev = NULL;
diff --git a/drivers/base/dd.c b/drivers/base/dd.c
index f6525a7ee8c54..21973cf0f1a41 100644
--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -599,6 +599,7 @@ static void device_unbind_cleanup(struct device *dev)
 	kfree(dev->dma_range_map);
 	dev->dma_range_map = NULL;
 	device_set_driver(dev, NULL);
+	device_reprobe_cancel(dev);
 	dev_set_drvdata(dev, NULL);
 	dev_pm_domain_detach(dev, dev->power.detach_power_off);
 	if (dev->pm_domain && dev->pm_domain->dismiss)
@@ -1436,3 +1437,174 @@ void driver_detach(const struct device_driver *drv)
 		put_device(dev);
 	}
 }
+
+struct device_reprobe {
+	struct delayed_work work;
+	struct device *dev;
+};
+
+/* Retry interval while probing is blocked; the caller's delay may be 0. */
+#define DEVICE_REPROBE_BLOCKED_RETRY	HZ
+
+/* Ties the slot in struct device_private to the queueing of its work. */
+static DEFINE_SPINLOCK(device_reprobe_lock);
+
+static void device_reprobe_free(struct device_reprobe *rp)
+{
+	put_device(rp->dev);
+	kfree(rp);
+}
+
+/*
+ * The slot holds a request from its scheduling until its work has released
+ * the driver. A pending work is freed here; a running one finds the slot no
+ * longer its own and frees itself.
+ */
+void device_reprobe_cancel(struct device *dev)
+{
+	struct device_reprobe *rp;
+	bool pending = false;
+
+	if (!dev->p)
+		return;
+	spin_lock(&device_reprobe_lock);
+	rp = dev->p->reprobe;
+	WRITE_ONCE(dev->p->reprobe, NULL);
+	if (rp)
+		pending = cancel_delayed_work(&rp->work);
+	spin_unlock(&device_reprobe_lock);
+	if (pending)
+		device_reprobe_free(rp);
+}
+
+static void device_reprobe_work_fn(struct work_struct *work)
+{
+	struct device_reprobe *rp = container_of(work, struct device_reprobe,
+						 work.work);
+	struct device *dev = rp->dev;
+	bool rearm;
+	int ret;
+
+	device_lock(dev);
+	/* The slot still holding rp means the binding that scheduled it does. */
+	if (dev->p->dead || READ_ONCE(dev->p->reprobe) != rp) {
+		device_unlock(dev);
+		goto out;
+	}
+	if (defer_all_probes) {
+		device_unlock(dev);
+		spin_lock(&device_reprobe_lock);
+		rearm = dev->p->reprobe == rp &&
+			(system_state < SYSTEM_HALT ||
+			 system_state == SYSTEM_SUSPEND) &&
+			queue_delayed_work(system_freezable_wq, &rp->work,
+					   DEVICE_REPROBE_BLOCKED_RETRY);
+		spin_unlock(&device_reprobe_lock);
+		if (rearm)
+			return;
+		goto out;
+	}
+	/* Releasing the driver clears the slot, which was this request. */
+	__device_release_driver(dev, NULL);
+	device_unlock(dev);
+
+	/* A failed probe is logged by the probe path, as for a first probe. */
+	ret = device_attach(dev);
+	dev_dbg(dev, "re-probe: device_attach() returned %d\n", ret);
+out:
+	spin_lock(&device_reprobe_lock);
+	if (dev->p->reprobe == rp)
+		WRITE_ONCE(dev->p->reprobe, NULL);
+	spin_unlock(&device_reprobe_lock);
+	device_reprobe_free(rp);
+}
+
+/**
+ * device_schedule_reprobe - schedule a deferred detach and re-probe
+ * @dev: device to detach and re-probe
+ * @delay_ms: delay in milliseconds before the re-probe runs
+ *
+ * Schedule a detach and re-probe of @dev after @delay_ms milliseconds,
+ * from built-in driver-core work rather than a driver-owned work item,
+ * so the bound driver may call it without pinning its own module.
+ *
+ * At most one request exists per device, from its scheduling until its
+ * work has released the driver, and the request identifies the binding
+ * that scheduled it. It is cancelled when @dev is deleted or when that
+ * binding ends, whether by a release or by a failed probe, so neither an
+ * unbind followed by a rebind nor a module reload within the delay gets a
+ * re-probe it did not ask for; its reference on @dev is dropped with it,
+ * or once a work already running has finished. A request that fires
+ * while probing is blocked for a system suspend or a hibernation restore
+ * re-arms itself every second and runs once the system has resumed; one
+ * that fires while probing is blocked for a halt, power-off or restart is
+ * dropped. A failed re-probe leaves @dev unbound, as a failed initial
+ * probe would, and is logged by the probe path like one.
+ *
+ * This is device_reprobe() deferred, and shares its limitations; the
+ * release path of __device_release_driver() is untouched. The detach and
+ * the re-attach are not one locked operation, so an administrative unbind
+ * arriving between them may be undone. If @dev has managed consumers,
+ * detaching it unbinds them as any driver release does, so a re-probe a
+ * concurrent device_shutdown() overtakes may run ->remove() after
+ * ->shutdown(). None of this is specific to this helper.
+ *
+ * Buses that take the parent lock to bind (only usb_bus_type) are refused
+ * with -EINVAL: the parent would have to be recorded before either lock
+ * is held, where device_move() can replace it.
+ *
+ * Context: May sleep (allocates with %GFP_KERNEL). Must be called by the
+ * driver bound to @dev, from a process context its ->remove() waits for,
+ * so that the binding outlives the call; @dev's own device lock may be
+ * held. A request from ->probe() runs once the probe has returned and is
+ * cancelled if the probe fails.
+ *
+ * Returns: 0 on success, -EINVAL if @dev is not a registered device
+ * bound to a driver or sits on a bus which takes the parent lock to
+ * bind, -EBUSY if a re-probe of @dev is pending or has not yet released
+ * the driver, -ENOMEM on allocation failure.
+ */
+int device_schedule_reprobe(struct device *dev, unsigned int delay_ms)
+{
+	const struct device_driver *drv;
+	struct device_reprobe *rp;
+	int ret = 0;
+
+	drv = READ_ONCE(dev->driver);
+	/*
+	 * A bus taking the parent lock would need @dev's parent pinned until
+	 * the work runs, which device_move() can invalidate.
+	 */
+	if (!drv || !dev->bus || dev->bus->need_parent_lock || !dev->p ||
+	    dev->p->dead || !device_is_registered(dev))
+		return -EINVAL;
+
+	rp = kzalloc_obj(*rp);
+	if (!rp)
+		return -ENOMEM;
+
+	rp->dev = get_device(dev);
+	INIT_DELAYED_WORK(&rp->work, device_reprobe_work_fn);
+
+	/*
+	 * A release or deletion that got here first has cleared the driver or
+	 * set dead before cancelling, so a request published after its cancel
+	 * is refused here; one published before is cancelled by it.
+	 */
+	spin_lock(&device_reprobe_lock);
+	if (dev->p->reprobe)
+		ret = -EBUSY;
+	else if (dev->p->dead || READ_ONCE(dev->driver) != drv)
+		ret = -EINVAL;
+	else
+		WRITE_ONCE(dev->p->reprobe, rp);
+	if (!ret)
+		queue_delayed_work(system_freezable_wq, &rp->work,
+				   msecs_to_jiffies(delay_ms));
+	spin_unlock(&device_reprobe_lock);
+	if (ret)
+		device_reprobe_free(rp);
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(device_schedule_reprobe);
diff --git a/include/linux/device.h b/include/linux/device.h
index aee79fd6b32b4..7a99169505772 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -1314,6 +1314,8 @@ int  __must_check device_attach(struct device *dev);
 int __must_check driver_attach(const struct device_driver *drv);
 void device_initial_probe(struct device *dev);
 int __must_check device_reprobe(struct device *dev);
+int __must_check device_schedule_reprobe(struct device *dev,
+					 unsigned int delay_ms);
 
 bool device_is_bound(struct device *dev);
 
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH net-next v19 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get
  2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
                   ` (2 preceding siblings ...)
  2026-10-03 15:52 ` [PATCH net-next v19 3/6] driver core: add device_schedule_reprobe() Daniel Golle
@ 2026-10-03 15:52 ` Daniel Golle
  2026-10-03 15:53 ` [PATCH net-next v19 5/6] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
  2026-10-03 15:54 ` [PATCH net-next v19 6/6] net: dsa: mxl862xx: document devlink flash and info support Daniel Golle
  5 siblings, 0 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:52 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

Implement "devlink dev flash" for firmware upgrade and "devlink dev
info" for version reporting. info reports the chip part number and
revision as the asic.id and asic.rev fixed versions, and the firmware
version as both the running and the stored version, since the switch
boots its firmware from its own flash; a zero part number or an all-zero
version is omitted rather than published as a bogus match for userspace.

flash validates the image and its payload CRCs, reboots the switch into
its MCUboot loader with SYS_MISC_FW_UPDATE, transfers the image over the
SB PDI clause-22 SMDIO protocol, and reboots into the new firmware once
the loader has verified it. For the duration the driver closes and
detaches the user ports so userspace cannot reopen them, closes the
conduit, and blocks firmware API access under the MDIO bus lock so
nothing reaches the bus after the switch has left for MCUboot. A blocked
write reports success, so a bridge tearing its ports down over a flash
does not see port_vlan_del() fail and leak its VLAN group. A transfer
that fails leaves the switch in the loader; the next patch recovers it
from there.

MCUboot offers no in-place path back, so the driver reinitialises with a
deferred re-probe scheduled through device_schedule_reprobe() from the
previous patch, which runs in the driver core off the devlink caller's
locks and without pinning the driver module. ->shutdown() and
.remove() take the devlink instance lock up front, since neither goes
through devlink and dsa_unregister_switch() frees the user netdevs
before devlink_unregister() would: this waits out a transfer in flight,
for as long as the transfer's own timeouts allow, rather than cutting
it in half, and refuses a flash that starts once teardown has begun. A
flash is refused as well until the DSA tree has finished its setup,
since devlink registers the switch before the core creates the user
ports.

Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v19:
 - refuse a flash while the DSA tree is still being set up: devlink
   registers the switch before the core creates the user ports, which
   it does without rtnl held (found by Sashiko AI review)
 - treat -EINVAL from the re-probe request as the device deletion it
   is, so a flash that completed on a device being deleted neither
   fails nor asks for a rebind (found by a local review)
 - read the flash gates under the MDIO bus lock in port_mdb_add(),
   port_mdb_del() and get_ethtool_stats(), where a lockless read could
   observe the hand-over from block_host to skip_teardown as neither
   flag set and report a bus error (found by Sashiko AI review)
 - tell devlink through extack that the new firmware is running but the
   driver needs rebinding when the re-probe cannot be scheduled after a
   successful transfer (found by Sashiko AI review)
 - log a rejected image once and name it in the extack, and report an
   erase or write that a bus error ended as failed rather than timed
   out (found by Sashiko AI review)
 - give the final slice's status poll the loader's verification window
   on top of the write budget, since the verdict it waits for follows
   the verification; the next patch shares that window with its drain
   (found by Sashiko AI review)
 - flush the DSA workqueue after closing the ports, where the bridge's
   host address deletions run, so they reach the firmware while it is
   still alive (found by a local review)
 - zero the cached chip ID before re-reading it, so a failed read on a
   freshly booted firmware does not keep publishing the previous one
   (found by a local review)
 - drop the check for a component, which the devlink core refuses first
   for a device that reports no component versions (found by a local
   review)
 - comments and kernel-doc: the reprobe starts from a fresh
   configuration, a failed flash also zeroes the part number,
   skip_teardown lasts until the reprobe or a rebind, WORK_STOPPED also
   covers a flash, the 16-bit status register bounds the slice, and the
   reboot and re-probe delays are not timeouts (found by Sashiko AI
   review and a local review)
 - commit message: the shutdown wait lasts as long as the transfer's own
   timeouts allow (found by a local review)

v18:
 - accept a read refused by the flash gate (-EBUSY) or the teardown gate
   (-ENODEV) alike in port_mdb_add() and port_mdb_del() while either
   gate is set, since the flash moves from one gate to the other after
   api_wrap() has dropped the MDIO bus lock (found by Sashiko AI review)
 - log a re-probe that cannot be scheduled after a failed transfer too,
   not only after a successful one (found by Sashiko AI review)
 - wait for the header byte count to change rather than for its ACK
   value, which the loader replaces with 0 as soon as the erase is done
   (found by Sashiko AI review)
 - return without a message from get_ethtool_stats() while the flash
   gates refuse the read, which unprivileged ethtool -S reaches (found by
   Sashiko AI review)
 - reword the -EBUSY extack of a flash requested while a previous one
   still awaits its reprobe, or could not schedule it, to hold in both
   cases
 - kernel-doc: mxl862xx_wait_ready() caches the chip ID only when it can
   be read, and @fw_version is refreshed after a flash and cleared after
   a failed one (found by Sashiko AI review)
 - commit message: the re-probe holds a device reference and only leaves
   the module unpinned, and a failed transfer leaves the switch for the
   next patch to recover (found by Sashiko AI review)

v17:
 - wait out a flash in flight in .remove() too, not only ->shutdown():
   dsa_unregister_switch() frees the user netdevs before it reaches the
   devlink instance lock, so an unbind racing a flash could touch a
   freed netdev; .remove() now takes that lock up front like
   ->shutdown(), and both announce the wait with dev_info() (found by
   Sashiko AI review)
 - report success for a firmware read blocked by a flash (-EBUSY) as
   well as the teardown -ENODEV in port_mdb_add() and port_mdb_del(), so
   an MDB change racing a flash does not fail and leave the entry linked
   (found by Sashiko AI review)
 - clear the SB PDI ADDR and DATA latches with 0 rather than the CTRL
   reset mode value, which only happened to be 0 (found by Sashiko AI
   review)
 - log that an unbind and rebind is needed when the post-flash re-probe
   cannot be scheduled although the new firmware is already running
   (found by Sashiko AI review)
 - kernel-doc: describe the block_host and skip_teardown policy as it
   is, a host read failing while a write reports success (found by
   Sashiko AI review)
 - commit message: scheduling the re-probe can also fail with -EINVAL
   when the device is being unbound, not only -ENOMEM (found by Sashiko
   AI review)

v16:
 - report success for a firmware write blocked by a running flash,
   instead of -EBUSY. Tearing a bridge down over a flash made
   port_vlan_del() fail, which leaves the VLAN on the bridge's list:
   __vlan_group_free() then warns and frees the group with the entries
   still linked
 - wait for a flash in flight in ->shutdown() and refuse one requested
   after it, so a reboot no longer cuts the image in half
 - state a fact in the -EBUSY extack of a pending reprobe, dropping the
   retry advice
 - the comment on mxl862xx_read_chip_id() no longer describes a
   rescue-mode cache that only the next patch creates
 - select CRC32, which nothing else selects for the crc32_le() calls
   validating the image checksums (found by Sashiko AI review)
 - Andrew's Reviewed-by is dropped for the ->shutdown() change

v15:
 - treat the closing END write as advisory: the loader has left the
   receive loop and verified the image by then, so a failed write no
   longer reports a completed flash as a transfer failure (found by
   Sashiko AI review)
 - admit only the flash task's own firmware reads past block_host,
   instead of every read from any context, which let ethtool and FDB
   queries reach the freshly booted firmware and left an mdb_add() half
   executed (found by Sashiko AI review)

v14: no changes, picked up Andrew's v13 Reviewed-by

v13: stop the stats poll with disable_delayed_work_sync() in the flash
     path, so a racing get_stats64() re-arm is a no-op, and drop the
     early return in the work function with it; the v12 reordering of
     remove() is gone as well, since the race it addressed predates
     this series and needs a fix of its own (found by Sashiko AI review)

v12:
 - cancel the stats poll after dsa_unregister_switch() in .remove(): a
   get_stats64() past its lockless WORK_STOPPED check could re-arm the
   work after the cancel and then run against the devres-freed priv
   (found by Sashiko AI review)
 - keep block_host set across the post-flash readiness poll and let
   only the flash path's own firmware reads through, so a concurrent
   bridge or STP write cannot reach the freshly booted firmware with
   stale pre-flash resource IDs while rtnl is dropped (found by Sashiko
   AI review)

v11:
 - schedule the post-flash re-probe with device_schedule_reprobe()
   instead of a driver-owned work item; the module and device references
   and the drvdata bound-check go away with it, and with them both
   findings of the v10 AI review -- the work function no longer ends in
   module text behind a module_put(), and the bound-check and detach now
   run under one __device_driver_lock() hold in the core where
   ->shutdown() cannot interleave between them
 - the dsa_switch allocation returns to devres; keeping it out only
   defused the check-vs-detach window that the core helper now closes
   outright
 - scheduling the re-probe is now the one step that can fail after the
   switch was flashed, since the helper allocates its own work item; an
   -ENOMEM there is returned as-is, since unbind and rebind reinitialises
   the driver

v10:
 - do not reprobe a device that has been shut down or unbound. The work
   cannot be cancelled from teardown: .shutdown() runs under
   device_lock(), which device_reprobe() takes as well, so a lock shared
   between the two would deadlock, and cancel_delayed_work_sync() would
   hang on the nested remove() the work triggers itself. It checks
   drvdata under device_lock() instead, which both .shutdown() and the
   driver core clear (found by Sashiko AI review of patch 4, but this is
   where the reprobe comes from)
 - allocate the dsa_switch outside devres and free it in .remove(), so
   losing the remaining race with .shutdown() cannot let devres free it
   while the DSA tree still holds dsa_ports pointing at it
 - lower the per-slice write timeout from 120 s to 60 s, so this path
   and the drain added in patch 4 agree on how long the same loader step
   may take (found by Sashiko AI review)

v9: no changes

v8:
 - refuse a second devlink dev flash while the previous one's reprobe is
   still pending: the firmware API is short-circuited by then, so the
   FW_UPDATE command faked success and the raw SB PDI writes ran against
   a switch the driver no longer tracks (found by Sashiko AI review)
 - omit the firmware version from devlink dev info while the cached copy
   is all-zero, so a failed transfer no longer publishes 0.0.0 as both
   running and stored, and clear asic_rev along with asic_id (found by
   Sashiko AI review)
 - only translate -ENODEV into success in port_mdb_del() during the
   post-flash teardown; outside it, a genuine bus error was reported to
   switchdev as a successful deletion (found by Sashiko AI review)
 - evaluate the image verification verdict the loader publishes after
   the last slice, instead of polling for a value it had already
   published, so a rejected image is no longer reported as a write
   timeout
 - rename the end_magic label to no_end, which is what it does, and
   correct the protocol comment: END is optional as the loader finalises
   on a 2 s timeout, the status register keeps the byte count while a
   chunk is programmed, and the half-bank switch does not clear DATA
   (found by Sashiko AI review)

v7:
 - reprobe from a single delayed work item instead of a kthread spawned
   by a workqueue kickoff; the kthread existed only to drop the module
   reference from core code, but its creation-failure path did the racy
   module_put() from module text anyway and could strand the driver
   bound with skip_teardown set. The collapsed form matches
   iwl_trans_reprobe_wk(), and a failed reprobe now leaves the device
   unbound like a failed probe
 - only signal END on a successful transfer; a failure leaves the loader
   mid-payload, where a STAT write is a byte count and END (0x3cc3) is
   read as one, risking a receive-counter underflow, so return the error
   and let the reprobe recover
 - add cond_resched() to the payload loop so a long transfer over a
   bit-banged MDIO bus under CONFIG_PREEMPT_NONE does not trip the
   soft-lockup detector
 - drop the cached firmware version and chip id on a failed flash so
   devlink dev info stops reporting the pre-flash version until the
   reprobe re-reads it
 - report the firmware version under DEVLINK_INFO_VERSION_GENERIC_FW
   instead of a bare "fw" string
 - correct the SB PDI header comment's SMDIO register map (page in MII
   reg 0x1f, register from the low nibble) and expand the note on why
   closing the shared conduit is safe

v6:
 - confirm the new firmware is running with mxl862xx_wait_ready() and
   lift the host block before reporting success, so devlink dev flash
   completes only once the update has taken effect instead of relying
   on the later reprobe to pick up the new version
 - run the post-flash reprobe from a kthread that drops the module
   reference with module_put_and_kthread_exit(), spawned from a
   workqueue kickoff, closing a use-after-free where a work item's
   trailing module_put() could return into module text a racing rmmod
   had already freed
 - jump to the end_magic teardown on every flash failure from the
   ready handshake onward, so an aborted transfer sends END and lets
   MCUboot reboot instead of leaving the loader waiting
 - poll the SB PDI status register with read_poll_timeout(), which
   evaluates the condition once more after the deadline, so a preempted
   poll cannot report a spurious -ETIMEDOUT
 - bail out of the periodic stats poll when the flash teardown has set
   WORK_STOPPED, closing a get_stats64() re-arm race
 - allocate the reprobe kickoff before disturbing the switch, so an
   -ENOMEM cannot leave it flashed but never reprobed with block_host
   and skip_teardown stuck set
 - omit asic.id/asic.rev when the CHIP ID read returned 0, instead of
   publishing a bogus "0000" for fwupd to match firmware against

v5:
 - report the numeric chip part number and version read from the
   static CHIP ID registers as "asic.id" and "asic.rev" instead of a
   model-name string, which does not belong in a devlink version
   identifier (Jakub Kicinski)
 - report the running firmware version as the "stored" version too,
   since the switch boots it from its own flash, so userspace can
   tell a flash-backed part from a flashless one by the presence of
   "stored" without a future API change
 - run the post-flash reprobe from a self-contained work item again
   instead of the v4 kernel thread, which tripped the hung-task
   watchdog while parked across the flash and returned -EINTR from
   kthread_create() when the devlink command was interrupted
 - re-read the new firmware version through the reprobe's fresh probe
   and drop the SYS_MISC_FW_VERSION exemption from the host block
 - raise the firmware command poll timeout so the FW_UPDATE command
   that reboots into MCUboot is not cut short
 - move the devlink documentation into its own patch

v4:
 - run the deferred reprobe from a kernel thread ending in
   module_put_and_kthread_exit() instead of a work item whose final
   module_put() raced against module unload
 - fail API read commands with -ENODEV after the update instead of
   faking success with an unfilled buffer, which sent
   port_fdb_dump() into an endless loop
 - keep block_host set across the post-update version query by
   exempting SYS_MISC_FW_VERSION instead of briefly lifting the
   block, and write the blocking flags under the MDIO bus lock
 - check the return value of every SB PDI control write; a failed
   address write during the half-bank switch could place the second
   half of the payload at the wrong flash offset undetected
 - report SMDIO write failures through one shared error path instead
   of per-site messages
 - initialise the progress notification deadline from jiffies so
   notifications are not suppressed on 32-bit shortly after boot
 - flush the switchdev deferred queue after closing the ports so the
   bridge's deferred STP DISABLED transitions reach the firmware
   while it is still running instead of failing with -EBUSY against
   the host block
 - treat -ENODEV as successful deletion in port_mdb_del() so the
   post-update teardown does not leave leftover host MDB entries
   behind for the DSA core to report

v3:
 - validate the image, including both CRCs, before closing any ports
   so a malformed file no longer triggers a flash and reprobe cycle
 - reject images whose declared payload sizes overflow when summed
   (check_add_overflow) or sum up to zero; the latter used to erase
   the flash without writing anything back
 - allocate the reprobe work item and take the module and device
   references before disturbing the switch instead of silently
   skipping the reprobe when the allocation fails afterwards
 - check block_host/skip_teardown under the MDIO bus lock to close
   the window where a command already past the check could reach the
   bus after the switch rebooted into MCUboot
 - prevent the stats poll work from being re-armed and cancel the
   CRC error work before the transfer
 - check the return value of SB PDI data word writes; control writes
   are verified by the subsequent status polls
 - report a per-model chip name from the OF match data as "asic.id"
   instead of the devicetree compatible string whose comma is
   awkward for userspace consumers (Andrew Lunn)
 - commit message: the conduit is only closed, not detached

v2:
 - factor out SB PDI slice flush and devlink status notification
   helpers, resolving checkpatch issues
 - use kzalloc_obj() (Manuel Ebner)
 - add kernel-doc for the new mxl862xx_priv members
 - trim comments and state the actual duration of a flash and reprobe
   cycle, just under a minute (Manuel Ebner)
 - reword commit message: split up run-on sentence, explain the
   dynamically allocated reprobe work item (Manuel Ebner), mention
   that closing the ports stops phylib polling (Andrew Lunn)

 drivers/net/dsa/mxl862xx/Kconfig         |   1 +
 drivers/net/dsa/mxl862xx/Makefile        |   2 +-
 drivers/net/dsa/mxl862xx/mxl862xx-api.h  |  10 +
 drivers/net/dsa/mxl862xx/mxl862xx-cmd.h  |   2 +
 drivers/net/dsa/mxl862xx/mxl862xx-fw.c   | 711 +++++++++++++++++++++++
 drivers/net/dsa/mxl862xx/mxl862xx-fw.h   |  18 +
 drivers/net/dsa/mxl862xx/mxl862xx-host.c |  31 +
 drivers/net/dsa/mxl862xx/mxl862xx-host.h |   1 +
 drivers/net/dsa/mxl862xx/mxl862xx.c      |  83 ++-
 drivers/net/dsa/mxl862xx/mxl862xx.h      |  33 +-
 10 files changed, 888 insertions(+), 4 deletions(-)
 create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.c
 create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.h

diff --git a/drivers/net/dsa/mxl862xx/Kconfig b/drivers/net/dsa/mxl862xx/Kconfig
index e51a67a3cf9bf..8bb524fe28d1f 100644
--- a/drivers/net/dsa/mxl862xx/Kconfig
+++ b/drivers/net/dsa/mxl862xx/Kconfig
@@ -3,6 +3,7 @@ config NET_DSA_MXL862
 	tristate "MaxLinear MxL862xx"
 	depends on NET_DSA
 	select CRC16
+	select CRC32
 	select NET_DSA_TAG_MXL_862XX
 	help
 	  This enables support for the MaxLinear MxL862xx switch family.
diff --git a/drivers/net/dsa/mxl862xx/Makefile b/drivers/net/dsa/mxl862xx/Makefile
index a7be0e6669dfa..bccac0d0f703f 100644
--- a/drivers/net/dsa/mxl862xx/Makefile
+++ b/drivers/net/dsa/mxl862xx/Makefile
@@ -1,3 +1,3 @@
 # SPDX-License-Identifier: GPL-2.0
 obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
-mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
+mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o mxl862xx-fw.o
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-api.h b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
index a180a5decffc0..6f771895984cb 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1224,6 +1224,16 @@ struct mxl862xx_sys_fw_image_version {
 	__le32 iv_build_num;
 } __packed;
 
+/**
+ * struct mxl862xx_sys_reg_rw - System register read/write
+ * @addr: 32-bit register address
+ * @val: register value
+ */
+struct mxl862xx_sys_reg_rw {
+	__le32 addr;
+	__le32 val;
+} __packed;
+
 /**
  * enum mxl862xx_port_type - Port Type
  * @MXL862XX_LOGICAL_PORT: Logical Port
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
index c87a955c13c48..a865425aa61e1 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -70,7 +70,9 @@
 #define INT_GPHY_READ			(GPY_GPY2XX_MAGIC + 0x1)
 #define INT_GPHY_WRITE			(GPY_GPY2XX_MAGIC + 0x2)
 
+#define SYS_MISC_FW_UPDATE		(SYS_MISC_MAGIC + 0x1)
 #define SYS_MISC_FW_VERSION		(SYS_MISC_MAGIC + 0x2)
+#define SYS_MISC_REG_RD			(SYS_MISC_MAGIC + 0x8)
 
 #define MXL862XX_XPCS_PCS_CONFIG	(MXL862XX_XPCS_MAGIC + 0x1)
 #define MXL862XX_XPCS_PCS_GET_STATE	(MXL862XX_XPCS_MAGIC + 0x2)
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
new file mode 100644
index 0000000000000..eae7048707870
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
@@ -0,0 +1,711 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Firmware flash and devlink support for MaxLinear MxL862xx
+ *
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ *
+ * SB PDI - firmware download interface over clause-22 SMDIO
+ * =========================================================
+ *
+ * The MxL862xx MCUboot loader accepts a firmware image through four "SB PDI"
+ * registers in the switch SMDIO register space. It runs whenever no WSP
+ * firmware is active: the normal firmware update enters it deliberately - the
+ * SYS_MISC_FW_UPDATE API command sets a sticky rescue bit and reboots into
+ * MCUboot - and the loader also stays here when the stored WSP firmware fails
+ * its boot-time integrity check. This driver drives the loader's 0xc55c
+ * "console" download path.
+ *
+ * SMDIO register access (mxl862xx_smdio_read/write):
+ *   MII reg 0x1f := (<sb_pdi_reg> & 0xfff0)   ; page latch
+ *   MII reg (<sb_pdi_reg> & 0x000f) := / => <u16 data>
+ * so CTRL/ADDR/DATA/STAT (0xe100..0xe103) are MII regs 0/1/2/3 of page
+ * 0xe100, not all reg 0x00.
+ *
+ * SB PDI registers (host name/addr  ->  MCU mailbox):
+ *   CTRL 0xe100 -> 0xc0938400   mode: RST=0x00  RD=0x01  WR=0x02
+ *   ADDR 0xe101 -> 0xc0938404   SB target word address (SB1 bank = 0x7800)
+ *   DATA 0xe102 -> 0xc0938408   16-bit data / reply word
+ *   STAT 0xe103 -> 0xc093840c   handshake: a magic (below) or a byte count
+ *
+ * STAT magics:
+ *   READY  0xc55c   loader idle in the console loop        (this driver)
+ *   START  0xf48f   host   -> begin download session
+ *   ACK    0xf490   loader -> START acknowledged (START + 1)
+ *   END    0x3cc3   host   -> finalise now (optional, see below)
+ *
+ * Console flash path (STAT=0xc55c) - mxl862xx_flash_firmware():
+ *
+ *   host                                   loader
+ *   ----                                   ------
+ *   reset (CTRL=ADDR=DATA=0)
+ *   read STAT ............................ 0xc55c   (READY, idle)
+ *   STAT := START(0xf48f)  -------------->
+ *                          <-------------- STAT = 0xf490 (ACK)
+ *   CTRL := WR
+ *   DATA := hdr[0..9]  (20-byte header: type,size1,crc1,size2,crc2)
+ *   reset; STAT := 20 (header len)  -----> parse hdr; r_remain=size1+size2;
+ *                                          ERASE target region(s)
+ *                          <-------------- STAT=21 (len+1), then STAT=0
+ *                                          (erased)
+ *   -- payload, streamed in slices: --
+ *   CTRL := WR
+ *   DATA := word x N ...
+ *     at word 16384: CTRL:=RST; ADDR:=0x7800; CTRL:=WR  (half-bank -> SB1)
+ *     at word 32760: flush slice:
+ *        reset; STAT := <bytes_this_slice> ---> r_remain -= bytes; program
+ *                          <------------------- STAT=0  (ready for next slice)
+ *   ... repeat until the whole payload is sent ...
+ *                          <------------------- STAT=0  image verified
+ *                                               (STAT=1: image rejected)
+ *   STAT := END(0x3cc3)  ---------------------> finalise and boot
+ *
+ * The r_remain == 0 rule (critical):
+ *   Every host STAT write in the payload phase is a byte count; the loader
+ *   does r_remain -= count and stays in the receive loop while r_remain != 0.
+ *   It leaves the loop ONLY when r_remain hits EXACTLY 0, and a count larger
+ *   than r_remain underflows the 32-bit counter and wedges the loader until a
+ *   power cycle. Having left it, the loader verifies the image, publishes the
+ *   verdict in STAT (0 good, 1 rejected) and waits 2 s for END before
+ *   finalising regardless -- clearing its rescue-enable bit so boot_go boots
+ *   the new image -- so END only saves that wait. Hence:
+ *     - never send a slice/chunk count larger than what is outstanding;
+ *     - a STAT write is a command only once the loader has left the loop;
+ *     - the loader leaves the count in STAT while it programs the chunk, so
+ *       a lingering count does not distinguish "busy" from "verdict";
+ *     - STAT is a 16-bit register, so a slice holds at most 65535 bytes,
+ *       which is what bounds MXL862XX_FW_BANK_SLICE.
+ */
+
+#include <linux/crc32.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/iopoll.h>
+#include <linux/netdevice.h>
+#include <linux/overflow.h>
+#include <linux/rtnetlink.h>
+#include <linux/sched.h>
+#include <linux/workqueue.h>
+#include <net/dsa.h>
+#include <net/switchdev.h>
+
+#include "mxl862xx.h"
+#include "mxl862xx-api.h"
+#include "mxl862xx-cmd.h"
+#include "mxl862xx-fw.h"
+#include "mxl862xx-host.h"
+
+/* SB PDI registers (clause-22 SMDIO address space) */
+#define MXL862XX_SB_PDI_CTRL		0xe100
+#define MXL862XX_SB_PDI_ADDR		0xe101
+#define MXL862XX_SB_PDI_DATA		0xe102
+#define MXL862XX_SB_PDI_STAT		0xe103
+
+/* SB PDI CTRL modes */
+#define MXL862XX_SB_PDI_CTRL_RST	0x00
+#define MXL862XX_SB_PDI_CTRL_WR		0x02
+
+/* SB PDI handshake magic (published/consumed via STAT) */
+#define MXL862XX_SB_PDI_READY		0xc55c	/* loader idle, console loop */
+#define MXL862XX_SB_PDI_START		0xf48f
+#define MXL862XX_SB_PDI_END		0x3cc3
+
+/* Image verification verdict published in STAT once the receive loop ends */
+#define MXL862XX_SB_PDI_VERIFY_OK	0
+#define MXL862XX_SB_PDI_VERIFY_BAD	1
+
+/* Firmware transfer geometry */
+#define MXL862XX_FW_HDR_SIZE		20
+#define MXL862XX_FW_BANK_HALF		16384	/* words per half-bank */
+#define MXL862XX_FW_BANK_SLICE		32760	/* words per full slice, 65520 B */
+#define MXL862XX_FW_SB1_ADDR		0x7800	/* SB1 word address */
+
+/* Timeouts (generous upper bounds) */
+#define MXL862XX_FW_READY_TIMEOUT_MS	3000
+#define MXL862XX_FW_ACK_TIMEOUT_MS	5000
+#define MXL862XX_FW_ERASE_TIMEOUT_MS	300000
+#define MXL862XX_FW_WRITE_TIMEOUT_MS	60000
+/* Covers the loader's END wait, verification and the reset into READY */
+#define MXL862XX_SB_PDI_VERIFY_MS	15000
+
+/* Delays spent in full: the reboot into the new image, and the re-probe */
+#define MXL862XX_FW_REBOOT_DELAY_MS	5000
+#define MXL862XX_FW_REPROBE_DELAY_MS	500
+
+static int mxl862xx_sb_pdi_reset(struct mxl862xx_priv *priv)
+{
+	int ret;
+
+	/* CTRL selects RST mode; ADDR and DATA are cleared to 0. */
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+				   MXL862XX_SB_PDI_CTRL_RST);
+	if (ret < 0)
+		return ret;
+
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR, 0x0000);
+	if (ret < 0)
+		return ret;
+
+	return mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, 0x0000);
+}
+
+static int mxl862xx_sb_pdi_poll_stat(struct mxl862xx_priv *priv, u16 expected,
+				     unsigned long timeout_ms)
+{
+	int ret, val;
+
+	ret = read_poll_timeout(mxl862xx_smdio_read, val,
+				val < 0 || (u16)val == expected,
+				10000, timeout_ms * 1000, false,
+				priv, MXL862XX_SB_PDI_STAT);
+	if (val < 0)
+		return val;
+	return ret;
+}
+
+static int mxl862xx_sb_pdi_flush_slice(struct mxl862xx_priv *priv,
+				       u32 data_written)
+{
+	int ret;
+
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		return ret;
+
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, data_written);
+	if (ret < 0)
+		return ret;
+
+	return mxl862xx_sb_pdi_poll_stat(priv, 0,
+					 MXL862XX_FW_WRITE_TIMEOUT_MS);
+}
+
+/* Flush the last slice, which ends the receive loop: the loader verifies the
+ * image and replaces the count in STAT with its verdict, so wait for the count
+ * to go rather than for a fixed value.
+ */
+static int mxl862xx_sb_pdi_flush_last(struct mxl862xx_priv *priv,
+				      u32 data_written)
+{
+	int ret, val;
+
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		return ret;
+
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, data_written);
+	if (ret < 0)
+		return ret;
+
+	ret = read_poll_timeout(mxl862xx_smdio_read, val,
+				val < 0 || (u16)val != (u16)data_written,
+				10000, (MXL862XX_FW_WRITE_TIMEOUT_MS +
+					MXL862XX_SB_PDI_VERIFY_MS) * 1000,
+				false, priv, MXL862XX_SB_PDI_STAT);
+	if (val < 0)
+		return val;
+
+	if (!ret && (u16)val == MXL862XX_SB_PDI_VERIFY_OK)
+		return 0;
+
+	/* A final count of 1 is indistinguishable from the reject verdict, so
+	 * a timeout still holding it lands here too.
+	 */
+	if ((u16)val == MXL862XX_SB_PDI_VERIFY_BAD) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: loader rejected the image\n");
+		return -EBADMSG;
+	}
+
+	return ret ? ret : -EPROTO;
+}
+
+static void mxl862xx_flash_notify(struct devlink *dl, const char *status,
+				  u32 done, u32 total)
+{
+	devlink_flash_update_status_notify(dl, status, NULL, done, total);
+}
+
+/* MCUboot firmware image header */
+struct mxl862xx_fw_hdr {
+	__le32 image_type;
+	__le32 image_size_1;
+	__le32 image_checksum_1;
+	__le32 image_size_2;
+	__le32 image_checksum_2;
+} __packed;
+
+static int mxl862xx_flash_validate(struct mxl862xx_priv *priv,
+				   const struct firmware *fw,
+				   u32 *payload_size)
+{
+	const struct mxl862xx_fw_hdr *hdr;
+	u32 size1, size2, total;
+	const u8 *payload;
+	u32 crc;
+
+	if (fw->size < MXL862XX_FW_HDR_SIZE)
+		return -EINVAL;
+
+	hdr = (const struct mxl862xx_fw_hdr *)fw->data;
+	payload = fw->data + MXL862XX_FW_HDR_SIZE;
+	size1 = le32_to_cpu(hdr->image_size_1);
+	size2 = le32_to_cpu(hdr->image_size_2);
+
+	if (check_add_overflow(size1, size2, &total) ||
+	    total > fw->size - MXL862XX_FW_HDR_SIZE) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: firmware file too small for declared size\n");
+		return -EINVAL;
+	}
+
+	if (!total) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: firmware file with empty payload\n");
+		return -EINVAL;
+	}
+
+	if (size1) {
+		crc = ~crc32_le(~0U, payload, size1);
+		if (crc != le32_to_cpu(hdr->image_checksum_1)) {
+			dev_err(&priv->mdiodev->dev,
+				"flash: image 1 CRC mismatch (got %08x, expected %08x)\n",
+				crc, le32_to_cpu(hdr->image_checksum_1));
+			return -EINVAL;
+		}
+	}
+
+	if (size2) {
+		crc = ~crc32_le(~0U, payload + size1, size2);
+		if (crc != le32_to_cpu(hdr->image_checksum_2)) {
+			dev_err(&priv->mdiodev->dev,
+				"flash: image 2 CRC mismatch (got %08x, expected %08x)\n",
+				crc, le32_to_cpu(hdr->image_checksum_2));
+			return -EINVAL;
+		}
+	}
+
+	*payload_size = total;
+
+	return 0;
+}
+
+static int mxl862xx_flash_firmware(struct mxl862xx_priv *priv,
+				   const struct firmware *fw,
+				   u32 payload_size, struct devlink *dl)
+{
+	const u8 *payload = fw->data + MXL862XX_FW_HDR_SIZE;
+	u32 word_idx = 0, data_written = 0, idx = 0;
+	unsigned long next_notify = jiffies - 1;
+	u16 word, fdata;
+	int ret, val, i;
+
+	/* Step 1: reboot the firmware into MCUboot rescue mode */
+	ret = mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0,
+				false, false);
+	if (ret) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: FW_UPDATE command failed: %pe\n",
+			ERR_PTR(ret));
+		return ret;
+	}
+
+	/* Step 2: wait for bootloader ready */
+	mxl862xx_flash_notify(dl, "Waiting for bootloader", 0, 0);
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		goto write_err;
+
+	/* Failures from here on end up at no_end, which returns the error
+	 * without signalling END -- see there.
+	 */
+	ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_READY,
+					MXL862XX_FW_READY_TIMEOUT_MS);
+	if (ret) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: bootloader not ready: %pe\n", ERR_PTR(ret));
+		goto no_end;
+	}
+
+	/* Step 3: start handshake */
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+				   MXL862XX_SB_PDI_START);
+	if (ret < 0)
+		goto write_err;
+
+	ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_START + 1,
+					MXL862XX_FW_ACK_TIMEOUT_MS);
+	if (ret) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: start handshake failed: %pe\n", ERR_PTR(ret));
+		goto no_end;
+	}
+
+	/* Step 4: transfer image header */
+	mxl862xx_flash_notify(dl, "Erasing flash", 0, 0);
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+				   MXL862XX_SB_PDI_CTRL_WR);
+	if (ret < 0)
+		goto write_err;
+
+	for (i = 0; i < MXL862XX_FW_HDR_SIZE / 2; i++) {
+		word = fw->data[i * 2] |
+		       ((u16)fw->data[i * 2 + 1] << 8);
+		ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, word);
+		if (ret < 0)
+			goto write_err;
+	}
+
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		goto write_err;
+
+	/* the byte count in STAT triggers the erase */
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+				   MXL862XX_FW_HDR_SIZE);
+	if (ret < 0)
+		goto write_err;
+
+	/* The ACK is the count + 1, replaced by 0 as soon as the erase is done,
+	 * so wait for the count to go: a short erase retires the ACK between
+	 * two polls.
+	 */
+	ret = read_poll_timeout(mxl862xx_smdio_read, val,
+				val < 0 || (u16)val != MXL862XX_FW_HDR_SIZE,
+				10000, MXL862XX_FW_ACK_TIMEOUT_MS * 1000, false,
+				priv, MXL862XX_SB_PDI_STAT);
+	if (val < 0)
+		ret = val;
+	if (!ret && (u16)val != MXL862XX_FW_HDR_SIZE + 1 && (u16)val != 0)
+		ret = -EPROTO;
+	if (ret) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: header ACK failed: %pe\n", ERR_PTR(ret));
+		goto no_end;
+	}
+
+	/* Step 5: wait for erase to complete */
+	ret = mxl862xx_sb_pdi_poll_stat(priv, 0,
+					MXL862XX_FW_ERASE_TIMEOUT_MS);
+	if (ret) {
+		dev_err(&priv->mdiodev->dev,
+			"flash: erase failed: %pe\n", ERR_PTR(ret));
+		goto no_end;
+	}
+
+	/* Step 6: transfer payload */
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+				   MXL862XX_SB_PDI_CTRL_WR);
+	if (ret < 0)
+		goto write_err;
+
+	while (idx < payload_size) {
+		cond_resched();
+		if (idx + 1 < payload_size) {
+			fdata = payload[idx] |
+				((u16)payload[idx + 1] << 8);
+			idx += 2;
+			data_written += 2;
+		} else {
+			fdata = payload[idx];
+			idx++;
+			data_written++;
+		}
+
+		ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, fdata);
+		if (ret < 0)
+			goto write_err;
+		word_idx++;
+
+		if (idx >= payload_size) {
+			ret = mxl862xx_sb_pdi_flush_last(priv, data_written);
+			break;
+		}
+
+		/* Half-bank boundary: switch to SB1 address */
+		if (word_idx == MXL862XX_FW_BANK_HALF) {
+			ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+						   MXL862XX_SB_PDI_CTRL_RST);
+			if (ret < 0)
+				goto write_err;
+
+			ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
+						   MXL862XX_FW_SB1_ADDR);
+			if (ret < 0)
+				goto write_err;
+
+			ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+						   MXL862XX_SB_PDI_CTRL_WR);
+			if (ret < 0)
+				goto write_err;
+		} else if (word_idx >= MXL862XX_FW_BANK_SLICE) {
+			ret = mxl862xx_sb_pdi_flush_slice(priv, data_written);
+			if (ret) {
+				dev_err(&priv->mdiodev->dev,
+					"flash: write failed at %u/%u: %pe\n",
+					idx, payload_size, ERR_PTR(ret));
+				goto no_end;
+			}
+			word_idx = 0;
+			data_written = 0;
+			ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+						   MXL862XX_SB_PDI_CTRL_WR);
+			if (ret < 0)
+				goto write_err;
+
+			if (time_after(jiffies, next_notify)) {
+				mxl862xx_flash_notify(dl, "Flashing", idx,
+						      payload_size);
+				next_notify = jiffies + msecs_to_jiffies(500);
+			}
+		}
+	}
+
+	if (ret) {
+		/* a rejected image (-EBADMSG) has been logged where it was seen */
+		if (ret != -EBADMSG)
+			dev_err(&priv->mdiodev->dev,
+				"flash: final slice failed: %pe\n", ERR_PTR(ret));
+		goto no_end;
+	}
+
+	mxl862xx_flash_notify(dl, "Flashing", payload_size, payload_size);
+
+	/* Success: the loader has left the receive loop at r_remain == 0 and
+	 * verified the image, so END(0x3cc3) is a finalise/boot request rather
+	 * than a byte count. Signal it here -- and only here -- to boot the new
+	 * image without waiting out the loader's 2 s END timeout.
+	 */
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+				   MXL862XX_SB_PDI_END);
+	if (ret < 0)
+		dev_warn(&priv->mdiodev->dev,
+			 "flash: END signalling failed, waiting the loader out: %pe\n",
+			 ERR_PTR(ret));
+
+	msleep(MXL862XX_FW_REBOOT_DELAY_MS);
+	return 0;
+
+write_err:
+	dev_err(&priv->mdiodev->dev, "flash: SMDIO write failed: %pe\n",
+		ERR_PTR(ret));
+no_end:
+	/* A failure may leave the loader mid transfer, where a STAT write is a
+	 * byte count and END would be misread as one, risking a receive-counter
+	 * underflow, so END is never signalled here. The caller reprobes.
+	 */
+	return ret;
+}
+
+int mxl862xx_devlink_info_get(struct dsa_switch *ds,
+			      struct devlink_info_req *req,
+			      struct netlink_ext_ack *extack)
+{
+	struct mxl862xx_priv *priv = ds->priv;
+	char buf[16];
+	int ret;
+
+	/* A 0 part number means the CHIP ID read failed, the part is unfused
+	 * or a flash failed; omit it rather than publish a bogus "0000" that
+	 * fwupd would match firmware against, it then falls back to the
+	 * driver name.
+	 */
+	if (priv->asic_id) {
+		snprintf(buf, sizeof(buf), "%04X", priv->asic_id);
+		ret = devlink_info_version_fixed_put(req,
+						     DEVLINK_INFO_VERSION_GENERIC_ASIC_ID,
+						     buf);
+		if (ret)
+			return ret;
+
+		snprintf(buf, sizeof(buf), "%u", priv->asic_rev);
+		ret = devlink_info_version_fixed_put(req,
+						     DEVLINK_INFO_VERSION_GENERIC_ASIC_REV,
+						     buf);
+		if (ret)
+			return ret;
+	}
+
+	/* An all-zero version is the cache a failed flash left behind, not a
+	 * released firmware; omit it like the part number above.
+	 */
+	if (!priv->fw_version.major && !priv->fw_version.minor &&
+	    !priv->fw_version.revision)
+		return 0;
+
+	snprintf(buf, sizeof(buf), "%u.%u.%u",
+		 priv->fw_version.major, priv->fw_version.minor,
+		 priv->fw_version.revision);
+
+	ret = devlink_info_version_running_put(req,
+			DEVLINK_INFO_VERSION_GENERIC_FW, buf);
+	if (ret)
+		return ret;
+
+	/* boots this image from its own flash: stored == running */
+	return devlink_info_version_stored_put(req,
+			DEVLINK_INFO_VERSION_GENERIC_FW, buf);
+}
+
+int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
+				  struct devlink_flash_update_params *params,
+				  struct netlink_ext_ack *extack)
+{
+	struct mxl862xx_priv *priv = ds->priv;
+	struct dsa_port *dp;
+	u32 payload_size;
+	int ret, err, i;
+
+	/* devlink registers the switch before the DSA core creates its user
+	 * ports, which it publishes without rtnl held.
+	 */
+	if (!ds->dst->setup) {
+		NL_SET_ERR_MSG_MOD(extack, "switch tree is not set up");
+		return -EBUSY;
+	}
+
+	/* Written under the instance lock this call is holding. */
+	if (priv->shutting_down) {
+		NL_SET_ERR_MSG_MOD(extack, "device is shutting down");
+		return -ENODEV;
+	}
+
+	/* A previous flash has short-circuited the firmware API until the
+	 * reprobe or a rebind, so the raw SB PDI writes below would run
+	 * against a switch this driver no longer tracks.
+	 */
+	if (priv->skip_teardown) {
+		NL_SET_ERR_MSG_MOD(extack,
+				   "a previous flash left the driver awaiting reinitialisation");
+		return -EBUSY;
+	}
+
+	ret = mxl862xx_flash_validate(priv, params->fw, &payload_size);
+	if (ret) {
+		NL_SET_ERR_MSG_MOD(extack, "firmware image validation failed");
+		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);
+
+	/* Close ports while the firmware is still alive so the DSA core's
+	 * MDB/FDB tracking is drained, and detach user ports so userspace
+	 * cannot reopen them during the flash. The conduit is only closed,
+	 * not detached: it belongs to the MAC driver. This driver binds a
+	 * single switch with a direct host link and no cascade ports, so the
+	 * conduit serves only this switch, and flashing it reboots the switch,
+	 * which takes the tree down regardless.
+	 */
+	rtnl_lock();
+	dsa_switch_for_each_user_port(dp, ds) {
+		if (dp->user) {
+			dev_close(dp->user);
+			netif_device_detach(dp->user);
+		}
+	}
+	dsa_switch_for_each_cpu_port(dp, ds)
+		dev_close(dp->conduit);
+	/* The bridge defers the STP state changes triggered by closing
+	 * the ports; let them reach the firmware while it is still alive.
+	 */
+	switchdev_deferred_process();
+	rtnl_unlock();
+	/* The bridge's host address deletions run on the DSA workqueue. */
+	dsa_flush_workqueue();
+
+	mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+	priv->block_host = true;
+	mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
+	disable_delayed_work_sync(&priv->stats_work);
+	cancel_work_sync(&priv->crc_err_work);
+	for (i = 0; i < ds->num_ports; i++)
+		cancel_work_sync(&priv->ports[i].host_flood_work);
+
+	ret = mxl862xx_flash_firmware(priv, params->fw, payload_size,
+				      ds->devlink);
+	if (ret == -EBADMSG)
+		NL_SET_ERR_MSG_MOD(extack, "loader rejected the image");
+	else if (ret)
+		NL_SET_ERR_MSG_MOD(extack, "firmware update failed");
+
+	if (!ret) {
+		mutex_lock_nested(&priv->mdiodev->bus->mdio_lock,
+				  MDIO_MUTEX_NESTED);
+		/* Keep block_host set so host writes stay blocked, but let the
+		 * readiness poll below read the freshly booted firmware.
+		 */
+		priv->flash_owner = current;
+		mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+		/* Refresh the cached versions so the flash update only
+		 * completes once the new firmware is confirmed running and
+		 * devlink dev info reports it. Must happen before setting
+		 * skip_teardown, which discards all firmware API reads.
+		 */
+		ret = mxl862xx_wait_ready(ds);
+		if (ret)
+			NL_SET_ERR_MSG_MOD(extack,
+					   "new firmware did not become ready");
+	}
+
+	if (ret) {
+		/* The loader may hold an erased or partly written image; drop
+		 * the cached identity so devlink dev info stops reporting the
+		 * pre-flash version until the reprobe re-reads the truth.
+		 */
+		memset(&priv->fw_version, 0, sizeof(priv->fw_version));
+		priv->asic_id = 0;
+		priv->asic_rev = 0;
+	}
+
+	mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+	priv->flash_owner = NULL;
+	priv->block_host = false;
+	priv->skip_teardown = true;
+	mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+	/* Reinitialise through a deferred re-probe: remove() runs with
+	 * skip_teardown set, then a fresh probe() starts against whatever
+	 * the switch now runs. The core drops the re-probe if the device is
+	 * unbound or deleted before it fires.
+	 */
+	err = device_schedule_reprobe(ds->dev, MXL862XX_FW_REPROBE_DELAY_MS);
+	/* -EINVAL is a device being deleted, whose teardown takes over. */
+	if (err == -EINVAL)
+		err = 0;
+	if (err)
+		dev_err(ds->dev,
+			"flash: re-probe could not be scheduled (%pe); unbind and rebind to reinitialise\n",
+			ERR_PTR(err));
+	if (err && !ret)
+		NL_SET_ERR_MSG_MOD(extack,
+				   "new firmware is running, the driver needs rebinding");
+
+	return ret ? ret : err;
+}
+
+/* The devlink core holds the instance lock across a flash, so taking it here
+ * waits for a transfer in flight instead of cutting the image in half, and
+ * bars one that has not started yet.
+ */
+void mxl862xx_flash_shutdown(struct dsa_switch *ds)
+{
+	struct mxl862xx_priv *priv = ds->priv;
+
+	if (!ds->devlink)
+		return;
+
+	/* A flash holds the instance lock for its whole run. Announce the
+	 * wait so the delay is not mistaken for a hang.
+	 */
+	if (!devl_trylock(ds->devlink)) {
+		dev_info(ds->dev,
+			 "firmware update in progress, waiting for it to finish\n");
+		devl_lock(ds->devlink);
+	}
+	priv->shutting_down = true;
+	devl_unlock(ds->devlink);
+}
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
new file mode 100644
index 0000000000000..15ed3a46bcfe2
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_FW_H
+#define __MXL862XX_FW_H
+
+#include <net/dsa.h>
+
+struct mxl862xx_priv;
+
+int mxl862xx_devlink_info_get(struct dsa_switch *ds,
+			      struct devlink_info_req *req,
+			      struct netlink_ext_ack *extack);
+int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
+				  struct devlink_flash_update_params *params,
+				  struct netlink_ext_ack *extack);
+void mxl862xx_flash_shutdown(struct dsa_switch *ds);
+
+#endif /* __MXL862XX_FW_H */
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
index 6fc04a38675ae..b229c4512b82c 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -12,9 +12,11 @@
 #include <linux/crc16.h>
 #include <linux/iopoll.h>
 #include <linux/limits.h>
+#include <linux/sched.h>
 #include <linux/unaligned.h>
 #include <net/dsa.h>
 #include "mxl862xx.h"
+#include "mxl862xx-cmd.h"
 #include "mxl862xx-host.h"
 
 #define CTRL_BUSY_MASK			BIT(15)
@@ -327,6 +329,17 @@ static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
 	return ret;
 }
 
+bool mxl862xx_api_gated(struct mxl862xx_priv *priv)
+{
+	bool gated;
+
+	mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+	gated = priv->block_host || priv->skip_teardown;
+	mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+	return gated;
+}
+
 int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
 		      u16 size, bool read, bool quiet)
 {
@@ -340,6 +353,24 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
 
 	mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
 
+	if (priv->skip_teardown) {
+		ret = read ? -ENODEV : 0;
+		goto out;
+	}
+
+	/* During the post-flash readiness poll block_host stays set, but the
+	 * flash path's own firmware version reads must reach the new image;
+	 * host writes stay blocked so stale resource IDs cannot corrupt it.
+	 * A blocked write reports success: the reprobe discards the switch
+	 * configuration anyway, and a bridge tearing down over a flash must
+	 * not see port_vlan_del() fail, which leaks its VLAN group.
+	 */
+	if (priv->block_host && cmd != SYS_MISC_FW_UPDATE &&
+	    !(read && priv->flash_owner == current)) {
+		ret = read ? -EBUSY : 0;
+		goto out;
+	}
+
 	max = (size + 1) / 2;
 
 	ret = mxl862xx_busy_wait(priv);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.h b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
index 4e054c6e4c0e4..8b3a41de065ff 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
@@ -7,6 +7,7 @@
 
 void mxl862xx_host_init(struct mxl862xx_priv *priv);
 void mxl862xx_host_shutdown(struct mxl862xx_priv *priv);
+bool mxl862xx_api_gated(struct mxl862xx_priv *priv);
 int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
 		      bool read, bool quiet);
 
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c
index e05ad52cd297e..e6f710c676e65 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -21,6 +21,7 @@
 #include "mxl862xx.h"
 #include "mxl862xx-api.h"
 #include "mxl862xx-cmd.h"
+#include "mxl862xx-fw.h"
 #include "mxl862xx-host.h"
 #include "mxl862xx-phylink.h"
 
@@ -71,6 +72,13 @@ static const struct ethtool_rmon_hist_range mxl862xx_rmon_ranges[] = {
 #define MXL862XX_READY_TIMEOUT_MS	10000
 #define MXL862XX_READY_POLL_MS		100
 
+/* Chip ID registers, read via SYS_MISC_REG_RD */
+#define MXL862XX_CHIPID_L		0xc0d28884
+#define MXL862XX_CHIPID_M		0xc0d28888
+#define MXL862XX_CHIPID_L_PNUML		GENMASK(15, 12)
+#define MXL862XX_CHIPID_M_PNUMM		GENMASK(11, 0)
+#define MXL862XX_CHIPID_M_VERSION	GENMASK(14, 12)
+
 #define MXL862XX_TCM_INST_SEL		0xe00
 #define MXL862XX_TCM_CBS		0xe12
 #define MXL862XX_TCM_EBS		0xe13
@@ -222,7 +230,48 @@ static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int addr,
 	return mxl862xx_phy_write_mmd(bus->priv, addr, devadd, regnum, val);
 }
 
-static int mxl862xx_wait_ready(struct dsa_switch *ds)
+/* The CHIP ID registers are only readable through the firmware mailbox, so
+ * the values are cached here and stay zero while no firmware answers.
+ */
+static int mxl862xx_read_chip_id(struct mxl862xx_priv *priv)
+{
+	struct mxl862xx_sys_reg_rw reg = {};
+	u16 chipid_l, chipid_m;
+	int ret;
+
+	priv->asic_id = 0;
+	priv->asic_rev = 0;
+
+	reg.addr = cpu_to_le32(MXL862XX_CHIPID_L);
+	ret = MXL862XX_API_READ(priv, SYS_MISC_REG_RD, reg);
+	if (ret)
+		return ret;
+	chipid_l = le32_to_cpu(reg.val);
+
+	reg.addr = cpu_to_le32(MXL862XX_CHIPID_M);
+	ret = MXL862XX_API_READ(priv, SYS_MISC_REG_RD, reg);
+	if (ret)
+		return ret;
+	chipid_m = le32_to_cpu(reg.val);
+
+	priv->asic_id = FIELD_GET(MXL862XX_CHIPID_L_PNUML, chipid_l) |
+			FIELD_GET(MXL862XX_CHIPID_M_PNUMM, chipid_m) << 4;
+	priv->asic_rev = FIELD_GET(MXL862XX_CHIPID_M_VERSION, chipid_m);
+
+	return 0;
+}
+
+/**
+ * mxl862xx_wait_ready - wait for the switch firmware to become operational
+ * @ds: DSA switch instance
+ *
+ * Poll the firmware until it reports its version and accepts
+ * configuration commands, then cache the firmware version, and the chip
+ * ID when it can be read. Takes at least two seconds.
+ *
+ * Return: 0 on success or a negative error code.
+ */
+int mxl862xx_wait_ready(struct dsa_switch *ds)
 {
 	struct mxl862xx_sys_fw_image_version ver = {};
 	unsigned long start = jiffies, timeout;
@@ -254,6 +303,11 @@ static int mxl862xx_wait_ready(struct dsa_switch *ds)
 		priv->fw_version.major = ver.iv_major;
 		priv->fw_version.minor = ver.iv_minor;
 		priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
+
+		ret = mxl862xx_read_chip_id(priv);
+		if (ret)
+			dev_warn(ds->dev, "failed to read chip ID: %pe\n",
+				 ERR_PTR(ret));
 		return 0;
 
 not_ready_yet:
@@ -1547,6 +1601,12 @@ static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port,
 	qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
 
 	ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
+	/* A flash blocks the API (-EBUSY) and its teardown drops the MAC
+	 * table (-ENODEV); there is then nothing to program, and the reprobe
+	 * starts from a fresh configuration. See mxl862xx_port_mdb_del().
+	 */
+	if ((ret == -EBUSY || ret == -ENODEV) && mxl862xx_api_gated(priv))
+		return 0;
 	if (ret)
 		return ret;
 
@@ -1584,6 +1644,12 @@ static int mxl862xx_port_mdb_del(struct dsa_switch *ds, int port,
 	ether_addr_copy(qparam.mac, mdb->addr);
 
 	ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
+	/* A flash blocks the API (-EBUSY) and its teardown drops the MAC
+	 * table (-ENODEV); a delete then has nothing to do. Outside these,
+	 * both are bus errors and must be reported.
+	 */
+	if ((ret == -EBUSY || ret == -ENODEV) && mxl862xx_api_gated(priv))
+		return 0;
 	if (ret)
 		return ret;
 
@@ -1818,6 +1884,11 @@ static void mxl862xx_get_ethtool_stats(struct dsa_switch *ds, int port,
 	void *field;
 
 	ret = mxl862xx_read_rmon(ds, port, &cnt);
+	/* The flash gates refuse with these, and ethtool -S needs no privilege
+	 * to reach them, so they must not log.
+	 */
+	if ((ret == -EBUSY || ret == -ENODEV) && mxl862xx_api_gated(ds->priv))
+		return;
 	if (ret) {
 		dev_err(ds->dev, "failed to read RMON stats on port %d\n", port);
 		return;
@@ -2097,6 +2168,8 @@ static const struct dsa_switch_ops mxl862xx_switch_ops = {
 	.get_pause_stats = mxl862xx_get_pause_stats,
 	.get_rmon_stats = mxl862xx_get_rmon_stats,
 	.get_stats64 = mxl862xx_get_stats64,
+	.devlink_info_get = mxl862xx_devlink_info_get,
+	.devlink_flash_update = mxl862xx_devlink_flash_update,
 };
 
 static int mxl862xx_probe(struct mdio_device *mdiodev)
@@ -2161,6 +2234,12 @@ static void mxl862xx_remove(struct mdio_device *mdiodev)
 
 	priv = ds->priv;
 
+	/* Wait out a flash in flight and bar a new one before the DSA core
+	 * frees the user netdevs; the devlink instance lock does not cover
+	 * that free, which dsa_unregister_switch() reaches first.
+	 */
+	mxl862xx_flash_shutdown(ds);
+
 	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
 
 	dsa_unregister_switch(ds);
@@ -2187,6 +2266,8 @@ static void mxl862xx_shutdown(struct mdio_device *mdiodev)
 
 	priv = ds->priv;
 
+	mxl862xx_flash_shutdown(ds);
+
 	dsa_switch_shutdown(ds);
 
 	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h
index 432a5f3f2e08e..7d6bee9fa33d7 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -298,11 +298,18 @@ struct mxl862xx_fw_version {
  *                      before CRC-triggered shutdown and cleared after;
  *                      %MXL862XX_FLAG_WORK_STOPPED is set before cancelling
  *                      stats_work to prevent rescheduling during teardown
+ *                      and a flash
  * @drop_meter:         index of the single shared zero-rate firmware meter
  *                      used to unconditionally drop traffic (used to block
  *                      flooding)
- * @fw_version:         cached firmware version, populated at probe and
- *                      compared with MXL862XX_FW_VER_MIN()
+ * @fw_version:         cached firmware version, compared with
+ *                      MXL862XX_FW_VER_MIN(); refreshed by
+ *                      mxl862xx_wait_ready() after a flash and cleared by
+ *                      a failed one until the reprobe
+ * @asic_id:            chip part number read from the CHIP ID registers,
+ *                      reported as the devlink "asic.id" fixed version
+ * @asic_rev:           chip version read from the CHIP ID registers,
+ *                      reported as the devlink "asic.rev" fixed version
  * @serdes_ports:       SerDes interfaces incl. sub-interfaces in case of
  *                      10G_QXGMII or QSGMII
  * @serdes_refcount:    per-XPCS count of sub-ports enabled by phylink;
@@ -319,6 +326,20 @@ struct mxl862xx_fw_version {
  * @evlan_ingress_size: per-port ingress Extended VLAN block size
  * @evlan_egress_size:  per-port egress Extended VLAN block size
  * @vf_block_size:      per-port VLAN Filter block size
+ * @block_host:         during a firmware flash, a host firmware read fails
+ *                      with -EBUSY and a write reports success without
+ *                      touching the bus, so a teardown racing the flash
+ *                      does not fail; FW_UPDATE and the flash owner's own
+ *                      reads still reach the bus
+ * @flash_owner:        task running the post-flash readiness poll; only its
+ *                      own firmware reads pass block_host
+ * @skip_teardown:      from the end of a flash until the reprobe, or a rebind
+ *                      when the reprobe could not be scheduled, a host
+ *                      firmware read fails with -ENODEV and a write reports
+ *                      success without touching the bus
+ * @shutting_down:      set under the devlink instance lock once ->shutdown()
+ *                      or .remove() has begun, so no flash starts while the
+ *                      switch is going away
  * @stats_work:         periodic work item that polls RMON hardware counters
  *                      and accumulates them into 64-bit per-port stats
  */
@@ -329,6 +350,8 @@ struct mxl862xx_priv {
 	unsigned long flags;
 	u16 drop_meter;
 	struct mxl862xx_fw_version fw_version;
+	u16 asic_id;
+	u8 asic_rev;
 	struct mxl862xx_pcs serdes_ports[8];
 	int serdes_refcount[2];
 	struct mutex serdes_lock;
@@ -337,7 +360,13 @@ struct mxl862xx_priv {
 	u16 evlan_ingress_size;
 	u16 evlan_egress_size;
 	u16 vf_block_size;
+	struct task_struct *flash_owner;
+	bool block_host;
+	bool skip_teardown;
+	bool shutting_down;
 	struct delayed_work stats_work;
 };
 
+int mxl862xx_wait_ready(struct dsa_switch *ds);
+
 #endif /* __MXL862XX_H */
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH net-next v19 5/6] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode
  2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
                   ` (3 preceding siblings ...)
  2026-10-03 15:52 ` [PATCH net-next v19 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
@ 2026-10-03 15:53 ` Daniel Golle
  2026-10-03 15:54 ` [PATCH net-next v19 6/6] net: dsa: mxl862xx: document devlink flash and info support Daniel Golle
  5 siblings, 0 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:53 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

A broken or interrupted firmware image, or the sticky rescue bit, keeps
the switch in its MCUboot loader, which exposes only the clause-22 SMDIO
download interface. The clause-45 firmware API never comes up there, so
an ordinary probe would spend its whole timeout on a mailbox nothing
answers before failing.

Detect the loader over SB PDI at setup, ahead of any clause-45 access.
The mailbox is reset and a scratch write to it must latch, or the
switch is absent or has its SB PDI window elsewhere (-ENODEV); the
status word then names the state. A console loader is confirmed with a
register-read challenge, which only the loader's command loop services
by re-arming its ready word, and refused with -ENXIO when the challenge
goes unserviced. The loader's flashless download loop is unsupported
(-EOPNOTSUPP), an interrupted opening handshake is rescue that only a
power cycle clears, and a zero byte count is a receive loop wedged
mid-payload. A running firmware holds any other value and sees nothing
but the mailbox resets and scratch writes, which land in registers it
never reads.

In rescue mode register the switch without user interfaces so devlink
stays available to reflash it: the user ports fail setup and the DSA
core re-registers them as unused, while the shared and CPU ports come
up, the CPU port on its fixed link. devlink dev info then reports the
running firmware version as "0.0.0", which no released firmware carries,
so fwupd offers every release as an upgrade and recovers the switch
through the regular flash flow; no stored version is reported, since
what the flash holds is unknown in the loader.

An interrupted download can leave the loader wedged mid-payload with an
outstanding byte count. A background work item off the devlink flash
path drains it back to a clean ready state by feeding that count one
byte at a time, since a larger step could underflow the loader's counter
and wedge it until a power cycle, then re-probes so the probe-time
detection reclassifies the switch. Until it finishes, devlink dev info
reports no version and devlink dev flash returns -EBUSY. A loader left
in its opening handshake, a drain that fails and a re-probe that cannot
be scheduled are final for the binding: devlink dev flash then returns
-EIO. A probe that
meets the loader while it still erases for a session that died runs the
clause-45 readiness poll against it, and every poll fails its CRC check
there. The poll's reads are quiet ones, issued in the knowledge that a
switch in its loader or still coming out of reset may not answer, so a
quiet command leaves the CRC-error work alone; only a command against a
firmware believed to be running arms it. mxl862xx_api_wrap() recognises
the CRC codes at its exit, whichever step reported them, so the data
transfer steps cannot pass a firmware CRC code up raw; the host's own
verdict is a code outside the firmware's result range, so no firmware
errno can be taken for it.

Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v19:
 - recognise the firmware's CRC codes at the exit of mxl862xx_api_wrap()
   whichever step reported them, so the data transfer steps no longer
   pass them up raw, and give the host's own verdict a code outside the
   firmware's result range, which -EBADMSG was not; the command log
   keeps the code as reported (found by Sashiko AI review and a local
   review)
 - count rescue mode among the gates mxl862xx_api_gated() reports, so an
   unprivileged ethtool -S on the conduit stays silent in rescue mode
   (found by a local review)
 - treat -EINVAL from the self-heal's re-probe request as the device
   deletion it is, which ends the work anyway (found by a local review)
 - extack of a switch that cannot be flashed from its binding and the
   probe-time messages for an unusable switch state name what happened,
   and the command log names the host's own CRC verdict as such (found
   by a local review)
 - return a bus error of the reset closing the register-read challenge
   as such, which -ENXIO hid when the poll had also expired (found by a
   local review)
 - commit message: the three states that are final for the binding
   (found by a local review)
 - confirm READY with the register-read challenge after the settle step
   too, and judge the challenge by the re-armed ready word alone: the
   key the loader expects in DATA could collide with the word it returns
   there (found by Sashiko AI review)
 - return an SMDIO bus timeout during the challenge as the bus error it
   is; only an unserviced challenge is -ENXIO (found by Sashiko AI
   review)
 - drop the "firmware booted while draining" outcome of the drain's
   finish: the drained image's zero fill fails the loader's CRC gate, so
   any count outliving the window is a stuck loader (found by Sashiko AI
   review)
 - log a self-heal re-probe that cannot be scheduled (found by Sashiko
   AI review)
 - set WORK_STOPPED before waiting out a flash in .remove() and
   ->shutdown(), so the drain stops as early as possible (found by
   Sashiko AI review)
 - drop the rescue-mode disjunct from port_mdb_add() and port_mdb_del():
   no user netdev exists in rescue mode to reach them (found by Sashiko
   AI review)
 - the final slice's verification window moves to the previous patch
 - name the power cycle in the probe-time handshake message and the
   devlink documentation without its path in the kernel-doc, since the
   next patch adds it (found by Sashiko AI review)
 - commit message: name every detection outcome and the mailbox resets,
   and the CRC mapping (found by Sashiko AI review)
 - comments: state the drain's bound as what it is, the settle step's
   classification as the policy it is, and every cause of -EIO in the
   file header (found by Sashiko AI review and a local review)
v18:
 - a quiet firmware command, as the readiness poll issues, does not arm
   the CRC-error work: a probe that meets the loader mid-erase fails
   every poll's CRC check, which closed the conduit and warned on each
   of them; the CRC sites report -EBADMSG and mxl862xx_api_wrap() arms
   the work in one place
 - after a failed clause-45 wait, classify a status word that outlives
   the settle step as rescue only when it is the erase count of the
   session that died; any other value is a firmware that answered the
   reset without becoming ready, and probe fails with the wait error as
   it did before this patch (found by Sashiko AI review)
 - report the rescue-mode version as running only; the driver cannot
   tell what the flash holds (found by Sashiko AI review)
 - name the interrupted opening handshake in the probe-time log message,
   the only failure known at that point, and word the -EIO extack for
   every cause (found by Sashiko AI review)
 - port_mdb_add()/port_mdb_del(): rescue mode joins the flags under
   which a gated read counts as success, in the form of the previous
   patch, whose silent get_ethtool_stats() return covers rescue mode as
   well (found by Sashiko AI review)
 - kernel-doc: WORK_STOPPED also ends the drain and its reprobe
   hand-off; the file header names the -EIO state (found by Sashiko AI
   review)
 - commit message: describe the detection outcomes again (found by
   Sashiko AI review)
v17:
 - bypass the firmware-version gate in mxl862xx_phylink_get_caps() in
   rescue mode, so a SerDes CPU port does not get an empty
   supported_interfaces mask and fail phylink_create(); the sibling
   mac_select_pcs() bypass was already there (found by Sashiko AI review)
 - extend the port_mdb_add()/port_mdb_del() flash-window tolerance to
   rescue mode as well (found by Sashiko AI review)
 - the @rescue_failed kernel-doc and the mxl862xx_setup_rescue() log
   message no longer prescribe a power cycle for every cause; the
   per-cause remedy is in the documentation (found by Sashiko AI review)
v16:
 - drop the claim that the clause-45 API floods the log with CRC errors
   when no firmware answers, here and in the comments. The mailbox
   commands run into their timeouts instead, and testing the
   stuck-in-MCUboot paths produces no such message. What probing SB PDI
   first saves is the ten seconds of polling and the -ETIMEDOUT that
   ends probe
 - move the rescue-mode branch of mxl862xx_setup() into a function of
   its own, which brings its messages back inside 100 columns
 - state a fact in the -EBUSY extack of a running recovery, dropping
   the retry advice
 - trim the comment on mxl862xx_rescue_drain(), whose protocol detail
   is in the file header already, and order its declarations longest
   line first
 - treat a byte count that outlives the settle step as a busy loader
   rather than a running firmware, and wait out an erase from the dead
   session before draining, so a host that died during the loader's
   erase no longer fails probe with the -ETIMEDOUT this patch exists to
   avoid (found by Sashiko AI review)
 - poll the status register every 10 ms rather than every 50 us for the
   waits now measured in minutes
 - drop the loader's clean ready state along with the cached identity
   when a flash fails, so devlink dev info stops reporting 0.0.0, which
   means "ready to accept an image", for a loader left mid transfer
   (found by Sashiko AI review)
 - state facts in the extack for a failed recovery; the remedies, which
   differ per cause, are in the documentation (found by Sashiko AI
   review)
 - name -ECANCELED in the documented return sets that can produce it,
   and the opening handshake among the detection outcomes in the commit
   message (found by Sashiko AI review)
v15:
 - classify a status register left in the download handshake as a loader
   needing a power cycle, rather than as a running firmware, which made
   probe fail with the CRC-error storm this patch avoids (found by
   Sashiko AI review)
 - give the loader one step to publish its next state when detection
   runs after a failed clause-45 wait, so the count of a chunk it is
   still programming is not read as a firmware status word (found by
   Sashiko AI review)
 - abort the drain polls as soon as teardown asks for it, instead of
   holding up unbind and shutdown for up to 17 s (found by Sashiko AI
   review)
 - pair the rescue_mode accesses with WRITE_ONCE()/READ_ONCE(), like the
   sibling rescue flags (found by Sashiko AI review)
v14:
 - initialise the SerDes state once mxl862xx_wait_ready() has cached
   the firmware version, still before the rescue-mode early return,
   so PCS setup can depend on the running firmware

v13: no changes
v12: no changes
v11:
 - schedule the post-drain re-probe with device_schedule_reprobe() too,
   instead of a driver-owned work item
 - a drain whose re-probe hand-off fails still marks recovery failed, so
   devlink does not keep promising a retry

v10:
 - share the SB PDI timeouts with the flash path: one constant for the
   verify wait (15 s) and one for a single 1-byte mailbox step (2 s),
   the latter also replacing the separate detection timeout. The
   last-slice flush gets the sum of the write and verify budgets, since
   it cannot see the boundary between programming and verifying (found
   by Sashiko AI review)
 - report a reprobe hand-off that cannot be set up after a successful
   drain, instead of leaving devlink answering "retry shortly" for good
   for a loader sitting at a clean READY (found by Sashiko AI review)
 - drop heal_lock and mxl862xx_stop_work() with it: making the flag test
   and the queueing atomic was never the guarantee its comment claimed,
   and the reprobe now decides for itself whether it may still run
   (found by Sashiko AI review)
 - return -ENXIO rather than a propagated -ETIMEDOUT when the loader
   never re-arms READY for the register-read challenge, and correct the
   documented return sets of mxl862xx_rescue_mode_detect() and
   mxl862xx_rescue_drain_finish() (found by Sashiko AI review)
 - explain why STAT == 0 during a drain is unambiguous: by the
   r_remain == 0 rule the loader cannot be both inside the receive loop
   asking for a chunk and publishing a verdict (found by Sashiko AI
   review)
 - commit message: a running firmware is not "left untouched", the
   presence probe writes two mailbox scratch registers which are inert
   to it; an SB PDI window away from the OTP reset offsets also yields
   -ENODEV; and describe the -ENXIO outcome for a READY loader that
   never services the challenge (found by Sashiko AI review)

v9: no changes

v8:
 - never send END from the drain: the loader keeps the host's byte count
   in its status register while it programs a chunk, so a lingering count
   cannot be told from the "image rejected" verdict, and END written into
   the receive loop is consumed as a 15555-byte count and underflows the
   receive counter. Wait for the loader to ask for the next chunk or to
   return to its console loop instead, since it finalises on its own
   (found by Sashiko AI review)
 - initialise the SerDes state before the rescue-mode early return, so
   the window between a successful rescue-mode flash clearing
   rescue_mode and the reprobe cannot hand phylink a PCS with no ops and
   an uninitialised mutex (found by Sashiko AI review)
 - do not fail probe when the 1-byte slice-advance leaves the wedged
   loader somewhere other than asking for the next chunk: a download
   interrupted with exactly one byte outstanding completes on that byte,
   after which the loader verifies and returns to its console loop
   (found by Sashiko AI review)
 - report a failed drain and refuse further flashes with -EIO and an
   extack asking for a power cycle, instead of leaving devlink to answer
   "retry shortly" forever for a switch that never becomes ready (found
   by Sashiko AI review)
 - reset the mailbox before the presence probe: a download interrupted
   with the write latch armed made the scratch write land in switch
   memory instead, so detection returned -ENODEV for the very state it
   exists to recover (found by Sashiko AI review)
 - tell an SMDIO bus error apart from a loader failing the register-read
   challenge, and check the reset issued after it (found by Sashiko AI
   review)
 - reject DSA links before the rescue-mode shortcut, so an unsupported
   cascade topology fails probe in rescue mode too (found by Sashiko AI
   review)
 - serialise the self-heal's reprobe hand-off against teardown with a
   mutex (found by Sashiko AI review)
 - log the drain's progress, name its timeouts, and describe its real
   duration (found by Sashiko AI review)
 - WRITE_ONCE() the rescue_ready stores, document what orders
   rescue_mode, and correct the detection kernel-doc and the note on the
   OTP-configurable SB PDI register offsets (found by Sashiko AI review)

v7:
 - queue the reprobe as a delayed work item from the background
   self-heal, following the previous patch's move off the reprobe
   kthread
 - report the rescue-mode firmware version under
   DEVLINK_INFO_VERSION_GENERIC_FW too
 - drop two redundant rescue-recovery log lines; the setup message
   ("switch in MCUboot with an interrupted download, recovering in
   background") already says it
 - return distinct errno from rescue_mode_detect() so an absent switch
   (-ENODEV), one strapped into flashless-download mode (-EOPNOTSUPP)
   and one that answers SB PDI READY but fails the register-read
   challenge, or wedges without draining (-ENXIO), are no longer all
   reported as -ENODEV

v6:
 - after the background drain finalises the interrupted transfer,
   reprobe and let the probe-time detection re-classify the switch, so
   a valid image a last-moment interruption left bootable is picked up
   as running firmware; rescue_drain() no longer inspects or reports
   the outcome (its stale kernel-doc claiming a "return 1" case is gone)
 - poll the drain status register with read_poll_timeout() as well,
   which evaluates the condition once more after the deadline, matching
   the poll fix in the previous patch
 - treat the flashless-download loop (STAT 0xc33c) as an unsupported
   configuration and fail probe with -ENODEV, rather than advertising it
   as flashable when the console flash path cannot drive it

v5:
 - detect the switch state from the value MCUboot publishes in the SB
   PDI STAT register (loader ready, wedged download, or running
   firmware), confirming a live console loader with a register-read
   challenge, instead of trusting a bare SMDIO scratch write
 - fail probe with -ENODEV over SB PDI when the switch does not respond
   at all -- absent, unpowered, or misdescribed in the device tree --
   instead of letting the clause-45 API flood the log with CRC errors
 - drain a wedged interrupted download back to a clean ready state from
   a background work item so the multi-minute recovery never holds the
   devlink instance lock, and refuse devlink dev info and flash until it
   is ready
 - report the null firmware version as the stored version too, matching
   the running/stored reporting of the previous patch
 - do not report asic.id/asic.rev in rescue mode as the CHIP ID
   registers are unreadable without firmware; recovery tools match on
   the driver name and the "0.0.0" version instead (follows the numeric
   asic.id change in the previous patch)
 - move the devlink documentation into its own patch

v4:
 - log a distinct diagnostic when rescue mode detection fails on an
   SMDIO bus error instead of silently treating it as "not in
   rescue mode"
 - clear the rescue_mode flag under the MDIO bus lock, following
   the flag write locking in the previous patch

v3:
 - report the canonical null version "0.0.0" instead of
   "mcuboot-rescue" so that version-comparing update tools like
   fwupd offer any available release as an upgrade for recovery
 - check the rescue_mode flag under the MDIO bus lock, following
   the block_host/skip_teardown change in the previous patch

v2: new patch, allowing recovery from a failed or interrupted update
    without having to use a special recovery OS image (Andrew Lunn)

 drivers/net/dsa/mxl862xx/mxl862xx-fw.c      | 501 +++++++++++++++++++-
 drivers/net/dsa/mxl862xx/mxl862xx-fw.h      |   3 +
 drivers/net/dsa/mxl862xx/mxl862xx-host.c    |  73 +--
 drivers/net/dsa/mxl862xx/mxl862xx-phylink.c |   9 +-
 drivers/net/dsa/mxl862xx/mxl862xx.c         | 108 ++++-
 drivers/net/dsa/mxl862xx/mxl862xx.h         |  25 +-
 6 files changed, 663 insertions(+), 56 deletions(-)

diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
index eae7048707870..eb48fbf7c7115 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
@@ -29,9 +29,11 @@
  *
  * STAT magics:
  *   READY  0xc55c   loader idle in the console loop        (this driver)
+ *   DL_RDY 0xc33c   loader idle in the flashless loop
  *   START  0xf48f   host   -> begin download session
  *   ACK    0xf490   loader -> START acknowledged (START + 1)
  *   END    0x3cc3   host   -> finalise now (optional, see below)
+ *   RDREG  0xe2c0   host   -> register-read command (| index), see below
  *
  * Console flash path (STAT=0xc55c) - mxl862xx_flash_firmware():
  *
@@ -73,7 +75,41 @@
  *     - the loader leaves the count in STAT while it programs the chunk, so
  *       a lingering count does not distinguish "busy" from "verdict";
  *     - STAT is a 16-bit register, so a slice holds at most 65535 bytes,
- *       which is what bounds MXL862XX_FW_BANK_SLICE.
+ *       which is what bounds MXL862XX_FW_BANK_SLICE;
+ *     - interrupted-download recovery feeds 1 byte at a time (see below).
+ *
+ * Interrupted-flash recovery (mxl862xx_rescue_drain):
+ *   A host that dies mid-payload leaves the loader in the receive loop holding
+ *   STAT=0. Feed single 1-byte chunks (one DATA word + STAT=1) until r_remain
+ *   reaches 0; the loader then verifies the (now corrupt) image, publishes its
+ *   verdict and comes back to READY by itself. END is never sent here: while
+ *   r_remain is non-zero it would be consumed as a 15555-byte count, and a
+ *   lingering STAT=1 cannot be told from a chunk still being programmed.
+ *
+ * Register-read challenge (non-destructive liveness proof):
+ *   DATA := 0x7c23 (key); STAT := 0xe2c0|idx
+ *     -> loader returns a runtime word in DATA and re-arms STAT=0xc55c.
+ *   Only the re-arm counts, to prove a live mailbox in
+ *   mxl862xx_rescue_mode_detect(); the word is loader BSS, not a chip id.
+ *
+ * The other STAT ready magic, 0xc33c, marks the loader's flashless
+ * chip-to-chip download mode (MxL86281S 16-port tier); this driver does not
+ * use it.
+ *
+ * Rescue lifecycle (devlink): probe runs mxl862xx_rescue_mode_detect(); a
+ * wedged loader is drained back to READY by a background self-heal
+ * (rescue_heal_work), so the long recovery never holds the devlink lock.
+ * devlink dev info exposes the fw version (the "flashable" signal) only once at
+ * READY; flash_update returns -EBUSY until then, -EIO for good once the
+ * loader cannot accept a download (a failed recovery, or a handshake no
+ * session can finish), and reprobes to WSP firmware on success.
+ *
+ * Notes:
+ *   - Chip id/revision (0xc0d28884/88) are NOT reachable on this channel; they
+ *     need the clause-45 MMD firmware mailbox, which is dead under MCUboot.
+ *     Rescue identity is by SB PDI behaviour only (mxl862xx_rescue_mode_detect).
+ *   - The SMDIO PHY address comes from the device tree; the 0xe1xx register
+ *     offsets are the OTP reset defaults and the only layout supported here.
  */
 
 #include <linux/crc32.h>
@@ -106,8 +142,15 @@
 
 /* SB PDI handshake magic (published/consumed via STAT) */
 #define MXL862XX_SB_PDI_READY		0xc55c	/* loader idle, console loop */
+#define MXL862XX_SB_PDI_DL_READY	0xc33c	/* loader idle, flashless loop */
 #define MXL862XX_SB_PDI_START		0xf48f
 #define MXL862XX_SB_PDI_END		0x3cc3
+#define MXL862XX_SB_PDI_RDREG		0xe2c0	/* register-read cmd (| index) */
+#define MXL862XX_SB_PDI_RDREG_MARK	0x7c23	/* key the loader expects in DATA */
+
+/* Behavioural presence probe: two distinct 16-bit latches on ADDR/DATA. */
+#define MXL862XX_SB_PDI_PROBE_A		0x5a5a
+#define MXL862XX_SB_PDI_PROBE_D		0xa5a5
 
 /* Image verification verdict published in STAT once the receive loop ends */
 #define MXL862XX_SB_PDI_VERIFY_OK	0
@@ -126,6 +169,11 @@
 #define MXL862XX_FW_WRITE_TIMEOUT_MS	60000
 /* Covers the loader's END wait, verification and the reset into READY */
 #define MXL862XX_SB_PDI_VERIFY_MS	15000
+/* One loader mailbox step: program a 1-byte chunk or service a command */
+#define MXL862XX_SB_PDI_STEP_MS		2000
+/* STAT poll intervals: a mailbox step is quick, an erase is not */
+#define MXL862XX_SB_PDI_POLL_US		50
+#define MXL862XX_SB_PDI_SLOW_POLL_US	10000
 
 /* Delays spent in full: the reboot into the new image, and the re-probe */
 #define MXL862XX_FW_REBOOT_DELAY_MS	5000
@@ -225,6 +273,395 @@ static void mxl862xx_flash_notify(struct devlink *dl, const char *status,
 	devlink_flash_update_status_notify(dl, status, NULL, done, total);
 }
 
+/* Byte-count of each chunk fed to the loader during drain. It MUST be 1: the
+ * loader only lets us observe "counter == 0", never "counter < step", so any
+ * step > 1 can subtract past zero, underflow the 32-bit counter and wedge the
+ * loader for ~2^32 more bytes (a state only a power cycle clears). Stepping by
+ * 1 walks the counter through every value and is guaranteed to land on zero
+ * whatever its (possibly odd) start; the loader counts bytes, not words, and
+ * single-byte chunks are what the drain was tested with.
+ */
+#define MXL862XX_DRAIN_CHUNK_BYTES	1
+
+/* Log the drain's progress every so many bytes; it can run for a long time */
+#define MXL862XX_DRAIN_LOG_BYTES	(128 * 1024)
+
+/* 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, or
+ * -ECANCELED once teardown asks the caller to stop. On timeout the value 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.
+ *
+ * STAT 0 is unambiguous here even though it is also the "image verified"
+ * verdict: by the r_remain == 0 rule the loader leaves the receive loop the
+ * moment the counter reaches zero, so it is never both inside the loop asking
+ * for a chunk and publishing a verdict. Once it has left, the next STAT write
+ * is a command rather than a count, so feeding one more chunk after a verdict
+ * cannot underflow anything either.
+ */
+static int mxl862xx_sb_pdi_poll_drain(struct mxl862xx_priv *priv,
+				      unsigned long sleep_us,
+				      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),
+			  sleep_us, 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. Before the first chunk it may also still be erasing
+ * for the session that died, which no verify window covers. Wait that out:
+ * STAT cannot tell the cases apart, and guessing would mean writing END into a
+ * live receive loop. The drained image never boots, since its zero fill fails
+ * the loader's CRC gate, so a count outliving the window is a loader that has
+ * stopped servicing the mailbox.
+ *
+ * Return: 0 once the loader is back at READY, -EAGAIN if it asks for another
+ * chunk after all, -EIO for a loader still holding a count when the window
+ * expires, -ECANCELED on teardown, or an SMDIO bus error.
+ */
+static int mxl862xx_rescue_drain_finish(struct mxl862xx_priv *priv, u32 chunk)
+{
+	struct device *dev = &priv->mdiodev->dev;
+	int stat;
+
+	if (chunk)
+		stat = mxl862xx_sb_pdi_poll_drain(priv, MXL862XX_SB_PDI_POLL_US,
+						  MXL862XX_SB_PDI_VERIFY_MS);
+	else
+		stat = mxl862xx_sb_pdi_poll_drain(priv,
+						  MXL862XX_SB_PDI_SLOW_POLL_US,
+						  MXL862XX_FW_ERASE_TIMEOUT_MS);
+	if (stat < 0)
+		return stat;
+	if (stat == MXL862XX_SB_PDI_READY)
+		return 0;
+	if (!stat)
+		return -EAGAIN;
+
+	if (chunk)
+		dev_err(dev,
+			"flash: loader stuck after %u chunks, power cycle it\n",
+			chunk);
+	else
+		dev_err(dev, "flash: loader still busy after the erase window\n");
+
+	return -EIO;
+}
+
+/* Walk the loader's receive counter to zero (see the header): the image size
+ * died with the host, and a chunk larger than what is outstanding underflows
+ * the counter, so only single bytes are safe. Tens of minutes for a multi-MiB
+ * remainder. Returns 0 once the loader has left the receive loop, <0 on error;
+ * a counter an earlier oversized chunk underflowed needs a power cycle.
+ */
+static int mxl862xx_rescue_drain(struct mxl862xx_priv *priv)
+{
+	/* Bound: 32 MiB in single-byte chunks, many times the largest image. */
+	u32 max_chunks = (32u << 20) / MXL862XX_DRAIN_CHUNK_BYTES;
+	struct device *dev = &priv->mdiodev->dev;
+	u32 chunk = 0;
+	int ret, stat;
+
+	while (chunk < max_chunks) {
+		/* Teardown can interrupt this long drain. */
+		if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+			return -ECANCELED;
+
+		stat = mxl862xx_sb_pdi_poll_drain(priv, MXL862XX_SB_PDI_POLL_US,
+						  MXL862XX_SB_PDI_STEP_MS);
+		if (stat < 0)
+			return stat;
+		if (stat == MXL862XX_SB_PDI_READY)
+			return 0;
+
+		if (stat) {
+			ret = mxl862xx_rescue_drain_finish(priv, chunk);
+			if (ret != -EAGAIN)
+				return ret;
+		}
+
+		/* Feed one zero byte; reset cleared the write latch. */
+		ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+					   MXL862XX_SB_PDI_CTRL_WR);
+		if (ret < 0)
+			return ret;
+		ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, 0x0000);
+		if (ret < 0)
+			return ret;
+		ret = mxl862xx_sb_pdi_reset(priv);
+		if (ret < 0)
+			return ret;
+		ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+					   MXL862XX_DRAIN_CHUNK_BYTES);
+		if (ret < 0)
+			return ret;
+		chunk++;
+		if (!(chunk % MXL862XX_DRAIN_LOG_BYTES))
+			dev_info(dev, "flash: drained %u KiB so far\n",
+				 chunk / 1024);
+		cond_resched();
+	}
+
+	dev_err(dev,
+		"flash: interrupted download did not drain after %u chunks\n",
+		chunk);
+
+	return -ETIMEDOUT;
+}
+
+/* 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 (ret) {
+		/* Nothing retries this, so say so: rescue_ready stays clear
+		 * and devlink dev flash reports why it refuses.
+		 */
+		dev_err(dev, "flash: download recovery failed: %pe\n",
+			ERR_PTR(ret));
+		WRITE_ONCE(priv->rescue_failed, true);
+		return;
+	}
+
+	/* The interrupted transfer is finalised; reprobe so the probe-time
+	 * detection brings the driver up flashable, with the loader at READY.
+	 * The core drops the re-probe on its own if the device is unbound
+	 * first; the flag test only avoids scheduling one certain to be
+	 * dropped. A failed hand-off leaves nothing to reclassify the switch,
+	 * so recovery is marked failed rather than promising a retry that
+	 * cannot succeed.
+	 */
+	if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+		return;
+
+	/* -EINVAL is a device being deleted, whose teardown ends this work. */
+	ret = device_schedule_reprobe(dev, MXL862XX_FW_REPROBE_DELAY_MS);
+	if (ret && ret != -EINVAL) {
+		dev_err(dev,
+			"flash: re-probe could not be scheduled (%pe); unbind and rebind to reinitialise\n",
+			ERR_PTR(ret));
+		WRITE_ONCE(priv->rescue_failed, true);
+	}
+}
+
+/* Prove a live console loader behind READY: the register-read command, keyed
+ * by the word in DATA, is serviced by re-arming READY, which nothing but the
+ * loader's command loop does. The reset afterwards clears the word it leaves
+ * in DATA.
+ */
+static int mxl862xx_sb_pdi_challenge(struct mxl862xx_priv *priv)
+{
+	int ret, err, val;
+
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA,
+				   MXL862XX_SB_PDI_RDREG_MARK);
+	if (ret < 0)
+		return ret;
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+				   MXL862XX_SB_PDI_RDREG);
+	if (ret < 0)
+		return ret;
+
+	ret = read_poll_timeout(mxl862xx_smdio_read, val,
+				val < 0 || (u16)val == MXL862XX_SB_PDI_READY,
+				MXL862XX_SB_PDI_SLOW_POLL_US,
+				MXL862XX_SB_PDI_STEP_MS * 1000, false,
+				priv, MXL862XX_SB_PDI_STAT);
+	err = mxl862xx_sb_pdi_reset(priv);
+	if (val < 0)
+		return val;
+	if (err < 0)
+		return err;
+	if (ret)
+		return -ENXIO;
+
+	return 0;
+}
+
+/* Detect MCUboot rescue mode over clause-22 SMDIO alone, so the caller can rule
+ * the loader out before any C45 API request (which only runs into its timeouts
+ * 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:
+ *
+ *  - 0xc33c: flashless loop; recognised but not supported here.
+ *  - 0xc55c: console loop, if the register-read challenge is serviced.
+ *  - 0xf48f/0xf490: a download handshake nobody can finish; rescue, but only
+ *    a power cycle gets the loader out of it.
+ *  - other non-zero: running firmware, left unpoked. With @settle the loader
+ *    gets one step to publish 0 or READY first; a count outliving that is
+ *    taken for rescue only when it is the erase of a session that died
+ *    (header length + 1), anything else for a firmware that never became
+ *    ready.
+ *  - 0: wedged receive loop; the 1-byte slice-advance then says whether it
+ *    still needs draining or has just finished.
+ *
+ * The scratch write reaches a running firmware too, but lands in mailbox
+ * registers it does not read, so it is inert there.
+ *
+ * Return: MXL862XX_IN_RESCUE, MXL862XX_NOT_RESCUE, -ENODEV when the scratch
+ * write does not latch, which is a switch that does not answer at all or one
+ * whose SB PDI window is not at the offsets above, -EOPNOTSUPP for the
+ * flashless loop, -ENXIO for a READY loader whose mailbox fails the challenge,
+ * -ECANCELED when teardown interrupts a poll, or an SMDIO bus error.
+ */
+int mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv, bool settle)
+{
+	int stat, ret, rb, a, d;
+
+	/* rescue_ready gates flashing; a wedged loader needs the drain first. */
+	WRITE_ONCE(priv->rescue_ready, false);
+
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		return ret;
+
+	/* Presence: a live chip latches the scratch write, an absent one floats. */
+	a = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
+				 MXL862XX_SB_PDI_PROBE_A);
+	if (a < 0)
+		return a;
+	d = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA,
+				 MXL862XX_SB_PDI_PROBE_D);
+	if (d < 0)
+		return d;
+	a = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_ADDR);
+	if (a < 0)
+		return a;
+	d = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_DATA);
+	if (d < 0)
+		return d;
+	if ((u16)a != MXL862XX_SB_PDI_PROBE_A ||
+	    (u16)d != MXL862XX_SB_PDI_PROBE_D)
+		return -ENODEV;
+
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		return ret;
+
+	stat = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_STAT);
+	if (stat < 0)
+		return stat;
+
+	/* Flashless-download loop (MxL86281S tier): this driver does not
+	 * support it -- the console flash path expects READY. Treat it as an
+	 * unusable configuration, like any other unsupported state.
+	 */
+	if ((u16)stat == MXL862XX_SB_PDI_DL_READY)
+		return -EOPNOTSUPP;
+
+	/* Console loop at READY: confirm the live mailbox with the register-read
+	 * challenge. A firmware publishing 0xc55c as its status word looks the
+	 * same until then, and flashing one would be far worse than refusing
+	 * to bind, so an unserviced challenge is unusable (-ENXIO).
+	 */
+	if ((u16)stat == MXL862XX_SB_PDI_READY) {
+		ret = mxl862xx_sb_pdi_challenge(priv);
+		if (ret < 0)
+			return ret;
+		WRITE_ONCE(priv->rescue_ready, true);
+		return MXL862XX_IN_RESCUE;
+	}
+
+	/* 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;
+	}
+
+	/* Any other non-zero value is a running firmware, not a loader, but
+	 * the loader also holds the count of a chunk it is programming or
+	 * erasing for, so let a caller whose clause-45 wait has failed give it
+	 * a step for the next state. A count that outlives the step is taken
+	 * for rescue only when it is the erase count of a session that died;
+	 * any other is taken for a firmware that answered the reset without
+	 * becoming ready, since a status word of that firmware could hold any
+	 * value.
+	 */
+	if (stat) {
+		if (!settle)
+			return MXL862XX_NOT_RESCUE;
+
+		stat = mxl862xx_sb_pdi_poll_drain(priv, MXL862XX_SB_PDI_POLL_US,
+						  MXL862XX_SB_PDI_STEP_MS);
+		if (stat < 0)
+			return stat;
+		if (stat == MXL862XX_SB_PDI_READY) {
+			ret = mxl862xx_sb_pdi_challenge(priv);
+			if (ret < 0)
+				return ret;
+			WRITE_ONCE(priv->rescue_ready, true);
+			return MXL862XX_IN_RESCUE;
+		}
+		if (stat == MXL862XX_FW_HDR_SIZE + 1)
+			return MXL862XX_IN_RESCUE;
+		if (stat)
+			return MXL862XX_NOT_RESCUE;
+	}
+
+	/* 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. Had that byte been the last one outstanding,
+	 * the loader leaves the loop instead and returns to READY, having
+	 * consumed the advance -- proof enough of a live mailbox to skip the
+	 * challenge. Anything else means it is still working on it. All three
+	 * are rescue, so this never fails probe; only the drain does.
+	 */
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+				   MXL862XX_SB_PDI_CTRL_WR);
+	if (ret < 0)
+		return ret;
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, 0x0000);
+	if (ret < 0)
+		return ret;
+	ret = mxl862xx_sb_pdi_reset(priv);
+	if (ret < 0)
+		return ret;
+	ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+				   MXL862XX_DRAIN_CHUNK_BYTES);
+	if (ret < 0)
+		return ret;
+
+	rb = mxl862xx_sb_pdi_poll_drain(priv, MXL862XX_SB_PDI_POLL_US,
+					MXL862XX_SB_PDI_STEP_MS);
+	if (rb < 0)
+		return rb;
+	if (rb == MXL862XX_SB_PDI_READY)
+		WRITE_ONCE(priv->rescue_ready, true);
+
+	return MXL862XX_IN_RESCUE;
+}
+
 /* MCUboot firmware image header */
 struct mxl862xx_fw_hdr {
 	__le32 image_type;
@@ -300,13 +737,15 @@ static int mxl862xx_flash_firmware(struct mxl862xx_priv *priv,
 	int ret, val, i;
 
 	/* Step 1: reboot the firmware into MCUboot rescue mode */
-	ret = mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0,
-				false, false);
-	if (ret) {
-		dev_err(&priv->mdiodev->dev,
-			"flash: FW_UPDATE command failed: %pe\n",
-			ERR_PTR(ret));
-		return ret;
+	if (!READ_ONCE(priv->rescue_mode)) {
+		ret = mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0,
+					false, false);
+		if (ret) {
+			dev_err(&priv->mdiodev->dev,
+				"flash: FW_UPDATE command failed: %pe\n",
+				ERR_PTR(ret));
+			return ret;
+		}
 	}
 
 	/* Step 2: wait for bootloader ready */
@@ -504,6 +943,22 @@ int mxl862xx_devlink_info_get(struct dsa_switch *ds,
 	char buf[16];
 	int ret;
 
+	/* No chip-id/revision in MCUboot (needs the firmware MMD mailbox). The
+	 * fw version doubles as the "ready to flash" signal: report it only
+	 * once the loader is at a clean READY, nothing while still draining,
+	 * and as running only, since what the flash holds is unknown here.
+	 */
+	if (READ_ONCE(priv->rescue_mode)) {
+		if (!READ_ONCE(priv->rescue_ready))
+			return 0;
+
+		snprintf(buf, sizeof(buf), "%u.%u.%u",
+			 priv->fw_version.major, priv->fw_version.minor,
+			 priv->fw_version.revision);
+		return devlink_info_version_running_put(req,
+				DEVLINK_INFO_VERSION_GENERIC_FW, buf);
+	}
+
 	/* A 0 part number means the CHIP ID read failed, the part is unfused
 	 * or a flash failed; omit it rather than publish a bogus "0000" that
 	 * fwupd would match firmware against, it then falls back to the
@@ -585,9 +1040,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,
+				   "switch is not flashable from this binding");
+		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");
+		return -EBUSY;
+	}
+
+	if (READ_ONCE(priv->rescue_mode))
+		dev_info(ds->dev,
+			 "flash: flashing switch via MCUboot rescue mode\n");
+	else
+		dev_info(ds->dev, "flash: running firmware %u.%u.%u\n",
+			 priv->fw_version.major, priv->fw_version.minor,
+			 priv->fw_version.revision);
 
 	/* Close ports while the firmware is still alive so the DSA core's
 	 * MDB/FDB tracking is drained, and detach user ports so userspace
@@ -638,6 +1112,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
@@ -654,11 +1129,13 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
 	if (ret) {
 		/* The loader may hold an erased or partly written image; drop
 		 * the cached identity so devlink dev info stops reporting the
-		 * pre-flash version until the reprobe re-reads the truth.
+		 * pre-flash version until the reprobe re-reads the truth, and
+		 * with it the loader's clean READY state.
 		 */
 		memset(&priv->fw_version, 0, sizeof(priv->fw_version));
 		priv->asic_id = 0;
 		priv->asic_rev = 0;
+		WRITE_ONCE(priv->rescue_ready, false);
 	}
 
 	mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
index 15ed3a46bcfe2..02e5a627e9471 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
@@ -6,7 +6,10 @@
 #include <net/dsa.h>
 
 struct mxl862xx_priv;
+struct work_struct;
 
+int mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv, bool settle);
+void mxl862xx_rescue_heal_work_fn(struct work_struct *work);
 int mxl862xx_devlink_info_get(struct dsa_switch *ds,
 			      struct devlink_info_req *req,
 			      struct netlink_ext_ack *extack);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
index b229c4512b82c..744bbb8d6a24e 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -17,6 +17,7 @@
 #include <net/dsa.h>
 #include "mxl862xx.h"
 #include "mxl862xx-cmd.h"
+#include "mxl862xx-fw.h"
 #include "mxl862xx-host.h"
 
 #define CTRL_BUSY_MASK			BIT(15)
@@ -55,9 +56,14 @@ static void mxl862xx_crc_err_work_fn(struct work_struct *work)
 	clear_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags);
 }
 
-/* Firmware CRC error codes (outside normal Zephyr errno range). */
+/* The firmware reports a CRC-6 mismatch on ctrl/len_ret and a CRC-16 mismatch
+ * on the data payload with codes outside the Zephyr errno range. The host's
+ * own verdict lies outside the 11-bit result range, so no firmware result
+ * can be mistaken for it.
+ */
 #define MXL862XX_FW_CRC6_ERR		(-1024)
 #define MXL862XX_FW_CRC16_ERR		(-1023)
+#define MXL862XX_HOST_CRC_ERR		(-1025)
 
 /* 3GPP CRC-6 lookup table (polynomial 0x6F).
  * Matches the firmware's default CRC-6 implementation.
@@ -216,7 +222,9 @@ static int mxl862xx_busy_wait(struct mxl862xx_priv *priv)
 /* Issue a firmware command with CRC-6 protection on the ctrl and len_ret
  * registers, wait for completion, and verify the response CRC-6.
  *
- * Return: firmware result value (>= 0) on success, or negative errno.
+ * Return: firmware result value (>= 0) on success, a negative firmware
+ * errno or CRC code, %MXL862XX_HOST_CRC_ERR for a response failing its
+ * CRC-6 check here, or a negative bus errno.
  */
 static int mxl862xx_issue_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 len)
 {
@@ -250,15 +258,18 @@ static int mxl862xx_issue_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 len)
 	len_enc = ret;
 
 	ret = mxl862xx_crc6_verify(ctrl_enc, len_enc, &fw_result);
-	if (ret) {
-		if (!test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
-			schedule_work(&priv->crc_err_work);
-		return -EIO;
-	}
+	if (ret)
+		return MXL862XX_HOST_CRC_ERR;
 
 	return fw_result;
 }
 
+static bool mxl862xx_crc_failed(int ret)
+{
+	return ret == MXL862XX_HOST_CRC_ERR || ret == MXL862XX_FW_CRC6_ERR ||
+	       ret == MXL862XX_FW_CRC16_ERR;
+}
+
 static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
 {
 	u16 cmd;
@@ -305,26 +316,20 @@ static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
 
 	ret = mxl862xx_issue_cmd(priv, cmd, size);
 
-	/* Handle errors returned by the firmware as -EIO.
+	/* Handle errors returned by the firmware as -EIO; a CRC code passes
+	 * through for mxl862xx_api_wrap() to act on.
 	 * The firmware is based on Zephyr OS and uses the errors as
 	 * defined in errno.h of Zephyr OS. See
 	 * https://github.com/zephyrproject-rtos/zephyr/blob/v3.7.0/lib/libc/minimal/include/errno.h
-	 *
-	 * The firmware signals CRC validation failures with dedicated
-	 * error codes outside the normal Zephyr errno range:
-	 *   -1024: CRC-6 mismatch on ctrl/len_ret registers
-	 *   -1023: CRC-16 mismatch on data payload
 	 */
-	if (ret < 0) {
-		if ((ret == MXL862XX_FW_CRC6_ERR ||
-		     ret == MXL862XX_FW_CRC16_ERR) &&
-		    !test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
-			schedule_work(&priv->crc_err_work);
-		if (!quiet)
-			dev_err(&priv->mdiodev->dev,
-				"CMD %04x returned error %d\n", cmd, ret);
-		return -EIO;
-	}
+	if (ret == MXL862XX_HOST_CRC_ERR && !quiet)
+		dev_err(&priv->mdiodev->dev,
+			"CMD %04x response failed its CRC check\n", cmd);
+	else if (ret < 0 && !quiet)
+		dev_err(&priv->mdiodev->dev,
+			"CMD %04x returned error %d\n", cmd, ret);
+	if (ret < 0)
+		return mxl862xx_crc_failed(ret) ? ret : -EIO;
 
 	return ret;
 }
@@ -334,7 +339,7 @@ bool mxl862xx_api_gated(struct mxl862xx_priv *priv)
 	bool gated;
 
 	mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
-	gated = priv->block_host || priv->skip_teardown;
+	gated = priv->block_host || priv->skip_teardown || priv->rescue_mode;
 	mutex_unlock(&priv->mdiodev->bus->mdio_lock);
 
 	return gated;
@@ -358,6 +363,11 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
 		goto out;
 	}
 
+	if (priv->rescue_mode) {
+		ret = -ENODEV;
+		goto out;
+	}
+
 	/* During the post-flash readiness poll block_host stays set, but the
 	 * flash path's own firmware version reads must reach the new image;
 	 * host writes stay blocked so stale resource IDs cannot corrupt it.
@@ -491,9 +501,7 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
 	}
 
 	if (crc16(0xffff, (const u8 *)data, size) != crc) {
-		if (!test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
-			schedule_work(&priv->crc_err_work);
-		ret = -EIO;
+		ret = MXL862XX_HOST_CRC_ERR;
 		goto out;
 	}
 
@@ -503,6 +511,15 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
 	dev_dbg(&priv->mdiodev->dev, "RET %d DATA %*ph\n", ret, size, data);
 
 out:
+	/* A quiet command is expected to go unanswered by a switch still
+	 * booting or sitting in its loader, so it does not arm the shutdown.
+	 */
+	if (mxl862xx_crc_failed(ret)) {
+		if (!quiet &&
+		    !test_and_set_bit(MXL862XX_FLAG_CRC_ERR, &priv->flags))
+			schedule_work(&priv->crc_err_work);
+		ret = -EIO;
+	}
 	mutex_unlock(&priv->mdiodev->bus->mdio_lock);
 
 	return ret;
@@ -570,9 +587,11 @@ int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val)
 void mxl862xx_host_init(struct mxl862xx_priv *priv)
 {
 	INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
+	INIT_WORK(&priv->rescue_heal_work, mxl862xx_rescue_heal_work_fn);
 }
 
 void mxl862xx_host_shutdown(struct mxl862xx_priv *priv)
 {
 	cancel_work_sync(&priv->crc_err_work);
+	cancel_work_sync(&priv->rescue_heal_work);
 }
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
index b689652aa9b92..df77bbf108d5e 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -47,7 +47,12 @@ void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
 		fallthrough;
 	case 10 ... 12:
 	case 14 ... 16:
-		if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 84))
+		/* Rescue mode has no firmware version, so bypass the gate and
+		 * advertise the full set; a CPU port on a quad sub-interface
+		 * would otherwise get an empty mask and fail phylink_create().
+		 */
+		if (!READ_ONCE(priv->rescue_mode) &&
+		    !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);
@@ -406,6 +411,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");
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c
index e6f710c676e65..9097b0c559256 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -670,27 +670,85 @@ static void mxl862xx_free_bridge(struct dsa_switch *ds,
 	priv->bridges[bridge->num] = 0;
 }
 
+static void mxl862xx_setup_rescue(struct dsa_switch *ds)
+{
+	struct mxl862xx_priv *priv = ds->priv;
+
+	if (priv->rescue_ready) {
+		dev_warn(ds->dev,
+			 "switch in MCUboot rescue mode, use devlink to flash new firmware\n");
+		return;
+	}
+
+	if (priv->rescue_failed) {
+		dev_warn(ds->dev,
+			 "switch in MCUboot with an unfinishable download handshake, only a power cycle clears it\n");
+		return;
+	}
+
+	/* Drain the wedged download in the background so it never holds the
+	 * devlink lock; info and flash become available once ready.
+	 */
+	dev_warn(ds->dev,
+		 "switch in MCUboot with an interrupted download, recovering in background\n");
+	queue_work(system_long_wq, &priv->rescue_heal_work);
+}
+
 static int mxl862xx_setup(struct dsa_switch *ds)
 {
 	struct mxl862xx_priv *priv = ds->priv;
 	int n_user_ports = 0, max_vlans;
 	int ingress_finals, vid_rules;
 	struct dsa_port *dp;
-	int ret, i;
+	int ret, i, rescue;
 
-	ret = mxl862xx_reset(priv);
-	if (ret)
-		return ret;
+	/* Detect the loader over SB PDI first: it needs no firmware, unlike the
+	 * C45 API (mxl862xx_reset/wait_ready), which spends its whole 10 s
+	 * window on a mailbox nobody answers. Touch C45 only once rescue is
+	 * ruled out.
+	 */
+	rescue = mxl862xx_rescue_mode_detect(priv, false);
+	if (rescue < 0) {
+		dev_err(ds->dev, "switch in an unusable state: %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 in an unusable state 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) {
+		mxl862xx_setup_rescue(ds);
+		return 0;
+	}
+
 	/* Calculate Extended VLAN block sizes.
 	 * With VLAN Filter handling VID membership checks:
 	 *   Ingress: only final catchall rules (PVID insertion, 802.1Q
@@ -781,11 +839,21 @@ static int mxl862xx_port_state(struct dsa_switch *ds, int port, bool enable)
 static int mxl862xx_port_enable(struct dsa_switch *ds, int port,
 				struct phy_device *phydev)
 {
+	struct mxl862xx_priv *priv = ds->priv;
+
+	if (READ_ONCE(priv->rescue_mode))
+		return 0;
+
 	return mxl862xx_port_state(ds, port, true);
 }
 
 static void mxl862xx_port_disable(struct dsa_switch *ds, int port)
 {
+	struct mxl862xx_priv *priv = ds->priv;
+
+	if (READ_ONCE(priv->rescue_mode))
+		return;
+
 	if (mxl862xx_port_state(ds, port, false))
 		dev_err(ds->dev, "failed to disable port %d\n", port);
 }
@@ -1403,6 +1471,17 @@ static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
 	bool is_cpu_port = dsa_port_is_cpu(dp);
 	int ret;
 
+	if (dsa_port_is_dsa(dp)) {
+		dev_err(ds->dev, "port %d: DSA links not supported\n", port);
+		return -EOPNOTSUPP;
+	}
+
+	/* DSA reinits failed user ports as unused; shared ports must
+	 * succeed for the tree to register.
+	 */
+	if (READ_ONCE(priv->rescue_mode))
+		return dsa_port_is_user(dp) ? -ENODEV : 0;
+
 	ret = mxl862xx_port_state(ds, port, false);
 	if (ret)
 		return ret;
@@ -1412,11 +1491,6 @@ static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
 	if (dsa_port_is_unused(dp))
 		return 0;
 
-	if (dsa_port_is_dsa(dp)) {
-		dev_err(ds->dev, "port %d: DSA links not supported\n", port);
-		return -EOPNOTSUPP;
-	}
-
 	ret = mxl862xx_configure_sp_tag_proto(ds, port, is_cpu_port);
 	if (ret)
 		return ret;
@@ -1706,6 +1780,9 @@ static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port,
 	struct mxl862xx_priv *priv = ds->priv;
 	int ret;
 
+	if (READ_ONCE(priv->rescue_mode))
+		return;
+
 	switch (state) {
 	case BR_STATE_DISABLED:
 		param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_DISABLE);
@@ -2234,14 +2311,14 @@ static void mxl862xx_remove(struct mdio_device *mdiodev)
 
 	priv = ds->priv;
 
+	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
+
 	/* Wait out a flash in flight and bar a new one before the DSA core
 	 * frees the user netdevs; the devlink instance lock does not cover
 	 * that free, which dsa_unregister_switch() reaches first.
 	 */
 	mxl862xx_flash_shutdown(ds);
 
-	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
-
 	dsa_unregister_switch(ds);
 
 	mxl862xx_host_shutdown(priv);
@@ -2266,11 +2343,12 @@ static void mxl862xx_shutdown(struct mdio_device *mdiodev)
 
 	priv = ds->priv;
 
+	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
+
 	mxl862xx_flash_shutdown(ds);
 
 	dsa_switch_shutdown(ds);
 
-	set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
 	disable_delayed_work_sync(&priv->stats_work);
 
 	mxl862xx_host_shutdown(priv);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h
index 7d6bee9fa33d7..98b3afdc5ef1f 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -4,7 +4,9 @@
 #define __MXL862XX_H
 
 #include <asm/byteorder.h>
+#include <linux/bitops.h>
 #include <linux/mdio.h>
+#include <linux/mutex.h>
 #include <linux/workqueue.h>
 #include <net/dsa.h>
 
@@ -14,6 +16,10 @@ struct mxl862xx_priv;
 #define MXL862XX_FIRST_SERDES_PORT	9
 #define MXL862XX_SERDES_SLOTS		4
 
+/* mxl862xx_rescue_mode_detect() return codes (negative values are errors) */
+#define MXL862XX_NOT_RESCUE		0
+#define MXL862XX_IN_RESCUE		1
+
 #define MXL862XX_DEFAULT_BRIDGE		0
 #define MXL862XX_MAX_BRIDGES		48
 #define MXL862XX_MAX_BRIDGE_PORTS	128
@@ -298,7 +304,8 @@ struct mxl862xx_fw_version {
  *                      before CRC-triggered shutdown and cleared after;
  *                      %MXL862XX_FLAG_WORK_STOPPED is set before cancelling
  *                      stats_work to prevent rescheduling during teardown
- *                      and a flash
+ *                      and a flash, and ends the rescue drain and its
+ *                      reprobe hand-off
  * @drop_meter:         index of the single shared zero-rate firmware meter
  *                      used to unconditionally drop traffic (used to block
  *                      flooding)
@@ -340,6 +347,18 @@ struct mxl862xx_fw_version {
  * @shutting_down:      set under the devlink instance lock once ->shutdown()
  *                      or .remove() has begun, so no flash starts while the
  *                      switch is going away
+ * @rescue_mode:        switch is in MCUboot; firmware API commands fail fast,
+ *                      only clause-22 SMDIO works. Set from setup() before the
+ *                      switch is registered and cleared with WRITE_ONCE() under
+ *                      the MDIO bus lock for the benefit of mxl862xx_api_wrap();
+ *                      readers outside that lock use READ_ONCE().
+ * @rescue_ready:       (rescue_mode) loader is at a clean READY and will accept
+ *                      a flash; false while rescue_heal_work is draining
+ * @rescue_failed:      (rescue_mode) no flash is possible from this binding:
+ *                      an unfinishable handshake, a failed drain or a
+ *                      re-probe that could not be scheduled; the log names
+ *                      the cause, the devlink documentation the remedy
+ * @rescue_heal_work:   background self-heal draining a wedged download to READY
  * @stats_work:         periodic work item that polls RMON hardware counters
  *                      and accumulates them into 64-bit per-port stats
  */
@@ -347,6 +366,7 @@ struct mxl862xx_priv {
 	struct dsa_switch *ds;
 	struct mdio_device *mdiodev;
 	struct work_struct crc_err_work;
+	struct work_struct rescue_heal_work;
 	unsigned long flags;
 	u16 drop_meter;
 	struct mxl862xx_fw_version fw_version;
@@ -364,6 +384,9 @@ struct mxl862xx_priv {
 	bool block_host;
 	bool skip_teardown;
 	bool shutting_down;
+	bool rescue_mode;
+	bool rescue_ready;
+	bool rescue_failed;
 	struct delayed_work stats_work;
 };
 
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH net-next v19 6/6] net: dsa: mxl862xx: document devlink flash and info support
  2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
                   ` (4 preceding siblings ...)
  2026-10-03 15:53 ` [PATCH net-next v19 5/6] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
@ 2026-10-03 15:54 ` Daniel Golle
  5 siblings, 0 replies; 7+ messages in thread
From: Daniel Golle @ 2026-10-03 15:54 UTC (permalink / raw)
  To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Daniel Golle, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, Andrew Lunn,
	Vladimir Oltean, Russell King, netdev, linux-doc, linux-kernel,
	driver-core

Describe the devlink info versions and the flash update behaviour,
including the MCUboot rescue mode recovery, in a dedicated file under
Documentation/networking/devlink/ and link it from the index. Add the
new file to the driver's MAINTAINERS entry.

Assisted-by: LLM
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v19:
 - name every recovery failure with its remedy, the loader still busy
   past the erase of the interrupted session and the exhausted drain
   bound included, and say that the kernel log names the cause (found
   by Sashiko AI review)
 - the fw row lists every state without a version, and devlink dev
   flash tells a recovering switch from one whose recovery has failed
   and needs an image file to load; asic.id is also omitted when the
   read fails on a running firmware (found by Sashiko AI review)
 - the conduit is closed as well and reopened with the user ports, the
   reprobe recreates the user ports and loses their configuration, and
   the duration depends on the board's flash chip (found by Sashiko AI
   review)
 - the loader checks the image's integrity, the conduit is closed but
   not held closed, the ports are unusable while a re-probe that could
   not be scheduled is awaited, a loader in the flashless loop or with
   an unserviced mailbox fails probe, the handshake left behind by an
   interrupted session is one the driver does not resume, and the
   extack and log of a failed recovery say what they say, the driver
   validates the file before touching the switch, a flash is refused
   while the switch is still being set up, a re-probe that could not be
   scheduled is among the final states, and a failed flash's refusal
   clears with its re-probe (found by a local review)

v18:
 - the rescue-mode firmware version is reported as running only, and a
   re-probe that cannot be scheduled is logged on every path and leaves
   later flashes refused with -EBUSY until a rebind (found by Sashiko AI
   review)

v17: no changes

v16:
 - document that a reboot waits for a running flash and that one
   requested afterwards is refused, and that a bus error ends the
   download recovery for good
 - title-case the "Flash Update" heading, as the other devlink driver
   documents do
 - say that the ports come back down from a flash, that a reprobe which
   cannot be scheduled needs a rebind, and which of the two recovery
   failures needs a power cycle and which a rebind (found by Sashiko AI
   review)

v15:
 - asic.rev is read from the CHIP ID registers as well, not from one
   register word shared with asic.id (found by Sashiko AI review)
 - -EIO says the driver gave up on the recovery, which may need a driver
   rebind rather than a power cycle, and an interrupted opening
   handshake is reported the same way (found by Sashiko AI review)

v14: no changes

v13: no changes

v12: no changes

v11: no changes

v10: document that a switch power cycled on its own needs the driver
     unbound and rebound before a failed recovery is re-examined

v9: no changes, picked up Andrew's v5 Reviewed-by

v8:
 - asic.id and asic.rev are omitted whenever the part number reads
   zero, not only in MCUboot rescue mode (found by Sashiko AI review)
 - drop the claim that "0.0.0" marks a switch that never ran firmware;
   rescue mode always reports it (found by Sashiko AI review)
 - document devlink dev flash as the signal that says whether a
   recovery is still running, including the -EIO it returns once the
   recovery has failed (found by Sashiko AI review)

v7: no changes

v6: no changes

v5: new patch, splitting the devlink documentation out of the flash
    update and rescue mode recovery patches so each keeps to code
    (Jakub Kicinski asked for the documentation)


 Documentation/networking/devlink/index.rst    |   1 +
 Documentation/networking/devlink/mxl862xx.rst | 118 ++++++++++++++++++
 MAINTAINERS                                   |   1 +
 3 files changed, 120 insertions(+)
 create mode 100644 Documentation/networking/devlink/mxl862xx.rst

diff --git a/Documentation/networking/devlink/index.rst b/Documentation/networking/devlink/index.rst
index 1af780c811ee2..53d3ef16d13f4 100644
--- a/Documentation/networking/devlink/index.rst
+++ b/Documentation/networking/devlink/index.rst
@@ -95,6 +95,7 @@ parameters, info versions, and other features it supports.
    mlx5
    mlxsw
    mv88e6xxx
+   mxl862xx
    netdevsim
    nfp
    octeontx2
diff --git a/Documentation/networking/devlink/mxl862xx.rst b/Documentation/networking/devlink/mxl862xx.rst
new file mode 100644
index 0000000000000..6ec2c793fd0f3
--- /dev/null
+++ b/Documentation/networking/devlink/mxl862xx.rst
@@ -0,0 +1,118 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+========================
+mxl862xx devlink support
+========================
+
+This document describes the devlink features implemented by the
+``mxl862xx`` device driver.
+
+Info versions
+=============
+
+The ``mxl862xx`` driver reports the following versions
+
+.. list-table:: devlink info versions implemented
+   :widths: 5 5 5 85
+
+   * - Name
+     - Type
+     - Example
+     - Description
+   * - ``asic.id``
+     - fixed
+     - 8628
+     - The chip part number read from the CHIP ID registers. Omitted
+       when the part number reads as zero, which happens for a switch
+       sitting in MCUboot rescue mode (the registers need a running
+       firmware), when the read fails on a running firmware, for an
+       unfused part, and after a failed flash.
+   * - ``asic.rev``
+     - fixed
+     - 0
+     - The chip version, read from the CHIP ID registers as well. Both
+       values are published behind the same check, so it is omitted
+       whenever ``asic.id`` is.
+   * - ``fw``
+     - running, stored
+     - 1.0.70
+     - Version of the firmware running on the switch, reported as both
+       running and stored since the switch boots it from its own flash.
+       It is omitted while no firmware version is known: after a failed
+       flash until the reprobe it schedules, and in MCUboot rescue mode
+       while an interrupted download is still being recovered in the
+       background, once that recovery has failed, or while the loader
+       waits in an opening handshake nobody can finish. Once the loader
+       is ready to accept a new image the version appears as "0.0.0",
+       which no released firmware reports, so version-comparing tools
+       offer any available release as an upgrade; it is reported as
+       running only, since the driver cannot tell what the flash holds
+       while the loader runs. A missing version on its own does not say
+       why; ``devlink dev flash``, given an image file that passes the
+       driver's validation, answers ``-EBUSY`` while the switch is still
+       recovering and ``-EIO`` once it cannot be flashed from this
+       binding, see below.
+
+Flash Update
+============
+
+The ``mxl862xx`` driver implements support for ``devlink dev flash``.
+The driver checks the image file's header and payload checksums before
+touching the switch and refuses a file that fails them. The image is
+then transferred to the switch over the same MDIO bus which is also
+used to manage the switch, checked for integrity again and installed
+by the MCUboot bootloader running on the switch. All ports of the
+switch are closed and held closed for the duration of the update, the
+conduit interface is closed with them, and the driver reprobes the
+switch after it has rebooted into the new firmware. The reprobe
+destroys and recreates the user ports, so their bridge membership,
+VLANs, addresses and every other per-port configuration are lost with
+them; they come back registered but down, and userspace configures and
+brings them up again, which opens the conduit with them. A complete
+flash and reprobe cycle takes on the order of a minute, depending on
+the board's flash chip. Until the reprobe has run, a further update is
+refused with ``-EBUSY``, as is one requested before the switch has
+finished setting up. In the rare case that the reprobe cannot be
+scheduled at all, the kernel log says so, ``devlink dev flash`` reports
+that error or the transfer's own if the transfer failed as well, and
+the driver stays bound to a switch it no longer tracks, with its ports
+unusable and further updates refused with ``-EBUSY``, until it is
+unbound and rebound. A reboot started while an update is running waits
+for the transfer to finish, and an update requested after the system
+has begun shutting down is refused with ``-ENODEV``.
+
+A switch stuck in MCUboot rescue mode, e.g. after an interrupted
+update, is registered without user ports. A loader found in its
+flashless download loop, or one that does not service its mailbox, is
+not usable from here and fails probe; the kernel log reports the
+state's errno, ``-EOPNOTSUPP`` for the flashless loop and ``-ENXIO``
+for the unserviced mailbox. If the previous download was interrupted
+mid-transfer the loader is wedged; the driver drains it back to a clean
+ready state in the background, one byte at a time, which takes tens of
+minutes for a large image and is reported through the kernel log as it
+progresses. During that recovery ``devlink dev flash`` returns
+``-EBUSY`` with an extack message saying so, and ``devlink dev info``
+reports no firmware version. Once the loader is ready the firmware
+version appears and flashing a firmware image through the regular
+update flow recovers the switch.
+
+If the switch cannot be flashed from its binding, ``devlink dev flash``
+returns ``-EIO`` and says so in its extack message; the kernel log
+reports why, naming a loader that stopped answering, a drain that
+reached its bound or a reprobe that could not be scheduled where that
+is the cause and the bus error otherwise. The drain runs once and is
+never resumed, so a failed MDIO transaction ends it as well. A loader
+that stops answering the drain, one still busy once the erase of the
+interrupted session should long have finished, and a drain that
+reaches its bound without the loader returning to its ready state need
+a power cycle; a completed drain whose reprobe could not be scheduled,
+and a drain a bus error cut short, need only a driver rebind. The
+driver re-examines the switch
+when it binds and at no other time, so a power cycle on a board where
+the switch can be cycled on its own still has to be followed by an
+unbind and rebind for the recovered switch to be recognised.
+
+A download interrupted during its opening handshake, before the image
+header reached the loader, is reported the same way. The driver starts
+a download only from the loader's ready state and does not resume that
+session, so the switch needs a power cycle.
diff --git a/MAINTAINERS b/MAINTAINERS
index 6de1ff058db64..cb1dad080f63c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -16250,6 +16250,7 @@ M:	Daniel Golle <daniel@makrotopia.org>
 L:	netdev@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/net/dsa/maxlinear,mxl862xx.yaml
+F:	Documentation/networking/devlink/mxl862xx.rst
 F:	drivers/net/dsa/mxl862xx/
 F:	net/dsa/tag_mxl862xx.c
 
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-10-03 15:54 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-03 15:50 [PATCH net-next v19 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
2026-10-03 15:51 ` [PATCH net-next v19 1/6] net: dsa: add devlink flash_update callback to dsa_switch_ops Daniel Golle
2026-10-03 15:51 ` [PATCH net-next v19 2/6] net: dsa: mxl862xx: add SMDIO clause-22 register access Daniel Golle
2026-10-03 15:52 ` [PATCH net-next v19 3/6] driver core: add device_schedule_reprobe() Daniel Golle
2026-10-03 15:52 ` [PATCH net-next v19 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
2026-10-03 15:53 ` [PATCH net-next v19 5/6] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
2026-10-03 15:54 ` [PATCH net-next v19 6/6] net: dsa: mxl862xx: document devlink flash and info support Daniel Golle

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®