Use short Darwin UDS runtime paths

`platformdirs` can place the runtime dir deep below a pytest temp
home, pushing `registry@1616.sock` past Darwin's 104-byte
`AF_UNIX` limit.

Use a compact `/tmp/<app>-<uid>` root on Darwin and secure it
before allocating socket paths:
- require a real, current-user-owned dir via `lstat()`
- tighten existing roots to mode `0700`
- reject symlinks and unsafe nested dirs

Cover the path budget, mode, and symlink rejection.

Caught-during: review remediation
Found-via: `/run-tests` test_macos_rt_dir_fits_uds_path_limit

Review: PR #480 (goodboy,copilot-pull-request-reviewer[bot])
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
wkt/uds_macos_473
Gud Boi 2026-08-13 16:39:32 -04:00
parent 451e0acf8a
commit 634d914161
2 changed files with 131 additions and 7 deletions

View File

@ -3,14 +3,17 @@ Unit-ish tests for specific IPC transport protocol backends.
'''
from __future__ import annotations
import os
from pathlib import Path
import stat
import sys
import pytest
import trio
import tractor
from tractor import Actor
from tractor.runtime import _state
from tractor.discovery import _addr
from tractor.runtime import _state
@pytest.fixture
@ -31,6 +34,69 @@ def bindspace_dir_str() -> str:
bs_dir.rmdir()
def test_macos_rt_dir_fits_uds_path_limit(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
):
'''
Keep the default Darwin UDS bindpath below its 104-byte limit.
`platformdirs` normally places the runtime directory below the
long `~/Library/Caches/TemporaryItems` path. Pytest also assigns
a deeply nested temporary home, so appending a registry socket
name made every macOS UDS listener fail with `AF_UNIX path too
long`. This test simulates Darwin and an intentionally long
platformdirs result, then proves `get_rt_dir()` uses the short
system temporary directory and leaves room for the socket name.
'''
long_rt_dir: Path = tmp_path / ('long' * 30)
monkeypatch.setattr(sys, 'platform', 'darwin')
monkeypatch.setattr(
'platformdirs.user_runtime_dir',
lambda appname: str(long_rt_dir / appname),
)
monkeypatch.setattr(_state, '_DARWIN_TMPDIR', tmp_path)
rt_dir: Path = _state.get_rt_dir()
sockpath: Path = (
Path('/tmp')
/ f'tractor-{os.getuid()}'
/ 'registry@1616.sock'
)
assert rt_dir == tmp_path / f'tractor-{os.getuid()}'
assert len(os.fsencode(sockpath)) < 104
assert stat.S_IMODE(rt_dir.stat().st_mode) == 0o700
def test_macos_rt_dir_rejects_symlink(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
):
'''
Reject a pre-created symlink at the Darwin runtime path.
Darwin uses the predictable `/tmp/tractor-<uid>` path to stay
below its `AF_UNIX` limit. A hostile local user could otherwise
point that path at a victim-owned directory and make
`get_rt_dir()` chmod or place sockets in the symlink target. The
test replaces `/tmp` with a controlled directory, installs the
malicious link, and proves non-following validation rejects it.
'''
runtime_link: Path = tmp_path / f'tractor-{os.getuid()}'
target_dir: Path = tmp_path / 'target'
target_dir.mkdir(mode=0o755)
runtime_link.symlink_to(target_dir, target_is_directory=True)
monkeypatch.setattr(sys, 'platform', 'darwin')
monkeypatch.setattr(_state, '_DARWIN_TMPDIR', tmp_path)
with pytest.raises(PermissionError, match='Unsafe Darwin'):
_state.get_rt_dir()
assert stat.S_IMODE(target_dir.stat().st_mode) == 0o755
def test_uds_bindspace_created_implicitly(
debug_mode: bool,
bindspace_dir_str: str,

View File

@ -22,7 +22,10 @@ from __future__ import annotations
from contextvars import (
ContextVar,
)
import os
from pathlib import Path
import stat
import sys
from typing import (
Any,
Callable,
@ -42,6 +45,9 @@ if TYPE_CHECKING:
from .._context import Context
_DARWIN_TMPDIR: Path = Path('/tmp')
# default IPC transport protocol settings
TransportProtocolKey = Literal[
'tcp',
@ -335,7 +341,40 @@ def get_rt_dir(
# `import tractor` path (gh #470).
import platformdirs
rt_dir: Path = Path(
rt_root: Path|None = None
if sys.platform == 'darwin':
# Darwin's AF_UNIX path limit is 104 bytes. The standard
# platformdirs path can consume that before the sock name.
rt_root = (
_DARWIN_TMPDIR
/ f'{appname}-{os.getuid()}'
)
try:
rt_stat: os.stat_result = rt_root.lstat()
except FileNotFoundError:
try:
rt_root.mkdir(mode=0o700)
except FileExistsError:
pass
rt_stat = rt_root.lstat()
if (
not stat.S_ISDIR(rt_stat.st_mode)
or
rt_stat.st_uid != os.getuid()
):
raise PermissionError(
f'Unsafe Darwin runtime directory!\n'
f'path: {rt_root}\n'
f'owner uid: {rt_stat.st_uid}\n'
f'mode: {stat.filemode(rt_stat.st_mode)}\n'
)
if stat.S_IMODE(rt_stat.st_mode) != 0o700:
rt_root.chmod(0o700)
rt_dir: Path = rt_root
else:
rt_dir = Path(
platformdirs.user_runtime_dir(
appname=appname,
),
@ -363,11 +402,30 @@ def get_rt_dir(
rt_dir: Path = rt_dir / subdir_path
if not rt_dir.is_dir():
try:
dir_stat: os.stat_result = rt_dir.lstat()
except FileNotFoundError:
rt_dir.mkdir(
mode=0o700,
parents=True,
exist_ok=True, # avoid `FileExistsError` from conc calls
)
dir_stat = rt_dir.lstat()
if rt_root is not None:
if (
not stat.S_ISDIR(dir_stat.st_mode)
or
dir_stat.st_uid != os.getuid()
):
raise PermissionError(
f'Unsafe Darwin runtime directory!\n'
f'path: {rt_dir}\n'
f'owner uid: {dir_stat.st_uid}\n'
f'mode: {stat.filemode(dir_stat.st_mode)}\n'
)
if rt_dir != rt_root:
rt_dir.relative_to(rt_root)
return rt_dir