[BUG] Make SocketAddr string parsing safe and reject malformed addresses#4292
Draft
thc1006 wants to merge 7 commits into
Draft
[BUG] Make SocketAddr string parsing safe and reject malformed addresses#4292thc1006 wants to merge 7 commits into
thc1006 wants to merge 7 commits into
Conversation
This was referenced Jul 24, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
thc1006
force-pushed
the
bugfix/socketaddr-parsing-4290-4291
branch
from
July 24, 2026 17:53
30af5e7 to
71b859b
Compare
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
force-pushed
the
bugfix/socketaddr-parsing-4290-4291
branch
from
July 25, 2026 03:51
71b859b to
78fbad9
Compare
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
force-pushed
the
bugfix/socketaddr-parsing-4290-4291
branch
from
July 25, 2026 08:44
b3fd232 to
7d41313
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
inet_ptonread uninitialized bytes for any host shorter than 15 characters.sizeof(buf), a byte count, on aWCHAR[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, WindowsWSAStringToAddress), both now use the same parser:inet_ptonfor the host (Winsock has provided it since Vista, via<ws2tcpip.h>) plus a strict decimal port. It parses into a localsockaddr_inand commits it into the storage withmemcpyonly on success, so a failed parse leaves the object atAF_UNSPECwithport() == -1. This removes the reasons the old Windows path was fragile (WSAStringToAddressreturnedWSAEINVALunlesssin_familywas 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:
0.0.0.0:<port>; it now leavesAF_UNSPEC, distinguishable from a real endpoint including the legitimate:0.255.255.255.2559) used to truncate into a different valid address; it is now rejected.80junk,80:90), a sign or whitespace (+80,-0,80), and out-of-range values are all rejected on both platforms rather than relying onstrtol's locale-sensitive leniency.Storage layout made explicit
port()/toString()and the parser used to access thesockaddrstorage through asockaddr_inreference (an alignment/type-access issue). They nowmemcpya localsockaddr_inin and out. Threestatic_asserts pin the assumptions this and the socket syscalls rely on:sizeof(sockaddr) == sizeof(sockaddr_in)(IPv4-only, so the length passed toconnect()/bind()is exactly right), the address-family field at the same offset, andsizeof(SocketAddr) == sizeof(sockaddr). The(u_long, int)constructor still uses areinterpret_castand truncates out-of-range ports; it has anaddListeningPortcaller, so it is tracked separately rather than changed here.Build
ws2_32is declared as an interface dependency ofopentelemetry_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.cchas 17 cases. Failures are asserted through anExpectInvalidhelper that checks bothAF_UNSPECandport() == -1, so a corrupted family byte cannot pass, plus positive cases for a legitimate:0, a leading-zero port (080parses as 80), the longest representable host, and the truncation, trailing-garbage and sign rejections above.Verification
-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.socket_tools.his unchanged at 2 pre-existing warnings.inet_ptonat least removes the Windows-specificWSAStringToAddressgrammar and itssin_familyprecondition.