May 2, 2023·6 min read·engineeringcmusldnslinking

musl 1.2.4 adds the missing DNS transport fallback

TCP fallback lets musl handle truncated DNS replies, while RELR support and large-file interface changes expose different runtime compatibility boundaries.

A DNS server can answer a UDP query by indicating that the full response does not fit. A resolver then needs another way to obtain the answer. musl 1.2.4 adds TCP fallback to its DNS stub resolver, removing a longstanding limitation that affected large records and servers that return no usable partial answer.

Rich Felker's release announcement explains both cases. The message appears in the May 2 archive and was sent late on May 1 in its sender's time zone. It describes the change as fixing the inability to query large DNS records and incompatibility with recursive servers that do not supply partial results in truncated UDP responses.

C's role in interoperation is one reason I continue to value it. This release is a good example of that role extending well beyond calling conventions. An application can issue an ordinary library lookup and still depend on the library handling the network protocol's response conditions correctly.

Truncation triggers another exchange

The 1.2.4 resolver implementation shows how the fallback fits into the existing query machinery. The resolver initially uses a nonblocking datagram socket. It sends unresolved queries to configured name servers and waits for replies through a polling loop.

A received reply must pass several checks before it can supply an answer. The implementation ignores packets too short to identify and responses from addresses to which it did not send. It also matches the response identifier to an outstanding query. These checks remain relevant when discussing fallback because a transport change must follow a reply associated with the actual lookup.

The new branch checks the DNS truncation bit and the socket's MSG_TRUNC indication. If either reports truncation, the resolver marks that query as awaiting TCP and starts a connection to the responding server. This is a targeted retry of the affected query, with the query's state tracked separately from other outstanding requests.

The implementation therefore does not replace every DNS request with TCP. Ordinary answers can complete through the existing datagram path. The extra connection occurs when truncation makes another exchange necessary. That preserves a distinction between the common lookup path and the path required to retrieve an answer that cannot arrive in the first response.

The announcement's mention of recursive servers is especially useful. A truncated reply need not contain a partial answer that an application can use. Depending on partial contents made resolver behavior sensitive to what a particular server chose to include. TCP fallback supplies a way to obtain the response without requiring that accommodation.

A stream needs message boundaries

The TCP helper opens a nonblocking stream socket and prepares the query with a two-byte length prefix. The query's length and its body are separate buffers in a message structure. This is visible protocol framing, since a stream transport cannot use a UDP datagram boundary to delimit the DNS message.

The resolver tracks how many query bytes have been sent. If only part of a message has gone out, its helper advances the buffer description before the next write. Once the complete prefix and query have been sent, the polling interest changes to reading. A connection becoming writable is therefore only one stage in completing the fallback.

Response handling has the same need to track progress. The code receives a two-byte length field and the answer data, then waits until enough bytes have arrived for the framed response or the available answer buffer. It checks the response code before committing the answer length. These details explain why adding fallback involves more than calling connect after a truncated packet.

Resource handling is part of the mechanism too. Once the implementation accepts a TCP answer, it closes that query's socket immediately. A cleanup handler closes remaining descriptors, and unfinished TCP results are discarded at the end. The resolver must account for partial exchanges without returning them as completed lookup results.

The source also attempts TCP Fast Open when its socket configuration permits it, then retains an ordinary connection path. That optimization does not change the framing requirement or the need to account for partial I/O. The resolver still tracks the bytes belonging to the prefix and the query before switching to response processing.

For me, these implementation details are more informative than a general claim that DNS now works better. The missing behavior was a specific protocol transition. The new code adds the connection, framing, and completion state required to perform that transition within the resolver's existing polling machinery.

Error distinctions survive into the API

The release announcement also says both modern and legacy lookup APIs now distinguish NODATA from NxDomain conditions. That gives callers information they previously could not handle separately through those results. A missing answer of a particular kind and a nonexistent name should not automatically collapse into the same application decision.

This improvement is related to fallback but addresses a different boundary. TCP changes how the resolver obtains a complete reply. Distinguishing lookup outcomes changes what information reaches the caller after the resolver interprets the reply. A successful transport exchange does not by itself imply that the requested data exists.

The distinction matters for application control flow. A caller may have a legitimate reason to treat different negative outcomes differently. Preserving the information makes that possible without requiring the application to replace the system resolver merely to recover the difference.

There is also an operational consequence of adding another transport. A lookup that requires fallback now has to establish the corresponding TCP exchange with the server. The implementation's behavior makes that requirement concrete. Receiving a truncated UDP response alone is not enough to complete that path if the connection cannot proceed.

The source uses the configured timeout and retry machinery around its polling loop. An incomplete stream result does not become a partial success at cleanup. That protects the caller from mistaking a fragment for a completed DNS response, while leaving failure visible when the exchange cannot finish.

RELR changes the loader requirement

musl 1.2.4 adds support for compact RELR relative relocations in its dynamic linker and static PIE entry code. The release announcement explicitly separates the deployment consequences for dynamically linked programs and self-contained static PIE binaries.

A dynamically linked program built using this linker output requires musl 1.2.4 or later at runtime. The executable can therefore acquire a new runtime requirement through its relocation format even if its application-level function calls remain familiar. Looking only at the list of called C interfaces would miss that dependency.

A static PIE binary containing the relevant startup support has no corresponding dependency on an installed musl runtime, according to the announcement. The distinction follows where the relocation-processing code comes from. A self-contained binary includes what it needs, while a dynamically linked program relies on the runtime available when it starts.

This is a separate compatibility issue from DNS behavior. The resolver change improves a runtime service used by an existing program. RELR allows a different representation in newly linked output, which makes the runtime's ability to interpret that representation part of deployment compatibility.

Legacy large-file names become opt-in

The API change concerns the legacy LFS64 names. musl had provided macros mapping names such as stat64 onto their standard equivalents. The release deprecates these interfaces and stops exposing them under _GNU_SOURCE. They remain available with an explicit _LARGEFILE64_SOURCE request.

Felker gives -D_LARGEFILE64_SOURCE as a short-term build repair and asks projects to move to the standard interfaces. This is a source-compatibility change in the selected feature profile. It should not be mistaken for a claim that musl has stopped supporting large files.

The temporary define expresses dependency on the legacy names. It can allow a build to proceed while the source still uses them, but it does not perform the source migration described in the announcement. A lasting change uses the standard interface names that those macros had been mapping onto.

I prefer that explicit account of compatibility. The release identifies which names change visibility and states the temporary accommodation. Combined with the RELR explanation, it gives two distinct questions for a build, whether its source relies on legacy aliases and whether its output requires a newer loader.

The DNS addition addresses another part of the same runtime responsibility. Applications should be able to rely on a resolver that can complete the protocol's truncation path. In 1.2.4, that path now has explicit TCP connection state, framed I/O, and cleanup for incomplete exchanges, while complete ordinary UDP replies retain their existing route through the resolver.