fix(sandbox): skip host uid mapping for system/service accounts

STRIX_HOST_UID from a dedicated system account (uid < 1000 or without a
usable home) triggers the sandbox entrypoint's demotion branch, leaving
the in-container Caido sidecar with an unwritable HOME — it panics on
its data directory and every scan silently degrades. Skip the mapping
for such accounts and log a warning; regular login users are unaffected
(regression tests included).

Fixes #1514
This commit is contained in:
J0kkeR 2026-10-13 00:04:46 +08:00
parent f7a32ae62d
commit 07be5a1b00
2 changed files with 68 additions and 1 deletions

View file

@ -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]]:

View file

@ -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",
}