From 31bf38d25e654a2f7a9f0da1f9163718ec26987d Mon Sep 17 00:00:00 2001 From: Wenwen Fu Date: Mon, 10 Aug 2026 11:20:32 +0800 Subject: [PATCH] gen_udev_rules: add raw partition rule generator Qualcomm platforms expose firmware and metadata partitions that are not intended to contain mountable filesystems. Probing each of them with blkid adds unnecessary work during udev coldplug. Keeping the exclusion policy in a distribution layer also prevents other qcom-ptool users from reusing it. Add a gen_udev_rules subcommand with a reviewed partition-name policy and udev template. Filter the policy against one or more selected layouts and emit only exact labels present in those layouts. Unknown or custom labels retain the default filesystem probing behavior. The generated rule disables only blkid probing and preserves persistent partition metadata links. Signed-off-by: Wenwen Fu --- README.md | 1 + pyproject.toml | 1 + qcom_ptool/cli.py | 1 + .../55-qcom-raw-partitions-noblkid.rules.in | 35 +++++ qcom_ptool/data/__init__.py | 2 + .../data/approved-raw-partition-patterns.list | 67 ++++++++++ qcom_ptool/gen_udev_rules.py | 124 ++++++++++++++++++ tests/unit/test_gen_udev_rules.py | 123 +++++++++++++++++ 8 files changed, 354 insertions(+) create mode 100644 qcom_ptool/data/55-qcom-raw-partitions-noblkid.rules.in create mode 100644 qcom_ptool/data/__init__.py create mode 100644 qcom_ptool/data/approved-raw-partition-patterns.list create mode 100644 qcom_ptool/gen_udev_rules.py create mode 100644 tests/unit/test_gen_udev_rules.py diff --git a/README.md b/README.md index 2976791..4d0672b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Once installed, the tool is invoked as: ```sh qcom-ptool gen_partition -i platforms///partitions.conf -o partitions.xml qcom-ptool gen_contents -p partitions.xml -t contents.xml.in -o contents.xml +qcom-ptool gen_udev_rules -i platforms///partitions.conf -o 55-qcom-raw-partitions-noblkid.rules qcom-ptool ptool -x partitions.xml qcom-ptool msp -r rawprogram0.xml -d /dev/sdX -p patch0.xml ``` diff --git a/pyproject.toml b/pyproject.toml index de20867..2c2d823 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ include = ["qcom_ptool*"] [tool.setuptools.package-data] "qcom_ptool.schema" = ["*.json"] +"qcom_ptool.data" = ["*.in", "*.list"] [tool.ruff] target-version = "py38" diff --git a/qcom_ptool/cli.py b/qcom_ptool/cli.py index bb011d4..3774141 100644 --- a/qcom_ptool/cli.py +++ b/qcom_ptool/cli.py @@ -10,6 +10,7 @@ SUBCOMMANDS = { "gen_partition": "qcom_ptool.gen_partition", "gen_contents": "qcom_ptool.gen_contents", + "gen_udev_rules": "qcom_ptool.gen_udev_rules", "ptool": "qcom_ptool.ptool", "msp": "qcom_ptool.msp", } diff --git a/qcom_ptool/data/55-qcom-raw-partitions-noblkid.rules.in b/qcom_ptool/data/55-qcom-raw-partitions-noblkid.rules.in new file mode 100644 index 0000000..5c3339b --- /dev/null +++ b/qcom_ptool/data/55-qcom-raw-partitions-noblkid.rules.in @@ -0,0 +1,35 @@ +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# Skip filesystem probing for reviewed Qualcomm raw GPT partitions. +# +# Match by GPT partition name instead of kernel disk name. Disk enumeration is +# not stable across all boards, and a disk can contain both raw firmware and +# mountable filesystem partitions. +# +# 60-persistent-storage.rules creates partition metadata links after the blkid +# import. Since this rule skips that import for raw partitions, emit those +# links here from kernel-provided PARTNAME/PARTUUID properties. +ACTION=="remove", GOTO="qcom_raw_noblkid_end" +SUBSYSTEM!="block", GOTO="qcom_raw_noblkid_end" +ENV{DEVTYPE}!="partition", GOTO="qcom_raw_noblkid_end" +ENV{PARTNAME}=="", GOTO="qcom_raw_links" + +# Generated from the selected qcom-ptool partition layouts. New labels are not +# excluded from blkid probing until their patterns have been reviewed. +@QCOM_RAW_PARTITION_RULES@ +GOTO="qcom_raw_noblkid_end" + +LABEL="qcom_raw_noblkid" +ENV{UDEV_DISABLE_PERSISTENT_STORAGE_BLKID_FLAG}="1" + +LABEL="qcom_raw_links" +# 60-persistent-storage.rules normally imports ID_PATH after this rule runs. +ENV{ID_PATH}!="?*", IMPORT{parent}="ID_PATH" +ENV{PARTUUID}=="?*", SYMLINK+="disk/by-partuuid/$env{PARTUUID}" +ENV{PARTNAME}=="?*", OPTIONS+="string_escape=replace", SYMLINK+="disk/by-partlabel/$env{PARTNAME}" +ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partnum/%n" +ENV{ID_PATH}=="?*", ENV{PARTUUID}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partuuid/$env{PARTUUID}" +ENV{ID_PATH}=="?*", ENV{PARTNAME}=="?*", OPTIONS+="string_escape=replace", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partlabel/$env{PARTNAME}" + +LABEL="qcom_raw_noblkid_end" diff --git a/qcom_ptool/data/__init__.py b/qcom_ptool/data/__init__.py new file mode 100644 index 0000000..21749a9 --- /dev/null +++ b/qcom_ptool/data/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause diff --git a/qcom_ptool/data/approved-raw-partition-patterns.list b/qcom_ptool/data/approved-raw-partition-patterns.list new file mode 100644 index 0000000..8c45161 --- /dev/null +++ b/qcom_ptool/data/approved-raw-partition-patterns.list @@ -0,0 +1,67 @@ +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# Partition-name patterns reviewed as safe to exclude from blkid probing. +# Keep one udev glob per line. A new partition label is not implicitly safe. +abl_[ab] +ALIGN_TO_128K_* +aop_[ab] +aop_config_[ab] +apdp +apdp_[ab] +catecontentfv +cdt +cmnlib64_[ab] +cmnlib_[ab] +cpucp_[ab] +ddr +ddr_[ab] +devcfg_[ab] +devinfo +diag_log +dip +dtb_[ab] +emac +featenabler_[ab] +fsc +fsg +gearvm_[ab] +gvm_log +hyp_[ab] +imagefv_[ab] +keymaster_[ab] +limits +limits-cdsp +logdump +modemst1 +modemst2 +multiimgoem +multiimgoem_[ab] +multiimgqti +multiimgqti_[ab] +pvm_log +qmcs +quantumfv +quantumsdk +questdatafv +qupfw_[ab] +qweslicstore_[ab] +recoveryinfo +secdata +shrm_[ab] +softsku +splash +spunvm +storsec +storsec_[ab] +SYSFW_VERSION +toolsfv +tz_[ab] +TZAPPS +uefi_[ab] +uefisecapp_[ab] +vbmeta_[ab] +xbl_[ab] +xbl_config_[ab] +xbl_logs +xbl_ramdump_[ab] diff --git a/qcom_ptool/gen_udev_rules.py b/qcom_ptool/gen_udev_rules.py new file mode 100644 index 0000000..c053a07 --- /dev/null +++ b/qcom_ptool/gen_udev_rules.py @@ -0,0 +1,124 @@ +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +"""Generate udev rules for reviewed raw partitions in selected layouts.""" + +from __future__ import annotations + +import argparse +import fnmatch +import getopt +import re +import sys +from pathlib import Path + +from qcom_ptool.loaders import load as load_spec + +DATA_DIR = Path(__file__).with_name("data") +POLICY_FILE = DATA_DIR / "approved-raw-partition-patterns.list" +TEMPLATE_FILE = DATA_DIR / "55-qcom-raw-partitions-noblkid.rules.in" +RULES_PLACEHOLDER = "@QCOM_RAW_PARTITION_RULES@" +LABEL_RE = re.compile(r"[A-Za-z0-9_.+-]+") +PATTERN_RE = re.compile(r"[A-Za-z0-9_.+!*?\[\]-]+") + + +def load_patterns() -> list[str]: + """Return non-comment entries from the packaged policy file.""" + patterns: list[str] = [] + seen: set[str] = set() + for line_number, line in enumerate( + POLICY_FILE.read_text(encoding="utf-8").splitlines(), start=1 + ): + pattern = line.partition("#")[0].strip() + if not pattern: + continue + if PATTERN_RE.fullmatch(pattern) is None: + raise ValueError(f"{POLICY_FILE}:{line_number}: invalid pattern: {pattern}") + if pattern in seen: + raise ValueError(f"{POLICY_FILE}:{line_number}: duplicate pattern: {pattern}") + patterns.append(pattern) + seen.add(pattern) + + if not patterns: + raise ValueError(f"approved pattern list is empty: {POLICY_FILE}") + return patterns + + +def collect_labels(input_paths: list[Path]) -> set[str]: + """Return partition labels defined by the selected layouts.""" + labels: set[str] = set() + for path in input_paths: + spec = load_spec(str(path)) + labels.update( + entry["label"] + for entries in spec["partitions"].values() + for entry in entries + ) + return labels + + +def generate_rules(input_paths: list[Path]) -> str: + """Render udev rules for approved labels present in the layouts.""" + patterns = load_patterns() + approved_labels = { + label + for label in collect_labels(input_paths) + if any(fnmatch.fnmatchcase(label, pattern) for pattern in patterns) + } + invalid_labels = sorted( + label for label in approved_labels if LABEL_RE.fullmatch(label) is None + ) + if invalid_labels: + raise ValueError(f"invalid approved partition label: {invalid_labels[0]!r}") + + labels = sorted(approved_labels) + if not labels: + return "" + + rules = "\n".join( + f'ENV{{PARTNAME}}=="{label}", GOTO="qcom_raw_noblkid"' + for label in labels + ) + template = TEMPLATE_FILE.read_text(encoding="utf-8") + if template.count(RULES_PLACEHOLDER) != 1: + raise ValueError("rules template must contain exactly one placeholder") + return template.replace(RULES_PLACEHOLDER, rules) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "-i", + "--input", + action="append", + required=True, + type=Path, + dest="inputs", + help="partition layout to include; repeat for additional storage", + ) + parser.add_argument("-o", "--output", required=True, type=Path) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + try: + content = generate_rules(args.inputs) + if not content: + print( + "skipped: selected layouts contain no approved raw partitions; " + "output left unchanged" + ) + return 0 + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(content, encoding="utf-8") + except (OSError, ValueError, getopt.GetoptError) as error: + print(f"error: {error}", file=sys.stderr) + return 2 + + print(f"generated: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_gen_udev_rules.py b/tests/unit/test_gen_udev_rules.py new file mode 100644 index 0000000..261b823 --- /dev/null +++ b/tests/unit/test_gen_udev_rules.py @@ -0,0 +1,123 @@ +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from qcom_ptool import gen_udev_rules + + +def write_layout(path: Path, labels: list[str]) -> None: + partitions = "".join( + f"--partition --name={label} --size=1KB " + "--type-guid=00000000-0000-0000-0000-000000000000\n" + for label in labels + ) + path.write_text( + "--disk --type=ufs --size=1048576\n" + partitions, + encoding="utf-8", + ) + + +def test_generate_rules_filters_and_combines_layouts(tmp_path: Path) -> None: + first = tmp_path / "first.conf" + second = tmp_path / "second.conf" + write_layout(first, ["rootfs", "xbl_a"]) + write_layout(second, ["xbl_a", "ALIGN_TO_128K_7"]) + + rules = gen_udev_rules.generate_rules([first, second]) + + assert 'ENV{PARTNAME}=="xbl_a"' in rules + assert 'ENV{PARTNAME}=="ALIGN_TO_128K_7"' in rules + assert rules.count('ENV{PARTNAME}=="xbl_a"') == 1 + assert "ALIGN_TO_128K_*" not in rules + assert "rootfs" not in rules + + +def test_generate_rules_supports_negated_character_class( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + policy = tmp_path / "patterns.list" + policy.write_text("xbl_[!b]\n", encoding="utf-8") + monkeypatch.setattr(gen_udev_rules, "POLICY_FILE", policy) + monkeypatch.setattr(gen_udev_rules, "collect_labels", lambda _: {"xbl_a", "xbl_b"}) + + rules = gen_udev_rules.generate_rules([]) + + assert 'ENV{PARTNAME}=="xbl_a"' in rules + assert 'ENV{PARTNAME}=="xbl_b"' not in rules + + +def test_generate_rules_rejects_unsafe_approved_label( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + gen_udev_rules, + "collect_labels", + lambda _: {'ALIGN_TO_128K_", RUN+="/bin/true'}, + ) + + with pytest.raises(ValueError, match="invalid approved partition label"): + gen_udev_rules.generate_rules([]) + + +def test_load_patterns_rejects_unsafe_pattern( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + policy = tmp_path / "patterns.list" + policy.write_text('xbl_[ab]\nxbl_", RUN+="/bin/true\n', encoding="utf-8") + monkeypatch.setattr(gen_udev_rules, "POLICY_FILE", policy) + + with pytest.raises(ValueError, match="invalid pattern") as error: + gen_udev_rules.load_patterns() + + assert f"{policy}:2:" in str(error.value) + + +@pytest.mark.parametrize( + ("content", "message"), + [ + ("xbl_a\nxbl_a\n", "duplicate pattern"), + ("# comments only\n", "approved pattern list is empty"), + ], +) +def test_load_patterns_rejects_invalid_policy( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + content: str, + message: str, +) -> None: + policy = tmp_path / "patterns.list" + policy.write_text(content, encoding="utf-8") + monkeypatch.setattr(gen_udev_rules, "POLICY_FILE", policy) + + with pytest.raises(ValueError, match=message): + gen_udev_rules.load_patterns() + + +def test_main_writes_rules(tmp_path: Path) -> None: + layout = tmp_path / "partitions.conf" + output = tmp_path / "rules.d" / "55-qcom.rules" + write_layout(layout, ["cdt"]) + + assert gen_udev_rules.main(["-i", str(layout), "-o", str(output)]) == 0 + assert 'ENV{PARTNAME}=="cdt"' in output.read_text(encoding="utf-8") + + +def test_main_leaves_output_unchanged_without_approved_labels( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + layout = tmp_path / "partitions.conf" + output = tmp_path / "rules.d" / "55-qcom.rules" + write_layout(layout, ["rootfs"]) + output.parent.mkdir(parents=True) + output.write_text("unrelated rules", encoding="utf-8") + + assert gen_udev_rules.main(["-i", str(layout), "-o", str(output)]) == 0 + assert output.read_text(encoding="utf-8") == "unrelated rules" + message = capsys.readouterr().out + assert "no approved raw partitions" in message + assert "output left unchanged" in message