Skip to content

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

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.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions