Skip to content
Prev Previous commit
Next Next commit
fix: Close the dual-stack socket on bind failure, cache IPv6 probe
An unguarded bind()/listen() leaked the socket fd on failure; wrap the
setup in try/except so it closes before re-raising. Also cache the
IPv6 support probe with lru_cache since it can't change at runtime.

Signed-off-by: dbbvitor <vitor.diniz@gympass.com>
  • Loading branch information
dbbvitor authored and ntkathole committed Oct 1, 2026
commit 3c936623d0a5f079af83376e60d074d22d89d15d
42 changes: 21 additions & 21 deletions sdk/python/feast/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import functools
import itertools
import logging
import os
Expand Down Expand Up @@ -64,13 +65,9 @@
USER_AGENT = "{}/{}".format(APPLICATION_NAME, get_version())


@functools.lru_cache()
def _ipv6_available() -> bool:
"""True if this host can bind a dual-stack IPv6 wildcard socket.

Some kernels (e.g. booted with ``ipv6.disable=1``) raise ``EAFNOSUPPORT``
for ``socket.AF_INET6``, so servers that want to bind dual-stack must
probe first and fall back to IPv4-only.
"""
"""Whether this process can bind AF_INET6 (cached: can't change at runtime)."""
try:
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: IPv6 support can't change during a process lifetime — caching with @functools.lru_cache() would make that intent explicit and skip the throwaway socket on repeated calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added @functools.lru_cache() decorator to the function.

Expand All @@ -81,27 +78,30 @@ def _ipv6_available() -> bool:


def _make_dual_stack_socket(port: int) -> socket.socket:
"""Build a listening socket bound dual-stack ("::") when the host supports
IPv6, or IPv4-only ("0.0.0.0") otherwise (e.g. ``ipv6.disable=1`` kernels).

A plain ``host="::"`` passed to a framework's own server (uvicorn's
``uvicorn.run``, asyncio's ``loop.create_server``) gets ``IPV6_V6ONLY=1``
set on the socket it creates for itself, which silently drops IPv4
clients on every host, not just IPv6-less ones. Binding the socket here,
with ``IPV6_V6ONLY`` explicitly cleared, and handing it to the server
pre-built avoids that.
"""Pre-bind a non-blocking dual-stack socket for a server to adopt.

A framework handed host="::" directly (uvicorn.run, loop.create_server)
sets IPV6_V6ONLY=1 on its own socket, silently dropping IPv4 clients.
Binding here with V6ONLY cleared avoids that; falls back to IPv4-only
when the host can't bind AF_INET6.
"""
if _ipv6_available():
dual_stack = _ipv6_available()
if dual_stack:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
address: tuple = ("::", port)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address = ("0.0.0.0", port)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(socket.SOMAXCONN)
sock.setblocking(False)
try:
if dual_stack:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(address)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
sock.listen(socket.SOMAXCONN)
sock.setblocking(False)
except Exception:
sock.close()
raise
return sock

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Socket leak: if bind() or listen() throws (e.g. port already in use), the socket is never closed. Wrap the post-creation steps:

try:
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(socket.SOMAXCONN)
    sock.setblocking(False)
except:
    sock.close()
    raise

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try-except pattern adopted



Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setblocking(False) bakes in an asyncio assumption. The function name is generic, but a caller expecting a blocking socket would fail silently. Consider either documenting this in the docstring or adding a blocking=False parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the blocking parameter to the func, defaulting to False, and added it to the docstring.

Expand Down
24 changes: 18 additions & 6 deletions sdk/python/tests/unit/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2025,20 +2025,32 @@ def test_metrics_httpd_falls_back_to_ipv4_without_ipv6():
def test_ipv6_available_false_when_af_inet6_unsupported():
from feast.utils import _ipv6_available

with patch(
"socket.socket", side_effect=OSError(97, "Address family not supported")
):
assert _ipv6_available() is False
# _ipv6_available is @lru_cache'd (IPv6 support can't change mid-process).
# Clear before *and* after: before, so this test isn't seeing another
# test's cached result; after, so this test's mocked result doesn't leak
# into a later test that expects a real, uncached probe.
_ipv6_available.cache_clear()
try:
with patch(
"socket.socket", side_effect=OSError(97, "Address family not supported")
):
assert _ipv6_available() is False
finally:
_ipv6_available.cache_clear()


def test_ipv6_available_true_uses_v6only_off_and_wildcard_bind():
from feast.utils import _ipv6_available

_ipv6_available.cache_clear()
mock_sock = MagicMock()
mock_sock.__enter__.return_value = mock_sock
mock_sock.__exit__.return_value = False
with patch("socket.socket", return_value=mock_sock) as mock_socket_cls:
assert _ipv6_available() is True
try:
with patch("socket.socket", return_value=mock_sock) as mock_socket_cls:
assert _ipv6_available() is True
finally:
_ipv6_available.cache_clear()

mock_socket_cls.assert_called_once_with(socket.AF_INET6, socket.SOCK_STREAM)
mock_sock.setsockopt.assert_called_once_with(
Expand Down
12 changes: 12 additions & 0 deletions sdk/python/tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,18 @@ def test_make_dual_stack_socket_falls_back_to_ipv4():
mock_sock.bind.assert_called_once_with(("0.0.0.0", 6580))


@pytest.mark.parametrize("ipv6_available", [True, False])
def test_make_dual_stack_socket_closes_on_bind_failure(ipv6_available):
mock_sock = MagicMock()
mock_sock.bind.side_effect = OSError("address in use")
with patch("feast.utils._ipv6_available", return_value=ipv6_available):
with patch("socket.socket", return_value=mock_sock):
with pytest.raises(OSError):
_make_dual_stack_socket(6580)

mock_sock.close.assert_called_once()


def test_make_dual_stack_socket_is_reachable_on_both_families():
# Regression test: a framework's own host="::" (uvicorn.run,
# asyncio.loop.create_server) binds IPv6-only, because IPV6_V6ONLY=1
Expand Down