"""Versioned offline qualification artifact, not a runtime installer. Reuse an attested upstream Tool build's object files unchanged. Only the adapter entry point is compiled. The previous staging, packages and services are untouched. All inputs, link objects, outputs and checks are hashed in the private report. """ import argparse from datetime import datetime, timezone import hashlib import json import os from pathlib import Path import platform import re import subprocess import time import zipfile COMMIT = "01d5f10901116c311e3fb84d5a1541f663d3ce20" UPSTREAM_SHA256 = "4565ecec4e41e094bcc43127a51970e810aa9080a9d7c025af64632bfd4c0189" def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def run(args): release = platform.freedesktop_os_release() if os.geteuid() == 0 or (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != ("ubuntu", "24.04", "x86_64"): raise RuntimeError("Unprivileged Ubuntu 24.04 amd64 required") group = Path("/sys/fs/cgroup") / Path("/proc/self/cgroup").read_text().strip().split("::", 1)[1].lstrip("/") limit = (group / "memory.max").read_text().strip() if limit == "max" or int(limit) > 3 * 1024**3: raise RuntimeError("A bounded user scope with MemoryMax <= 3G is required") previous = json.loads(args.upstream_report.read_text()) if previous.get("state") != "complete" or previous.get("source_sha256") != UPSTREAM_SHA256: raise RuntimeError("Unqualified upstream build") upstream = Path(previous["binary"]) if digest(upstream) != previous["binary_sha256"]: raise RuntimeError("Upstream binary changed") source = upstream.parents[2] if source.name != "vesc_tool-" + COMMIT: raise RuntimeError("Upstream source path mismatch") os.umask(0o077) root = args.output.resolve() root.mkdir(parents=True, mode=0o700, exist_ok=False) # Paths enter a generated makefile, never a shell command assembled from JSON. if any(not re.fullmatch(r"[A-Za-z0-9_./-]+", str(p)) for p in (root, source)): raise RuntimeError("Build paths must be make-safe") with zipfile.ZipFile(args.artifact) as bundle: for name in ("offline_main.cpp", "config_export.h", "engine_main.cpp", "native_bundle.py"): (root / name).write_bytes(bundle.read(name)) report = {"schema": "missioncore.vesc.native-probe/v1", "state": "running", "started_at": datetime.now(timezone.utc).isoformat(), "monotonic_started": time.monotonic(), "source_commit": COMMIT, "artifact_sha256": digest(args.artifact), "upstream_report_sha256": digest(args.upstream_report), "adapter_sha256": digest(root / "offline_main.cpp"), "hardware_access": False, "runtime_installed": False, "system_packages_installed": False, "jobs": [], "checks": []} def publish(): (root / "report.json").write_text(json.dumps(report, indent=2) + "\n") staging = source.parents[1] qtbase = staging / "sysroot/usr" env = dict(os.environ, LC_ALL="C", QT_QPA_PLATFORM="offscreen", LD_LIBRARY_PATH=str(qtbase / "lib/x86_64-linux-gnu"), QT_PLUGIN_PATH=str(qtbase / "lib/x86_64-linux-gnu/qt5/plugins"), XDG_CONFIG_HOME=str(root / "config"), XDG_CACHE_HOME=str(root / "cache")) def execute(name, command, data=None, expected=0, timeout=60): start = time.monotonic() proc = subprocess.run(command, cwd=source, env=env, input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout) (root / (name + ".stdout")).write_bytes(proc.stdout) (root / (name + ".stderr")).write_bytes(proc.stderr) report["jobs"].append({"id": name, "exit_code": proc.returncode, "duration_seconds": time.monotonic() - start, "stdout_sha256": hashlib.sha256(proc.stdout).hexdigest(), "stderr_sha256": hashlib.sha256(proc.stderr).hexdigest()}) publish() if proc.returncode != expected: raise RuntimeError("Native probe step failed: " + name) return proc.stdout try: makefile = (source / "Makefile").read_text().replace("\\\n", " ") match = re.search(r"^OBJECTS\s*=\s*(.+)$", makefile, re.MULTILINE) if not match: raise RuntimeError("Upstream link objects missing") objects = [source / name for name in match.group(1).split() if name != "build/lin/obj/main.o"] if not 100 < len(objects) < 1000 or any(not p.is_file() for p in objects): raise RuntimeError("Upstream object inventory incomplete") report["link_objects"] = [{"file": str(p.relative_to(source)), "sha256": digest(p)} for p in objects] report["upstream_makefile_sha256"] = digest(source / "Makefile") target = root / "mission-core-vesc-offline" wrapper = root / "Makefile.native" wrapper.write_text( "include " + str(source / "Makefile") + "\n" ".PHONY: mission-core-native-probe\n" "mission-core-native-probe:\n" "\t$(CXX) -c $(CXXFLAGS) $(INCPATH) -o " + str(root / "offline_main.o") + " " + str(root / "offline_main.cpp") + "\n" "\t$(LINK) $(LFLAGS) -o " + str(target) + " " + str(root / "offline_main.o") + " $(filter-out build/lin/obj/main.o,$(OBJECTS)) $(OBJCOMP) $(LIBS)\n") execute("compile-link", ["/usr/bin/make", "-f", str(wrapper), "mission-core-native-probe"], timeout=180) report["binary_sha256"] = digest(target) for index, archive in enumerate(args.archives): raw = archive.read_bytes() native = json.loads(execute("archive-%d" % index, [str(target)], raw)) if not native["ok"] or not native["compatibility"]["legacy_power_loss_correction"]: raise RuntimeError("Upstream compatibility check failed") import base64 packet = base64.b64decode(native["compatibility"]["offline_detect_example_base64"]) if packet[:2] != bytes([58, 0]) or int.from_bytes(packet[2:6], "big", signed=True) != 50000: raise RuntimeError("Expected native 5.02 detect correction was not applied") report["checks"].append({"id": "native-archive-%d" % index, "ok": True, "archive_sha256": hashlib.sha256(raw).hexdigest(), "motor_parameter_count": len(native["motor"]["parameters"]), "application_parameter_count": len(native["application"]["parameters"]), "binary_round_trip_exact": True, "xml_equivalent_by_upstream_comparison": True, "xml_binary_exact": all(native[k]["xml_round_trip_exact"] for k in ("motor", "application")), "legacy_power_loss_correction": True}) # Fail closed on archive corruption and unsupported firmware instead # of silently presenting the bundled defaults as actual settings. original = json.loads(raw) damaged = json.loads(raw); damaged["configs"]["motor"]["sha256"] = "0" * 64 unknown = json.loads(raw); unknown["identity"]["major"] = 99 wrong_signature = json.loads(raw) payload = bytearray(base64.b64decode(original["configs"]["motor"]["payload"])); payload[1] ^= 1 wrong_signature["configs"]["motor"]["payload"] = base64.b64encode(payload).decode() wrong_signature["configs"]["motor"]["sha256"] = hashlib.sha256(payload).hexdigest() truncated = json.loads(raw) payload = base64.b64decode(original["configs"]["motor"]["payload"])[:-1] truncated["configs"]["motor"].update(payload=base64.b64encode(payload).decode(), sha256=hashlib.sha256(payload).hexdigest(), bytes=len(payload)) for name, value in (("corrupt", damaged), ("unsupported", unknown), ("signature", wrong_signature), ("truncated", truncated)): rejected = json.loads(execute("%s-%d" % (name, index), [str(target)], json.dumps(value).encode(), expected=1)) if rejected["ok"] or rejected["hardware_access"]: raise RuntimeError("Invalid archive was not rejected") report["checks"].append({"id": "%s-%d" % (name, index), "ok": True}) for item in report["link_objects"]: if digest(source / item["file"]) != item["sha256"]: raise RuntimeError("Upstream objects were modified") engine = root / "mission-core-vesc-engine" wrapper.write_text(wrapper.read_text() + "\n.PHONY: mission-core-native-engine\nmission-core-native-engine:\n" "\t$(CXX) -c $(CXXFLAGS) $(INCPATH) -o " + str(root / "engine_main.o") + " " + str(root / "engine_main.cpp") + "\n" "\t$(LINK) $(LFLAGS) -o " + str(engine) + " " + str(root / "engine_main.o") + " $(filter-out build/lin/obj/main.o,$(OBJECTS)) $(OBJCOMP) $(LIBS)\n") execute("engine-compile", ["/usr/bin/make", "-f", str(wrapper), "mission-core-native-engine"], timeout=180) requests = [{"id": 1, "method": "engine"}, {"id": 2, "method": "current", "current_a": 30}, {"id": 3, "method": "hall_start", "current_a": 5}, {"id": 4, "method": "arbitrary_packet"}] responses = [json.loads(line) for line in execute("engine-offline", [str(engine), "--offline"], b"".join(json.dumps(r).encode()+b"\n" for r in requests)).splitlines()] if (len(responses) != 5 or not responses[0]["ready"] or not responses[1]["ok"] or any(r["ok"] for r in responses[2:]) or responses[1]["result"]["hardware_enabled"]): raise RuntimeError("Native engine offline boundary failed") report["checks"].append({"id": "engine-offline-denies-hardware", "ok": True}) execute("runtime-bundle", ["/usr/bin/python3", str(root / "native_bundle.py"), "--engine", str(engine), "--sysroot", str(staging / "sysroot"), "--source", str(source), "--output", str(root / "runtime")], timeout=180) report["native_runtime"] = json.loads((root / "runtime/bundle.json").read_text()) report.update(state="complete", binary=str(target), upstream_objects_unchanged=True) except Exception as error: report.update(state="error", error=str(error)) raise finally: report["finished_at"] = datetime.now(timezone.utc).isoformat() report["duration_seconds"] = time.monotonic() - report["monotonic_started"] publish() def main(): parser = argparse.ArgumentParser() parser.add_argument("--artifact", type=Path, required=True) parser.add_argument("--upstream-report", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--archives", type=Path, nargs="+", required=True) run(parser.parse_args()) if __name__ == "__main__": main()