Repository navigation
fix(web): drop a non-TLS client on the TLS port before the handshake (#274) - #277
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:8080every 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-retryingmulticore_lockout), so it was not the gap here; the loneHIST_SAMPLEreset 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:0x16. Any other first byte (an HTTPGETis0x47) is handed toWiFiServer::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'sHC_NONEloop 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 bytools/arduino_pico_overrides/patch.sh(section 2b-ter) and reverted byrestore.sh, like the other TLS patches. CI runspatch.shin every build job.Proof — bench,
pico_wiretft, bench TLS pair on:8080, 2026-10-15HTTPS 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_HTTPSis 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_alphaandpico_w_airbuild no HTTPS server and are unchanged.pico_w_assertsis left 3,291 B under the OTA ceiling (a bench image).Not covered
pico_w_test_httpssuites 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