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 from __future__ import annotations
import os
from pathlib import Path from pathlib import Path
import stat
import sys
import pytest import pytest
import trio import trio
import tractor import tractor
from tractor import Actor from tractor import Actor
from tractor.runtime import _state
from tractor.discovery import _addr from tractor.discovery import _addr
from tractor.runtime import _state
@pytest.fixture @pytest.fixture
@ -31,6 +34,69 @@ def bindspace_dir_str() -> str:
bs_dir.rmdir() 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( def test_uds_bindspace_created_implicitly(
debug_mode: bool, debug_mode: bool,
bindspace_dir_str: str, bindspace_dir_str: str,

View File

@ -22,7 +22,10 @@ from __future__ import annotations
from contextvars import ( from contextvars import (
ContextVar, ContextVar,
) )
import os
from pathlib import Path from pathlib import Path
import stat
import sys
from typing import ( from typing import (
Any, Any,
Callable, Callable,
@ -42,6 +45,9 @@ if TYPE_CHECKING:
from .._context import Context from .._context import Context
_DARWIN_TMPDIR: Path = Path('/tmp')
# default IPC transport protocol settings # default IPC transport protocol settings
TransportProtocolKey = Literal[ TransportProtocolKey = Literal[
'tcp', 'tcp',
@ -335,11 +341,44 @@ def get_rt_dir(
# `import tractor` path (gh #470). # `import tractor` path (gh #470).
import platformdirs import platformdirs
rt_dir: Path = Path( rt_root: Path|None = None
platformdirs.user_runtime_dir( if sys.platform == 'darwin':
appname=appname, # 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,
),
)
# Normalize and validate that `subdir` is a relative path # Normalize and validate that `subdir` is a relative path
# without any parent-directory ("..") components, to prevent # without any parent-directory ("..") components, to prevent
@ -363,11 +402,30 @@ def get_rt_dir(
rt_dir: Path = rt_dir / subdir_path 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( rt_dir.mkdir(
mode=0o700,
parents=True, parents=True,
exist_ok=True, # avoid `FileExistsError` from conc calls 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 return rt_dir