Skip to content

Commit 38890e6

Browse files
authored
Accept abstract namespace paths for unix domain sockets (#782)
Accept paths starting with a null byte in create_unix_listener() and connect_unix_socket() to allow creating abstract UNIX sockets. Fixes #781.
1 parent 0c8ad51 commit 38890e6

3 files changed

Lines changed: 114 additions & 40 deletions

File tree

docs/versionhistory.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
3737
- Fixed quitting the debugger in a pytest test session while in an active task group
3838
failing the test instead of exiting the test session (because the exit exception
3939
arrives in an exception group)
40+
- Fixed support for Linux abstract namespaces in UNIX sockets that was broken in v4.2
41+
(#781 <https://github.com/agronholm/anyio/issues/781>_; PR by @tapetersen)
4042

4143
**4.4.0**
4244

src/anyio/_core/_sockets.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -680,19 +680,26 @@ async def setup_unix_local_socket(
680680
:param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM
681681
682682
"""
683-
path_str: str | bytes | None
683+
path_str: str | None
684684
if path is not None:
685-
path_str = os.fspath(path)
686-
687-
# Copied from pathlib...
688-
try:
689-
stat_result = os.stat(path)
690-
except OSError as e:
691-
if e.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EBADF, errno.ELOOP):
692-
raise
693-
else:
694-
if stat.S_ISSOCK(stat_result.st_mode):
695-
os.unlink(path)
685+
path_str = os.fsdecode(path)
686+
687+
# Linux abstract namespace sockets aren't backed by a concrete file so skip stat call
688+
if not path_str.startswith("\0"):
689+
# Copied from pathlib...
690+
try:
691+
stat_result = os.stat(path)
692+
except OSError as e:
693+
if e.errno not in (
694+
errno.ENOENT,
695+
errno.ENOTDIR,
696+
errno.EBADF,
697+
errno.ELOOP,
698+
):
699+
raise
700+
else:
701+
if stat.S_ISSOCK(stat_result.st_mode):
702+
os.unlink(path)
696703
else:
697704
path_str = None
698705

tests/test_sockets.py

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
has_ipv6 = True
8484

8585
skip_ipv6_mark = pytest.mark.skipif(not has_ipv6, reason="IPv6 is not available")
86+
skip_unix_abstract_mark = pytest.mark.skipif(
87+
not sys.platform.startswith("linux"),
88+
reason="Abstract namespace sockets is a Linux only feature",
89+
)
8690

8791

8892
@pytest.fixture
@@ -735,12 +739,20 @@ async def test_bind_link_local(self) -> None:
735739
sys.platform == "win32", reason="UNIX sockets are not available on Windows"
736740
)
737741
class TestUNIXStream:
738-
@pytest.fixture
739-
def socket_path(self) -> Generator[Path, None, None]:
742+
@pytest.fixture(
743+
params=[
744+
"path",
745+
pytest.param("abstract", marks=[skip_unix_abstract_mark]),
746+
]
747+
)
748+
def socket_path(self, request: SubRequest) -> Generator[Path, None, None]:
740749
# Use stdlib tempdir generation
741750
# Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path
742751
with tempfile.TemporaryDirectory() as path:
743-
yield Path(path) / "socket"
752+
if request.param == "path":
753+
yield Path(path) / "socket"
754+
else:
755+
yield Path(f"\0{path}") / "socket"
744756

745757
@pytest.fixture(params=[False, True], ids=["str", "path"])
746758
def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str:
@@ -764,7 +776,15 @@ async def test_extra_attributes(
764776
assert (
765777
stream.extra(SocketAttribute.local_address) == raw_socket.getsockname()
766778
)
767-
assert stream.extra(SocketAttribute.remote_address) == str(socket_path)
779+
remote_addr = stream.extra(SocketAttribute.remote_address)
780+
if isinstance(remote_addr, str):
781+
assert stream.extra(SocketAttribute.remote_address) == str(socket_path)
782+
else:
783+
assert isinstance(remote_addr, bytes)
784+
assert stream.extra(SocketAttribute.remote_address) == bytes(
785+
socket_path
786+
)
787+
768788
pytest.raises(
769789
TypedAttributeLookupError, stream.extra, SocketAttribute.local_port
770790
)
@@ -1031,8 +1051,12 @@ async def test_send_after_close(
10311051
await stream.send(b"foo")
10321052

10331053
async def test_cannot_connect(self, socket_path: Path) -> None:
1034-
with pytest.raises(FileNotFoundError):
1035-
await connect_unix(socket_path)
1054+
if str(socket_path).startswith("\0"):
1055+
with pytest.raises(ConnectionRefusedError):
1056+
await connect_unix(socket_path)
1057+
else:
1058+
with pytest.raises(FileNotFoundError):
1059+
await connect_unix(socket_path)
10361060

10371061
async def test_connecting_using_bytes(
10381062
self, server_sock: socket.socket, socket_path: Path
@@ -1057,12 +1081,20 @@ async def test_connecting_with_non_utf8(self, socket_path: Path) -> None:
10571081
sys.platform == "win32", reason="UNIX sockets are not available on Windows"
10581082
)
10591083
class TestUNIXListener:
1060-
@pytest.fixture
1061-
def socket_path(self) -> Generator[Path, None, None]:
1084+
@pytest.fixture(
1085+
params=[
1086+
"path",
1087+
pytest.param("abstract", marks=[skip_unix_abstract_mark]),
1088+
]
1089+
)
1090+
def socket_path(self, request: SubRequest) -> Generator[Path, None, None]:
10621091
# Use stdlib tempdir generation
10631092
# Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path
10641093
with tempfile.TemporaryDirectory() as path:
1065-
yield Path(path) / "socket"
1094+
if request.param == "path":
1095+
yield Path(path) / "socket"
1096+
else:
1097+
yield Path(f"\0{path}") / "socket"
10661098

10671099
@pytest.fixture(params=[False, True], ids=["str", "path"])
10681100
def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str:
@@ -1461,12 +1493,20 @@ async def test_send_after_close(self, family: AnyIPAddressFamily) -> None:
14611493
sys.platform == "win32", reason="UNIX sockets are not available on Windows"
14621494
)
14631495
class TestUNIXDatagramSocket:
1464-
@pytest.fixture
1465-
def socket_path(self) -> Generator[Path, None, None]:
1496+
@pytest.fixture(
1497+
params=[
1498+
"path",
1499+
pytest.param("abstract", marks=[skip_unix_abstract_mark]),
1500+
]
1501+
)
1502+
def socket_path(self, request: SubRequest) -> Generator[Path, None, None]:
14661503
# Use stdlib tempdir generation
14671504
# Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path
14681505
with tempfile.TemporaryDirectory() as path:
1469-
yield Path(path) / "socket"
1506+
if request.param == "path":
1507+
yield Path(path) / "socket"
1508+
else:
1509+
yield Path(f"\0{path}") / "socket"
14701510

14711511
@pytest.fixture(params=[False, True], ids=["str", "path"])
14721512
def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str:
@@ -1506,12 +1546,18 @@ async def test_send_receive(self, socket_path_or_str: Path | str) -> None:
15061546
await sock.sendto(b"blah", path)
15071547
request, addr = await sock.receive()
15081548
assert request == b"blah"
1509-
assert addr == path
1549+
if isinstance(addr, bytes):
1550+
assert addr == path.encode()
1551+
else:
1552+
assert addr == path
15101553

15111554
await sock.sendto(b"halb", path)
15121555
response, addr = await sock.receive()
15131556
assert response == b"halb"
1514-
assert addr == path
1557+
if isinstance(addr, bytes):
1558+
assert addr == path.encode()
1559+
else:
1560+
assert addr == path
15151561

15161562
async def test_iterate(self, peer_socket_path: Path, socket_path: Path) -> None:
15171563
async def serve() -> None:
@@ -1589,18 +1635,33 @@ async def test_local_path_invalid_ascii(self, socket_path: Path) -> None:
15891635
sys.platform == "win32", reason="UNIX sockets are not available on Windows"
15901636
)
15911637
class TestConnectedUNIXDatagramSocket:
1592-
@pytest.fixture
1593-
def socket_path(self) -> Generator[Path, None, None]:
1638+
@pytest.fixture(
1639+
params=[
1640+
"path",
1641+
pytest.param("abstract", marks=[skip_unix_abstract_mark]),
1642+
]
1643+
)
1644+
def socket_path(self, request: SubRequest) -> Generator[Path, None, None]:
15941645
# Use stdlib tempdir generation
15951646
# Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path
15961647
with tempfile.TemporaryDirectory() as path:
1597-
yield Path(path) / "socket"
1648+
if request.param == "path":
1649+
yield Path(path) / "socket"
1650+
else:
1651+
yield Path(f"\0{path}") / "socket"
15981652

15991653
@pytest.fixture(params=[False, True], ids=["str", "path"])
16001654
def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str:
16011655
return socket_path if request.param else str(socket_path)
16021656

1603-
@pytest.fixture
1657+
@pytest.fixture(
1658+
params=[
1659+
pytest.param("path", id="path-peer"),
1660+
pytest.param(
1661+
"abstract", marks=[skip_unix_abstract_mark], id="abstract-peer"
1662+
),
1663+
]
1664+
)
16041665
def peer_socket_path(self) -> Generator[Path, None, None]:
16051666
# Use stdlib tempdir generation
16061667
# Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path
@@ -1634,10 +1695,12 @@ async def test_extra_attributes(
16341695
raw_socket = unix_dg.extra(SocketAttribute.raw_socket)
16351696
assert raw_socket is not None
16361697
assert unix_dg.extra(SocketAttribute.family) == AddressFamily.AF_UNIX
1637-
assert unix_dg.extra(SocketAttribute.local_address) == str(socket_path)
1638-
assert unix_dg.extra(SocketAttribute.remote_address) == str(
1639-
peer_socket_path
1640-
)
1698+
assert os.fsencode(
1699+
cast(os.PathLike, unix_dg.extra(SocketAttribute.local_address))
1700+
) == os.fsencode(socket_path)
1701+
assert os.fsencode(
1702+
cast(os.PathLike, unix_dg.extra(SocketAttribute.remote_address))
1703+
) == os.fsencode(peer_socket_path)
16411704
pytest.raises(
16421705
TypedAttributeLookupError, unix_dg.extra, SocketAttribute.local_port
16431706
)
@@ -1657,11 +1720,11 @@ async def test_send_receive(
16571720
peer_socket_path_or_str,
16581721
local_path=socket_path_or_str,
16591722
) as unix_dg2:
1660-
socket_path = str(socket_path_or_str)
1723+
socket_path = os.fsdecode(socket_path_or_str)
16611724

16621725
await unix_dg2.send(b"blah")
1663-
request = await unix_dg1.receive()
1664-
assert request == (b"blah", socket_path)
1726+
data, remote_addr = await unix_dg1.receive()
1727+
assert (data, os.fsdecode(remote_addr)) == (b"blah", socket_path)
16651728

16661729
await unix_dg1.sendto(b"halb", socket_path)
16671730
response = await unix_dg2.receive()
@@ -1682,13 +1745,15 @@ async def serve() -> None:
16821745
async with await create_connected_unix_datagram_socket(
16831746
peer_socket_path, local_path=socket_path
16841747
) as unix_dg2:
1685-
path = str(socket_path)
1748+
path = os.fsdecode(socket_path)
16861749
async with create_task_group() as tg:
16871750
tg.start_soon(serve)
16881751
await unix_dg1.sendto(b"FOOBAR", path)
1689-
assert await unix_dg1.receive() == (b"RABOOF", path)
1752+
data, addr = await unix_dg1.receive()
1753+
assert (data, os.fsdecode(addr)) == (b"RABOOF", path)
16901754
await unix_dg1.sendto(b"123456", path)
1691-
assert await unix_dg1.receive() == (b"654321", path)
1755+
data, addr = await unix_dg1.receive()
1756+
assert (data, os.fsdecode(addr)) == (b"654321", path)
16921757
tg.cancel_scope.cancel()
16931758

16941759
async def test_concurrent_receive(

0 commit comments

Comments
 (0)