Skip to content

investigate(rp2040): readable BOOTSEL volume stalls on first write; make watchdog cleanup handle-safe #1161

Description

@zackees

Context

Two RP2040 boards have now refused programming. The current failure has a specific, repeatable state:

  • The RP2040 enters BOOTSEL and enumerates as USB\VID_2E8A&PID_0003\E0C9125B0D9B.
  • Its mass-storage interface (MI_00) is healthy.
  • Windows mounts RPI-RP2 on G: and reports the volume healthy.
  • INFO_UF2.TXT and other reads succeed.
  • Writes do not complete:
    • fbuild's NEW.UF2 write stalls;
    • a normal Windows file copy stalls;
    • a paced write-through test using 8 KiB chunks stalled before its first completed chunk and recorded zero progress.
  • Windows logged Disk events 51 and 153 during the attempts, including retries at LBAs 0x134 and 0x14c.
  • Restart Manager reported no user-mode holder, and an exclusive volume open succeeded between attempts.

The last point does not prove that the failure is device-side. Kernel storage filters, filesystem behavior, USB transport faults, QSPI programming behavior, and a host/controller problem can all exist without a user-mode volume handle. A USB trace is required before claiming that the RP2040 bulk-OUT endpoint failed to acknowledge the transfer.

The separate MI_01 PICOBOOT interface currently has no Windows driver. That does not explain why the healthy MI_00 mass-storage interface can be read but not written.

fbuild regression question

RP2040 deploy hardening was merged in commit debc9963 through PR #1083.

The relevant commit boundary is:

  1. d5144e0111d04b390f431e9700211a65cfee2f9d — immediately before the hardening
  2. a3d8c58b94111a4e055f66d67d4ff8ad702fd730 — fresh-enumeration retries
  3. c472f56f5989a4507321e862fdb62d7c1bcf309f — watchdog and broader hardening
  4. debc9963527e0a5a09f1224883b02d63ba174425 — merged hardening
  5. 16bb9ffae6aeab38957176357425110b002d98b1 — current main during this reproduction

Static comparison shows that the low-level first-write shape was not materially changed by #1083: both sides open/truncate NEW.UF2 and make a whole-buffer write_all call.

The hardening did introduce a definite recovery hazard. run_with_watchdog deliberately abandons a timed-out worker thread:

/// Run `work` on a dedicated thread and give up after `budget`. A storport
/// retry storm behind a sick hub can block the NEW.UF2 `write_all` for
/// minutes with no output; this turns that hang into an actionable failure
/// that feeds the fresh-enumeration retry gate. On timeout the worker thread
/// is deliberately abandoned, not joined: it only touches its own locals and
/// its channel send fails silently once this receiver is dropped, so the
/// wedged kernel write can unblock (or not) without holding up the deploy.
fn run_with_watchdog<T: Send + 'static>(
budget: Duration,
label: &str,
work: impl FnOnce() -> Result<T> + Send + 'static,
) -> Result<T> {
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = sender.send(work());
});
match receiver.recv_timeout(budget) {

That worker owns the open NEW.UF2 file. If the kernel write remains blocked, the thread and handle can remain alive inside the daemon after fbuild reports a timeout. This can contaminate retries and later deploy attempts even if the watchdog did not cause the original first-write stall.

The existing watchdog test verifies that the caller returns quickly by leaving a five-second worker running. It does not verify worker termination, handle release, or whether an immediate second write can open the destination.

There is also a local version-isolation risk:

  • Global executable: C:\tools\python13\Scripts\fbuild.exe, reporting 2.5.2
  • FastLED virtual-environment executable: reporting 2.5.3
  • Installed Python metadata in that environment: reporting 2.5.2

Every hardware result therefore needs to record the exact executable, executable hash, source commit, daemon port, and daemon binary used.

Pinned artifact evidence

The current artifact is:

  • Size: 882,688 bytes
  • SHA-256: 1C770999678BB7563A81D36C696DC44C074A712E25F0DE68A8A83F859E8AF0E0
  • 1,724 UF2 blocks of 512 bytes
  • RP2040 family ID: 0xE48BFF56
  • Target range: 0x10000000 through 0x1006BB00
  • Block magic, block numbering, payload lengths, family ID, and declared block count are structurally valid

An earlier deploy transcript reported a different artifact size. Commit comparisons must therefore use one pinned artifact without rebuilding between runs.

Proposal

1. Produce a deterministic reproduction record

Before every attempt, record:

  • physical board serial and VID:PID;
  • PnP health for both BOOTSEL interfaces;
  • drive letter and USB hub/root-port topology;
  • exact fbuild executable path, version, hash, source commit, and daemon binary;
  • a unique five-digit FBUILD_DAEMON_PORT;
  • artifact path, size, and SHA-256;
  • relevant Windows Disk 51/153 events before and after the attempt;
  • whether any fbuild worker/process/volume handle remains after timeout.

Use an outer killable process boundary for any experiment that may block. Do not allow one timed-out attempt to contaminate the next case.

2. Run a transport/artifact matrix

On the same board, cable, direct motherboard port, and clean BOOTSEL enumeration, compare:

  1. the previously proven RAM-only UF2;
  2. a small official known-good RP2040 flash Blink UF2;
  3. the pinned fbuild-generated flash UF2.

Interpretation:

  • RAM succeeds but both flash images fail: investigate the flash-target/QSPI path.
  • Official flash succeeds but fbuild flash fails: investigate artifact generation or layout.
  • All three fail on the first storage request: investigate the Windows/USB/volume path.
  • The result changes with fbuild commit while the pinned artifact and physical setup remain identical: investigate an fbuild deploy regression.

Also repeat the decisive case using:

  • a known-good data cable;
  • a direct USB 2.0-capable motherboard port;
  • a second clean host, preferably Linux.

On Linux, a guarded comparison between a normal mounted-volume copy and an unmounted raw block-device write may distinguish filesystem integration from USB/device behavior. The exact block device must be verified before any raw write.

3. Capture the failing layer

Capture one failure with:

  • USBPcap/Wireshark, to determine whether the first mass-storage bulk-OUT transaction leaves the host and what response/reset sequence follows;
  • Process Monitor filtered to the selected volume and fbuild process;
  • Windows Disk, StorPort, Kernel-PnP, and USB events;
  • read-only fltmc/PnP inspection when elevated access is available.

The trace should classify the failure without assuming that readable media plus a missing user-mode handle proves a board fault.

4. Perform an exact hardening A/B

Build and run isolated versions at the commit boundary listed above:

  • use separate worktrees/installations;
  • use separate daemon ports and state directories;
  • verify both CLI and daemon provenance;
  • use the same pinned UF2;
  • start with the pre-hardening build after a clean reboot;
  • do not run an older build after a watchdog failure unless every newer worker/process/handle has been proven gone.

Decision criteria:

  • d5144e01 repeatedly succeeds while c472f56f fails in the clean identical setup: hardening regression.
  • Both fail on the same first storage operation: the initial stall predates or is independent of the hardening.
  • Both fail initially, but only watchdog-enabled builds retain a worker/handle or poison the next attempt: underlying transport failure plus a separate fbuild recovery regression.

5. Make timeout cleanup handle-safe

If the watchdog path is retained, it must not abandon a thread that owns a removable-volume file handle.

Use either:

  • a killable helper process whose termination closes the handle; or
  • cancellable Windows overlapped I/O using CancelIoEx, followed by bounded completion and thread joining.

After timeout:

  • the worker must be terminated or joined;
  • the NEW.UF2 handle must be closed;
  • stale volume paths must be discarded;
  • retries may occur only after a verified fresh BOOTSEL enumeration;
  • an immediate second open/write probe must not fail because of the prior attempt.

Add a deterministic regression test covering timeout followed by immediate handle reacquisition and a second write.

6. Use PICOBOOT only as a controlled comparison

PICOBOOT/picotool can be used as a transport control on another host, or where the interface already has a compatible driver. Do not install or rebind drivers on the current machine as part of this issue.

Acceptance criteria

  • A reproducible log captures exact board, artifact, executable, daemon, USB topology, and Windows event identities.
  • The RAM-only, official flash, and fbuild flash artifact matrix is completed without rebuilding between cases.
  • At least one USB trace identifies whether the first write leaves the host and what fails next.
  • The pre-hardening through current-main A/B identifies whether harden(rp2040): hub-path deploy robustness — retries, picotool fallback, topology diagnostics, tunable budgets #1083 changed the initial failure.
  • The initial transport failure and any post-timeout contamination are reported as separate findings.
  • A watchdog timeout leaves no live worker, helper, or NEW.UF2 handle.
  • A deterministic test proves that a second destination open/write succeeds immediately after timeout cleanup.
  • Hardware closeout runs bash autoresearch rp2040 --rpc-smoke --timeout 120s --skip-lint with a unique daemon port and records the exact decisive output.
  • Conclusions do not label either RP2040 board defective without the artifact, host, and transport controls above.

Fixed environmental constraint / non-goals

  • Acronis must remain installed, enabled, and unmodified.
  • Do not stop, disable, uninstall, upgrade, reconfigure, detach, or mutate any Acronis service, driver, filter, or installation.
  • Read-only observation of the existing storage stack is allowed; changing it is not.
  • Do not run chkdsk or attempt filesystem repair on the synthetic BOOTSEL FAT volume.
  • Do not treat “Restart Manager found no process” as proof that no kernel component participated.
  • Do not require a BOOTSEL button sequence as the final fbuild recovery design.
  • Do not install Zadig or rebind the current machine's RP2040 interfaces as part of this investigation.

External reports

Similar symptoms exist outside fbuild:

These are analogous reports, not proof of the same root cause.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions