diff --git a/src/libs/kata-types/src/gpt_disk.rs b/src/libs/kata-types/src/gpt_disk.rs index 50f2c5e578..6e7c4af5f0 100644 --- a/src/libs/kata-types/src/gpt_disk.rs +++ b/src/libs/kata-types/src/gpt_disk.rs @@ -21,7 +21,9 @@ use anyhow::{anyhow, Context, Result}; use crc::Crc; use gpt::{disk::LogicalBlockSize, mbr::ProtectiveMBR, partition_types, GptConfig}; use scopeguard; +use serde::{Deserialize, Serialize}; use std::convert::TryFrom; +use std::convert::TryInto; use std::fs; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; @@ -52,6 +54,17 @@ const CRC_32: Crc = Crc::::new(&crc::CRC_32_ISO_HDLC); const GPT_META_HEAD_IMG: &str = "gpt_meta_head.img"; /// Temporary full GPT image used to synthesize head metadata const GPT_META_FULL_IMG: &str = "gpt_meta_full.img"; +/// Annotation key used by containerd to specify dm-verity metadata path +const X_CONTAINERD_DMVERITY: &str = "X-containerd.dmverity"; +/// dm-verity v1 superblock magic number: the ASCII string "verity" followed by two NUL bytes. +const DM_VERITY_MAGIC: [u8; 8] = [0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x00, 0x00]; +/// dm-verity superblock version that we support +const DM_VERITY_SB_VERSION: u32 = 1; +/// Default dm-verity salt used by containerd's erofs snapshotter. +const CONTAINERD_DEFAULT_DMVERITY_SALT: &str = + "0000000000000000000000000000000000000000000000000000000000000000"; +/// Maximum expected size of dm-verity metadata JSON file (arbitrary limit to prevent abuse) +const MAX_DMVERITY_METADATA_SIZE: u64 = 65536; /// Represents a read-only EROFS layer to be placed in a GPT partition #[derive(Debug, Clone)] @@ -100,6 +113,202 @@ pub struct GptMetadataFiles { /// Paths to generated padding files (between partitions) pub pad_paths: Vec, } +/// dm-verity metadata structure matching containerd's JSON format +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DmVerityMetadata { + /// Root hash of the dm-verity tree + pub roothash: String, + /// Offset in bytes where the hash data starts + pub hashoffset: u64, + /// Salt value for dm-verity (hex-encoded, optional). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub salt: Option, +} + +/// Parse dm-verity metadata from a JSON file +/// The metadata path looks like: "/var/lib/containerd/.../layer.erofs.dmverity" +/// and the file is expected to be in the format produced by containerd's dm-verity implementation, e.g.: +/// ```json +/// { +/// "roothash": "abcdef1234567890...", +/// "hashoffset": 12345678, +/// }``` +pub fn parse_dmverity_metadata_file(path: &str) -> Result { + let file_meta = fs::metadata(path).context("failed to stat dm-verity metadata file")?; + if file_meta.len() > MAX_DMVERITY_METADATA_SIZE { + return Err(anyhow!( + "dm-verity metadata file too large: {} bytes (max {})", + file_meta.len(), + MAX_DMVERITY_METADATA_SIZE + )); + } + + let content = fs::read_to_string(path).context("failed to read dm-verity metadata file")?; + let meta: DmVerityMetadata = + serde_json::from_str(&content).context("failed to parse dm-verity metadata JSON")?; + Ok(meta) +} + +/// Extract dm-verity metadata path from mount annotations +pub fn extract_dmverity_annotation( + annotations: &std::collections::HashMap, +) -> Option<&str> { + annotations.get(X_CONTAINERD_DMVERITY).map(|s| s.as_str()) +} + +/// Check whether a dm-verity v1 superblock exists at the given offset in an EROFS image file. +/// +/// The dm-verity v1 superblock starts with the ASCII signature "verity\0\0". +/// This is critical for correctly computing `hash_start_block` in the dm-verity table: +/// - With superblock: hash tree starts after the superblock (hashoffset + superblock_size) +/// - Without superblock: hash tree starts directly at hashoffset +fn has_verity_superblock(erofs_path: &str, hashoffset: u64) -> bool { + let mut file = match fs::File::open(erofs_path) { + Ok(f) => f, + Err(_) => return false, + }; + + if file.seek(SeekFrom::Start(hashoffset)).is_err() { + return false; + } + + let mut magic = [0u8; 8]; + if file.read_exact(&mut magic).is_err() { + return false; + } + + // dm-verity v1 superblock signature: "verity\0\0" (ASCII, 8 bytes) + magic == DM_VERITY_MAGIC +} + +/// Read the salt from a dm-verity v1 superblock embedded in an EROFS image file. +/// +/// The dm-verity v1 superblock is located at `hashoffset` bytes from the start +/// of the file. Its layout matches the go-dmverity `Superblock` struct +/// (see `pkg/verity/superblock.go`), serialized in little-endian: +/// +/// Offset Size Field +/// 0 8 Signature ("verity\0\0") +/// 8 4 Version (uint32, must be 1) +/// 12 4 HashType (uint32, 0 or 1) +/// 16 16 UUID +/// 32 32 Algorithm (null-terminated, e.g. "sha256") +/// 64 4 DataBlockSize (uint32) +/// 68 4 HashBlockSize (uint32) +/// 72 8 DataBlocks (uint64) +/// 80 2 SaltSize (uint16) +/// 82 6 Pad1 +/// 88 256 Salt +/// 344 168 Pad2 +/// Total: 512 bytes +/// +/// We only need to extract the salt. If the superblock cannot be read or +/// parsed, we return None and the agent will use its default salt. +fn read_verity_superblock_salt(erofs_path: &str, hashoffset: u64) -> Option { + let mut file = fs::File::open(erofs_path).ok()?; + file.seek(SeekFrom::Start(hashoffset)).ok()?; + + // Read the fixed portion of the superblock up to and including salt_size + // We need bytes 0..82 to get the salt_size field + let mut sb_header = [0u8; 88]; + file.read_exact(&mut sb_header).ok()?; + + // Verify magic + if sb_header[0..8] != DM_VERITY_MAGIC { + return None; + } + + // Verify version + let version = u32::from_le_bytes(sb_header[8..12].try_into().ok()?); + if version != DM_VERITY_SB_VERSION { + return None; + } + + // salt_size is at offset 80 (uint16, LE) + let salt_size = u16::from_le_bytes(sb_header[80..82].try_into().ok()?) as usize; + + if salt_size == 0 { + // No salt used + return None; + } + + if salt_size > 256 { + return None; + } + + // Read salt bytes (starts at offset 88 in the superblock, after Pad1) + // We already read up to offset 82 (sb_header is 88 bytes), so we need + // to skip 88 - 82 = 6 bytes of Pad1, then read salt_size bytes. + // Actually, sb_header is 88 bytes which includes the 6 bytes of Pad1 + // (offset 82..88). So the file cursor is now at offset 88, which is + // exactly where the salt starts. We can read directly. + let mut salt_bytes = vec![0u8; salt_size]; + file.read_exact(&mut salt_bytes).ok()?; + + // Encode as lowercase hex string + let salt_hex: String = salt_bytes.iter().map(|b| format!("{:02x}", b)).collect(); + Some(salt_hex) +} + +/// Generate dm-verity storage options for kata-agent +pub fn generate_dmverity_options( + metadata: &DmVerityMetadata, + erofs_path: Option<&str>, +) -> Vec { + // Auto-detect whether a dm-verity v1 superblock exists at hashoffset. + let no_superblock = if let Some(path) = erofs_path { + !has_verity_superblock(path, metadata.hashoffset) + } else { + // Cannot detect without the file path; default to no-superblock=false + // which matches the containerd erofs snapshotter default behavior + false + }; + + let mut options = vec![ + "X-kata.dmverity-enabled=true".to_string(), + format!("X-kata.dmverity.roothash={}", metadata.roothash), + format!("X-kata.dmverity.hashoffset={}", metadata.hashoffset), + format!("X-kata.dmverity.no-superblock={}", no_superblock), + ]; + + let salt_resolved = if let Some(ref salt_hex) = metadata.salt { + // Priority 1: salt from .dmverity JSON metadata + info!( + sl!(), + "Using dm-verity salt from .dmverity metadata: {} ({} bytes)", + salt_hex, + salt_hex.len() / 2 + ); + Some(salt_hex.clone()) + } else if let Some(path) = erofs_path { + if !no_superblock { + // Priority 2: read salt from superblock (only when superblock exists) + read_verity_superblock_salt(path, metadata.hashoffset).or_else(|| { + info!( + sl!(), + "Failed to read dm-verity salt from superblock, using containerd default salt (32 zero bytes)" + ); + Some(CONTAINERD_DEFAULT_DMVERITY_SALT.to_string()) + }) + } else { + // Priority 3: NoSuperblock mode — use containerd's default salt + Some(CONTAINERD_DEFAULT_DMVERITY_SALT.to_string()) + } + } else { + // No EROFS path provided and no salt in metadata — fall back to containerd default + info!( + sl!(), + "No EROFS path for salt detection, using containerd default salt (32 zero bytes)" + ); + Some(CONTAINERD_DEFAULT_DMVERITY_SALT.to_string()) + }; + + if let Some(salt) = salt_resolved { + options.push(format!("X-kata.dmverity.salt={}", salt)); + } + + options +} /// Extract snapshot ID from a source path ///