diff --git a/containers/barc-stock-headless/README.md b/containers/barc-stock-headless/README.md index 9034b0c..d37fe08 100644 --- a/containers/barc-stock-headless/README.md +++ b/containers/barc-stock-headless/README.md @@ -31,6 +31,16 @@ The builder fetches the existing HighBar vcpkg baseline256acc640… and verifies The builder records `/opt/barc/toolchain/ubuntu-packages.txt`, `vcpkg-status.txt`, generated manifest, tool-byte SHA256SUMS and MANIFEST.SHA256. Subsequent stages verify those produced bytes. The maintained platform runner separately retains the actual resulting immutable image ID and logs. These records are qualification OUTPUTS; they neither require a previously qualified image nor establish a successful build in advance. Official parent metadata is verified, but raw layer/config verification and the full resulting toolchain/build/ELF closure remain first-build obligations. +## Static ELF correspondence + +Before the integration stage launches the downloader, it writes `/opt/barc/STATIC-ELF.json` from the actual selected engine and produced plugin. Both root hashes must match the pinned engine digest and the existing plugin digest manifest. The collector reads ELF bytes directly; it never invokes a loader, `ldd`, the engine or the plugin. It follows `DT_NEEDED` and the selected x86-64 interpreter recursively, records file hashes, byte sizes, metadata, dependency edges and resolved aliases, then checks the complete inventory again. A previous output is never overwritten. A failed collection produces no success manifest. + +This is a deliberately limited image-local **candidate correspondence**, not the dynamic loader's search algorithm. Candidate directories are each object's directory, its explicit `$ORIGIN`-relative RPATH or RUNPATH, and the fixed image directories `/lib/x86_64-linux-gnu`, `/usr/lib/x86_64-linux-gnu`, `/lib64` and `/usr/lib64`. Aliases to the same canonical file deduplicate; distinct candidates refuse even when their bytes match. Missing dependencies, escaping or changing aliases, SONAME mismatches, malformed ELF, audit/filter dependencies, NODEFLIB, combined RPATH/RUNPATH and unsupported path expansions refuse. Only little-endian ELF64 x86-64 executables/shared objects and `/lib64/ld-linux-x86-64.so.2` are supported. A real build exposing another arrangement requires a reviewed source correction in the same Delivery. + +Bounds are256 files/512MiB aggregate,256MiB per file,256 program headers,4096 dynamic entries,1MiB string metadata/2MiB cumulative metadata reads,128 dependencies and16 search directories per object. Hashing charges actual accepted bytes and reads at most the initially admitted descriptor size. Nonblocking/no-follow opens, regular-file descriptor checks and before/after descriptor/path identity checks refuse growing, replaced or nonregular inputs. Repeated alias lookups must retain the original resolution. The admitted outer image operation still owns the original deadline, process custody and resource ceiling. This source does not establish loader precedence, inherited RPATH, cache/hardware-capability selection, symbol/version compatibility, dynamic `dlopen` populations or loaded ABI. `loadedAbiAccepted` remains false. Actual image creation/export/load/readback, static correspondence against that image, scoped retirement and native gameplay keep their separate acceptance requirements and the existing old-writer/NVMe resource hold. + +`python3 containers/barc-stock-headless/test_source.py` exercises the production collector with authored ELF bytes, including recursive closure, digest drift, missing/ambiguous dependencies, confinement, cycles, alias changes and bounded populations. One bounded `readelf` invocation independently checks an authored fixture; no fixture is compiled or executed. These controls establish source behavior, not a qualified image. + ## Build and runtime boundaries `python3 containers/barc-stock-headless/build-image.py --mode preflight` performs inert data-only pin/shape checks before any image pull. After existing machine admission, use the repository root as the container context and this Containerfile. No toolchain build arguments are needed. Its default final stage acquires the full selected engine/game/map runtime. `--target development` is available for source/plugin work. Image creation does not run spring-headless or invent a native acceptance attempt. diff --git a/containers/barc-stock-headless/build-image.py b/containers/barc-stock-headless/build-image.py index 5e05841..dc66c58 100644 --- a/containers/barc-stock-headless/build-image.py +++ b/containers/barc-stock-headless/build-image.py @@ -10,6 +10,8 @@ import re import shutil import subprocess +import contextlib +import stat ROOT = pathlib.Path(__file__).resolve().parent SHA = re.compile(r'[0-9a-f]{64}') @@ -38,14 +40,186 @@ def check_inputs(pins): require(pins['game']['name'] == 'Beyond All Reason test-29926-0571aa8', 'moving/unselected game name refused') require(pins['game']['sdp'] == '7fd33d9ceb79c052065b8d3c2f6f2687.sdp', 'game package identity differs') -def sha(path, maximum): - require(path.is_file() and not path.is_symlink(), 'missing/linked input: ' + str(path)) - require(path.stat().st_size <= maximum, 'input exceeds byte budget: ' + str(path)) - result = hashlib.sha256() - with path.open('rb') as stream: - while block := stream.read(1024 * 1024): +def file_identity(info): + return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns) + +@contextlib.contextmanager +def regular_input(path, maximum): + # NONBLOCK prevents a raced FIFO from blocking open before the fstat check. + require(not path.is_symlink(), 'linked input refused') + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_size <= maximum, 'input is nonregular or exceeds byte budget') + require(file_identity(path.lstat()) == file_identity(before), 'input replaced before read') + yield stream, before.st_size + require(file_identity(os.fstat(stream.fileno())) == file_identity(before) + and file_identity(path.lstat()) == file_identity(before), 'input grew or was replaced during read') + +def file_digest(path, maximum): + result = hashlib.sha256(); count = 0 + with regular_input(path, maximum) as (stream, size): + while count < size: + block = stream.read(min(1024 * 1024, size - count)) + require(bool(block), 'input truncated during read') + count += len(block) + require(count <= maximum, 'input read exceeds byte budget') result.update(block) - return result.hexdigest() + require(count == size, 'input size changed during read') + return result.hexdigest(), count + +def sha(path, maximum): + return file_digest(path, maximum)[0] + +def elf_metadata(path): + """Read bounded ELF64 metadata as data; never invoke a loader or the file.""" + with regular_input(path, 256 * 1024 * 1024) as (stream, size): + require(size >= 64, 'truncated ELF file') + metadata_bytes = 0 + def read(offset, count): + nonlocal metadata_bytes + require(0 <= offset <= size and 0 <= count <= 1024 * 1024 and offset + count <= size, + 'truncated or excessive ELF metadata') + require(metadata_bytes + count <= 2 * 1024 * 1024, 'ELF cumulative metadata read budget exceeded') + stream.seek(offset); value = stream.read(count) + metadata_bytes += len(value) + require(len(value) == count, 'ELF changed during metadata read') + return value + header = struct.unpack('<16sHHIQQQIHHHHHH', read(0, 64)) + require(header[0][:7] == b'\x7fELF\x02\x01\x01' and header[0][7] in (0, 3) and header[1] in (2, 3) + and header[2:4] == (62, 1) and header[8] == 64, + 'only executable/shared ELF64 little-endian x86-64 is supported') + require(header[5] >= 64 and header[9] == 56 and 0 < header[10] <= 256, 'unsupported ELF program-header population') + segments = [struct.unpack('