Skip to content

Segfault on musl: _modbus_tcp_pi_connect calls freeaddrinfo(NULL) on getaddrinfo failure (mirror of #831) #852

@NeriaIfrah

Description

@NeriaIfrah

libmodbus version

v3.1.12 (also present on current master)

OS and/or distribution

Triggers a hard crash on any musl-libc system. Confirmed reasoning applies to
Alpine Linux, OpenWrt, Void Linux (musl variant), and any Buildroot/Yocto
image built against musl. Latent (silently tolerated) on glibc.

Description

_modbus_tcp_pi_connect in src/modbus-tcp.c calls freeaddrinfo(ai_list)
unconditionally inside the getaddrinfo() failure branch:

https://github.com/stephane/libmodbus/blob/v3.1.12/src/modbus-tcp.c#L440

ai_list = NULL;
rc = getaddrinfo(ctx_tcp_pi->node, ctx_tcp_pi->service, &ai_hints, &ai_list);
if (rc != 0) {
    if (ctx->debug) {
        ...
    }
    freeaddrinfo(ai_list);   /* <-- ai_list may be NULL */
    errno = ECONNREFUSED;
    return -1;
}

Per POSIX, the contents of *res are unspecified when getaddrinfo returns
a non-zero error code, and freeaddrinfo(NULL) is undefined behaviour. In
practice:

  • glibc silently tolerates freeaddrinfo(NULL) — bug is latent.
  • uClibc partially tolerates it.
  • musl dereferences a NULL pointer and produces SIGSEGV.

Relationship to #831 / commit 26dc8a5

This is the exact same defect that was reported in #831 and fixed on master
in commit
26dc8a5
("Check if ai_list is null before freeing it (#831)"), but the fix was
only applied to the server-side mirror function modbus_tcp_pi_listen
.

The client-side mirror _modbus_tcp_pi_connect has byte-for-byte identical
error-handling logic and was overlooked. It is still vulnerable on master
HEAD today.

The original #831 commit message accurately diagnoses the root cause:

Passing null to freeaddrinfo is undefined behaviour. While glibc and
uClibc both handle it safely, musl does not, so a bad nodename or
servname causes a segfault.

That diagnosis applies identically to the client path. This issue tracks
completing the fix.

Steps to reproduce

On any musl-linked build of libmodbus (e.g. an Alpine container):

modbus_t *ctx = modbus_new_tcp_pi("nonexistent.invalid.example", "502");
modbus_set_debug(ctx, TRUE);
modbus_connect(ctx);   /* SIGSEGV inside libmodbus */

Expected behavior

modbus_connect should return -1 with errno == ECONNREFUSED, as the
existing API contract promises.

Actual behavior

Process is killed with SIGSEGV inside _modbus_tcp_pi_connect
freeaddrinfo(NULL). The caller has no opportunity to recover, log, or
fall back to a backup peer.

Real-world impact

The trigger is not a malformed packet — it is a hostname that fails to
resolve, which is routine operational reality:

  • Power-on before DHCP/DNS is ready (industrial gateways)
  • Transient DNS outage during reconnection attempts
  • Operator typo in a configuration file
  • IPv6-only hostname when AI_ADDRCONFIG reports no IPv6 (or vice versa)
  • Invalid service name string

Alpine is the de-facto base image for containerized Modbus gateways;
OpenWrt powers a large share of edge routers and fieldbus bridges. These
are core libmodbus deployment targets, and this bug converts a recoverable
network condition into a hard crash of the host application.

Proposed fix

Apply the identical pattern from 26dc8a5 to _modbus_tcp_pi_connect:

-        freeaddrinfo(ai_list);
+        if (ai_list != NULL) {
+            freeaddrinfo(ai_list);
+        }

I'm happy to open a PR for this — it's a one-line, zero-risk mirror of the
already-accepted fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions