Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
13d127f
feat(uki): activate correct verity addon for target A/B slot (ACL only)
bfjelds Jun 1, 2026
63009d3
Skip A/B duplicate FS UUID check for ACL
bfjelds Jun 1, 2026
c7da44d
Fix rustfmt formatting in verity addon tests
bfjelds Jun 2, 2026
bd612ec
Include UKI addons in findUkiEntries COSI metadata
bfjelds Jun 2, 2026
d722c07
Search UKI addon cmdlines for usrhash= parameter
bfjelds Jun 2, 2026
4205568
Handle ACL BTRFS UUID collision during A/B update mount
bfjelds Jun 3, 2026
596a696
Verify verity root hash before ACL BTRFS UUID bind-mount
bfjelds Jun 3, 2026
1b9435c
Add forceAbUpdate internal param to bypass SHA384 check
bfjelds Jun 3, 2026
5a00ebd
Fix rustfmt formatting in ab_update.rs and newroot.rs
bfjelds Jun 3, 2026
f3918ab
Remove stray files accidentally committed
bfjelds Jun 3, 2026
e04b9e0
Validate ACL duplicate FS UUIDs with verity root hash comparison
bfjelds Jun 4, 2026
f34c5ee
Fix build: compare PARTUUIDs directly instead of using is_acl_usr()
bfjelds Jun 4, 2026
9726875
Fix build: add mut to partitions iterator binding
bfjelds Jun 4, 2026
952f763
Clean up old target-slot UKIs before staging new one
bfjelds Jun 4, 2026
5cb26e3
Fix rustfmt in osimage.rs
bfjelds Jun 4, 2026
5131bae
Fix UKI cleanup to scope by slot+os-index, not just slot
bfjelds Jun 4, 2026
43b5ad0
Only clean original UKI for install_index 0 (multiboot safety)
bfjelds Jun 4, 2026
85025dd
refactor: pass active_usr_roothash to validate_acl_duplicate_uuid
bfjelds Jun 4, 2026
8b44b16
fix: restore functional_test module header after edit
bfjelds Jun 4, 2026
c4be920
style: rustfmt fixes
bfjelds Jun 4, 2026
5421ae0
fix: address deep review findings DR-001 through DR-007
bfjelds Jun 5, 2026
195956f
style: rustfmt fixes in uki.rs
bfjelds Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion crates/trident/src/engine/ab_update.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{path::PathBuf, time::Instant};
use std::{
path::{Path, PathBuf},
time::Instant,
};

use log::{debug, info, warn};

Expand Down Expand Up @@ -59,13 +62,25 @@ pub(super) fn stage_update(
verity::stop_trident_servicing_devices(&ctx.spec).structured(ServicingError::CleanupVerity)?;

storage::initialize_block_devices(&ctx)?;

// Extract the staging USR verity root hash from the COSI image metadata.
// This is used to cryptographically verify that the active and staging USR
// partitions have identical content before allowing a bind-mount workaround
// for the BTRFS kernel UUID collision on ACL.
let staging_usr_roothash = ctx.image.as_ref().and_then(|img| {
img.filesystems()
.find(|fs| fs.mount_point == Path::new("/usr"))
.and_then(|fs| fs.verity.as_ref().map(|v| v.roothash.clone()))
});

let newroot_mount = NewrootMount::create_and_mount(
&ctx.spec,
&ctx.partition_paths,
ctx.get_ab_update_volume()
.structured(InternalError::Internal(
"No update volume despite there being an A/B update in progress",
))?,
staging_usr_roothash.as_deref(),
)?;

engine::provision(subsystems, &ctx, newroot_mount.path())?;
Expand Down
107 changes: 107 additions & 0 deletions crates/trident/src/engine/acl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! ACL (Azure Container Linux) UKI-specific constants and helpers.
//!
//! ACL uses fixed PARTUUIDs for USR A/B partitions and a verity addon that
//! places the root hash in the kernel command line as `usrhash=<hex>`.

use std::fs;

// ACL UKI disk layout defines fixed PARTUUIDs for the USR A/B data partitions.
// These are from acl-scripts disk_layout_uki.json.
pub const ACL_USR_A_PARTUUID: &str = "7130c94a-213a-4e5a-8e26-6cce9662f132";
pub const ACL_USR_B_PARTUUID: &str = "e03dd35c-7c2d-4a47-b3fe-27f15780a57c";

/// Reads the active USR verity root hash from `/proc/cmdline`.
///
/// ACL UKI images include a `usrhash=<hex>` parameter in the kernel command
/// line (contributed by the verity addon). Returns `None` if the parameter
/// is not present or `/proc/cmdline` cannot be read.
pub fn read_active_usr_roothash() -> Option<String> {
let cmdline = fs::read_to_string("/proc/cmdline").ok()?;
cmdline
.split_whitespace()
.find_map(|field| field.strip_prefix("usrhash="))
.map(|hash| hash.to_owned())
}

/// Compares two verity root hashes for equality after trimming whitespace and
/// lowercasing. Returns `false` if either hash is empty (an empty hash is not
/// a valid identity — `"" == ""` would incorrectly pass).
pub fn verity_hashes_match(a: &str, b: &str) -> bool {
let a = a.trim().to_lowercase();
let b = b.trim().to_lowercase();
!a.is_empty() && !b.is_empty() && a == b
}

/// Returns a char-safe preview of a hash string for log messages.
/// Uses `chars().take(16)` instead of byte-index slicing to avoid panics
/// on non-ASCII input (which shouldn't happen for hex hashes, but defense
/// in depth).
pub fn hash_preview(hash: &str) -> String {
hash.trim().to_lowercase().chars().take(16).collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn verity_hashes_match_identical() {
assert!(verity_hashes_match("abc123", "abc123"));
}

#[test]
fn verity_hashes_match_case_insensitive() {
assert!(verity_hashes_match("ABC123", "abc123"));
assert!(verity_hashes_match("abc123", "ABC123"));
}

#[test]
fn verity_hashes_match_trims_whitespace() {
assert!(verity_hashes_match(" abc123 ", "abc123"));
assert!(verity_hashes_match("abc123", " abc123\n"));
}

#[test]
fn verity_hashes_match_rejects_empty() {
assert!(!verity_hashes_match("", ""));
assert!(!verity_hashes_match("abc123", ""));
assert!(!verity_hashes_match("", "abc123"));
}

#[test]
fn verity_hashes_match_rejects_whitespace_only() {
assert!(!verity_hashes_match(" ", " "));
assert!(!verity_hashes_match("abc123", " "));
}

#[test]
fn verity_hashes_match_rejects_different() {
assert!(!verity_hashes_match("abc123", "def456"));
}

#[test]
fn hash_preview_truncates_to_16() {
let long_hash = "0123456789abcdef0123456789abcdef";
assert_eq!(hash_preview(long_hash), "0123456789abcdef");
}

#[test]
fn hash_preview_short_hash_unchanged() {
assert_eq!(hash_preview("abc"), "abc");
}

#[test]
fn hash_preview_lowercases() {
assert_eq!(hash_preview("ABCDEF"), "abcdef");
}

#[test]
fn hash_preview_trims() {
assert_eq!(hash_preview(" abc "), "abc");
}

#[test]
fn hash_preview_empty() {
assert_eq!(hash_preview(""), "");
}
}
Loading
Loading