Skip to content

LEAmDNS: _parseResponse keeps every answer record of a packet on the heap, so one large answer can abort with out of memory #9343

Open

Description

@sanerdemirel

What happens

An ESP8266 (core 3.1.2, a GeekMagic SmallTV-Ultra with about 6 KB of free heap while running) restarted after
11.4 hours with an out-of-memory abort inside LEAmDNS. Our crash record, decoded with addr2line:

operator new (288 bytes) failed
  <- MDNSResponder::_readRRAnswer
  <- MDNSResponder::_parseResponse
  <- MDNSResponder::_parseMessage
  <- MDNSResponder::_process
free heap 952 bytes, largest free block 248 bytes

The lowest values recorded before, during those hours, were about 4.4 KB free and a 1.5 KB largest block. So
about 3.5 KB went within a moment. The sketch only advertises (MDNS.begin, addService, addServiceTxt, and
MDNS.update() in loop()); it sends no queries.

Why, from reading the code (master, libraries/ESP8266mDNS/src/LEAmDNS_Control.cpp)

  • _parseResponse reads answers only while a service query waits or a probe runs (line 742-743). Probing runs
    after MDNS.begin and again after every interface status change (LEAmDNS.cpp, the statusChangeCB that
    calls _restart()), for example a Wi-Fi reconnect.
  • In that state, every record of the incoming packet (ANCount + NSCount + ARCount, taken from the packet
    header) is read with _readRRAnswer into its own heap object and kept in a list (lines 764-775).
  • The list goes to _processAnswers as a whole (line 794) and is freed only after that (lines 804-810).

So the heap needed grows with the number of records in someone else's answer. A phone, TV or speaker that
answers with many records during the probe window can take several KB at once. new aborts on failure in this
core, so the library's own checks for a failed allocation never run, and the result is a restart.

What we did not measure: we have not captured the packet, and we don't know for sure that a reconnect came just
before. That part is our reading of the code.

_parseQuery handles known answers differently: it frees each record right after reading it (so one record at a
time), which is why we think the same can work for responses.

Possible fix

Handle each answer record in _parseResponse as soon as it is read, and free it before reading the next, so the
peak is one record instead of the whole packet. That means splitting _processAnswers so it can take one record
at a time. A simpler, partial alternative: skip (read past without allocating) records whose names cannot
concern this responder.

We are trying the first approach in a patched copy for our own firmware and can open a PR if this direction is
welcome.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions