"""Build checked-hash caches from trusted source; never execute cache contents.""" import importlib.util import os import re import stat import sys from pathlib import Path from .policy import BYTECODE_ROOTS, DeliveryError, bytecode_source, relative_name MAX_SOURCE_BYTES = 4 * 1024**2 def _stamp(info: os.stat_result) -> tuple[int, ...]: return ( info.st_dev, info.st_ino, info.st_mode, info.st_nlink, info.st_uid, info.st_gid, info.st_size, info.st_mtime_ns, info.st_ctime_ns, ) def source_bytes(path: Path) -> bytes: descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK) with os.fdopen(descriptor, "rb") as stream: before = os.fstat(stream.fileno()) if ( not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or stat.S_IMODE(before.st_mode) not in (0o444, 0o555) or before.st_size > MAX_SOURCE_BYTES ): raise DeliveryError("bytecode source requires a bounded read-only ordinary file") data = stream.read(MAX_SOURCE_BYTES + 1) after = os.fstat(stream.fileno()) if ( len(data) != before.st_size or _stamp(before) != _stamp(after) or _stamp(after) != _stamp(path.lstat()) ): raise DeliveryError("bytecode source changed during verification") return data def verify_header(header: bytes, source: Path) -> None: # Bundle SHA-256 verification authenticates every cache byte. This extra # pairing binds the CPython header to the actual, separately hashed source. # Never deserialize or execute an incoming marshal payload to inspect it. if sys.version_info[:3] != (3, 13, 16): raise DeliveryError("bytecode verification requires the pinned delivery interpreter") expected = importlib.util.MAGIC_NUMBER + b"\x03\x00\x00\x00" if len(header) != 16 or header[:8] != expected: raise DeliveryError("only pinned checked-hash Python bytecode is supported") if header[8:] != importlib.util.source_hash(source_bytes(source)): raise DeliveryError("bytecode source hash differs") def compile_payload(payload: Path, release_id: str) -> int: import py_compile if ( sys.version_info[:3] != (3, 13, 16) or sys.implementation.cache_tag != "cpython-313" or not re.fullmatch(r"sd-[0-9a-f]{12}", release_id) ): raise DeliveryError("bytecode build requires the pinned delivery interpreter") sources = [] for directory, directories, files in os.walk(payload, followlinks=False): directories.sort() if "__pycache__" in directories: raise DeliveryError("inherited bytecode cache refused") if any((Path(directory) / name).is_symlink() for name in directories): raise DeliveryError("bytecode build directory link refused") for name in sorted(files): path = Path(directory) / name relative = path.relative_to(payload).as_posix() if relative.startswith(BYTECODE_ROOTS) and relative.endswith(".py"): relative_name(relative) sources.append((path, relative)) if len(sources) > 10000: raise DeliveryError("bytecode source count exceeds build bound") for source, relative in sources: before = _stamp(source.lstat()) source_bytes(source) # Explicit cfile prevents an inherited PYTHONPYCACHEPREFIX from sending # output outside this new bundle. Source filenames never expose Host paths. cache = source.parent / "__pycache__" / (source.stem + ".cpython-313.pyc") if bytecode_source(cache.relative_to(payload).as_posix()) != relative: raise DeliveryError("bytecode source pairing differs") if cache.exists() or cache.is_symlink(): raise DeliveryError("existing bytecode cache refused") py_compile.compile( str(source), cfile=str(cache), dfile=f"/opt/kf-onlinejudge/releases/{release_id}/payload/{relative}", doraise=True, optimize=0, invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH, ) cache.chmod(0o444) with cache.open("rb") as stream: verify_header(stream.read(16), source) if _stamp(source.lstat()) != before: raise DeliveryError("source changed during bytecode compilation") return len(sources)