From f79d4beab0a31652faba0aef5608c2bd16627b7a Mon Sep 17 00:00:00 2001 From: 0cwa Date: Fri, 25 Sep 2026 10:56:34 +0000 Subject: [PATCH 1/3] fix: map product image paths to runtime SELinux contexts --- src/boot_animation.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/boot_animation.py b/src/boot_animation.py index 84b8e4fc..b216fc90 100644 --- a/src/boot_animation.py +++ b/src/boot_animation.py @@ -233,6 +233,46 @@ def build_runtime_payload(path: str | os.PathLike[str]) -> bytes: return output.getvalue() +def _install_partition_context_aliases( + fs: Any, + partition: str, + raw_targets: Iterable[str], +) -> None: + """Map partition-relative image paths to their runtime SELinux labels.""" + + contexts = list(fs.contexts) + aliases: list[tuple[re.Pattern[str], str]] = [] + image_paths: set[PurePosixPath] = set() + for raw_target in raw_targets: + target = PurePosixPath(raw_target) + image_paths.add(target.parent) + image_paths.add(target) + + for image_path in sorted(image_paths, key=str): + image_path_str = str(image_path) + if any(pattern.fullmatch(image_path_str) for pattern, _ in contexts + aliases): + continue + + runtime_path = PurePosixPath("/") / partition / image_path.relative_to("/") + runtime_path_str = str(runtime_path) + try: + label = next( + label + for pattern, label in contexts + if pattern.fullmatch(runtime_path_str) + ) + except StopIteration as exc: + raise RuntimeError( + "no SELinux context maps boot animation runtime path: " + f"{runtime_path_str}" + ) from exc + + aliases.append((re.compile(re.escape(image_path_str)), label)) + + if aliases: + fs.contexts = aliases + contexts + + def install_runtime_payload( ext_fs: dict[str, Any], light_payload: bytes, @@ -247,6 +287,17 @@ def install_runtime_payload( "/media/bootanimation-dark.zip": dark_payload, } + targets_by_partition: dict[str, list[str]] = {} + for partition, raw_target in BOOT_ANIMATION_TARGETS: + targets_by_partition.setdefault(partition, []).append(raw_target) + for partition, raw_targets in targets_by_partition.items(): + fs = ext_fs.get(partition) + if fs is None: + raise RuntimeError( + f"boot animation target partition is missing: {partition}" + ) + _install_partition_context_aliases(fs, partition, raw_targets) + for partition, raw_target in BOOT_ANIMATION_TARGETS: fs = ext_fs.get(partition) if fs is None: From 9e48a14eafa81f928300b95f722e9c66c452bc06 Mon Sep 17 00:00:00 2001 From: 0cwa Date: Fri, 25 Sep 2026 10:56:47 +0000 Subject: [PATCH 2/3] build: version product context boot animation contract --- src/ci/selection_variant.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ci/selection_variant.sh b/src/ci/selection_variant.sh index d714fff2..3038b5bb 100755 --- a/src/ci/selection_variant.sh +++ b/src/ci/selection_variant.sh @@ -7,7 +7,7 @@ # this helper owns the byte-for-byte representation that is hashed by both the # build and release preflight paths. -BOOT_ANIMATION_SELECTION_CONTRACT="product-image-root-theme-fallback-stored-v5" +BOOT_ANIMATION_SELECTION_CONTRACT="product-image-root-theme-context-stored-v6" function selection_variant_manifest() { local field value From b5162da047ebd2585d67e3f2059de340fee344ac Mon Sep 17 00:00:00 2001 From: 0cwa Date: Fri, 25 Sep 2026 10:56:50 +0000 Subject: [PATCH 3/3] test: cover product boot animation context aliases --- tests/boot_animation_test.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/boot_animation_test.py b/tests/boot_animation_test.py index c9e741ac..509d0b0c 100644 --- a/tests/boot_animation_test.py +++ b/tests/boot_animation_test.py @@ -5,6 +5,7 @@ import hashlib import io +import re import stat import sys import tempfile @@ -43,6 +44,9 @@ def __init__(self, root: Path) -> None: self.root = root self.mkdir_calls: list[tuple[str, int, bool, bool]] = [] self.open_calls: list[tuple[str, str, int]] = [] + self.contexts = [ + (re.compile(r"/product(/.*)?"), "u:object_r:system_file:s0"), + ] @property def tree(self) -> Path: @@ -94,6 +98,21 @@ def test_runtime_payload_installation() -> None: for target in expected_targets: assert (product.root / target.lstrip("/")).read_bytes() == runtime + expected_alias_paths = { + "/media", + "/media/bootanimation.zip", + "/media/bootanimation-dark.zip", + } + alias_labels = { + path: next( + label + for pattern, label in product.contexts + if pattern.fullmatch(path) + ) + for path in expected_alias_paths + } + assert set(alias_labels.values()) == {"u:object_r:system_file:s0"} + # A standalone product.img is mounted at /product. Its filesystem root # therefore contains /media, not another nested /product directory. assert not (product.root / "product").exists() @@ -142,6 +161,15 @@ def test_runtime_payload_installation() -> None: else: raise AssertionError("missing light and dark payloads were accepted") + missing_contexts = FakeExtFs(root / "missing-contexts") + missing_contexts.contexts = [] + try: + install_runtime_payload({"product": missing_contexts}, runtime) + except RuntimeError: + pass + else: + raise AssertionError("missing runtime SELinux context was accepted") + try: install_runtime_payload({}, runtime) except RuntimeError: