From c96c26808fdb4f819101009549ca3395af7ff61f Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Thu, 27 Aug 2026 22:10:18 -0400 Subject: [PATCH 1/2] fix(deploy): refuse extension verity only, not the rootfs root hash The guard grepped the whole manifest for "root_hash" and so refused every runtime with rootfs.image.verity on: that hash is recorded at the manifest's top level and travels in the boot FIT, needing no sidecar. Check extensions[].root_hash, the case the guard exists for, the same way `connect upload` already does. --- src/commands/runtime/deploy.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/commands/runtime/deploy.rs b/src/commands/runtime/deploy.rs index 6113fd45..ea902a47 100644 --- a/src/commands/runtime/deploy.rs +++ b/src/commands/runtime/deploy.rs @@ -701,7 +701,13 @@ fi # Extension dm-verity trees (.verity) are not published by this path # yet - only the images are - and a device given a root_hash without its tree # refuses the extension. Refuse here instead, where the author can act on it. -if grep -q '"root_hash"' "$MANIFEST_FILE"; then +# Only EXTENSION root hashes matter: the manifest's top-level root_hash is the +# rootfs verity hash, which travels in the boot FIT and needs no sidecar here. +if python3 -c ' +import json, sys +m = json.load(open(sys.argv[1])) +sys.exit(0 if any(e.get("root_hash") for e in m.get("extensions", [])) else 1) +' "$MANIFEST_FILE"; then echo "ERROR: this runtime has extensions with image.verity: true; deploy does not publish their dm-verity hash trees yet, so the device would refuse them. Provision instead, or build without verity." >&2 exit 1 fi @@ -1139,6 +1145,27 @@ async fn prepare_mac_deploy_net( #[cfg(test)] mod tests { + #[test] + fn hash_collection_refuses_only_extension_root_hashes() { + // A rootfs verity hash lives at the manifest's top level and needs no + // sidecar; only extensions[].root_hash must trip the refusal. + let cmd = RuntimeDeployCommand::new( + "dev".to_string(), + "avocado.yaml".to_string(), + false, + None, + "root@host".to_string(), + None, + None, + ); + let s = cmd.create_hash_collection_script("x86_64"); + assert!( + s.contains("m.get(\"extensions\", [])"), + "checks extensions, not the whole file" + ); + assert!(!s.contains("grep -q '\"root_hash\"'"), "no whole-file grep"); + } + use super::*; // --- DeviceSpec parsing tests --- From 5066b265bea33b8fc263c38d7549605ce5a7415a Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Fri, 28 Aug 2026 07:31:54 -0400 Subject: [PATCH 2/2] fix(deploy): verity guard checks key presence and fails on an unreadable manifest Match connect upload: an extension counts when the root_hash key is present, not when its value is truthy. A manifest that cannot be read or is not the expected shape exits 2 and fails the deploy explicitly instead of passing as "no extension verity". --- src/commands/runtime/deploy.rs | 39 ++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/commands/runtime/deploy.rs b/src/commands/runtime/deploy.rs index ea902a47..3bb0a5a1 100644 --- a/src/commands/runtime/deploy.rs +++ b/src/commands/runtime/deploy.rs @@ -703,14 +703,31 @@ fi # refuses the extension. Refuse here instead, where the author can act on it. # Only EXTENSION root hashes matter: the manifest's top-level root_hash is the # rootfs verity hash, which travels in the boot FIT and needs no sidecar here. -if python3 -c ' +# Exit 0: an extension carries root_hash (key present, as `connect upload` +# tests it). Exit 1: none does. Exit 2: the manifest could not be read or is +# not the expected shape - that must stop the deploy too, never pass as "no +# verity". +python3 -c ' import json, sys -m = json.load(open(sys.argv[1])) -sys.exit(0 if any(e.get("root_hash") for e in m.get("extensions", [])) else 1) -' "$MANIFEST_FILE"; then - echo "ERROR: this runtime has extensions with image.verity: true; deploy does not publish their dm-verity hash trees yet, so the device would refuse them. Provision instead, or build without verity." >&2 - exit 1 -fi +try: + m = json.load(open(sys.argv[1])) + exts = m["extensions"] + if not isinstance(exts, list): + raise TypeError("extensions is not a list") +except Exception as e: + print("manifest %s: %s" % (sys.argv[1], e), file=sys.stderr) + sys.exit(2) +sys.exit(0 if any(isinstance(e, dict) and "root_hash" in e for e in exts) else 1) +' "$MANIFEST_FILE" +case $? in + 0) + echo "ERROR: this runtime has extensions with image.verity: true; deploy does not publish their dm-verity hash trees yet, so the device would refuse them. Provision instead, or build without verity." >&2 + exit 1 ;; + 1) ;; + *) + echo "ERROR: could not read the runtime manifest to check for extension verity" >&2 + exit 1 ;; +esac # Read root.json ROOT_JSON_FILE="$VAR_STAGING/lib/avocado/metadata/root.json" @@ -1160,8 +1177,12 @@ mod tests { ); let s = cmd.create_hash_collection_script("x86_64"); assert!( - s.contains("m.get(\"extensions\", [])"), - "checks extensions, not the whole file" + s.contains("\"root_hash\" in e for e in exts"), + "checks key presence on extensions, not the whole file" + ); + assert!( + s.contains("sys.exit(2)"), + "an unreadable manifest is its own failure" ); assert!(!s.contains("grep -q '\"root_hash\"'"), "no whole-file grep"); }