mirror of
https://github.com/kata-containers/kata-containers.git
synced 2026-07-01 14:38:33 +00:00
runtime-rs: Add dm-verity utility functions to gpt_disk module
When containerd creates dm-verity-protected EROFS layers, it stores the root hash and parameters as OCI annotations — but the format does not directly map to the kernel dm-verity table that the guest agent needs to construct. Bridge this gap with functions that parse containerd's dm-verity annotation JSON, detect whether a v1 superblock is embedded at the hash offset (to extract the salt automatically rather than relying on containerd's hardcoded default), and produce the X-kata.dmverity.* storage options the agent expects. This keeps all dm-verity metadata translation on the host side, so the agent can consume a flat list of options without understanding the containerd annotation schema. Signed-off-by: Alex Lyn <alex.lyn@antgroup.com>
This commit is contained in:
@@ -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<u32> = Crc::<u32>::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<PathBuf>,
|
||||
}
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<DmVerityMetadata> {
|
||||
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<String, String>,
|
||||
) -> 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<String> {
|
||||
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<String> {
|
||||
// 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
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user