Repository navigation
Fix socketpair() on platforms without _socket.socketpair - #334
Conversation
Windows has no _socket.socketpair(), so socket.socketpair() falls back to a pure-Python implementation built on an AF_INET loopback pair. mocket replaced socket.socket and socket.socketpair, so that fallback either resolved to MocketSocket or raised AttributeError, and any asyncio event loop created inside Mocketizer() died before the test body ran. Build the pair from true_socket when the C function is missing, take the accepted descriptor through _accept() so it is not wrapped back into MocketSocket, and authenticate the peer the same way CPython's fallback does since the CVE-2024-3219 fix. socket.socket stays patched, so mocking is unchanged for everything except the loopback pair the event loop makes. Refs kiwicom/pytest-recording#212, kiwicom/pytest-recording#213.
|
Hi @simin-yuan, please have a look at the failing pipeline. |
tests/test_http_gevent.py patches socket at import time, so the hijack has to be installed on mocket.socket.true_socket. Signed-off-by: Simin Yuan <281164480+simin-yuan@users.noreply.github.com>
|
Hi @mindflayer, thanks for approving the run. The failure was in the test, not in the fix: tests/test_http_gevent.py patches socket at import time, so by the time the suite runs, socket.socket is gevent's class and the test's hijack never reached mocket.socket.true_socket. The guard saw our own socket and correctly did not raise. The test now patches true_socket directly, and it still fails if the peer check is removed. |
_loopback_socketpair() connects with setblocking(False) -- the same trick CPython's own fallback uses -- and counts on the accept() that follows to wait for the handshake, because only then is getpeername() meaningful. The hijacked _accept() here returned a foreign peer without waiting, so on Linux csock was still in SYN_SENT when the CVE check called getpeername() and the test failed with ENOTCONN instead of the ConnectionError it is about. Windows connects to the loopback synchronously and never showed it. Drain the real connection inside the hijack, then close it. Signed-off-by: Simin Yuan <281164480+simin-yuan@users.noreply.github.com>
58f8f4d to
373984a
Compare
|
The 3.13 failure was in the test, not the patch: it mocked accept() away, so on Linux the non-blocking connect was still in SYN_SENT when getpeername() ran. The test now drains the real accept first. Full matrix (3.8–3.15, pypy) passes on my fork: https://github.com/simin-yuan/python-mocket/actions/runs/41505783453 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #334 +/- ##
==========================================
+ Coverage 98.85% 98.88% +0.03%
==========================================
Files 22 22
Lines 1224 1261 +37
==========================================
+ Hits 1210 1247 +37
Misses 14 14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Hi @simin-yuan, the only remaining thing is having a look at codecov's results and add what's missing. Many thanks for your contribution! |
The fallback only had its happy path covered, so the checks that guard the loopback pair had nothing behind them: - an address family other than AF_INET/AF_INET6, a socket type other than SOCK_STREAM and a non-zero protocol are refused before any socket is created; - when accept() fails after the client has connected, the client half is closed instead of leaked; - when the peer check added for CVE-2024-3219 rejects the connection, both halves are closed; - _socket.socketpair() is used as is when the interpreter has it. The IPv6 case connects over ::1 and is skipped where IPv6 loopback is not available. Signed-off-by: Simin Yuan <281164480+simin-yuan@users.noreply.github.com>
|
|
Thanks! I pushed 55a62de with tests for the error paths of the loopback socketpair fallback: the IPv6 (::1) branch, invalid arguments (AF_UNSPEC / SOCK_DGRAM / proto=6), closing the client socket when accept fails, closing both sockets on a peer mismatch, and the _socket.socketpair fast path. mocket/socket.py is unchanged in this commit. |



Fixes #333
Summary
Mocketizer()on Windows raisesAttributeError: module '_socket' has no attribute 'socketpair'as soon asasynciobuilds an event loop, becausemock_socketpair()calls the C function unconditionally and that function isPOSIX-only. This builds the loopback pair from the real socket class when the C
function is missing.
Root cause
On Windows
socket.socketpair()is implemented in Python: it binds anAF_INETlistener on127.0.0.1, connects a second socket to it, and returnsthe pair. That implementation resolves the module-level
socketname, whichthe injection layer has already swapped for
MocketSocket, so it cannot buildthe pair while the mock layer is active.
Change
mocket/socket.py:mock_socketpair()keeps using_socket.socketpair()where it exists._loopback_socketpair(), which builds the pair fromtrue_socket, binds127.0.0.1or::1, and takes the accepted descriptorthrough
_accept()so the accepted side is not wrapped intoMocketSocketeither.
CVE-2024-3219 fix:
getpeername()andgetsockname()on both ends have toagree, and a mismatch closes both sockets and raises
ConnectionError.socket.socketstays patched for the whole call, so mocking is unaffectedfor everything except the loopback pair the event loop makes for itself.
Tests
New
tests/test_socketpair.py:test_socketpair_returns_a_real_connected_pair: byte round trip both ways.test_asyncio_event_loop_can_be_created:asyncio.new_event_loop()insideMocketizer().test_socketpair_does_not_disable_the_mock_layer:socket.socketis stillMocketSocketaftersocketpair()returns, and the returned pair is not aMocketSocket.test_mock_socketpair_without_the_c_function: removes_socket.socketpairwith monkeypatch, so the new branch is exercised on Linux CI as well.
test_loopback_socketpair_rejects_a_foreign_peer: a connection from anotherlocal peer is accepted by the listener first, and the call must raise
ConnectionError. Without the authentication step this test fails.New tests:
Existing suite (
test_socket,test_inject,test_asyncio,test_utils,test_mocket,test_compat), same failure set with and without the patch:The remaining failures are pre-existing on Windows and unrelated to this
patch.
ruff checkandruff format --checkpass on the touched files. Nochange on POSIX:
_socket.socketpair()is still the path taken there.Related: kiwicom/pytest-recording#212 and kiwicom/pytest-recording#213. Same
class of bug in another network-blocking tool, where the loopback pair created
by
socketpair()was being treated as an outbound connection.