Fix mdns object lifetimes: don't hold pointers we don't own - #11181
Merged
Merged
Conversation
Collaborator
Author
|
@thzinc — the TXT record changes here touch the code you added in #8262, EDIT: This comment was written by Claude before I could review it. @thzinc Don't worry about the changes, they fix problems, just want to keep you informed. - @dhalbert Two things:
|
tannewt
requested changes
Aug 6, 2026
tannewt
left a comment
Member
There was a problem hiding this comment.
Pretty good overall. Just a weird string copy thing
mdns.RemoteService and mdns.Server both stored borrowed pointers whose owners could die first. Copy the data out instead. RemoteService held an IDF mdns_result_t allocated on the IDF heap. GC finalisers run in heap-address order, and the Server is allocated before the RemoteServices that find() returns, so at VM teardown the Server's __del__ ran first: mdns_free() deletes _mdns_service_semaphore, and the subsequent mdns_query_results_free() then called xSemaphoreTake(NULL) and hard faulted. Copy the fields we expose into the object at find() time and free the IDF results immediately, mirroring what raspberrypi already did. RemoteService then needs no finaliser at all, so the ordering problem stops existing rather than being worked around. mdns_server_obj_t stored the hostname and instance_name pointers handed to it by mp_obj_str_get_str(). Those point into the GC heap, which is recycled on VM reset while the web workflow's static mdns_server_obj_t lives on, so /cp/version.json served whatever landed in that memory next -- often fragments of REPL input. Store fixed-size copies instead. On raspberrypi, advertise_service() stashed borrowed txt_records pointers that lwip dereferences later from srv_txt_cb() at packet-build time, so a collection between advertising and being queried published freed memory. Pack owned copies into a single buffer. TXT records can only arrive through the Python binding, so the GC heap is necessarily available; assign_txt_records() carries a warning explaining what would break that assumption. Also make both ports reject TXT records they can't honour instead of silently truncating: more than 32 raises ValueError on raspberrypi, and espressif raises NotImplementedError rather than accepting and discarding them. Fix the advertise_service docstring signature, which omitted txt_records even though the parameter list documented it.
dhalbert
force-pushed
the
mdns-lifetime-fixes
branch
from
August 6, 2026 17:37
d59be34 to
8886852
Compare
Collaborator
Author
|
I didn't expect Claude to amend the commit and |
srv_txt_cb() runs from the lwip IRQ -- raspberrypi builds cyw43_arch_threadsafe_background -- so it can preempt the VM thread part way through reading self->txt_records[]. Zeroing num_txt_records first doesn't help a callback that already loaded a nonzero count. Build the replacement buffer first, so the allocation and any MemoryError stay outside the lock, then swap the pointers under MICROPY_PY_LWIP_ENTER/EXIT and free the old storage once nothing can reference it. Dropping the reference and leaving it to the GC, as before, did not avoid that hazard -- it only deferred the free to an unpredictable moment.
Collaborator
Author
|
The close/open of the PR was to get the CI unstuck after GitHub's troubles today. |
|
Thanks for fixing this! I just recently tried to update my device and ran into #10048 again! |
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.
🤖 Generated with Claude Code
This originally started as a simple fix for #10197 to copy out the IDF-held values on
espressif, as was done inraspberrypi, but Claude pointed out several other problems that were all addressed here. - @dhalbertmdns.RemoteServiceandmdns.Serverboth stored borrowed pointers whose owners could die first. This copies the data out instead.RemoteService held IDF-owned memory (#10197)
RemoteServiceheld anmdns_result_tallocated on the IDF heap. GC finalisers run in heap-address order, and theServeris allocated before theRemoteServices thatfind()returns, so at VM teardown the Server's__del__ran first:mdns_free()deletes_mdns_service_semaphoreand NULLs it, and the subsequentmdns_query_results_free()then calledxSemaphoreTake(NULL, portMAX_DELAY)and hard faulted.There is no general way to order two finalisers —
gc_sweep_run_finalisers()is a single pass in block-address order with no dependency information — so rather than work around the ordering, this removes the dependency.find()copies the fields we expose into the object and frees the IDF results immediately, mirroring what the raspberrypi port already did.RemoteServicethen needs no finaliser at all.Server held GC-heap strings across VM resets (#10048)
mdns_server_obj_tstored thehostnameandinstance_namepointers handed to it bymp_obj_str_get_str(). Those point into the GC heap, which is recycled on VM reset while the web workflow's staticmdns_server_obj_tlives on, so/cp/version.jsonserved whatever landed in that memory next — often fragments of REPL input. Both are fixed-size copies now.raspberrypi published freed TXT records
advertise_service()stashed borrowedtxt_recordspointers, but lwip stores onlysrv_txt_cbplus the object and dereferences them later at packet-build time. A collection between advertising and being queried published freed memory onto the wire. This packs owned copies into a single buffer.TXT records can only arrive through the Python binding, so the GC heap is necessarily available and the object shares its lifetime;
assign_txt_records()carries a comment explaining what would break that assumption. This path dates to #8262.Reject TXT records that can't be honoured
Previously more than 32 records were silently truncated on raspberrypi, and espressif accepted and discarded them entirely. Now raspberrypi raises
ValueError: txt_records length must be <= 32and espressif raisesNotImplementedError: txt_records. Both reuse existing translatable strings, solocale/is unchanged.Also fixes the
advertise_service()docstring signature, which omittedtxt_recordseven though the parameter list documented it.Testing
Verified on hardware, Metro ESP32-S3 and Pico 2 W:
find()then Ctrl-Dversion.jsongc.collect()before being queriedavahi-browseValueErrorNotImplementedError