Isolate Model Runtime Caches By User

This commit is contained in:
firestar5683
2026-11-09 15:45:03 +00:00
parent c6cdc7fa73
commit febb4ebdb2
3 changed files with 132 additions and 3 deletions
@@ -1,5 +1,7 @@
"""Load existing bank and stock artifacts with one explicitly selected runtime."""
import hashlib
import os
import stat
from pathlib import Path
from typing import TYPE_CHECKING, cast
@@ -20,6 +22,40 @@ _selection = None
_protection = None
def _inference_cache_root(role):
if role not in ('driving', 'monitoring'):
raise ValueError('Unsupported model runtime role')
root = Path('/data/starpilot-model-runtime')
leaf = root / LEGACY_REVISION / role / MANIFEST_SHA256
uid = os.geteuid()
trusted_parent = True
for current in (*reversed(root.parents), root, root / LEGACY_REVISION,
root / LEGACY_REVISION / role, leaf, leaf / 'xdg'):
try:
status = current.lstat()
except FileNotFoundError:
break
if stat.S_ISLNK(status.st_mode) or not stat.S_ISDIR(status.st_mode):
raise ValueError('Invalid runtime cache directory')
if current in root.parents:
trusted_parent = trusted_parent and status.st_uid in (0, uid) and not stat.S_IMODE(status.st_mode) & 0o022
if current.is_relative_to(root) and status.st_uid != uid:
if uid == 0:
raise ValueError('Root ownership mismatch requires an explicit private runtime cache')
if not trusted_parent:
raise ValueError('Untrusted parent for per-user runtime cache')
sibling = root.with_name(root.name + '-uid' + str(uid))
try:
owner = sibling.lstat()
except FileNotFoundError:
return sibling
if (not stat.S_ISDIR(owner.st_mode) or owner.st_uid != uid or
stat.S_IMODE(owner.st_mode) & 0o022):
raise ValueError('Invalid per-user runtime cache root')
return sibling
return root
def prepare(role):
global _selection
if _selection is not None and _selection.get('purpose') == 'compiler':
@@ -32,7 +68,7 @@ def prepare(role):
raise RuntimeError('A model child cannot change its execution role')
return _selection
_verify_policy()
cache = Path('/data/starpilot-model-runtime')
cache = _inference_cache_root(role)
_selection = activate(BUNDLE, MANIFEST_SHA256, LEGACY_REVISION, role, cache)
return _selection
@@ -1,5 +1,8 @@
"""Persistent startup cache selection; not GPU or road qualification."""
from pathlib import Path
import copy
import stat
from types import SimpleNamespace
from pathlib import Path, PurePosixPath
from unittest.mock import patch
import pytest
@@ -13,6 +16,7 @@ def test_inference_cache_survives_reboot_without_changing_role_isolation(role):
with (patch("openpilot.common.hardware.AGNOS", True),
patch.object(runtime, "_selection", None),
patch.object(runtime, "_verify_policy") as verify,
patch.object(runtime, "_inference_cache_root", return_value=Path("/data/starpilot-model-runtime")),
patch.object(runtime, "activate", return_value=selected) as activate):
assert runtime.prepare(role) is selected
verify.assert_called_once()
@@ -27,3 +31,92 @@ def test_host_does_not_create_device_cache():
patch.object(runtime, "_selection", None),
patch.object(runtime, "activate", side_effect=AssertionError("must not touch device cache"))):
assert runtime.prepare("driving") is None
class CachePath(PurePosixPath):
entries = {}
def lstat(self):
try:
uid, mode = self.entries[str(self)]
except KeyError:
raise FileNotFoundError(str(self)) from None
if isinstance(uid, BaseException):
raise uid
return SimpleNamespace(st_uid=uid, st_mode=mode)
@pytest.fixture
def cache_metadata():
root = CachePath("/data/starpilot-model-runtime")
revision = root / runtime.LEGACY_REVISION
CachePath.entries = {"/": (0, stat.S_IFDIR | 0o755), "/data": (1000, stat.S_IFDIR | 0o755)}
for role in ("driving", "monitoring"):
leaf = revision / role / runtime.MANIFEST_SHA256
for path in (root, revision, revision / role, leaf, leaf / "xdg"):
CachePath.entries[str(path)] = (1000, stat.S_IFDIR | 0o700)
def route(role="monitoring", uid=1000):
with patch.object(runtime, "Path", CachePath), patch.object(runtime.os, "geteuid", return_value=uid):
return runtime._inference_cache_root(role)
yield root, revision, route
CachePath.entries = {}
@pytest.mark.parametrize("role", ["driving", "monitoring"])
def test_owned_hot_cache_keeps_canonical_path_without_writes(cache_metadata, role):
root, _, route = cache_metadata
before = copy.deepcopy(CachePath.entries)
assert route(role) == root
assert CachePath.entries == before
def test_foreign_monitoring_cache_uses_only_fixed_uid_sibling(cache_metadata):
root, revision, route = cache_metadata
CachePath.entries[str(revision / "monitoring")] = (0, stat.S_IFDIR | 0o700)
before = copy.deepcopy(CachePath.entries)
assert route() == root.with_name(root.name + "-uid1000")
assert route("driving") == root
assert CachePath.entries == before
def test_root_mismatch_requires_explicit_private_cache(cache_metadata):
_, _, route = cache_metadata
before = copy.deepcopy(CachePath.entries)
with pytest.raises(ValueError, match="explicit private"):
route(uid=0)
assert CachePath.entries == before
@pytest.mark.parametrize("uid,mode", [(2000, 0o755), (0, 0o777), (1000, 0o775)])
def test_uid_fallback_rejects_untrusted_parent(cache_metadata, uid, mode):
_, revision, route = cache_metadata
CachePath.entries[str(revision / "monitoring")] = (0, stat.S_IFDIR | 0o700)
CachePath.entries["/data"] = (uid, stat.S_IFDIR | mode)
with pytest.raises(ValueError, match="Untrusted parent"):
route()
@pytest.mark.parametrize("mode", [stat.S_IFLNK | 0o777, stat.S_IFREG | 0o600])
def test_cache_route_rejects_symlink_or_nondirectory(cache_metadata, mode):
_, revision, route = cache_metadata
CachePath.entries[str(revision / "monitoring")] = (1000, mode)
with pytest.raises(ValueError, match="Invalid runtime cache directory"):
route()
@pytest.mark.parametrize("error", [PermissionError("denied"), OSError("unknown metadata")])
def test_cache_metadata_errors_propagate_without_fallback(cache_metadata, error):
root, _, route = cache_metadata
CachePath.entries[str(root)] = (error, 0)
with pytest.raises(type(error)):
route()
@pytest.mark.parametrize("uid,mode", [(0, stat.S_IFDIR | 0o700), (1000, stat.S_IFDIR | 0o777),
(1000, stat.S_IFLNK | 0o777)])
def test_existing_uid_sibling_must_be_private_and_owned(cache_metadata, uid, mode):
root, revision, route = cache_metadata
CachePath.entries[str(revision / "monitoring")] = (0, stat.S_IFDIR | 0o700)
CachePath.entries[str(root.with_name(root.name + "-uid1000"))] = (uid, mode)
with pytest.raises(ValueError, match="Invalid per-user"):
route()
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "58286887a983750a4bc8792358871756b56daa7125675133f3405f16821314d6", "schema_version": 1}
{"protected_inventory_sha256": "8e7227036605dc335abd5d53efb4b4eef019a46e826d8260330e20ef381d5872", "schema_version": 1}