1278 lines
46 KiB
Python
Executable File
1278 lines
46 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Build and verify the deterministic xattr, ACL, and metabox G4 fixtures."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import stat
|
|
import struct
|
|
import subprocess
|
|
|
|
|
|
SUPER = 1024
|
|
MAGIC = 0xE0F5E1E2
|
|
FEATURE_COMPAT_SB_CHKSUM = 0x00000001
|
|
FEATURE_COMPAT_SHARED_EA_IN_METABOX = 0x00000008
|
|
FEATURE_COMPAT_PLAIN_XATTR_PFX = 0x00000010
|
|
FEATURE_COMPAT_ISHARE_XATTRS = 0x00000020
|
|
FEATURE_INCOMPAT_FRAGMENTS = 0x00000020
|
|
FEATURE_INCOMPAT_XATTR_PREFIXES = 0x00000040
|
|
FEATURE_INCOMPAT_METABOX = 0x00000100
|
|
XATTR_INDEX_USER = 1
|
|
XATTR_INDEX_ACL_ACCESS = 2
|
|
XATTR_INDEX_ACL_DEFAULT = 3
|
|
XATTR_INDEX_TRUSTED = 4
|
|
XATTR_INDEX_SECURITY = 6
|
|
XATTR_LONG_PREFIX = 0x80
|
|
METABOX_NID_BIT = 1 << 63
|
|
LAYOUT_FLAT_PLAIN = 0
|
|
LAYOUT_COMPRESSED_FULL = 1
|
|
LAYOUT_FLAT_INLINE = 2
|
|
LAYOUT_COMPRESSED_COMPACT = 3
|
|
ACL_VERSION = 2
|
|
ACL_USER_OBJ = 0x01
|
|
ACL_USER = 0x02
|
|
ACL_GROUP_OBJ = 0x04
|
|
ACL_GROUP = 0x08
|
|
ACL_MASK = 0x10
|
|
ACL_OTHER = 0x20
|
|
ACL_UNDEFINED_ID = 0xFFFFFFFF
|
|
BLOCK_SIZE = 4096
|
|
METABOX_SIZE = 32768
|
|
METABOX_INODE_A = 16
|
|
METABOX_INODE_B = 32
|
|
METABOX_PREFIX_OFFSET = 8192
|
|
METABOX_XATTR_BASE = 4096
|
|
CRC32C_POLYNOMIAL = 0x82F63B78
|
|
|
|
|
|
class FixtureError(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Inode:
|
|
nid: int
|
|
offset: int
|
|
inode_format: int
|
|
inode_size: int
|
|
xattr_size: int
|
|
layout: int
|
|
mode: int
|
|
size: int
|
|
start_block: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DirectoryEntry:
|
|
name: bytes
|
|
nid: int
|
|
offset: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class XattrEntry:
|
|
offset: int
|
|
size: int
|
|
name_index: int
|
|
name: bytes
|
|
value: bytes
|
|
|
|
|
|
def align(value: int, alignment: int) -> int:
|
|
return (value + alignment - 1) & -alignment
|
|
|
|
|
|
def sha256_bytes(data: bytes | bytearray) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def sha256_path(path: Path) -> str:
|
|
return sha256_bytes(path.read_bytes())
|
|
|
|
|
|
def crc32c(data: bytes | bytearray) -> int:
|
|
checksum = 0xFFFFFFFF
|
|
for byte in data:
|
|
checksum ^= byte
|
|
for _ in range(8):
|
|
checksum = (checksum >> 1) ^ (
|
|
CRC32C_POLYNOMIAL if checksum & 1 else 0
|
|
)
|
|
return checksum & 0xFFFFFFFF
|
|
|
|
|
|
class ErofsImage:
|
|
def __init__(self, data: bytes | bytearray, source: Path) -> None:
|
|
self.data = bytearray(data)
|
|
self.source = source
|
|
self.validate_superblock()
|
|
|
|
@classmethod
|
|
def load(cls, path: Path) -> "ErofsImage":
|
|
return cls(path.read_bytes(), path)
|
|
|
|
def clone(self) -> "ErofsImage":
|
|
return ErofsImage(self.data, self.source)
|
|
|
|
def u16(self, offset: int) -> int:
|
|
return struct.unpack_from("<H", self.data, offset)[0]
|
|
|
|
def u32(self, offset: int) -> int:
|
|
return struct.unpack_from("<I", self.data, offset)[0]
|
|
|
|
def u64(self, offset: int) -> int:
|
|
return struct.unpack_from("<Q", self.data, offset)[0]
|
|
|
|
def put_u16(self, offset: int, value: int) -> None:
|
|
struct.pack_into("<H", self.data, offset, value)
|
|
|
|
def put_u32(self, offset: int, value: int) -> None:
|
|
struct.pack_into("<I", self.data, offset, value)
|
|
|
|
def put_u64(self, offset: int, value: int) -> None:
|
|
struct.pack_into("<Q", self.data, offset, value)
|
|
|
|
@property
|
|
def block_bits(self) -> int:
|
|
return self.data[SUPER + 12]
|
|
|
|
@property
|
|
def block_size(self) -> int:
|
|
return 1 << self.block_bits
|
|
|
|
@property
|
|
def feature_compat(self) -> int:
|
|
return self.u32(SUPER + 8)
|
|
|
|
@property
|
|
def feature_incompat(self) -> int:
|
|
return self.u32(SUPER + 80)
|
|
|
|
@property
|
|
def blocks(self) -> int:
|
|
return self.u32(SUPER + 36)
|
|
|
|
@property
|
|
def declared_size(self) -> int:
|
|
return self.blocks << self.block_bits
|
|
|
|
@property
|
|
def root_nid(self) -> int:
|
|
return self.u16(SUPER + 14)
|
|
|
|
@property
|
|
def meta_offset(self) -> int:
|
|
return self.u32(SUPER + 40) << self.block_bits
|
|
|
|
@property
|
|
def packed_nid(self) -> int:
|
|
return self.u64(SUPER + 96)
|
|
|
|
@property
|
|
def checksum_end(self) -> int:
|
|
return SUPER + self.block_size - SUPER
|
|
|
|
def calculated_checksum(self) -> int:
|
|
window = bytearray(self.data[SUPER : self.checksum_end])
|
|
struct.pack_into("<I", window, 4, 0)
|
|
return crc32c(window)
|
|
|
|
def update_checksum(self) -> None:
|
|
if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM:
|
|
raise FixtureError(f"{self.source}: checksum feature is required")
|
|
self.put_u32(SUPER + 4, 0)
|
|
self.put_u32(SUPER + 4, self.calculated_checksum())
|
|
if self.u32(SUPER + 4) != self.calculated_checksum():
|
|
raise AssertionError("updated checksum does not verify")
|
|
|
|
def validate_superblock(self) -> None:
|
|
if len(self.data) < SUPER + 144:
|
|
raise FixtureError(f"{self.source}: image is shorter than 1168 bytes")
|
|
if self.u32(SUPER) != MAGIC:
|
|
raise FixtureError(f"{self.source}: bad magic")
|
|
if not 9 <= self.block_bits <= 16:
|
|
raise FixtureError(f"{self.source}: invalid block bits")
|
|
if self.blocks == 0 or self.declared_size > len(self.data):
|
|
raise FixtureError(f"{self.source}: invalid declared size")
|
|
if (
|
|
self.feature_compat & FEATURE_COMPAT_SB_CHKSUM
|
|
and self.u32(SUPER + 4) != self.calculated_checksum()
|
|
):
|
|
raise FixtureError(f"{self.source}: bad checksum")
|
|
|
|
def inode(self, nid: int) -> Inode:
|
|
offset = self.meta_offset + (nid << 5)
|
|
if offset > self.declared_size - 32:
|
|
raise FixtureError(f"{self.source}: nid {nid} is outside the image")
|
|
inode_format = self.u16(offset)
|
|
inode_size = 64 if inode_format & 1 else 32
|
|
count = self.u16(offset + 2)
|
|
xattr_size = 0 if count == 0 else 12 + 4 * (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 inode_data(self, inode: Inode) -> bytes:
|
|
if inode.layout == LAYOUT_FLAT_PLAIN:
|
|
start = inode.start_block << self.block_bits
|
|
end = start + inode.size
|
|
if end > self.declared_size:
|
|
raise FixtureError("plain inode data exceeds the declared image")
|
|
return bytes(self.data[start:end])
|
|
if inode.layout != LAYOUT_FLAT_INLINE:
|
|
raise FixtureError(f"inode {inode.nid}: unsupported host layout {inode.layout}")
|
|
full_size = inode.size // self.block_size * self.block_size
|
|
if inode.size and inode.size % self.block_size == 0:
|
|
full_size -= self.block_size
|
|
full_start = inode.start_block << self.block_bits
|
|
inline_start = inode.offset + inode.inode_size + inode.xattr_size
|
|
full = self.data[full_start : full_start + full_size]
|
|
tail = self.data[inline_start : inline_start + inode.size - full_size]
|
|
if len(full) + len(tail) != inode.size:
|
|
raise FixtureError("inline inode data is truncated")
|
|
return bytes(full + tail)
|
|
|
|
def directory_entries(self, inode: Inode) -> list[DirectoryEntry]:
|
|
data = self.inode_data(inode)
|
|
entries: list[DirectoryEntry] = []
|
|
data_base = (
|
|
inode.start_block << self.block_bits
|
|
if inode.layout == LAYOUT_FLAT_PLAIN
|
|
else inode.offset + inode.inode_size + inode.xattr_size
|
|
)
|
|
for block_start in range(0, len(data), self.block_size):
|
|
block = data[block_start : block_start + self.block_size]
|
|
if len(block) < 12:
|
|
raise FixtureError("short directory block")
|
|
first_name = struct.unpack_from("<H", block, 8)[0]
|
|
if first_name < 12 or first_name % 12 or first_name > len(block):
|
|
raise FixtureError("invalid directory first-name offset")
|
|
count = first_name // 12
|
|
for index in range(count):
|
|
item = index * 12
|
|
name_start = struct.unpack_from("<H", block, item + 8)[0]
|
|
name_end = (
|
|
struct.unpack_from("<H", block, item + 20)[0]
|
|
if index + 1 < count
|
|
else len(block)
|
|
)
|
|
name = block[name_start:name_end].split(b"\0", 1)[0]
|
|
entries.append(
|
|
DirectoryEntry(
|
|
name=name,
|
|
nid=struct.unpack_from("<Q", block, item)[0],
|
|
offset=data_base + block_start + item,
|
|
)
|
|
)
|
|
return entries
|
|
|
|
def resolve_root(self, name: str) -> tuple[DirectoryEntry, Inode]:
|
|
encoded = name.removeprefix("/").encode("ascii")
|
|
root = self.inode(self.root_nid)
|
|
for entry in self.directory_entries(root):
|
|
if entry.name == encoded:
|
|
return entry, self.inode(entry.nid)
|
|
raise FixtureError(f"{self.source}: root entry not found: {name}")
|
|
|
|
def inline_xattrs(self, inode: Inode) -> tuple[int, list[XattrEntry]]:
|
|
if inode.xattr_size < 12:
|
|
raise FixtureError(f"inode {inode.nid} has no xattr header")
|
|
body = inode.offset + inode.inode_size
|
|
shared_count = self.data[body + 4]
|
|
cursor = body + 12 + shared_count * 4
|
|
end = body + inode.xattr_size
|
|
if cursor > end:
|
|
raise FixtureError("shared xattr array exceeds inode body")
|
|
entries = []
|
|
while cursor < end:
|
|
if cursor > end - 4:
|
|
raise FixtureError("truncated inline xattr header")
|
|
name_len = self.data[cursor]
|
|
name_index = self.data[cursor + 1]
|
|
value_len = self.u16(cursor + 2)
|
|
size = align(4 + name_len + value_len, 4)
|
|
if size > end - cursor:
|
|
raise FixtureError("inline xattr entry exceeds inode body")
|
|
entries.append(
|
|
XattrEntry(
|
|
offset=cursor,
|
|
size=size,
|
|
name_index=name_index,
|
|
name=bytes(self.data[cursor + 4 : cursor + 4 + name_len]),
|
|
value=bytes(
|
|
self.data[
|
|
cursor + 4 + name_len : cursor + 4 + name_len + value_len
|
|
]
|
|
),
|
|
)
|
|
)
|
|
cursor += size
|
|
return shared_count, entries
|
|
|
|
def shared_ids(self, inode: Inode) -> list[int]:
|
|
body = inode.offset + inode.inode_size
|
|
count = self.data[body + 4]
|
|
return [self.u32(body + 12 + index * 4) for index in range(count)]
|
|
|
|
def shared_xattr(self, shared_id: int) -> XattrEntry:
|
|
offset = (self.u32(SUPER + 44) << self.block_bits) + shared_id * 4
|
|
if offset > self.declared_size - 4:
|
|
raise FixtureError(f"shared id {shared_id} starts outside the image")
|
|
name_len = self.data[offset]
|
|
value_len = self.u16(offset + 2)
|
|
size = align(4 + name_len + value_len, 4)
|
|
if size > self.declared_size - offset:
|
|
raise FixtureError(f"shared id {shared_id} exceeds the image")
|
|
return XattrEntry(
|
|
offset=offset,
|
|
size=size,
|
|
name_index=self.data[offset + 1],
|
|
name=bytes(self.data[offset + 4 : offset + 4 + name_len]),
|
|
value=bytes(
|
|
self.data[offset + 4 + name_len : offset + 4 + name_len + value_len]
|
|
),
|
|
)
|
|
|
|
def find_xattr(self, path: str, name: bytes) -> tuple[str, XattrEntry, int | None]:
|
|
_, inode = self.resolve_root(path)
|
|
_, inline = self.inline_xattrs(inode)
|
|
for entry in inline:
|
|
if entry.name == name:
|
|
return "inline", entry, None
|
|
for index, shared_id in enumerate(self.shared_ids(inode)):
|
|
entry = self.shared_xattr(shared_id)
|
|
if entry.name == name:
|
|
return "shared", entry, index
|
|
raise FixtureError(f"{path}: xattr {name!r} not found")
|
|
|
|
def prefix_records(self) -> list[tuple[int, int, bytes]]:
|
|
count = self.data[SUPER + 91]
|
|
if count == 0:
|
|
return []
|
|
if self.feature_compat & FEATURE_COMPAT_PLAIN_XATTR_PFX:
|
|
backing = bytes(self.data[: self.declared_size])
|
|
else:
|
|
if self.packed_nid == 0:
|
|
raise FixtureError("prefix table has no packed backing")
|
|
backing = self.inode_data(self.inode(self.packed_nid))
|
|
cursor = self.u32(SUPER + 92) << 2
|
|
records = []
|
|
for _ in range(count):
|
|
cursor = align(cursor, 4)
|
|
if cursor > len(backing) - 3:
|
|
raise FixtureError("prefix record header exceeds backing")
|
|
length = struct.unpack_from("<H", backing, cursor)[0]
|
|
if not 1 <= length <= 256 or cursor + 2 + length > len(backing):
|
|
raise FixtureError("invalid prefix record")
|
|
records.append((cursor, backing[cursor + 2], backing[cursor + 3 : cursor + 2 + length]))
|
|
cursor += 2 + length
|
|
return records
|
|
|
|
def save(self, path: Path) -> None:
|
|
path.write_bytes(self.data)
|
|
|
|
|
|
def xattr_entry(name_index: int, name: bytes, value: bytes) -> bytes:
|
|
raw = struct.pack("<BBH", len(name), name_index, len(value)) + name + value
|
|
return raw + bytes(align(len(raw), 4) - len(raw))
|
|
|
|
|
|
def acl_value(entries: list[tuple[int, int, int]]) -> bytes:
|
|
return struct.pack("<I", ACL_VERSION) + b"".join(
|
|
struct.pack("<HHI", tag, permissions, identifier)
|
|
for tag, permissions, identifier in entries
|
|
)
|
|
|
|
|
|
def canonical_acl(*named_users: int) -> bytes:
|
|
entries = [(ACL_USER_OBJ, 7, ACL_UNDEFINED_ID)]
|
|
entries.extend((ACL_USER, 5, identifier) for identifier in named_users)
|
|
entries.extend(
|
|
[
|
|
(ACL_GROUP_OBJ, 5, ACL_UNDEFINED_ID),
|
|
(ACL_MASK, 5, ACL_UNDEFINED_ID),
|
|
(ACL_OTHER, 1, ACL_UNDEFINED_ID),
|
|
]
|
|
)
|
|
return acl_value(entries)
|
|
|
|
|
|
def prefix_record(base_index: int, infix: bytes) -> bytes:
|
|
payload = bytes([base_index]) + infix
|
|
return struct.pack("<H", len(payload)) + payload
|
|
|
|
|
|
def add_aligned(blob: bytearray, offset: int, payload: bytes) -> int:
|
|
start = align(offset, 4)
|
|
blob[start : start + len(payload)] = payload
|
|
return start
|
|
|
|
|
|
def synthetic_inode(content: bytes, shared_ids: list[int], item_value: bytes) -> bytes:
|
|
header = bytearray(12 + 4 * len(shared_ids))
|
|
header[4] = len(shared_ids)
|
|
for index, shared_id in enumerate(shared_ids):
|
|
struct.pack_into("<I", header, 12 + index * 4, shared_id)
|
|
body = header + xattr_entry(XATTR_INDEX_USER, b"item", item_value)
|
|
body.extend(bytes(align(len(body), 4) - len(body)))
|
|
icount = 1 + (len(body) - 12) // 4
|
|
inode = bytearray(32)
|
|
struct.pack_into("<HHHHIII", inode, 0, 4, icount, stat.S_IFREG | 0o644, 1, len(content), 0, 0)
|
|
return bytes(inode + body + content)
|
|
|
|
|
|
def make_metabox_payload() -> tuple[bytes, dict[str, object]]:
|
|
payload = bytearray(METABOX_SIZE)
|
|
shared_offset = METABOX_XATTR_BASE
|
|
shared_records = []
|
|
for name_index, name, value in (
|
|
(XATTR_INDEX_USER, b"metaboxshared", b"nonzero-base"),
|
|
(XATTR_LONG_PREFIX | 0, b"setting", b"long-prefix-value"),
|
|
(XATTR_INDEX_USER, b"shared-prefix-key", b"shared-value"),
|
|
):
|
|
raw = xattr_entry(name_index, name, value)
|
|
start = add_aligned(payload, shared_offset, raw)
|
|
shared_id = (start - METABOX_XATTR_BASE) // 4
|
|
shared_records.append((shared_id, start, raw))
|
|
shared_offset = start + len(raw)
|
|
|
|
prefix_cursor = METABOX_PREFIX_OFFSET
|
|
prefix_fields = []
|
|
for base_index, infix in (
|
|
(XATTR_INDEX_USER, b"repo22.application.component."),
|
|
(XATTR_INDEX_TRUSTED, b"repo22.trusted.deep."),
|
|
):
|
|
raw = prefix_record(base_index, infix)
|
|
start = add_aligned(payload, prefix_cursor, raw)
|
|
prefix_fields.append((start, base_index, infix))
|
|
prefix_cursor = start + len(raw)
|
|
|
|
inode_a = synthetic_inode(
|
|
b"metabox-file-a\n",
|
|
[shared_records[0][0], shared_records[1][0], shared_records[2][0]],
|
|
b"value-000",
|
|
)
|
|
inode_b = synthetic_inode(
|
|
b"metabox-file-b\n",
|
|
[shared_records[0][0]],
|
|
b"value-001",
|
|
)
|
|
offset_a = METABOX_INODE_A << 5
|
|
offset_b = METABOX_INODE_B << 5
|
|
if offset_a + len(inode_a) >= offset_b:
|
|
raise AssertionError("synthetic metabox inodes overlap")
|
|
payload[offset_a : offset_a + len(inode_a)] = inode_a
|
|
payload[offset_b : offset_b + len(inode_b)] = inode_b
|
|
|
|
manifest = {
|
|
"size": len(payload),
|
|
"sha256": sha256_bytes(payload),
|
|
"inode_a": {"nid": METABOX_INODE_A, "offset": offset_a},
|
|
"inode_b": {"nid": METABOX_INODE_B, "offset": offset_b},
|
|
"xattr_base": METABOX_XATTR_BASE,
|
|
"shared": [
|
|
{
|
|
"id": shared_id,
|
|
"offset": offset,
|
|
"sha256": sha256_bytes(raw),
|
|
}
|
|
for shared_id, offset, raw in shared_records
|
|
],
|
|
"prefix_start": METABOX_PREFIX_OFFSET // 4,
|
|
"prefixes": [
|
|
{"offset": offset, "base_index": base_index, "infix": infix.decode("ascii")}
|
|
for offset, base_index, infix in prefix_fields
|
|
],
|
|
}
|
|
return bytes(payload), manifest
|
|
|
|
|
|
def write_file(path: Path, data: bytes = b"") -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(data)
|
|
|
|
|
|
def set_user_xattr(path: Path, name: str, value: bytes) -> None:
|
|
os.setxattr(path, f"user.{name}", value)
|
|
|
|
|
|
def normalize_times(root: Path) -> None:
|
|
for path in sorted(root.rglob("*"), reverse=True):
|
|
os.utime(path, (0, 0), follow_symlinks=False)
|
|
os.utime(root, (0, 0), follow_symlinks=False)
|
|
|
|
|
|
def make_sources(output: Path) -> dict[str, object]:
|
|
source_root = output / "source"
|
|
basic = source_root / "basic"
|
|
carrier = source_root / "carrier"
|
|
basic.mkdir(parents=True)
|
|
carrier.mkdir(parents=True)
|
|
metabox_payload, metabox_manifest = make_metabox_payload()
|
|
|
|
basic_files: dict[str, dict[str, bytes]] = {
|
|
"inline-user": {
|
|
"comment": b"inline-user-value\x00tail",
|
|
"special.chars": b"special-value",
|
|
},
|
|
"inline-multi": {
|
|
"attr1": b"one",
|
|
"attr2": b"two\x00binary",
|
|
"attr3": bytes(range(32)),
|
|
},
|
|
"inline-trusted": {"admin": b"trusted-inline-value"},
|
|
"inline-security": {
|
|
"capability": bytes.fromhex("0100000200000000aabbccdd"),
|
|
"selinux": b"system_u:object_r:repo22_t:s0\x00",
|
|
},
|
|
"corrupt-inline": {"bad": b"bad-format-target"},
|
|
"user-subset": {
|
|
"comment": b"subset-comment",
|
|
"author": b"repo22",
|
|
"checksum": b"sha256:0123456789abcdef",
|
|
"com.repo22.app.setting": b"enabled",
|
|
},
|
|
}
|
|
for name, attrs in basic_files.items():
|
|
path = basic / name
|
|
write_file(path, f"{name}\n".encode("ascii"))
|
|
for xattr_name, value in attrs.items():
|
|
set_user_xattr(path, xattr_name, value)
|
|
|
|
for name in ("shared-a", "shared-b", "shared-multi"):
|
|
path = basic / name
|
|
write_file(path, f"{name}\n".encode("ascii"))
|
|
set_user_xattr(path, "shared_key", b"shared-value\x00exact")
|
|
set_user_xattr(path, "shared_comment", b"shared-comment")
|
|
set_user_xattr(path, "shared_binary", bytes(range(16)))
|
|
set_user_xattr(basic / "shared-a", "local", b"in-bounds-local")
|
|
|
|
for name in ("trusted-shared-a", "trusted-shared-b", "trusted-shared-c"):
|
|
path = basic / name
|
|
write_file(path, f"{name}\n".encode("ascii"))
|
|
set_user_xattr(path, "config", b"trusted-shared-value")
|
|
|
|
for name in ("security-shared-a", "security-shared-b", "security-shared-c"):
|
|
path = basic / name
|
|
write_file(path, f"{name}\n".encode("ascii"))
|
|
set_user_xattr(path, "selinux", b"system_u:object_r:shared_repo22_t:s0\x00")
|
|
|
|
for index in range(4):
|
|
path = basic / f"prefix-user-{index}"
|
|
write_file(path, f"prefix user {index}\n".encode("ascii"))
|
|
set_user_xattr(
|
|
path,
|
|
"repo22.application.component.setting",
|
|
f"prefix-value-{index}".encode("ascii"),
|
|
)
|
|
for index in range(3):
|
|
path = basic / f"prefix-trusted-{index}"
|
|
write_file(path, f"prefix trusted {index}\n".encode("ascii"))
|
|
set_user_xattr(
|
|
path,
|
|
"repo22.trusted.deep.setting",
|
|
f"trusted-prefix-value-{index}".encode("ascii"),
|
|
)
|
|
|
|
acl_values = {
|
|
"acl-unordered": canonical_acl(3002, 2002),
|
|
"acl-header": struct.pack("<I", ACL_VERSION),
|
|
"acl-duplicate": canonical_acl(2002, 2002),
|
|
"acl-phase": acl_value(
|
|
[
|
|
(ACL_USER_OBJ, 7, ACL_UNDEFINED_ID),
|
|
(ACL_GROUP_OBJ, 5, ACL_UNDEFINED_ID),
|
|
(ACL_USER, 5, 2002),
|
|
(ACL_MASK, 5, ACL_UNDEFINED_ID),
|
|
(ACL_OTHER, 1, ACL_UNDEFINED_ID),
|
|
]
|
|
),
|
|
"acl-perm": acl_value(
|
|
[
|
|
(ACL_USER_OBJ, 8, ACL_UNDEFINED_ID),
|
|
(ACL_GROUP_OBJ, 5, ACL_UNDEFINED_ID),
|
|
(ACL_OTHER, 1, ACL_UNDEFINED_ID),
|
|
]
|
|
),
|
|
}
|
|
for name, value in acl_values.items():
|
|
path = basic / name
|
|
write_file(path)
|
|
set_user_xattr(path, "acl_access_placeholder", value)
|
|
|
|
fifo = basic / "fifo-node"
|
|
write_file(fifo)
|
|
fifo.chmod(0o640)
|
|
set_user_xattr(fifo, "acl_access_placeholder", canonical_acl(2002))
|
|
set_user_xattr(fifo, "fifo_note", b"fifo-readonly-xattr")
|
|
|
|
write_file(basic / "metabox-carrier.bin", metabox_payload)
|
|
write_file(basic / "metabox-a", b"primary placeholder a\n")
|
|
write_file(basic / "metabox-b", b"primary placeholder b\n")
|
|
|
|
write_file(carrier / "metabox-carrier.bin", metabox_payload)
|
|
write_file(carrier / "metabox-a", b"primary placeholder a\n")
|
|
write_file(carrier / "metabox-b", b"primary placeholder b\n")
|
|
set_user_xattr(
|
|
carrier / "metabox-a",
|
|
"repo22.application.component.seed",
|
|
b"prefix-seed",
|
|
)
|
|
|
|
normalize_times(source_root)
|
|
return {"metabox_payload": metabox_manifest}
|
|
|
|
|
|
def source_inventory(root: Path) -> dict[str, object]:
|
|
entries = []
|
|
for path in sorted([root, *root.rglob("*")]):
|
|
info = path.lstat()
|
|
relative = path.relative_to(root).as_posix() or "."
|
|
item: dict[str, object] = {
|
|
"path": relative,
|
|
"mode": stat.S_IFMT(info.st_mode) | stat.S_IMODE(info.st_mode),
|
|
"uid": info.st_uid,
|
|
"gid": info.st_gid,
|
|
"size": info.st_size,
|
|
}
|
|
if path.is_file():
|
|
item["content_sha256"] = sha256_path(path)
|
|
names = sorted(os.listxattr(path, follow_symlinks=False))
|
|
item["xattrs"] = {
|
|
name: os.getxattr(path, name, follow_symlinks=False).hex()
|
|
for name in names
|
|
}
|
|
entries.append(item)
|
|
canonical = json.dumps(entries, separators=(",", ":"), sort_keys=True).encode("ascii")
|
|
return {"sha256": sha256_bytes(canonical), "entries": entries}
|
|
|
|
|
|
def tool_version() -> str:
|
|
result = subprocess.run(
|
|
["mkfs.erofs", "-V"],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
)
|
|
version = result.stdout.strip()
|
|
if "erofs-utils) 1.8.6" not in version:
|
|
raise FixtureError(f"mkfs.erofs 1.8.6 is required, got: {version}")
|
|
return version
|
|
|
|
|
|
def run_mkfs(source: Path, image: Path, uuid: str, *extra: str) -> list[str]:
|
|
command = [
|
|
"mkfs.erofs",
|
|
"-d0",
|
|
"-T0",
|
|
"--all-time",
|
|
"--all-root",
|
|
"--workers=1",
|
|
"--sort=path",
|
|
"-U",
|
|
uuid,
|
|
"--xattr-prefix=user.repo22.application.component.",
|
|
"--xattr-prefix=user.repo22.trusted.deep.",
|
|
*extra,
|
|
str(image),
|
|
str(source),
|
|
]
|
|
subprocess.run(command, check=True)
|
|
return command
|
|
|
|
|
|
class TransformLog:
|
|
def __init__(self) -> None:
|
|
self.records: list[dict[str, object]] = []
|
|
|
|
def bytes(self, image: ErofsImage, field: str, offset: int, after: bytes) -> None:
|
|
before = bytes(image.data[offset : offset + len(after)])
|
|
if len(before) != len(after):
|
|
raise FixtureError(f"{field}: patch exceeds image")
|
|
image.data[offset : offset + len(after)] = after
|
|
self.records.append(
|
|
{
|
|
"field": field,
|
|
"offset": offset,
|
|
"size": len(after),
|
|
"before_hex": before.hex(),
|
|
"after_hex": after.hex(),
|
|
}
|
|
)
|
|
|
|
def u8(self, image: ErofsImage, field: str, offset: int, value: int) -> None:
|
|
self.bytes(image, field, offset, bytes([value]))
|
|
|
|
def u16(self, image: ErofsImage, field: str, offset: int, value: int) -> None:
|
|
self.bytes(image, field, offset, struct.pack("<H", value))
|
|
|
|
def u32(self, image: ErofsImage, field: str, offset: int, value: int) -> None:
|
|
self.bytes(image, field, offset, struct.pack("<I", value))
|
|
|
|
def u64(self, image: ErofsImage, field: str, offset: int, value: int) -> None:
|
|
self.bytes(image, field, offset, struct.pack("<Q", value))
|
|
|
|
|
|
def find_prefix_id(image: ErofsImage, infix: bytes) -> int:
|
|
for prefix_id, (_, _, record_infix) in enumerate(image.prefix_records()):
|
|
if record_infix == infix:
|
|
return prefix_id
|
|
raise FixtureError(f"prefix not found: {infix!r}")
|
|
|
|
|
|
def patch_namespace_entries(image: ErofsImage, log: TransformLog) -> None:
|
|
for path, name, expected_storage, new_index in (
|
|
("/inline-trusted", b"admin", "inline", XATTR_INDEX_TRUSTED),
|
|
("/inline-security", b"capability", "inline", XATTR_INDEX_SECURITY),
|
|
("/inline-security", b"selinux", "inline", XATTR_INDEX_SECURITY),
|
|
("/trusted-shared-a", b"config", "shared", XATTR_INDEX_TRUSTED),
|
|
("/security-shared-a", b"selinux", "shared", XATTR_INDEX_SECURITY),
|
|
):
|
|
storage, entry, _ = image.find_xattr(path, name)
|
|
if storage != expected_storage or entry.name_index != XATTR_INDEX_USER:
|
|
raise FixtureError(f"{path}:{name!r} has unexpected storage/index")
|
|
log.u8(image, f"{path}:{name.decode()}.e_name_index", entry.offset + 1, new_index)
|
|
|
|
trusted_prefix_id = find_prefix_id(image, b"repo22.trusted.deep.")
|
|
packed = image.inode(image.packed_nid)
|
|
packed_data = image.inode_data(packed)
|
|
record_offset = image.prefix_records()[trusted_prefix_id][0]
|
|
if packed.layout != LAYOUT_FLAT_INLINE:
|
|
raise FixtureError("expected inline packed prefix carrier")
|
|
physical = packed.offset + packed.inode_size + packed.xattr_size + record_offset + 2
|
|
if packed_data[record_offset + 2] != XATTR_INDEX_USER:
|
|
raise FixtureError("trusted prefix base is not the user placeholder")
|
|
log.u8(image, "packed_prefix.trusted.base_index", physical, XATTR_INDEX_TRUSTED)
|
|
|
|
|
|
def replace_acl_entry(image: ErofsImage, path: str, value: bytes, log: TransformLog) -> None:
|
|
_, inode = image.resolve_root(path)
|
|
shared_count, entries = image.inline_xattrs(inode)
|
|
placeholders = [entry for entry in entries if entry.name == b"acl_access_placeholder"]
|
|
if shared_count != 0 or len(placeholders) != 1:
|
|
raise FixtureError(f"{path}: ACL placeholder is not one inline xattr")
|
|
body = bytearray(12)
|
|
for entry in entries:
|
|
if entry.name == b"acl_access_placeholder":
|
|
body.extend(xattr_entry(XATTR_INDEX_ACL_ACCESS, b"", value))
|
|
else:
|
|
body.extend(xattr_entry(entry.name_index, entry.name, entry.value))
|
|
body.extend(bytes(align(len(body), 4) - len(body)))
|
|
if len(body) > inode.xattr_size:
|
|
raise FixtureError(f"{path}: rebuilt ACL body grew")
|
|
padded = body + bytes(inode.xattr_size - len(body))
|
|
log.bytes(image, f"{path}.xattr_body", inode.offset + inode.inode_size, padded)
|
|
icount = 1 + (len(body) - 12) // 4
|
|
log.u16(image, f"{path}.i_xattr_icount", inode.offset + 2, icount)
|
|
|
|
|
|
def patch_acls(image: ErofsImage, log: TransformLog) -> None:
|
|
acl_values = {
|
|
"/acl-unordered": canonical_acl(3002, 2002),
|
|
"/acl-header": struct.pack("<I", ACL_VERSION),
|
|
"/acl-duplicate": canonical_acl(2002, 2002),
|
|
"/acl-phase": acl_value(
|
|
[
|
|
(ACL_USER_OBJ, 7, ACL_UNDEFINED_ID),
|
|
(ACL_GROUP_OBJ, 5, ACL_UNDEFINED_ID),
|
|
(ACL_USER, 5, 2002),
|
|
(ACL_MASK, 5, ACL_UNDEFINED_ID),
|
|
(ACL_OTHER, 1, ACL_UNDEFINED_ID),
|
|
]
|
|
),
|
|
"/acl-perm": acl_value(
|
|
[
|
|
(ACL_USER_OBJ, 8, ACL_UNDEFINED_ID),
|
|
(ACL_GROUP_OBJ, 5, ACL_UNDEFINED_ID),
|
|
(ACL_OTHER, 1, ACL_UNDEFINED_ID),
|
|
]
|
|
),
|
|
"/fifo-node": canonical_acl(2002),
|
|
}
|
|
for path, value in acl_values.items():
|
|
replace_acl_entry(image, path, value, log)
|
|
_, fifo = image.resolve_root("/fifo-node")
|
|
log.u16(image, "/fifo-node.i_mode", fifo.offset + 4, stat.S_IFIFO | 0o640)
|
|
|
|
|
|
def checksum_and_save(image: ErofsImage, path: Path) -> None:
|
|
image.update_checksum()
|
|
image.validate_superblock()
|
|
image.save(path)
|
|
|
|
|
|
def transformed_basic(base: Path) -> tuple[ErofsImage, TransformLog]:
|
|
image = ErofsImage.load(base)
|
|
log = TransformLog()
|
|
patch_namespace_entries(image, log)
|
|
patch_acls(image, log)
|
|
image.update_checksum()
|
|
return image, log
|
|
|
|
|
|
def add_metabox_fields(
|
|
image: ErofsImage,
|
|
log: TransformLog,
|
|
carrier_nid: int,
|
|
clear_packed: bool,
|
|
) -> None:
|
|
if image.u32(SUPER + 40) != 0:
|
|
raise FixtureError("metabox conversion expects metadata in block zero")
|
|
metadata_copy = bytes(image.data[: image.block_size])
|
|
relocated_offset = align(len(image.data), image.block_size)
|
|
image.data.extend(bytes(relocated_offset - len(image.data)))
|
|
image.data.extend(bytes(image.block_size))
|
|
image.data[relocated_offset : relocated_offset + image.block_size] = metadata_copy
|
|
log.u32(image, "super.blocks_lo", SUPER + 36, len(image.data) // image.block_size)
|
|
log.u32(image, "super.meta_blkaddr", SUPER + 40, relocated_offset // image.block_size)
|
|
log.u32(
|
|
image,
|
|
"super.feature_compat",
|
|
SUPER + 8,
|
|
image.feature_compat | FEATURE_COMPAT_SHARED_EA_IN_METABOX,
|
|
)
|
|
log.u8(image, "super.sb_extslots", SUPER + 13, 1)
|
|
log.u32(
|
|
image,
|
|
"super.feature_incompat",
|
|
SUPER + 80,
|
|
image.feature_incompat | FEATURE_INCOMPAT_METABOX | FEATURE_INCOMPAT_XATTR_PREFIXES,
|
|
)
|
|
log.u32(image, "super.xattr_blkaddr", SUPER + 44, METABOX_XATTR_BASE // BLOCK_SIZE)
|
|
log.u8(image, "super.xattr_prefix_count", SUPER + 91, 2)
|
|
log.u32(image, "super.xattr_prefix_start", SUPER + 92, METABOX_PREFIX_OFFSET // 4)
|
|
log.u64(image, "super.metabox_nid", SUPER + 128, carrier_nid)
|
|
if clear_packed:
|
|
log.u64(image, "super.packed_nid", SUPER + 96, 0)
|
|
for path, nid in (("/metabox-a", METABOX_INODE_A), ("/metabox-b", METABOX_INODE_B)):
|
|
entry, _ = image.resolve_root(path)
|
|
log.u64(image, f"{path}.dirent_nid", entry.offset, METABOX_NID_BIT | nid)
|
|
|
|
|
|
def make_primary_prefix(image: ErofsImage, log: TransformLog) -> None:
|
|
packed_data = image.inode_data(image.inode(image.packed_nid))
|
|
start = align(len(image.data), image.block_size)
|
|
if len(image.data) < start:
|
|
image.data.extend(bytes(start - len(image.data)))
|
|
image.data.extend(packed_data)
|
|
image.data.extend(bytes(align(len(image.data), image.block_size) - len(image.data)))
|
|
new_blocks = len(image.data) // image.block_size
|
|
log.u32(image, "super.blocks_lo", SUPER + 36, new_blocks)
|
|
log.u32(
|
|
image,
|
|
"super.feature_compat",
|
|
SUPER + 8,
|
|
image.feature_compat | FEATURE_COMPAT_PLAIN_XATTR_PFX,
|
|
)
|
|
log.u32(image, "super.xattr_prefix_start", SUPER + 92, start // 4)
|
|
log.u64(image, "super.packed_nid", SUPER + 96, 0)
|
|
|
|
|
|
def save_variant(
|
|
output: Path,
|
|
name: str,
|
|
image: ErofsImage,
|
|
log: TransformLog,
|
|
transforms: dict[str, object],
|
|
base_name: str,
|
|
semantics: dict[str, object] | None = None,
|
|
) -> None:
|
|
path = output / "images" / name
|
|
checksum_and_save(image, path)
|
|
transforms[name] = {
|
|
"base": base_name,
|
|
"image_sha256": sha256_path(path),
|
|
"provider_size": path.stat().st_size,
|
|
"declared_size": image.declared_size,
|
|
"fields": log.records,
|
|
"semantics": semantics or {},
|
|
}
|
|
|
|
|
|
def make_variants(output: Path, base_commands: dict[str, list[str]]) -> dict[str, object]:
|
|
bases = output / "bases"
|
|
images = output / "images"
|
|
images.mkdir()
|
|
transforms: dict[str, object] = {}
|
|
|
|
basic, basic_log = transformed_basic(bases / "basic-base.erofs")
|
|
save_variant(output, "basic.erofs", basic.clone(), basic_log, transforms, "basic-base.erofs")
|
|
|
|
primary = basic.clone()
|
|
primary_log = TransformLog()
|
|
make_primary_prefix(primary, primary_log)
|
|
save_variant(output, "prefix-primary.erofs", primary, primary_log, transforms, "basic.erofs")
|
|
|
|
metabox_plain = ErofsImage.load(bases / "carrier-plain-base.erofs")
|
|
metabox_plain_log = TransformLog()
|
|
_, carrier_inode = metabox_plain.resolve_root("/metabox-carrier.bin")
|
|
if carrier_inode.layout != LAYOUT_FLAT_PLAIN or carrier_inode.size != METABOX_SIZE:
|
|
raise FixtureError("plain metabox carrier has the wrong layout or size")
|
|
add_metabox_fields(metabox_plain, metabox_plain_log, carrier_inode.nid, True)
|
|
save_variant(
|
|
output,
|
|
"metabox-plain.erofs",
|
|
metabox_plain,
|
|
metabox_plain_log,
|
|
transforms,
|
|
"carrier-plain-base.erofs",
|
|
{"carrier_nid": carrier_inode.nid, "carrier_layout": carrier_inode.layout},
|
|
)
|
|
|
|
compressed = ErofsImage.load(bases / "carrier-compressed-base.erofs")
|
|
compressed_log = TransformLog()
|
|
_, compressed_carrier = compressed.resolve_root("/metabox-carrier.bin")
|
|
if compressed_carrier.layout not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT):
|
|
raise FixtureError("compressed metabox carrier is not compressed")
|
|
add_metabox_fields(compressed, compressed_log, compressed_carrier.nid, True)
|
|
save_variant(
|
|
output,
|
|
"metabox-compressed.erofs",
|
|
compressed,
|
|
compressed_log,
|
|
transforms,
|
|
"carrier-compressed-base.erofs",
|
|
{"carrier_nid": compressed_carrier.nid, "carrier_layout": compressed_carrier.layout},
|
|
)
|
|
|
|
fragment = ErofsImage.load(bases / "carrier-fragment-base.erofs")
|
|
fragment_log = TransformLog()
|
|
_, fragment_carrier = fragment.resolve_root("/metabox-carrier.bin")
|
|
if fragment_carrier.layout not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT):
|
|
raise FixtureError("fragment metabox carrier is not compressed")
|
|
add_metabox_fields(fragment, fragment_log, fragment_carrier.nid, False)
|
|
_, fragment_carrier = fragment.resolve_root("/metabox-carrier.bin")
|
|
header_offset = align(
|
|
fragment_carrier.offset + fragment_carrier.inode_size + fragment_carrier.xattr_size,
|
|
8,
|
|
)
|
|
fragment_header = fragment.u64(header_offset)
|
|
if not fragment_header & METABOX_NID_BIT:
|
|
raise FixtureError("metabox carrier is not a whole-file fragment")
|
|
fragment_offset = fragment_header ^ METABOX_NID_BIT
|
|
packed_inode = fragment.inode(fragment.packed_nid)
|
|
if fragment_offset + fragment_carrier.size > packed_inode.size:
|
|
raise FixtureError("positive fragment range exceeds packed inode")
|
|
fragment_semantics = {
|
|
"carrier_nid": fragment_carrier.nid,
|
|
"carrier_layout": fragment_carrier.layout,
|
|
"fragment_header_offset": header_offset,
|
|
"fragment_header": fragment_header,
|
|
"fragment_offset": fragment_offset,
|
|
"carrier_size": fragment_carrier.size,
|
|
"packed_nid": fragment.packed_nid,
|
|
"packed_size": packed_inode.size,
|
|
}
|
|
save_variant(
|
|
output,
|
|
"metabox-fragment.erofs",
|
|
fragment,
|
|
fragment_log,
|
|
transforms,
|
|
"carrier-fragment-base.erofs",
|
|
fragment_semantics,
|
|
)
|
|
|
|
bad_inline = basic.clone()
|
|
bad_inline_log = TransformLog()
|
|
storage, entry, _ = bad_inline.find_xattr("/corrupt-inline", b"bad")
|
|
if storage != "inline":
|
|
raise FixtureError("corrupt-inline target is not inline")
|
|
bad_inline_log.u16(bad_inline, "/corrupt-inline.e_value_size", entry.offset + 2, 0xFFFF)
|
|
save_variant(output, "bad-inline-entry.erofs", bad_inline, bad_inline_log, transforms, "basic.erofs")
|
|
|
|
bad_shared = basic.clone()
|
|
bad_shared_log = TransformLog()
|
|
storage, shared_entry, _ = bad_shared.find_xattr("/shared-a", b"shared_key")
|
|
if storage != "shared":
|
|
raise FixtureError("shared corruption target is not shared")
|
|
bad_shared_log.u16(bad_shared, "shared_key.e_value_size", shared_entry.offset + 2, 0xFFFF)
|
|
save_variant(output, "bad-shared-entry.erofs", bad_shared, bad_shared_log, transforms, "basic.erofs")
|
|
|
|
shared_oob = basic.clone()
|
|
shared_oob_log = TransformLog()
|
|
_, shared_inode = shared_oob.resolve_root("/shared-a")
|
|
target_id = None
|
|
target_array_index = None
|
|
for array_index, shared_id in enumerate(shared_oob.shared_ids(shared_inode)):
|
|
if shared_oob.shared_xattr(shared_id).name == b"shared_key":
|
|
target_id = shared_id
|
|
target_array_index = array_index
|
|
break
|
|
if target_id is None or target_array_index is None:
|
|
raise FixtureError("shared OOB target ID not found")
|
|
declared_end = shared_oob.declared_size
|
|
sentinel = xattr_entry(XATTR_INDEX_USER, b"shared_key", b"sentinel-must-not-leak")
|
|
shared_oob.data.extend(bytes(BLOCK_SIZE))
|
|
shared_oob.data[declared_end : declared_end + len(sentinel)] = sentinel
|
|
body = shared_inode.offset + shared_inode.inode_size
|
|
shared_oob_log.u32(
|
|
shared_oob,
|
|
"/shared-a.shared_key_id",
|
|
body + 12 + target_array_index * 4,
|
|
declared_end // 4,
|
|
)
|
|
save_variant(
|
|
output,
|
|
"bad-shared-declared-bounds.erofs",
|
|
shared_oob,
|
|
shared_oob_log,
|
|
transforms,
|
|
"basic.erofs",
|
|
{"sentinel_offset": declared_end, "old_shared_id": target_id},
|
|
)
|
|
|
|
prefix_oob = basic.clone()
|
|
prefix_oob_log = TransformLog()
|
|
prefix_declared_end = prefix_oob.declared_size
|
|
sentinel_prefix = prefix_record(XATTR_INDEX_USER, b"sentinel-never-visible.")
|
|
prefix_oob.data.extend(bytes(BLOCK_SIZE))
|
|
prefix_oob.data[prefix_declared_end : prefix_declared_end + len(sentinel_prefix)] = sentinel_prefix
|
|
prefix_oob_log.u32(
|
|
prefix_oob,
|
|
"super.feature_compat",
|
|
SUPER + 8,
|
|
prefix_oob.feature_compat | FEATURE_COMPAT_PLAIN_XATTR_PFX,
|
|
)
|
|
prefix_oob_log.u8(prefix_oob, "super.xattr_prefix_count", SUPER + 91, 1)
|
|
prefix_oob_log.u32(
|
|
prefix_oob,
|
|
"super.xattr_prefix_start",
|
|
SUPER + 92,
|
|
prefix_declared_end // 4,
|
|
)
|
|
prefix_oob_log.u64(prefix_oob, "super.packed_nid", SUPER + 96, 0)
|
|
save_variant(
|
|
output,
|
|
"bad-prefix-declared-bounds.erofs",
|
|
prefix_oob,
|
|
prefix_oob_log,
|
|
transforms,
|
|
"basic.erofs",
|
|
{"sentinel_offset": prefix_declared_end},
|
|
)
|
|
|
|
truncated = basic.clone()
|
|
truncated_log = TransformLog()
|
|
_, truncated_carrier = truncated.resolve_root("/metabox-carrier.bin")
|
|
truncated_log.u32(
|
|
truncated,
|
|
"super.feature_incompat",
|
|
SUPER + 80,
|
|
truncated.feature_incompat | FEATURE_INCOMPAT_METABOX,
|
|
)
|
|
truncated_log.u64(truncated, "super.metabox_nid", SUPER + 128, truncated_carrier.nid)
|
|
save_variant(output, "bad-metabox-truncated-extension.erofs", truncated, truncated_log, transforms, "basic.erofs")
|
|
|
|
ishare = basic.clone()
|
|
ishare_log = TransformLog()
|
|
ishare_log.u32(
|
|
ishare,
|
|
"super.feature_compat",
|
|
SUPER + 8,
|
|
ishare.feature_compat | FEATURE_COMPAT_ISHARE_XATTRS,
|
|
)
|
|
ishare_log.u8(
|
|
ishare,
|
|
"super.ishare_xattr_prefix_id",
|
|
SUPER + 105,
|
|
ishare.data[SUPER + 91],
|
|
)
|
|
save_variant(output, "bad-ishare-prefix-id.erofs", ishare, ishare_log, transforms, "basic.erofs")
|
|
|
|
positive_fragment_path = output / "images" / "metabox-fragment.erofs"
|
|
positive_fragment = ErofsImage.load(positive_fragment_path)
|
|
positive_semantics = transforms["metabox-fragment.erofs"]["semantics"]
|
|
|
|
self_loop = positive_fragment.clone()
|
|
self_loop_log = TransformLog()
|
|
self_loop_log.u64(
|
|
self_loop,
|
|
"super.packed_nid",
|
|
SUPER + 96,
|
|
int(positive_semantics["carrier_nid"]),
|
|
)
|
|
save_variant(output, "bad-fragment-self-loop.erofs", self_loop, self_loop_log, transforms, "metabox-fragment.erofs")
|
|
|
|
range_bad = positive_fragment.clone()
|
|
range_log = TransformLog()
|
|
range_log.u64(
|
|
range_bad,
|
|
"metabox.fragment_header",
|
|
int(positive_semantics["fragment_header_offset"]),
|
|
METABOX_NID_BIT | int(positive_semantics["packed_size"]),
|
|
)
|
|
save_variant(output, "bad-fragment-range.erofs", range_bad, range_log, transforms, "metabox-fragment.erofs")
|
|
|
|
recursive_metabox = positive_fragment.clone()
|
|
recursive_metabox_log = TransformLog()
|
|
recursive_metabox_log.u64(
|
|
recursive_metabox,
|
|
"super.metabox_nid",
|
|
SUPER + 128,
|
|
METABOX_NID_BIT | int(positive_semantics["carrier_nid"]),
|
|
)
|
|
save_variant(
|
|
output,
|
|
"bad-metabox-recursive-nid.erofs",
|
|
recursive_metabox,
|
|
recursive_metabox_log,
|
|
transforms,
|
|
"metabox-fragment.erofs",
|
|
)
|
|
|
|
recursive_packed = positive_fragment.clone()
|
|
recursive_packed_log = TransformLog()
|
|
recursive_packed_log.u64(
|
|
recursive_packed,
|
|
"super.packed_nid",
|
|
SUPER + 96,
|
|
METABOX_NID_BIT | int(positive_semantics["packed_nid"]),
|
|
)
|
|
save_variant(
|
|
output,
|
|
"bad-packed-recursive-nid.erofs",
|
|
recursive_packed,
|
|
recursive_packed_log,
|
|
transforms,
|
|
"metabox-fragment.erofs",
|
|
)
|
|
|
|
return {"base_commands": base_commands, "images": transforms}
|
|
|
|
|
|
def write_checksums(output: Path) -> None:
|
|
paths = sorted((output / "bases").glob("*.erofs")) + sorted((output / "images").glob("*.erofs"))
|
|
with (output / "IMAGE-SHA256SUMS").open("w", encoding="ascii") as sums:
|
|
for path in paths:
|
|
sums.write(f"{sha256_path(path)} {path.relative_to(output)}\n")
|
|
|
|
|
|
def verify_output(output: Path) -> None:
|
|
manifest = json.loads((output / "fixture-manifest.json").read_text(encoding="ascii"))
|
|
inventory = source_inventory(output / "source")
|
|
if inventory["sha256"] != manifest["source"]["sha256"]:
|
|
raise FixtureError("source inventory hash changed")
|
|
for name, item in manifest["transforms"]["images"].items():
|
|
path = output / "images" / name
|
|
if sha256_path(path) != item["image_sha256"]:
|
|
raise FixtureError(f"{name}: image hash changed")
|
|
data = path.read_bytes()
|
|
for field in item["fields"]:
|
|
offset = field["offset"]
|
|
size = field["size"]
|
|
if data[offset : offset + size].hex() != field["after_hex"]:
|
|
raise FixtureError(f"{name}: field check failed: {field['field']}")
|
|
image = ErofsImage.load(path)
|
|
if image.declared_size != item["declared_size"]:
|
|
raise FixtureError(f"{name}: declared size changed")
|
|
if len(data) != item["provider_size"]:
|
|
raise FixtureError(f"{name}: provider size changed")
|
|
checksums = {}
|
|
for line in (output / "IMAGE-SHA256SUMS").read_text(encoding="ascii").splitlines():
|
|
digest, relative = line.split(" ", 1)
|
|
checksums[relative] = digest
|
|
for relative, digest in checksums.items():
|
|
if sha256_path(output / relative) != digest:
|
|
raise FixtureError(f"checksum list mismatch: {relative}")
|
|
|
|
|
|
def make(output: Path) -> None:
|
|
if shutil.which("mkfs.erofs") is None:
|
|
raise FixtureError("mkfs.erofs is required")
|
|
if output.exists():
|
|
raise FixtureError(f"output already exists: {output}")
|
|
output.mkdir(parents=True)
|
|
version = tool_version()
|
|
source_details = make_sources(output)
|
|
source = source_inventory(output / "source")
|
|
bases = output / "bases"
|
|
bases.mkdir()
|
|
base_commands = {
|
|
"basic-base.erofs": run_mkfs(
|
|
output / "source" / "basic",
|
|
bases / "basic-base.erofs",
|
|
"44444444-4444-4444-8444-444444444404",
|
|
),
|
|
"carrier-plain-base.erofs": run_mkfs(
|
|
output / "source" / "carrier",
|
|
bases / "carrier-plain-base.erofs",
|
|
"44444444-4444-4444-8444-444444444434",
|
|
),
|
|
"carrier-compressed-base.erofs": run_mkfs(
|
|
output / "source" / "carrier",
|
|
bases / "carrier-compressed-base.erofs",
|
|
"44444444-4444-4444-8444-444444444440",
|
|
"-zlz4",
|
|
"-C4096",
|
|
"-Elegacy-compress",
|
|
),
|
|
"carrier-fragment-base.erofs": run_mkfs(
|
|
output / "source" / "carrier",
|
|
bases / "carrier-fragment-base.erofs",
|
|
"44444444-4444-4444-8444-444444444442",
|
|
"-zlz4",
|
|
"-C4096",
|
|
"-Eall-fragments",
|
|
),
|
|
}
|
|
transforms = make_variants(output, base_commands)
|
|
write_checksums(output)
|
|
manifest = {
|
|
"generator": "g4_fixtures.py",
|
|
"mkfs_version": version,
|
|
"source": source,
|
|
"source_details": source_details,
|
|
"transforms": transforms,
|
|
}
|
|
(output / "fixture-manifest.json").write_text(
|
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
|
)
|
|
(output / "SOURCE-SHA256").write_text(f"{source['sha256']} source-inventory\n", encoding="ascii")
|
|
(output / "mkfs-version.txt").write_text(version + "\n", encoding="ascii")
|
|
verify_output(output)
|
|
print(f"source_sha256={source['sha256']}")
|
|
print((output / "IMAGE-SHA256SUMS").read_text(encoding="ascii"), end="")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
make_parser = subparsers.add_parser("make")
|
|
make_parser.add_argument("--output", required=True, type=Path)
|
|
verify_parser = subparsers.add_parser("verify")
|
|
verify_parser.add_argument("--output", required=True, type=Path)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
if args.command == "make":
|
|
make(args.output)
|
|
else:
|
|
verify_output(args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (FixtureError, OSError, subprocess.CalledProcessError) as error:
|
|
raise SystemExit(f"g4_fixtures.py: {error}") from error
|