robustness: cache mutates shared `DNSRecord` instances in place to expire TTL
Problem
async_mark_unique_records_older_than_1s_to_expire walks the cache and calls _async_set_created_ttl(record, now, 1), which mutates record.created and record.ttl in place via record._set_created_ttl(...), then re-_async_adds the same object. The code itself flags this — line 345 says "It would be better if we made a copy instead of mutating the record in place, but records currently don't have a copy method." Listeners (RecordUpdate.old references obtained from cache.async_get_unique elsewhere in the same dispatch tick), ServiceInfo instances holding references to cached records, and anything in _handlers/multicast_outgoing_queue.AnswerGroup.answers that keys on a DNSAddress/DNSPointer instance will silently see their TTL/created flip to the new values mid-dispatch. The RFC-mandated "expire in 1s" path (RFC 6762 §10.2) is therefore not isolated from in-flight consumers.
Why This Matters
Subtle correctness drift in hot path: any code path that captured a DNSRecord reference and decided "this record is fresh, use it" can have the same object turn stale between two of its own lines. It's also hostile to free-threading (3.14t) — concurrent reads of record.created / record.ttl while another thread is in the middle of _set_created_ttl are an unsynchronized write race.
Suggested Fix
Add a copy_with_ttl(now, ttl) (or _cdef-typed _clone_with_created_ttl) method on DNSRecord (and update the .pxd so the new method is visible on the cython hot path), then in _async_set_created_ttl replace the in-place mutation with replacement = record.copy_with_ttl(now, 1); self._async_add(replacement). The old record will fall out of the cache when the new one supersedes it via async_add_records. Update the comment to remove the "It would be better" footnote once done.
Details
| Severity | 🟡 Medium |
| Category | robustness |
| Location | src/zeroconf/_cache.py:325-348 |
| Effort | 🛠️ Moderate effort |
🤖 Created by Kōan from audit session