mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-10 14:33:42 +08:00
Isolate Model Runtime Caches By User
This commit is contained in:
@@ -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
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "58286887a983750a4bc8792358871756b56daa7125675133f3405f16821314d6", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "8e7227036605dc335abd5d53efb4b4eef019a46e826d8260330e20ef381d5872", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user