#!/bin/sh set -eu umask 022 gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P) input=$gate_dir/P15-019-input.json freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src} utils_src=${EROFS_UTILS_SRC:-/work/build/erofs-utils-main} base= output= while test "$#" -gt 0; do case "$1" in --base) test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; } base=$2 shift 2 ;; --output) test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; } output=$2 shift 2 ;; *) printf 'unknown argument: %s\n' "$1" >&2 exit 2 ;; esac done test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; } test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; } test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; } test -d "$freebsd_src/sys" || { printf 'missing FreeBSD source: %s\n' "$freebsd_src" >&2; exit 2; } test -d "$utils_src/lib" || { printf 'missing erofs-utils source: %s\n' "$utils_src" >&2; exit 2; } for tool in dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum timeout; do command -v "$tool" >/dev/null 2>&1 || { printf 'missing required host tool: %s\n' "$tool" >&2 exit 2 } done case "$output" in /*) ;; *) output=$PWD/$output ;; esac test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; } mkdir -p "$output/host" python3 -B - "$root" "$input" "$base" "$output/host" "$freebsd_src" "$utils_src" <<'PY' from __future__ import annotations import hashlib import json import os from pathlib import Path import re import shutil import stat import struct import subprocess import sys import tempfile import traceback from typing import Any ROOT = Path(sys.argv[1]) INPUT = Path(sys.argv[2]) REQUESTED_BASE = sys.argv[3] OUTPUT = Path(sys.argv[4]) FREEBSD_SRC = Path(sys.argv[5]) UTILS_SRC = Path(sys.argv[6]) SPEC = json.loads(INPUT.read_text(encoding="ascii")) DUT = ROOT / "repo-pre-15" SUPER = 1024 MAGIC = 0xE0F5E1E2 CRC32C_POLY = 0x82F63B78 FEATURE_COMPAT_SB_CHKSUM = 0x00000001 FEATURE_INCOMPAT_CHUNKED_FILE = 0x00000004 EROFS_FT_REG_FILE = 1 EROFS_FT_SYMLINK = 7 S_IFMT = 0o170000 S_IFREG = 0o100000 S_IFLNK = 0o120000 CHUNK_FORMAT_INDEXES = 0x0020 LAYOUT_NAMES = { 0: "plain", 1: "compressed-full", 2: "inline", 3: "compressed-compact", 4: "chunk", } class GateStop(Exception): pass class InfraBlocked(Exception): pass def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sha256_path(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): digest.update(block) return digest.hexdigest() def write_json(path: Path, value: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii") def git(path: Path, *args: str) -> str: completed = subprocess.run( ["git", "-C", str(path), *args], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) if completed.returncode != 0: raise InfraBlocked(f"git {' '.join(args)} failed: {completed.stdout.strip()}") return completed.stdout.strip() COMMANDS: list[dict[str, Any]] = [] def display_arg(value: str, work: Path) -> str: replacements = ( (str(work), "OWNED_WORK"), (str(OUTPUT), "GATE_OUTPUT"), (str(ROOT), "ROOT"), ) for old, new in replacements: value = value.replace(old, new) return value def run(argv: list[str], work: Path, timeout: int = 30) -> subprocess.CompletedProcess[str]: try: completed = subprocess.run( argv, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout, ) except subprocess.TimeoutExpired as error: raise InfraBlocked(f"command timed out: {' '.join(argv)}") from error COMMANDS.append( { "argv": [display_arg(item, work) for item in argv], "exit": completed.returncode, "stdout_sha256": sha256_bytes(completed.stdout.encode("utf-8")), } ) return completed def run_required(argv: list[str], work: Path, timeout: int = 30) -> str: completed = run(argv, work, timeout) if completed.returncode != 0: raise InfraBlocked( f"command failed ({completed.returncode}): {' '.join(argv)}: " f"{completed.stdout.strip()}" ) return completed.stdout def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int: value = seed for byte in data: value ^= byte for _ in range(8): value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0) return value & 0xFFFFFFFF class Inode: def __init__( self, nid: int, offset: int, inode_format: int, inode_size: int, xattr_size: int, layout: int, mode: int, size: int, start_block: int, ): self.nid = nid self.offset = offset self.inode_format = inode_format self.inode_size = inode_size self.xattr_size = xattr_size self.layout = layout self.mode = mode self.size = size self.start_block = start_block class DirectoryEntry: def __init__(self, nid: int, offset: int, file_type: int, name: bytes): self.nid = nid self.offset = offset self.file_type = file_type self.name = name class Image: def __init__(self, data: bytes | bytearray): self.data = bytearray(data) if len(self.data) < SUPER + 128 or self.u32(SUPER) != MAGIC: raise GateStop("fixture is not an EROFS image") self.block_bits = self.data[SUPER + 12] if self.block_bits < 9 or self.block_bits > 16: raise GateStop("fixture has invalid block bits") self.block_size = 1 << self.block_bits self.meta_blkaddr = self.u32(SUPER + 40) self.feature_compat = self.u32(SUPER + 8) self.feature_incompat = self.u32(SUPER + 80) self.blocks = self.u32(SUPER + 36) self.root_nid = self.u16(SUPER + 14) self.packed_nid = self.u64(SUPER + 96) if self.blocks == 0 or self.blocks << self.block_bits > len(self.data): raise GateStop("fixture primary image bounds are invalid") self.verify_checksum() @classmethod def load(cls, path: Path) -> "Image": return cls(path.read_bytes()) def u16(self, offset: int) -> int: if offset < 0 or offset + 2 > len(self.data): raise GateStop("u16 read outside image") return struct.unpack_from(" int: if offset < 0 or offset + 4 > len(self.data): raise GateStop("u32 read outside image") return struct.unpack_from(" int: if offset < 0 or offset + 8 > len(self.data): raise GateStop("u64 read outside image") return struct.unpack_from(" None: struct.pack_into(" None: struct.pack_into(" None: struct.pack_into(" int: span = self.block_size - SUPER if self.block_size > SUPER else self.block_size end = SUPER + span if end > len(self.data): raise GateStop("checksum span exceeds image") return end def calculated_checksum(self) -> int: block = bytearray(self.data[SUPER : self.checksum_end]) block[4:8] = bytes(4) return crc32c(block) def verify_checksum(self) -> None: if self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: if self.u32(SUPER + 4) != self.calculated_checksum(): raise GateStop("fixture checksum is invalid") def update_checksum(self) -> None: if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: raise GateStop("fixture does not advertise a checksum") self.put_u32(SUPER + 4, 0) self.put_u32(SUPER + 4, self.calculated_checksum()) self.verify_checksum() def inode(self, nid: int) -> Inode: offset = (self.meta_blkaddr << self.block_bits) + (nid << 5) if offset > len(self.data) - 32: raise GateStop(f"nid {nid} lies outside the primary image") inode_format = self.u16(offset) inode_size = 64 if inode_format & 1 else 32 if offset > len(self.data) - inode_size: raise GateStop(f"nid {nid} is truncated") xattr_count = self.u16(offset + 2) xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1) size = self.u64(offset + 8) if inode_size == 64 else self.u32(offset + 8) return Inode( nid=nid, offset=offset, inode_format=inode_format, inode_size=inode_size, xattr_size=xattr_size, layout=(inode_format >> 1) & 7, mode=self.u16(offset + 4), size=size, start_block=self.u32(offset + 16), ) def uncompressed_data(self, inode: Inode, blob: bytes | None = None) -> bytes: if inode.layout == 2: offset = inode.offset + inode.inode_size + inode.xattr_size if (offset & (self.block_size - 1)) + inode.size > self.block_size: raise GateStop("inline fixture crosses its metadata block") source = self.data elif inode.layout == 0: offset = inode.start_block << self.block_bits source = self.data elif inode.layout == 4: if blob is None: raise GateStop("chunk fixture has no external blob") chunk_format = self.u16(inode.offset + 16) entry_size = 8 if chunk_format & CHUNK_FORMAT_INDEXES else 4 index_base = (inode.offset + inode.inode_size + inode.xattr_size + entry_size - 1) & ~(entry_size - 1) output = bytearray() logical = 0 chunk_size = 1 << (self.block_bits + (chunk_format & 0x1F)) while logical < inode.size: index = logical // chunk_size position = index_base + index * entry_size if entry_size == 8: high, device_id, low = struct.unpack_from(" len(source) or count > len(source) - offset: raise GateStop("chunk payload exceeds its device") output.extend(source[offset : offset + count]) logical += count return bytes(output) else: raise GateStop(f"layout {inode.layout} is not uncompressed") if offset > len(source) or inode.size > len(source) - offset: raise GateStop("uncompressed payload exceeds its image") return bytes(source[offset : offset + inode.size]) def directory_entries(self, inode: Inode) -> list[DirectoryEntry]: data = self.uncompressed_data(inode) if len(data) < 12: raise GateStop("root directory is too short") first_name = struct.unpack_from("= len(data): raise GateStop("root directory has an invalid first name offset") count = first_name // 12 entries = [] for index in range(count): slot = index * 12 name_start = struct.unpack_from("= name_end or name_end > len(data): raise GateStop("root directory name bounds are invalid") name = data[name_start:name_end].split(b"\0", 1)[0] data_offset = ( inode.offset + inode.inode_size + inode.xattr_size if inode.layout == 2 else inode.start_block << self.block_bits ) entries.append( DirectoryEntry( nid=struct.unpack_from(" tuple[Inode, DirectoryEntry, Inode]: root = self.inode(self.root_nid) entry = next((item for item in self.directory_entries(root) if item.name == name), None) if entry is None: raise GateStop("fixture root has no /link entry") return root, entry, self.inode(entry.nid) def lz4_decode_exact(source: bytes, output_size: int) -> bytes: ip = 0 output = bytearray() while ip < len(source): token = source[ip] ip += 1 literal_length = token >> 4 if literal_length == 15: while True: if ip >= len(source): raise ValueError("truncated literal length") value = source[ip] ip += 1 literal_length += value if value != 255: break if ip + literal_length > len(source): raise ValueError("truncated literals") output.extend(source[ip : ip + literal_length]) ip += literal_length if ip == len(source): break if ip + 2 > len(source): raise ValueError("truncated match offset") offset = source[ip] | source[ip + 1] << 8 ip += 2 if offset == 0 or offset > len(output): raise ValueError("invalid match offset") match_length = token & 15 if match_length == 15: while True: if ip >= len(source): raise ValueError("truncated match length") value = source[ip] ip += 1 match_length += value if value != 255: break for _ in range(match_length + 4): output.append(output[-offset]) if len(output) > output_size: raise ValueError("decoded output exceeds inode size") if ip != len(source) or len(output) != output_size: raise ValueError("raw LZ4 size mismatch") return bytes(output) def read_compressed_inline(image: Image, inode: Inode) -> tuple[bytes, dict[str, int]]: header = (inode.offset + inode.inode_size + inode.xattr_size + 7) & ~7 if header > len(image.data) - 8: raise GateStop("compressed map header exceeds image") raw0, advise, algorithm, clusterbits = struct.unpack_from("> 16 if inode.layout != 3 or not advise & 0x0008 or idata_size == 0: raise GateStop("compressed fixture is not a compact inline-pcluster inode") block_end = min((inode.offset | (image.block_size - 1)) + 1, len(image.data)) matches = [] for position in range(header + 8, block_end - idata_size + 1): encoded = bytes(image.data[position : position + idata_size]) try: decoded = lz4_decode_exact(encoded, inode.size) except ValueError: continue matches.append((position, decoded)) if len(matches) != 1: raise GateStop(f"compressed fixture has {len(matches)} independent decode candidates") return matches[0][1], { "map_header": header, "advise": advise, "algorithm": algorithm & 0x0F, "clusterbits": clusterbits, "idata_size": idata_size, "encoded_offset": matches[0][0], } def read_fragment(image: Image, inode: Inode) -> tuple[bytes, dict[str, int]]: header = (inode.offset + inode.inode_size + inode.xattr_size + 7) & ~7 raw = image.u64(header) if inode.layout != 1 or raw & (1 << 63) == 0: raise GateStop("fragment fixture lacks the whole-fragment header") if image.packed_nid == 0 or image.packed_nid == inode.nid: raise GateStop("fragment fixture has an invalid packed nid") fragment_offset = raw ^ (1 << 63) packed = image.inode(image.packed_nid) packed_data = image.uncompressed_data(packed) if fragment_offset > len(packed_data) or inode.size > len(packed_data) - fragment_offset: raise GateStop("fragment target exceeds packed inode") return bytes(packed_data[fragment_offset : fragment_offset + inode.size]), { "map_header": header, "fragment_offset": fragment_offset, "packed_nid": image.packed_nid, "packed_layout": packed.layout, "packed_size": packed.size, } def target_bytes(length: int, nul_position: str | None = None) -> bytes: pattern = b"p15-019-safe-target/" target = bytearray((pattern * ((length + len(pattern) - 1) // len(pattern)))[:length]) if nul_position == "first": target[0] = 0 elif nul_position == "middle": target[length // 2] = 0 elif nul_position == "last": target[-1] = 0 return bytes(target) def protected_state() -> dict[str, Any]: protected = SPEC["protected"] base = Path(protected["base_image"]) base_stat = base.stat() if base.exists() else None pid_path = Path("/proc") / str(protected["pid"]) command = None if (pid_path / "cmdline").exists(): command = (pid_path / "cmdline").read_bytes().replace(b"\0", b" ").decode("utf-8", "replace").strip() port_hex = f"{protected['port']:04X}" listeners = 0 for table in (Path("/proc/net/tcp"), Path("/proc/net/tcp6")): if not table.exists(): continue for line in table.read_text(encoding="ascii").splitlines()[1:]: fields = line.split() if len(fields) >= 4 and fields[1].endswith(f":{port_hex}") and fields[3] == "0A": listeners += 1 return { "base": None if base_stat is None else { "device": base_stat.st_dev, "inode": base_stat.st_ino, "mtime_ns": base_stat.st_mtime_ns, "size": base_stat.st_size, }, "pid": protected["pid"], "pid_command_sha256": None if command is None else sha256_bytes(command.encode("utf-8")), "port": protected["port"], "port_listener_count": listeners, } def verify_identities() -> dict[str, Any]: required = SPEC["required_base"] resolved = git(ROOT, "rev-parse", REQUESTED_BASE) if resolved != required: raise GateStop(f"requested base {resolved} is not required base {required}") source_dirty = git(ROOT, "diff", "--name-only", required, "--", "repo-pre-15/src") if source_dirty: raise GateStop(f"production source differs from frozen base: {source_dirty}") source_hashes = {} for relative, expected in SPEC["source_sha256"].items(): content = subprocess.run( ["git", "-C", str(ROOT), "show", f"{required}:{relative}"], check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if content.returncode != 0: raise InfraBlocked(f"cannot read frozen source {relative}") actual = sha256_bytes(content.stdout) if actual != expected: raise GateStop(f"frozen source hash differs for {relative}") source_hashes[relative] = actual if git(FREEBSD_SRC, "rev-parse", "HEAD") != SPEC["freebsd"]["head"]: raise GateStop("FreeBSD source HEAD differs") for relative, expected in SPEC["freebsd"]["sha256"].items(): if sha256_path(FREEBSD_SRC / relative) != expected: raise GateStop(f"FreeBSD source hash differs for {relative}") if git(UTILS_SRC, "rev-parse", "HEAD") != SPEC["utils"]["head"]: raise GateStop("erofs-utils source HEAD differs") for relative, expected in SPEC["utils"]["sha256"].items(): actual = sha256_path(UTILS_SRC / relative) if actual != expected: raise GateStop(f"erofs-utils source hash differs for {relative}") for name, tool in SPEC["tools"].items(): path = Path(tool["path"]) if sha256_path(path) != tool["sha256"]: raise GateStop(f"tool hash differs for {name}") version = subprocess.run( [SPEC["tools"]["mkfs.erofs"]["path"], "-V"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ).stdout.splitlines()[0] if version != SPEC["tools"]["mkfs.erofs"]["version"]: raise GateStop(f"mkfs version differs: {version}") return {"base": resolved, "source_sha256": source_hashes, "mkfs_version": version} def verify_semantics() -> dict[str, Any]: linux = (ROOT / "src-linux/inode.c").read_text(encoding="utf-8") freebsd_inode = (DUT / "src/inode.c").read_text(encoding="utf-8") freebsd_data = (DUT / "src/data.c").read_text(encoding="utf-8") freebsd_vnops = (DUT / "src/erofs_vnops.c").read_text(encoding="utf-8") lookup = (FREEBSD_SRC / "sys/kern/vfs_lookup.c").read_text(encoding="utf-8") errno_h = (FREEBSD_SRC / "sys/sys/errno.h").read_text(encoding="utf-8") syslimits = (FREEBSD_SRC / "sys/sys/syslimits.h").read_text(encoding="utf-8") linux_markers = ( "vi->datalayout == EROFS_INODE_FLAT_INLINE", "kmemdup_nul(bptr + ofs, inode->i_size, GFP_KERNEL)", "!inode->i_size || strlen(link) != inode->i_size", "return -EFSCORRUPTED", ".get_link = page_get_link", ".get_link = simple_get_link", ) freebsd_markers = ( "return (erofs_read_uio(MTOE(vp->v_mount), VTOE(vp), uio));", "return (erofs_readlink_target(ap->a_vp, ap->a_uio));", ".vop_readlink = erofs_readlink", ) if not all(marker in linux for marker in linux_markers): raise GateStop("Linux fast/page symlink path anchors changed") if not all(marker in freebsd_data + freebsd_vnops for marker in freebsd_markers): raise GateStop("FreeBSD vnode/readlink path anchors changed") if "erofs_validate_symlink" in freebsd_inode + freebsd_data + freebsd_vnops: raise GateStop("P15-019 production implementation already exists at the gate base") if "aiov.iov_len = MAXPATHLEN;" not in lookup or "linklen == 0" not in lookup: raise GateStop("FreeBSD namei readlink boundary anchors changed") expected_errno = SPEC["errno"] for name, value in expected_errno.items(): if re.search(rf"#define\s+{name}\s+{value}\b", errno_h) is None: raise GateStop(f"FreeBSD errno {name} is not {value}") maxpath = SPEC["freebsd"]["maxpathlen"] if re.search(rf"#define\s+PATH_MAX\s+{maxpath}\b", syslimits) is None: raise GateStop("FreeBSD PATH_MAX differs") if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", freebsd_inode + freebsd_data + freebsd_vnops): raise GateStop("FreeBSD EROFS source contains Linux negative errno returns") return { "linux": { "fast_inline_validation": True, "noninline_get_link": "page_get_link", "errno_convention": "negative Linux errno", }, "freebsd": { "entry": "VOP_READLINK", "path": "vnode -> erofs_readlink_target -> map/read -> GEOM or compressed backing", "maxpathlen": maxpath, "empty_namei_baseline": "ENOENT after zero-byte VOP_READLINK success", "errno_convention": "positive FreeBSD errno", }, "candidate": { "empty": "EINTEGRITY", "embedded_nul": "EINTEGRITY before uiomove", "oversize": "ENAMETOOLONG before target I/O", "normal": "exact byte target; no trailing NUL required", }, } def make_source(source: Path, payload: bytes, native_symlink: bool) -> None: source.mkdir() source.chmod(0o755) link = source / "link" if native_symlink: os.symlink(payload.decode("ascii"), link) else: link.write_bytes(payload) link.chmod(0o644) os.utime(link, (0, 0), follow_symlinks=False) os.utime(source, (0, 0), follow_symlinks=False) def mkfs_case( layout: str, case_id: str, payload: bytes, native_symlink: bool, work: Path, fixtures: Path, ) -> tuple[Path, Path | None, list[int]]: source = work / f"source-{layout}-{case_id}" make_source(source, payload, native_symlink) image_path = fixtures / f"{layout}-{case_id}.erofs" blob_path = fixtures / f"{layout}-{case_id}.blob" if layout == "chunk" else None options: list[str] = [] if layout == "plain": options = ["-E^inline_data"] elif layout == "chunk": assert blob_path is not None blob_path.write_bytes(b"") options = ["--chunksize=4096", f"--blobdev={blob_path}"] elif layout == "compressed": options = ["-zlz4", "-C4096", "-Eztailpacking"] elif layout == "fragment": options = ["-zlz4", "-C4096", "-Eall-fragments"] command = [ SPEC["tools"]["mkfs.erofs"]["path"], "-T0", "--all-time", "--all-root", "--workers=1", "--sort=path", "-x-1", f"-U{SPEC['uuid']}", *options, str(image_path), str(source), ] run_required(command, work) before = image_path.read_bytes() image = Image(before) _, entry, inode = image.resolve() changed = [] if stat.S_IFMT(inode.mode) == S_IFREG: image.put_u16(inode.offset + 4, S_IFLNK | 0o777) image.data[entry.offset + 10] = EROFS_FT_SYMLINK elif stat.S_IFMT(inode.mode) != S_IFLNK or entry.file_type != EROFS_FT_SYMLINK: raise GateStop(f"{layout}/{case_id}: source inode type is unexpected") if case_id == "empty": if inode.inode_size == 64: image.put_u64(inode.offset + 8, 0) else: image.put_u32(inode.offset + 8, 0) image.update_checksum() after = bytes(image.data) for offset, (old, new) in enumerate(zip(before, after)): if old != new: changed.append(offset) allowed = set(range(SUPER + 4, SUPER + 8)) allowed.update(range(inode.offset + 4, inode.offset + 6)) allowed.add(entry.offset + 10) if case_id == "empty": allowed.update(range(inode.offset + 8, inode.offset + (16 if inode.inode_size == 64 else 12))) unexpected = sorted(set(changed) - allowed) if unexpected: raise GateStop(f"{layout}/{case_id}: transform changed unexpected offsets {unexpected}") image_path.write_bytes(after) return image_path, blob_path, changed def expected_case(layout: str, case_id: str) -> tuple[int, bytes, str | None, str]: short = SPEC["layouts"][layout]["short_length"] if case_id == "normal-short": return short, target_bytes(short), None, "PASS" if case_id == "normal-max": limit = SPEC["freebsd"]["maxpathlen"] return limit, target_bytes(limit), None, "PASS" if case_id == "empty": return 0, target_bytes(short), None, "EINTEGRITY" if case_id.startswith("nul-"): position = case_id.removeprefix("nul-") limit = SPEC["freebsd"]["maxpathlen"] return limit, target_bytes(limit, position), position, "EINTEGRITY" if case_id == "too-long": length = SPEC["freebsd"]["maxpathlen"] + 1 return length, target_bytes(length), None, "ENAMETOOLONG" raise AssertionError(case_id) def independent_target(image: Image, inode: Inode, blob: bytes | None) -> tuple[bytes, dict[str, Any]]: if inode.layout in (0, 2, 4): return image.uncompressed_data(inode, blob), {} if inode.layout == 3: return read_compressed_inline(image, inode) if inode.layout == 1: return read_fragment(image, inode) raise GateStop(f"unsupported target layout {inode.layout}") def oracle(image: Image, inode: Inode, blob: bytes | None) -> tuple[str, bytes | None, dict[str, Any]]: if inode.size == 0: return "EINTEGRITY", None, {"point": "symlink.empty"} if inode.size > SPEC["freebsd"]["maxpathlen"]: return "ENAMETOOLONG", None, {"point": "symlink.maxpathlen"} target, details = independent_target(image, inode, blob) nul = target.find(b"\0") if nul >= 0: return "EINTEGRITY", target, {"point": "symlink.embedded-nul", "nul_offset": nul, **details} return "PASS", target, {"point": "symlink.valid", **details} def fixture_set_hash(fixtures: Path) -> tuple[str, dict[str, str]]: hashes = {} for path in sorted(item for item in fixtures.iterdir() if item.is_file()): if path.name == "SHA256SUMS": continue hashes[path.name] = sha256_path(path) lines = "".join(f"{digest} {name}\n" for name, digest in hashes.items()) (fixtures / "SHA256SUMS").write_text(lines, encoding="ascii") return sha256_bytes(lines.encode("ascii")), hashes def execute() -> dict[str, Any]: if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-019" or SPEC.get("gate") != "G03": raise InfraBlocked("input identity is invalid") identity = verify_identities() semantics = verify_semantics() write_json(OUTPUT / "semantics.json", semantics) fixtures = OUTPUT / "fixtures" fixtures.mkdir() cases = ( "normal-short", "normal-max", "empty", "nul-first", "nul-middle", "nul-last", "too-long", ) records = [] with tempfile.TemporaryDirectory(prefix=".p15-019-work-", dir=OUTPUT) as raw_work: work = Path(raw_work) for layout, layout_spec in sorted(SPEC["layouts"].items()): for case_id in cases: expected_size, payload, nul_position, expected = expected_case(layout, case_id) native = layout == "inline" and nul_position is None image_path, blob_path, changed = mkfs_case( layout, case_id, payload, native, work, fixtures ) image = Image.load(image_path) _, entry, inode = image.resolve() if inode.layout != layout_spec["expected_layout"]: raise GateStop( f"{layout}/{case_id}: layout {inode.layout} != {layout_spec['expected_layout']}" ) if stat.S_IFMT(inode.mode) != S_IFLNK or entry.file_type != EROFS_FT_SYMLINK: raise GateStop(f"{layout}/{case_id}: transformed inode is not a symlink") if inode.size != expected_size: raise GateStop(f"{layout}/{case_id}: inode size {inode.size} != {expected_size}") blob = blob_path.read_bytes() if blob_path is not None else None observed, target, details = oracle(image, inode, blob) if observed != expected: raise GateStop(f"{layout}/{case_id}: oracle {observed} != {expected}") if expected != "EINTEGRITY" or case_id == "empty": if case_id != "empty" and target is not None and target != payload: raise GateStop(f"{layout}/{case_id}: target bytes differ from source") if case_id.startswith("nul-"): expected_nul = {"first": 0, "middle": len(payload) // 2, "last": len(payload) - 1}[nul_position] if details.get("nul_offset") != expected_nul: raise GateStop(f"{layout}/{case_id}: NUL offset differs") dump = run( [SPEC["tools"]["dump.erofs"]["path"], "--path=/link", str(image_path)], work, ) dump_layout = re.search(r"Layout:\s+(\d+)", dump.stdout) dump_mode = "symlink file" in dump.stdout if dump.returncode != 0 or dump_layout is None or int(dump_layout.group(1)) != inode.layout or not dump_mode: raise GateStop(f"{layout}/{case_id}: dump.erofs cross-check failed") fsck_argv = [SPEC["tools"]["fsck.erofs"]["path"], "-d0"] if blob_path is not None: fsck_argv.append(f"--device={blob_path}") fsck_argv.append(str(image_path)) fsck = run(fsck_argv, work) extracted = False if expected == "PASS": extract = work / f"extract-{layout}-{case_id}" extract.mkdir() extract_argv = [ SPEC["tools"]["fsck.erofs"]["path"], f"--extract={extract}", "--no-preserve", ] if blob_path is not None: extract_argv.append(f"--device={blob_path}") extract_argv.append(str(image_path)) run_required(extract_argv, work) extracted_link = extract / "link" if not extracted_link.is_symlink() or os.readlink(extracted_link).encode("ascii") != payload: raise GateStop(f"{layout}/{case_id}: fsck extraction target differs") extracted = True records.append( { "case": case_id, "changed_offsets": changed, "dump_cross_check": "PASS", "expected": expected, "fsck_check_exit": fsck.returncode, "fsck_extract": extracted, "image": image_path.name, "inode": { "layout": inode.layout, "layout_name": LAYOUT_NAMES[inode.layout], "mode": oct(inode.mode), "nid": inode.nid, "size": inode.size, }, "layout": layout, "oracle": details, "source_payload_sha256": sha256_bytes(payload), "target_sha256": None if target is None else sha256_bytes(target), } ) fixture_sha256, hashes = fixture_set_hash(fixtures) expected_fixture = SPEC.get("expected_fixture_set_sha256", "") if expected_fixture and fixture_sha256 != expected_fixture: raise GateStop( f"fixture set hash {fixture_sha256} != frozen {expected_fixture}" ) write_json(OUTPUT / "cases.json", records) write_json(OUTPUT / "commands.json", COMMANDS) by_layout = { layout: { record["case"]: record["expected"] for record in records if record["layout"] == layout } for layout in sorted(SPEC["layouts"]) } required = set(cases) if any(set(result) != required for result in by_layout.values()): raise GateStop("one or more layouts lack a complete oracle") return { "candidate": "P15-019", "case_count": len(records), "decision": "GO", "fixture_file_count": len(hashes), "fixture_set_sha256": fixture_sha256, "gate": "G03", "identity": identity, "layout_results": by_layout, "qemu": "NOT_RUN: Stage0 host fixture/oracle is complete", "source_modified": False, } before_protected = protected_state() status = "INFRA_BLOCKED" exit_code = 2 result: dict[str, Any] try: result = execute() status = "GO" exit_code = 0 except GateStop as error: status = "STOP" exit_code = 1 result = {"candidate": "P15-019", "decision": status, "reason": str(error)} except (InfraBlocked, OSError, ValueError, KeyError, json.JSONDecodeError) as error: result = {"candidate": "P15-019", "decision": status, "reason": str(error)} except Exception as error: result = { "candidate": "P15-019", "decision": status, "reason": f"unexpected gate error: {error}", "traceback": traceback.format_exc(), } after_protected = protected_state() cleanup = { "owned_temp_remaining": sorted(path.name for path in OUTPUT.glob(".p15-019-work-*")), "protected_before": before_protected, "protected_after": after_protected, "protected_unchanged": before_protected == after_protected, } write_json(OUTPUT / "cleanup.json", cleanup) if cleanup["owned_temp_remaining"] or not cleanup["protected_unchanged"]: status = "INFRA_BLOCKED" exit_code = 2 result = { "candidate": "P15-019", "decision": status, "reason": "cleanup or protected-resource identity changed", } result["cleanup_sha256"] = sha256_path(OUTPUT / "cleanup.json") result["input_sha256"] = sha256_path(INPUT) write_json(OUTPUT / "result.json", result) print(f"P15-019 G03: {status}") if "fixture_set_sha256" in result: print(f"fixture_set_sha256={result['fixture_set_sha256']}") if "reason" in result: print(f"reason={result['reason']}") sys.exit(exit_code) PY