extmod/modtls_mbedtls: Close transport on handshake failure - #19630
extmod/modtls_mbedtls: Close transport on handshake failure#19630lukaskremla wants to merge 2 commits into
Conversation
Fatal TLS handshake errors during read or write cleared SSLSocket.sock without closing the underlying transport. Later close() calls therefore became no-ops, which could leave native sockets allocated until their TCP timeout expired. Close the retained transport as part of fatal handshake cleanup and use the same helper for ordinary closure. Signed-off-by: Lukas Kremla <contact@lukaskremla.com>
Track MP_STREAM_CLOSE calls made to the fake transport. Verify that ordinary closure and fatal handshake cleanup each close the transport exactly once, and that subsequent close() calls do not close it again. Also fix the assert_raises helper. It raised AssertionError inside its try block and immediately caught it, so the AssertionError would never surface. Signed-off-by: Lukas Kremla <contact@lukaskremla.com>
|
Code size report: |
|
My understanding of the regression is that the new clean-up behavior closes the socket sooner than the peer/client can react to the error we attempt to flush before it. The one trade-off/alternative I ruled out in the initial PR as a bad-practice trade off was removing the call that deletes the reference to the socket - but not cleaning up either. That makes MicroPython (and the application running on it, say Microdot) responsible for calling That would create a short delay between sending the error to the peer and tearing down the socket - which might avoid the regression. But it isn't a guaranteed fix, and if the processing is slowed down for any reason the same regression manifests. And I'm not sure that making Python code responsible for socket clean-up like this is appropriate - it's fragile and does what I deem to be the job of C-level code. I'm unsure what the right solution from here is. The bug that would be fixed by the PR in its current state seems more severe to me than the regression it might introduce, but neither is ideal. I think that the decision for the best course of action from this point is better fit for the core/active maintainers of this project. There also might be a better solution which avoids both problems that I don't see due to insufficient familiarity with this project. |
Summary
The existing MicroPython mbedtls implementation doesn't correctly close the underlying transport sockets on fatal SSL handshake errors that occur during read/write operations. It only deletes the reference to them, making future cleanup attempts no-ops.
This bug affects web server projects (Such as those leveraging Microdot) which target chromium-based browsers.
Unlike Firefox - when chromium encounters a self-signed certificate during the TLS handshake - It closes the connection before re-opening a new one even if the browser user explicitly accepted the warning prompt. This sub-optimal behavior is admitted in chromium sources as well (see the spoiler below).
Chrome's behavior combined with MicroPython's clean-up bug resulted in the device running out of free sockets to use, because all got stuck waiting for their time-outs, causing all requests to be dropped with ERR_CONNECTION_RESET errors. The server would become responsive once more after the underlying transports timed out and cleaned-up on their own.
Chromium isn't to blame for this bug - it merely triggers aggressively and makes it apparent.
This bug presumably affects all ports relying on mbedtls.
Chromium sources
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/http/http_network_transaction.cc#943

Chrome's behavior was confirmed by NetLog in my debugging session, and also by further observations of the code in the source file shared above.
Since I believe this to be a self-contained bug even without my Microdot reproduction scripts, I have decided to omit them from this PR. I can send them should they be important.
Testing
I was able to replicate this bug on ESP32 S2 and ESP32 C3 boards using MicroPython version 1.28.0 through actual socket exhaustion. And on the Unix port as well using the updated ssl_poll.py coverage test.
The Microdot-utilizing project in which I discovered this bug no longer experiences the ERR_CONNECTION_RESET errors anymore after the fix either, but it did before the code changes included in this PR.
I have included an additional check in the ssl_poll.py that ensures the socket was closed exactly once. In the bug-affected builds, this test caught the problem. After testing the code in this PR the test passes cleanly.
NOTE:
I suspect the original ssl_poll.py had an unrelated bug:
The
AssertionError("should have raised")is raised in a way where it immediately gets caught and discarded. I assume the intent was to raise anAssertionError()only whencb()doesn't raise anything. So I changed this function to look like this:Returning plainly if the
cb()function raises as expected, raising anAssertionError()if not. Please let me know if the intent behind the code was different than what I took it to be.Trade-offs and Alternatives
I am not aware of any worthwhile alternative approach that wouldn't only be mitigating the symptoms of this bug - or moving the burden of cleaning up to the Python code (which feels like inappropriately making Python code responsible for C-level lifecycles).
Generative AI
I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.
This PR was created after a long ai-assisted debugging session examining why self-signed certificates caused odd ERR_CONNECTION_RESET failures in chromium-based browsers and not others (like Firefox).
The code included in it was manually adjusted by me and manually reviewed by me and my colleagues.