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.
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_connectinsrc/modbus-tcp.ccallsfreeaddrinfo(ai_list)unconditionally inside the
getaddrinfo()failure branch:https://github.com/stephane/libmodbus/blob/v3.1.12/src/modbus-tcp.c#L440
Per POSIX, the contents of
*resare unspecified whengetaddrinforeturnsa non-zero error code, and
freeaddrinfo(NULL)is undefined behaviour. Inpractice:
freeaddrinfo(NULL)— bug is latent.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_connecthas byte-for-byte identicalerror-handling logic and was overlooked. It is still vulnerable on
masterHEAD today.
The original #831 commit message accurately diagnoses the root cause:
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):
Expected behavior
modbus_connectshould return-1witherrno == ECONNREFUSED, as theexisting 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, orfall 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:
AI_ADDRCONFIGreports no IPv6 (or vice versa)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:I'm happy to open a PR for this — it's a one-line, zero-risk mirror of the
already-accepted fix.