Skip to content

fix(web): drop a non-TLS client on the TLS port before the handshake (#274) - #277

Merged
angeloINTJ merged 1 commit into
mainfrom
fix/wire-tft-flash-render-274
Oct 15, 2026
Merged

angeloINTJ merged 1 commit into
mainfrom
fix/wire-tft-flash-render-274

Conversation

@angeloINTJ

Copy link
Copy Markdown
Owner

The bug (#274)

A stale http:// browser tab left open on the device's TLS port after HTTPS is turned on reloads itself, speaks cleartext HTTP to the TLS port, and retries over a few parallel connections. On the SIMUT Wire with the TFT panel (pico_wiretft) this resets the device within tens of seconds; on the Wire without the panel (pico_wire) it never does.

Root cause

The RP2040 latent flash-vs-render fault (D-C1). The BearSSL server handshake for a plaintext client — and its teardown — keep Core 0 busy in the web path; a Core-0 flash access that coincides with Core 1 fetching the panel's code over XIP wedges the QSPI arbiter, the watchdog goes unfed, and the hardware watchdog resets the device.

Reproduced deterministically on the bench (7 cleartext GETs to :8080 every 3 s, a bench TLS pair installed): reset in 26–55 s, every run. Autopsy over 6 captured resets: 5× C0=[WEB_POLL] C1=[DISPLAY] (the TLS accept/handshake path), 1× C0=[HIST_SAMPLE]. The history write already pauses Core 1 (a cooperative quiesce plus a forever-retrying multicore_lockout), so it was not the gap here; the lone HIST_SAMPLE reset only appears under this load, which the fix removes.

The fix

WiFiServerSecure::accept() now tells a TLS client from a plaintext one by its first byte, before building the ~16 KB BearSSL context and entering the handshake:

  • A TLS ClientHello begins with 0x16. Any other first byte (an HTTP GET is 0x47) is handed to WiFiServer::accept() — which claims it as a plain client, advances the queue and frees the backlog slot (the TLS accept never did, so plaintext churn could starve the five pending slots) — then closed.
  • accept() is called the moment lwIP establishes the connection, usually before the first byte arrives, so the decision waits for it across polls (non-blocking: return a no-op client; the WebServer's HC_NONE loop calls again next pass). Bounded by 400 ms so a silent peer falls through to the handshake (reaped there by the global handshake deadline) rather than holding a slot forever.

It is a framework override (wifiserversecure_drop_plaintext.patch), applied by tools/arduino_pico_overrides/patch.sh (section 2b-ter) and reverted by restore.sh, like the other TLS patches. CI runs patch.sh in every build job.

Proof — bench, pico_wiretft, bench TLS pair on :8080, 2026-10-15

plaintext attempts TLS reads (legit client) watchdog resets
before — — reset in 26–55 s, every run
after 453 873 0 in 301 s

HTTPS is unaffected — the legitimate TLS reader kept reading throughout.

Measured on the shipping image, not an instrumented one. A first attempt "passed" 301 s only on a diagnostic build whose extra instructions shifted the timing enough to mask the collision; the same code on the clean image still reset at 51 s. That first version also acted only when a byte was already buffered, which under this timing was rarely true — corrected to wait for the byte. The table above is the clean image.

Flash

The override compiles wherever SIMUT_WEB_HTTPS is set, so the five HTTPS images grow and their budgets + the OTA headroom table (PLANO_STABLE.md §3.1) move in this change: pico_wiretft +56 B, pico_w_release +48 B, pico_w_test_https +48 B, pico_w_asserts +56 B, pico2_w_release +24 B. pico_wire, pico_w_test, pico_w_alpha and pico_w_air build no HTTPS server and are unchanged. pico_w_asserts is left 3,291 B under the OTA ceiling (a bench image).

Not covered

  • The pico_w_test_https suites on the rig were not re-run. The core HTTPS path (login over TLS plus 873 reads of /api/status) runs on the same accept code in the proof above, with no failure.

🤖 Generated with Claude Code

https://claude.ai/code/session_013C6NHM2fJi3M7EMr47WyQ3

…274)

A stale http:// browser tab left on the device's TLS port after HTTPS is
turned on sends cleartext GETs to it. The BearSSL server handshake and its
teardown, under the TFT panel's Core-1 XIP render, wedge the RP2040 QSPI
arbiter and the hardware watchdog resets the device (the latent D-C1
flash-vs-render fault). Bench autopsy 2026-10-15: C0=[WEB_POLL] C1=[DISPLAY],
5 of 6 resets; the same load on a build without the panel (pico_wire) never
resets. The history write already pauses Core 1, so it is not the gap here.

WiFiServerSecure::accept() now tells a TLS client from a plaintext one by its
first byte before building the ~16 KB TLS context: a ClientHello begins with
0x16, so any other first byte (an HTTP GET is 0x47) is handed to
WiFiServer::accept() -- which claims it as a plain client, advances the queue
and frees the backlog slot -- and closed. accept() runs before the first byte
usually arrives, so the decision waits for it across polls (non-blocking),
bounded by 400 ms so a silent peer falls through to the handshake rather than
holding one of the five pending slots. Framework override, applied by
tools/arduino_pico_overrides/patch.sh (2b-ter) and reverted by restore.sh.

Proof (bench, pico_wiretft, bench TLS pair on :8080, 2026-10-15):
  before: watchdog reset in 26-55 s, every run.
  after:  0 resets in 301 s; 453 plaintext attempts, 873 TLS reads on the
          legit client (HTTPS unaffected). Measured on the shipping image, not
          an instrumented one: a diagnostic build masked the fault by shifting
          the timing, so an early "pass" on it was an artifact; the clean image
          is what was proved.

The override compiles wherever SIMUT_WEB_HTTPS is set, so the five HTTPS images
grow: pico_wiretft +56 B (774,380), release +48 (1,022,516), test_https +48
(1,032,004), asserts +56 (1,024,820), pico2_w_release +24 (998,624); budgets
and the OTA headroom table (PLANO_STABLE.md 3.1) moved to match. pico_wire,
pico_w_test, alpha and air build no HTTPS server and are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013C6NHM2fJi3M7EMr47WyQ3
@github-actions github-actions Bot added documentation Improvements or additions to documentation tools Build tools, scripts, and developer tooling labels Oct 15, 2026
@angeloINTJ
angeloINTJ merged commit 80c3c88 into main Oct 15, 2026
13 checks passed
@angeloINTJ
angeloINTJ deleted the fix/wire-tft-flash-render-274 branch October 15, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation tools Build tools, scripts, and developer tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant