unsloth/tests/security/conftest.py
Daniel Han 2b18aced20
Scope the security suite's offline guard to the tests that want it (#8054)
* Scope the security suite's offline guard to the tests that want it

`tests/security/conftest.py` replaces `socket.socket` so a scanner reaching the
internet fails loudly. The fixture was session-scoped: a directory conftest
limits which tests a fixture APPLIES to, but a session-scoped one is still torn
down when the session ends, so the patch stayed installed for every test that
ran afterwards.

CI does not hit this. `studio-backend-ci.yml` runs `tests/` with
`tests/vllm_compat` and `tests/version_compat` ignored, and that selection is
green either way (4335 passed on the unfixed tree, no blocked sockets). The
pinned-symbol suites that fetch upstream sources run in their own workflow.

A plain `pytest tests/` does hit it, which is what a developer runs locally.
`security` sorts before `version_compat` and `vllm_compat`, so about 1300 of
their checks died on a socket this file had replaced:

    RuntimeError: network access blocked by tests/security/conftest.py

Each passed alone and failed in the suite, in that order only, which reads as
upstream drift rather than as a fixture. Per-test scope keeps the guard exactly
where it was meant to be; the security suite is unchanged at 201 passing.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Observe the teardown, not just the installed guard

Every assertion in the regression test ran while the autouse fixture was still
active, so all six proved the blocker was installed and none could see what the
finalizer hands back. Emptying that `finally` left them green while the
cross-suite leak returned, which is the failure mode the file exists to stop.

Two additions. One drives the fixture's own generator and checks the restored
class, standing the outer guard down first so it is not nesting a second
install and restoring the blocker to itself. The other runs a nested pytest
over a miniature of the original layout -- the security suite, then an ordinary
test after it -- and asserts the second one gets a working socket, which is the
same ordering that cost about 1300 tests.

Confirmed both fail with the `finally` body emptied and pass with it intact.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Trim the comments in the leak tests

---------

Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-07 02:22:37 -07:00

92 lines
2.9 KiB
Python

"""Security suite fixtures: an autouse network blocker refuses non-loopback socket.connect() so a regression reaching the internet fails loudly."""
from __future__ import annotations
import socket
import sys
from pathlib import Path
import pytest
# Make `scripts/` importable so tests can grab scanner constants directly.
REPO_ROOT = Path(__file__).resolve().parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
_LOOPBACK_PREFIXES = ("127.", "::1", "localhost")
def _is_loopback(host: str | bytes) -> bool:
if isinstance(host, bytes):
try:
host = host.decode("utf-8")
except UnicodeDecodeError:
return False
if not host:
return False
host = host.strip()
if host in {"::1", "localhost", "0.0.0.0"}:
return True
return host.startswith("127.")
class _BlockedSocket(socket.socket):
"""Socket subclass that refuses any non-loopback connect()."""
def connect(self, address): # type: ignore[override]
host = None
if isinstance(address, tuple) and address:
host = address[0]
if not _is_loopback(host or ""):
raise RuntimeError(
f"network access blocked by tests/security/conftest.py "
f"(attempted connect to {address!r}); the scanner suite "
"must run fully offline"
)
return super().connect(address)
def connect_ex(self, address): # type: ignore[override]
host = None
if isinstance(address, tuple) and address:
host = address[0]
if not _is_loopback(host or ""):
raise RuntimeError(
f"network access blocked by tests/security/conftest.py "
f"(attempted connect_ex to {address!r})"
)
return super().connect_ex(address)
@pytest.fixture(autouse = True)
def network_blocker():
"""Swap socket.socket for the blocker, restored after each test.
Per test, not per session. A session-scoped fixture in a directory conftest
applies only to this directory, but it tears down when the SESSION ends, so
the patch outlived the suite that wanted it and every later test that
reaches the network died on a socket this file replaced. `security` sorts
before `version_compat` and `vllm_compat`, whose pinned-symbol checks fetch
upstream sources, so a full run lost about 1300 of them:
RuntimeError: network access blocked by tests/security/conftest.py
They passed alone and failed together, in that order only.
"""
original = socket.socket
socket.socket = _BlockedSocket # type: ignore[assignment]
try:
yield
finally:
socket.socket = original # type: ignore[assignment]
@pytest.fixture(scope = "session")
def repo_root() -> Path:
return REPO_ROOT
@pytest.fixture(scope = "session")
def fixtures_dir() -> Path:
return Path(__file__).resolve().parent / "fixtures"