mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-24 08:13:59 +00:00
* 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>
92 lines
2.9 KiB
Python
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"
|