feat: strengthen BSP and PSP validation and handoff

This commit is contained in:
tangfahong committed 2026-09-24 11:52:22 +08:00
1 parent bcea35a91e
commit 1641fac631
39 files changed
+2695 -90

No files matched your search

@@ -1,17 +1,22 @@
#!/usr/bin/env python3
"""Validate a BSP-to-PSP handoff and its immutable artifact hashes."""
"""Validate a BSP-to-PSP handoff, output permissions, and artifact hashes."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import stat
import xml.etree.ElementTree as ET
from pathlib import Path
REQUIRED_IMAGE_MARGIN_BYTES = 512 * 1024
REQUIRED_PARTITION_IMAGES = {"kernel.bin", "rootfs.bin"}
DIRECTORY_ACCESS_BITS = 0o555
FILE_READ_BITS = 0o444
FILE_EXECUTE_BITS = 0o111
def sha256_file(path: Path) -> str:
@@ -30,6 +35,159 @@ def within(path: Path, root: Path) -> bool:
return False
def validate_output_permissions(
payload: object,
build: dict[str, object],
producer: dict[str, object],
imgs_path: Path | None,
errors: list[str],
repair: bool,
) -> dict[str, object] | None:
"""Check BSP evidence and live modes; optionally repair only its exact output."""
if not isinstance(payload, dict) or payload.get("passed") is not True:
errors.append("BSP handoff 缺少通过的输出路径权限证据")
return None
if payload.get("schema_version") != 1 or payload.get("policy") != "a+rX":
errors.append("输出路径权限证据的 schema_version/policy 不匹配")
return None
root_value = payload.get("output_root")
version_value = payload.get("imgs_version_path")
project_name = producer.get("project")
build_version = build.get("version")
if (
not isinstance(root_value, str)
or not Path(root_value).is_absolute()
or not isinstance(version_value, str)
or not Path(version_value).is_absolute()
or not isinstance(project_name, str)
or project_name in ("", ".", "..")
or Path(project_name).name != project_name
or not isinstance(build_version, str)
or build_version in ("", ".", "..")
or Path(build_version).name != build_version
or imgs_path is None
):
errors.append("输出路径权限证据缺少精确的绝对输出目录、项目或构建版本")
return None
output_root = Path(root_value)
if (
len(output_root.parts) < 3
or output_root == Path.home()
or output_root.is_symlink()
):
errors.append("输出路径权限证据的根路径过宽或是软链接")
return None
expected_imgs_path = output_root / project_name / "imgs" / build_version
if Path(version_value) != expected_imgs_path or imgs_path != expected_imgs_path:
errors.append("输出路径权限证据与 BSP 项目、构建版本或 imgs_version_path 不一致")
return None
evidence_value = build.get("output_permissions_path")
project_value = producer.get("project_path")
if (
not isinstance(project_value, str)
or not Path(project_value).is_absolute()
or not isinstance(evidence_value, str)
or not Path(evidence_value).is_absolute()
):
errors.append("build.output_permissions_path 必须是 BSP 项目内的绝对路径")
return None
evidence_path = Path(evidence_value)
expected_evidence = (
Path(project_value)
/ "doc" / "bsp_adaptation" / "validation"
/ f"output_permissions_{build_version}.json"
)
if evidence_path != expected_evidence or evidence_path.is_symlink():
errors.append("输出路径权限证据文件路径不是预期 BSP 项目内的普通文件")
return None
try:
external_payload = json.loads(evidence_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
errors.append(f"输出路径权限证据文件不可读或解析失败: {exc}")
return None
if external_payload != payload:
errors.append("handoff 内嵌权限证据与外部证据文件不一致")
return None
paths: list[tuple[Path, int, str]] = []
for path in (
output_root,
output_root / project_name,
output_root / project_name / "imgs",
expected_imgs_path,
):
if path.is_symlink() or not path.is_dir():
errors.append(f"构建输出路径节点不是普通目录: {path}")
return None
paths.append((path, DIRECTORY_ACCESS_BITS, "目录"))
def walk_error(exc: OSError) -> None:
raise exc
skipped_symlinks = 0
try:
for parent, directory_names, file_names in os.walk(
expected_imgs_path, followlinks=False, onerror=walk_error
):
parent_path = Path(parent)
for name in list(directory_names):
path = parent_path / name
if path.is_symlink():
directory_names.remove(name)
skipped_symlinks += 1
elif path.is_dir():
paths.append((path, DIRECTORY_ACCESS_BITS, "目录"))
else:
errors.append(f"构建输出包含非普通目录: {path}")
return None
for name in file_names:
path = parent_path / name
if path.is_symlink():
skipped_symlinks += 1
elif path.is_file():
bits = FILE_READ_BITS
if stat.S_IMODE(path.stat().st_mode) & FILE_EXECUTE_BITS:
bits |= FILE_EXECUTE_BITS
paths.append((path, bits, "文件"))
else:
errors.append(f"构建输出包含非普通文件: {path}")
return None
except OSError as exc:
errors.append(f"无法遍历构建输出目录: {exc}")
return None
changed = 0
for path, bits, kind in paths:
try:
if path.is_symlink():
errors.append(f"权限处理拒绝软链接: {path}")
return None
before = stat.S_IMODE(path.stat().st_mode)
if before & bits != bits:
if not repair:
errors.append(f"构建输出{kind}缺少共享读取/进入权限: {path}")
continue
os.chmod(path, before | bits, follow_symlinks=False)
changed += 1
after = stat.S_IMODE(path.stat().st_mode)
if after & bits != bits:
errors.append(f"构建输出{kind}权限修复后仍不满足要求: {path}")
except OSError as exc:
errors.append(f"无法检查或修复构建输出权限 {path}: {exc}")
return {
**payload,
"current_check": {
"passed": not errors,
"repaired": changed > 0,
"changed_count": changed,
"checked_paths": len(paths),
"symlinks_skipped": skipped_symlinks,
},
}
def validate_image_partition_margin(
payload: object,
build: dict[str, object],
@@ -155,6 +313,7 @@ def validate_handoff(
expected_sdk: str | None = None,
expected_build_type: str | None = None,
expected_system_form: str | None = None,
repair_output_permissions: bool = False,
) -> dict[str, object]:
errors: list[str] = []
warnings: list[str] = []
@@ -196,6 +355,8 @@ def validate_handoff(
for field, (actual, expected) in expected_values.items():
if expected is not None and actual != expected:
errors.append(f"{field} 不匹配:期望 {expected!r},实际 {actual!r}")
if repair_output_permissions and expected_project is None:
errors.append("自动修复输出权限时必须提供预期的 BSP 项目名 --project")
if build.get("status") != "build_passed" or build.get("exit_code") != 0:
errors.append("BSP 构建状态不是 build_passed/exit_code=0")
@@ -210,6 +371,16 @@ def validate_handoff(
if not imgs_path.is_dir() or imgs_path.is_symlink():
errors.append(f"imgs_version_path 不存在或不是普通目录: {imgs_path}")
# Never modify BSP output if the handoff identity is already inconsistent.
output_permissions = validate_output_permissions(
validation.get("output_permissions"),
build,
producer,
imgs_path,
errors,
repair=repair_output_permissions and not errors and expected_project == producer.get("project"),
)
verified = 0
verified_artifacts: list[dict[str, object]] = []
seen_relative_paths: set[str] = set()
@@ -297,6 +468,8 @@ def validate_handoff(
"verified_artifact_count": verified,
"artifacts": verified_artifacts,
"image_partition_margin": image_partition_margin,
"output_permissions_path": build.get("output_permissions_path"),
"output_permissions": output_permissions,
"hardware_validation": validation.get("hardware"),
"owner_acceptance": validation.get("owner_acceptance"),
"release": validation.get("release"),
@@ -312,6 +485,11 @@ def main() -> int:
parser.add_argument("--sdk")
parser.add_argument("--build-type")
parser.add_argument("--system-form", choices=("single-os", "dual-os"))
parser.add_argument(
"--repair-output-permissions",
action="store_true",
help="仅对已核对项目身份及证据的本次 imgs 路径补齐 a+rX 权限",
)
args = parser.parse_args()
try:
result = validate_handoff(
@@ -321,6 +499,7 @@ def main() -> int:
args.sdk,
args.build_type,
args.system_form,
repair_output_permissions=args.repair_output_permissions,
)
except (OSError, json.JSONDecodeError) as exc:
result = {"passed": False, "errors": [str(exc)], "warnings": []}