Fix mdns object lifetimes: don't hold pointers we don't own - #11181
Fix mdns object lifetimes: don't hold pointers we don't own#11181dhalbert wants to merge 1 commit into
Conversation
|
@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
left a comment
There was a problem hiding this comment.
Pretty good overall. Just a weird string copy thing
|
|
||
| // Copy a NUL-terminated string into a fixed-size buffer, truncating if it | ||
| // doesn't fit. src may be NULL, which yields an empty string. | ||
| static void copy_string(char *dest, const char *src, size_t dest_len) { |
There was a problem hiding this comment.
Doesn't this exist already? strncpy or some such
There was a problem hiding this comment.
oh yes, I meant to fix that, but there were about a dozen balls in the air.
There was a problem hiding this comment.
Fixed by using strlcpy, which exists in newlib but we never happened to use. The Espressif board still needs a tiny helper strlcpy_or_empty because the source string might be NULL.
|
|
||
| // Copy a NUL-terminated string into a fixed-size buffer, truncating if it | ||
| // doesn't fit. src may be NULL, which yields an empty string. | ||
| static void copy_string(char *dest, const char *src, size_t dest_len) { |
| // Dropping the old storage is enough; the GC reclaims it. Freeing it here | ||
| // could pull it out from under an in-flight srv_txt_cb. |
There was a problem hiding this comment.
Dropping still means the GC could free it early. I think it'd be better to be precise.
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.
d59be34 to
8886852
Compare
🤖 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