diff --git a/strix/runtime/session_manager.py b/strix/runtime/session_manager.py index a8447ebb..5bdfa3f4 100644 --- a/strix/runtime/session_manager.py +++ b/strix/runtime/session_manager.py @@ -49,6 +49,27 @@ _EXTRA_FILE_EXTRACT_TIMEOUT_S = 120 _EXTRA_FILE_MODE = 0o644 +def _is_regular_login_user(uid: int) -> bool: + """Whether ``uid`` refers to an interactive login user whose home directory + can take part in the sandbox uid-remapping flow. + + System/service accounts (uid below the login range, or without a usable + home directory) must not opt in: the sandbox image's entrypoint demotes + itself to ``STRIX_HOST_UID``, and a demoted system uid ends up with an + unwritable HOME, which panics the in-container Caido sidecar + ("Unable to create data directory") and silently degrades every scan. + """ + import pwd + + if uid < 1000: + return False + try: + pw_dir = pwd.getpwuid(uid).pw_dir + except KeyError: + return False + return bool(pw_dir) and pw_dir not in ("/", "/nonexistent") + + def _host_identity_env() -> dict[str, str]: # Read the platform through a local so it is not narrowed to whichever OS is # type-checking: comparing sys.platform directly makes one of these branches @@ -58,7 +79,15 @@ def _host_identity_env() -> dict[str, str]: return {} # Bind-mount ownership only needs mapping on Linux, where the container uid # must match the host's. - return {"STRIX_HOST_UID": str(os.getuid()), "STRIX_HOST_GID": str(os.getgid())} + uid, gid = os.getuid(), os.getgid() + if not _is_regular_login_user(uid): + logger.warning( + "Skipping sandbox uid mapping for uid %s (system/service account): " + "bind-mounted workspace files will keep the container user's ownership", + uid, + ) + return {} + return {"STRIX_HOST_UID": str(uid), "STRIX_HOST_GID": str(gid)} def build_bind_mounts(local_sources: list[dict[str, Any]]) -> list[dict[str, Any]]: diff --git a/tests/test_host_identity_env.py b/tests/test_host_identity_env.py new file mode 100644 index 00000000..542d4df7 --- /dev/null +++ b/tests/test_host_identity_env.py @@ -0,0 +1,38 @@ +import os +from types import SimpleNamespace + +from strix.runtime import session_manager + + +def _as(monkeypatch, uid: int, gid: int) -> None: + monkeypatch.setattr(os, "getuid", lambda: uid) + monkeypatch.setattr(os, "getgid", lambda: gid) + + +def test_system_account_uid_skips_mapping(monkeypatch): + _as(monkeypatch, 997, 980) + assert session_manager._host_identity_env() == {} + + +def test_uid_below_login_range_skips_mapping(monkeypatch): + _as(monkeypatch, 999, 999) + assert session_manager._host_identity_env() == {} + + +def test_homeless_uid_skips_mapping(monkeypatch): + _as(monkeypatch, 1500, 1500) + monkeypatch.setattr( + "pwd.getpwuid", lambda uid: SimpleNamespace(pw_dir="/nonexistent") + ) + assert session_manager._host_identity_env() == {} + + +def test_regular_user_is_mapped(monkeypatch): + _as(monkeypatch, 1500, 1500) + monkeypatch.setattr( + "pwd.getpwuid", lambda uid: SimpleNamespace(pw_dir="/home/dev") + ) + assert session_manager._host_identity_env() == { + "STRIX_HOST_UID": "1500", + "STRIX_HOST_GID": "1500", + }