Skip to content

[BUG] Make SocketAddr string parsing safe and reject malformed addresses#4292

Draft
thc1006 wants to merge 7 commits into
open-telemetry:mainfrom
thc1006:bugfix/socketaddr-parsing-4290-4291
Draft

[BUG] Make SocketAddr string parsing safe and reject malformed addresses#4292
thc1006 wants to merge 7 commits into
open-telemetry:mainfrom
thc1006:bugfix/socketaddr-parsing-4290-4291

Conversation

@thc1006

@thc1006 thc1006 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #4290
Fixes #4291
Refs #4287

This started as the two memory-safety fixes for #4290/#4291 and grew, in response to review, into one address parser shared by both platforms with a clear valid/invalid contract. SocketAddr(char const *) has no in-repo caller (the server uses the (u_long, int) overload), so the stricter failure semantics change no existing call site.

The memory-safety bugs (original scope)

  • POSIX terminated the host buffer at a fixed index, so inet_pton read uninitialized bytes for any host shorter than 15 characters.
  • Windows bounded the host copy loop with sizeof(buf), a byte count, on a WCHAR[200], so an address over 200 characters wrote past the end.

The fix: one parser for both platforms

Rather than patch two separate paths (POSIX strtol + inet_pton, Windows WSAStringToAddress), both now use the same parser: inet_pton for the host (Winsock has provided it since Vista, via <ws2tcpip.h>) plus a strict decimal port. It parses into a local sockaddr_in and commits it into the storage with memcpy only on success, so a failed parse leaves the object at AF_UNSPEC with port() == -1. This removes the reasons the old Windows path was fragile (WSAStringToAddress returned WSAEINVAL unless sin_family was preset, and Windows CI is build-only so that path was never run) by not using it at all.

It also fixes the contract issues review surfaced:

  • A failed parse used to leave a usable 0.0.0.0:<port>; it now leaves AF_UNSPEC, distinguishable from a real endpoint including the legitimate :0.
  • An overlong host (255.255.255.2559) used to truncate into a different valid address; it is now rejected.
  • The port grammar is decimal digits only with a pre-multiply overflow check, so trailing garbage (80junk, 80:90), a sign or whitespace (+80, -0, 80), and out-of-range values are all rejected on both platforms rather than relying on strtol's locale-sensitive leniency.

Storage layout made explicit

port()/toString() and the parser used to access the sockaddr storage through a sockaddr_in reference (an alignment/type-access issue). They now memcpy a local sockaddr_in in and out. Three static_asserts pin the assumptions this and the socket syscalls rely on: sizeof(sockaddr) == sizeof(sockaddr_in) (IPv4-only, so the length passed to connect()/bind() is exactly right), the address-family field at the same offset, and sizeof(SocketAddr) == sizeof(sockaddr). The (u_long, int) constructor still uses a reinterpret_cast and truncates out-of-range ports; it has an addListeningPort caller, so it is tracked separately rather than changed here.

Build

ws2_32 is declared as an interface dependency of opentelemetry_ext (CMake and Bazel), so consumers and the test inherit it rather than relinking it; the header's #pragma comment(lib) autolink is MSVC-only and MinGW/GCC ignore it.

Tests

socket_tools_test.cc has 17 cases. Failures are asserted through an ExpectInvalid helper that checks both AF_UNSPEC and port() == -1, so a corrupted family byte cannot pass, plus positive cases for a legitimate :0, a leading-zero port (080 parses as 80), the longest representable host, and the truncation, trailing-garbage and sign rejections above.

Verification

  • 17 tests pass under -fsanitize=address,undefined.
  • inet_pton's rejection of the shorthand (127.1) and leading-zero (01.02.03.004) host forms was confirmed empirically on glibc.
  • clang-format, cmake-format (with the repo config) and buildifier clean; clang-tidy 18 on socket_tools.h is unchanged at 2 pre-existing warnings.
  • Windows CI is build-only, so a reviewer confirming a real Windows parse would be worth more than my word; using inet_pton at least removes the Windows-specific WSAStringToAddress grammar and its sin_family precondition.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.29%. Comparing base (cf62199) to head (b3fd232).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4292      +/-   ##
==========================================
+ Coverage   81.25%   81.29%   +0.05%     
==========================================
  Files         446      446              
  Lines       18872    18906      +34     
==========================================
+ Hits        15332    15368      +36     
+ Misses       3540     3538       -2     
Files with missing lines Coverage Δ
...clude/opentelemetry/ext/http/server/socket_tools.h 96.20% <100.00%> (+2.20%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@thc1006
thc1006 force-pushed the bugfix/socketaddr-parsing-4290-4291 branch from 30af5e7 to 71b859b Compare July 24, 2026 17:53
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Jul 25, 2026
Review of open-telemetry#4294 noted the setsockopt return value was discarded. On a
platform whose only defence is SO_NOSIGPIPE (no MSG_NOSIGNAL) a silent
failure would leave writes able to raise SIGPIPE, so it is logged now.
Kept as a warning rather than closing the socket: the listening socket
cannot refuse itself, and on every platform CI builds MSG_NOSIGNAL is
present so send() is protected regardless, which makes the accept-time
failure path untestable in CI.

Also corrected the comment. Modern Linux, macOS and BSD all define
MSG_NOSIGNAL, so SO_NOSIGPIPE is a second layer covering writes that do
not go through send() rather than the only mechanism.

The closed-peer regression test the review asks for depends on the
socket_tools_test.cc target introduced by open-telemetry#4292 and will be added on top
of it. A local harness confirmed the write must exceed the socket buffer
to trigger the broken pipe deterministically; a small write is buffered
and does not, which the test must account for.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Jul 25, 2026
Addresses review of open-telemetry#4292.

Windows: WSAStringToAddress fails with WSAEINVAL unless the sockaddr's
family is preset, so the old parser left AF_UNSPEC and every address
failed to parse. sin_family is now set before the call. Windows CI is
build-only, so this path was compiled but never run.

A failed parse now leaves an unusable address (AF_UNSPEC, port() == -1)
instead of a plausible fallback that connect()/bind() would accept. That
lets callers tell a parse failure from a real endpoint, including the
legitimate ":0".

POSIX host handling rejects a host longer than the buffer instead of
truncating it into a different valid address ("255.255.255.2559" no
longer becomes "255.255.255.255"), and the port parse now requires the
whole string to be consumed, so ":80junk" and ":80:90" are rejected
rather than silently truncated to the leading number.

Tests assert port() == -1 for rejection rather than == 0, which could
not tell a failure from a valid ":0", and cover the truncation,
trailing-garbage and invalid-host cases the earlier tests missed.

The test target links ws2_32 explicitly on Windows so it also builds
under MinGW/GCC, where the header's #pragma comment(lib) autolink is
ignored.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006
thc1006 force-pushed the bugfix/socketaddr-parsing-4290-4291 branch from 71b859b to 78fbad9 Compare July 25, 2026 03:51
@thc1006 thc1006 changed the title [BUG] Make SocketAddr string parsing memory safe on Windows and POSIX [BUG] Make SocketAddr string parsing safe and reject malformed addresses Jul 25, 2026
thc1006 added 7 commits July 25, 2026 16:42
SocketAddr(char const *) had one memory bug per platform.

On Windows the copy loop was bounded by sizeof(buf), which is a byte
count, while indexing a WCHAR array, so an address longer than 200
characters wrote up to 400 bytes past the end. It also assigned a
possibly negative char straight to a WCHAR.

On POSIX the host was copied into an uninitialized char[16] that was
terminated at a fixed index, so inet_pton read indeterminate bytes for
any host shorter than 15 characters. With a dirtied stack this silently
resolves "1.2.3.4:80" to 0.0.0.0:80.

Both now bound and terminate by what was actually copied, and the
WSAStringToAddressW and inet_pton results are no longer discarded.

Adds ext/test/http/socket_tools_test.cc, the first in-repo caller of
this constructor.

Fixes open-telemetry#4290
Fixes open-telemetry#4291

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Addresses review of open-telemetry#4292.

Windows: WSAStringToAddress fails with WSAEINVAL unless the sockaddr's
family is preset, so the old parser left AF_UNSPEC and every address
failed to parse. sin_family is now set before the call. Windows CI is
build-only, so this path was compiled but never run.

A failed parse now leaves an unusable address (AF_UNSPEC, port() == -1)
instead of a plausible fallback that connect()/bind() would accept. That
lets callers tell a parse failure from a real endpoint, including the
legitimate ":0".

POSIX host handling rejects a host longer than the buffer instead of
truncating it into a different valid address ("255.255.255.2559" no
longer becomes "255.255.255.255"), and the port parse now requires the
whole string to be consumed, so ":80junk" and ":80:90" are rejected
rather than silently truncated to the leading number.

Tests assert port() == -1 for rejection rather than == 0, which could
not tell a failure from a valid ":0", and cover the truncation,
trailing-garbage and invalid-host cases the earlier tests missed.

The test target links ws2_32 explicitly on Windows so it also builds
under MinGW/GCC, where the header's #pragma comment(lib) autolink is
ignored.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Second review round. The Windows and POSIX paths were different parsers,
so they could not guarantee the same grammar: WSAStringToAddress fills
missing components with defaults and its provider grammar differs from
the POSIX split, and the Windows copy loop also truncated input over 199
characters before parsing it.

Both platforms now share one parser: a null check, inet_pton for the
host (Winsock has provided it since Vista, via <ws2tcpip.h>), and a
decimal-only port scan that rejects the sign and whitespace strtol would
have accepted and checks overflow before it can wrap. The result is
parsed into a local sockaddr_in and committed with memcpy only on
success, so a failure leaves AF_UNSPEC and the storage is never accessed
through a misaligned sockaddr_in glvalue. port() and toString() copy out
the same way, which removes the type-access/alignment UB there too
(confirmed with UBSan at a 2-mod-4 address); the storage type is
unchanged, so this is ABI preserving.

Because the parser is now platform independent it is fully exercised by
the Linux test run rather than only compiled on Windows. New tests cover
null, empty host/port, sign and whitespace, overflow, and non-dotted-quad
hosts (127.1, leading zeros).

The integer SocketAddr(u_long, int) overload still narrows an
out-of-range port; that has a real caller (addListeningPort) so it is
left for its own issue rather than widened into this change.

Fixes open-telemetry#4290
Fixes open-telemetry#4291

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The char* parser memcpys a sockaddr_in into the sockaddr storage and the
socket syscalls pass sizeof(SocketAddr) as the length, both of which
assume sockaddr and sockaddr_in share a size and family offset. Make
those assumptions static_asserts so a hostile ABI fails to compile
rather than corrupting memory at run time.

Assert the address family in the tests, not just port(): a rejected
input must stay AF_UNSPEC and a legitimate ":0" must parse as AF_INET,
so the two states can never be confused by a stray family byte. The
negative assertions move into an ExpectInvalid helper, and the
non-dotted-quad comment now scopes its "rejected everywhere" claim to
the BIND-derived inet_pton parsers (glibc, musl, macOS, Winsock) that
were verified to reject the shorthand and leading-zero forms.

Declare ws2_32 as an interface dependency of opentelemetry_ext in both
CMake and Bazel so consumers and the test inherit it, instead of each
test target relinking it and papering over the public target's missing
usage requirement.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Require sockaddr and sockaddr_in to have identical size, not just
sockaddr large enough: with sizeof(SocketAddr) == sizeof(sockaddr), the
length connect()/bind() pass is then exactly sizeof(sockaddr_in), which
avoids a too-large address length that POSIX may reject with EINVAL.

Drop the redundant ws2_32 linkopt from the Bazel socket_tools_test; it
is inherited from //ext:headers, so the test now verifies the public
target's usage requirements rather than relinking. Drop the unverifiable
"BIND-derived" lineage claim from the non-dotted-quad test comment and
just state inet_pton's canonical-form contract, and add a test showing a
leading-zero port ("080") parses as 80. Reflow the two CMakeLists
comments to satisfy cmake-format.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006
thc1006 force-pushed the bugfix/socketaddr-parsing-4290-4291 branch from b3fd232 to 7d41313 Compare July 25, 2026 08:44
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Jul 25, 2026
Review of open-telemetry#4294 noted the setsockopt return value was discarded. On a
platform whose only defence is SO_NOSIGPIPE (no MSG_NOSIGNAL) a silent
failure would leave writes able to raise SIGPIPE, so it is logged now.
Kept as a warning rather than closing the socket: the listening socket
cannot refuse itself, and on every platform CI builds MSG_NOSIGNAL is
present so send() is protected regardless, which makes the accept-time
failure path untestable in CI.

Also corrected the comment. Modern Linux, macOS and BSD all define
MSG_NOSIGNAL, so SO_NOSIGPIPE is a second layer covering writes that do
not go through send() rather than the only mechanism.

The closed-peer regression test the review asks for depends on the
socket_tools_test.cc target introduced by open-telemetry#4292 and will be added on top
of it. A local harness confirmed the write must exceed the socket buffer
to trigger the broken pipe deterministically; a small write is buffered
and does not, which the test must account for.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] SocketAddr(char const *) reads uninitialized bytes when parsing host:port [BUG] Out-of-bounds write in SocketAddr(char const *) on Windows

1 participant