agent: add systemd template unit for extension image mount/unmount

Add a systemd template unit kata-extension-mount@.service and companion
helper scripts (kata-extension-mount.sh, kata-extension-umount.sh) that handle
the guest-side setup of extension block device images.

The mount script:
- Discovers the block device by matching its virtio serial (extension-<name>)
- Reads verity parameters from kata.extension.<name>.verity_params on the
  kernel command line
- Creates a dm-verity device and mounts the EROFS filesystem read-only
  at /run/kata-extensions/<name>/

The unit uses ConditionKernelCommandLine=kata.extension.%i.verity_params so
it only activates when that extension is configured for the VM, and is
ordered before kata-agent.service so extension contents are available when
the agent starts.

A systemd generator (kata-extension-mount-generator) instantiates the
template for every kata.extension.<name>.verity_params entry on the kernel
command line. Because the runtime emits one such entry per configured
guest_extension_images, extensions are enabled purely from the cmdline and
adding a new one needs no change to the rootfs build.

The agent Makefile is updated to install the unit, scripts and generator.

Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
Assisted-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Fabiano Fidêncio
2026-05-10 19:03:37 +02:00
committed by Fabiano Fidêncio
parent 968d6e9431
commit e85a19a8a6
5 changed files with 265 additions and 0 deletions

View File

@@ -76,6 +76,8 @@ INIT := no
# Path to systemd unit directory if installed as not init.
UNIT_DIR := /usr/lib/systemd/system
# Path to systemd system generator directory.
GENERATOR_DIR := /usr/lib/systemd/system-generators
GENERATED_CODE = src/version.rs
@@ -100,6 +102,13 @@ ifeq ($(INIT),no)
GENERATED_FILES += $(UNIT_FILES)
# Target to be reached in systemd services
UNIT_FILES += kata-containers.target
# Template unit for mounting extension block devices (CoCo, GPU, etc.)
UNIT_FILES += kata-extension-mount@.service
# Helper scripts for extension mount/umount
EXTENSION_SCRIPTS = kata-extension-mount.sh kata-extension-umount.sh
# systemd generator that enables the mount template per extension declared
# on the kernel command line, so new extensions need no rootfs build change
EXTENSION_GENERATOR = kata-extension-mount-generator.sh
endif
# Display name of command and it's version (or a message if not available).
@@ -163,6 +172,10 @@ install-services: $(GENERATED_FILES)
ifeq ($(INIT),no)
@echo "Installing systemd unit files..."
$(foreach f,$(UNIT_FILES),$(call INSTALL_FILE,$f,$(UNIT_DIR)))
@echo "Installing extension helper scripts..."
$(foreach f,$(EXTENSION_SCRIPTS),install -D -m 755 $f $(DESTDIR)/usr/libexec/$f || exit 1;)
@echo "Installing extension mount generator..."
@install -D -m 755 $(EXTENSION_GENERATOR) $(DESTDIR)$(GENERATOR_DIR)/kata-extension-mount-generator
endif
show-header:

View File

@@ -0,0 +1,59 @@
#!/bin/bash
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# systemd system generator that instantiates the kata-extension-mount@.service
# template for every guest extension image declared on the kernel command line.
#
# The runtime emits one "kata.extension.<name>.verity_params=..." entry per
# configured guest_extension_images, so this generator enables exactly the
# extensions the VM is configured with -- without the rootfs build needing to
# know any extension name in advance. Adding a new extension therefore requires
# no change to the image build: it is wired up purely from the kernel cmdline.
#
# systemd invokes generators with three directory arguments; $1 is the "normal"
# output directory, which is where dependency symlinks belong.
set -u
normal_dir="${1:-/tmp}"
unit="/usr/lib/systemd/system/kata-extension-mount@.service"
target="kata-containers.target"
wants_dir="${normal_dir}/${target}.wants"
# /proc/cmdline is a single line; read it defensively so a missing /proc does
# not abort the generator (which would only delay the boot, never fix it).
read -r cmdline < /proc/cmdline || exit 0
for param in ${cmdline}; do
# An extension may carry verity params ("...verity_params=<...>") or none
# at all -- an unmeasured extension (e.g. on s390x) renders as a bare
# "...verity_params" with no value. Match both forms so the extension is
# activated either way; the mount helper decides verity vs. raw from the
# parameter value.
case "${param}" in
kata.extension.*.verity_params=*)
name="${param#kata.extension.}"
name="${name%%.verity_params=*}"
;;
kata.extension.*.verity_params)
name="${param#kata.extension.}"
name="${name%.verity_params}"
;;
*)
continue
;;
esac
# Only accept names safe to use as a systemd instance and as a filename.
# A stray space or '=' would mean a malformed cmdline entry; skip it rather
# than create a bogus unit instance.
[[ "${name}" =~ ^[a-zA-Z0-9_-]+$ ]] || continue
mkdir -p "${wants_dir}"
ln -sf "${unit}" "${wants_dir}/kata-extension-mount@${name}.service"
done
exit 0

141
src/agent/kata-extension-mount.sh Executable file
View File

@@ -0,0 +1,141 @@
#!/bin/bash
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Mount a kata extension block device with optional dm-verity verification.
# Usage: kata-extension-mount.sh <extension-name>
#
# The block device is discovered by scanning /sys/block/*/serial for
# a device whose serial matches "extension-<name>".
# Verity params are read from kernel cmdline: kata.extension.<name>.verity_params=...
# The extension is mounted read-only (erofs) at /run/kata-extensions/<name>/.
set -euo pipefail
EXTENSION_NAME="${1:?extension name required}"
SERIAL="extension-${EXTENSION_NAME}"
MOUNT_DIR="/run/kata-extensions/${EXTENSION_NAME}"
find_block_dev_by_serial() {
local wanted="$1"
for s in /sys/block/*/serial; do
[[ -f "${s}" ]] || continue
local cur
cur="$(cat "${s}" 2>/dev/null)" || continue
if [[ "${cur}" == "${wanted}" ]]; then
echo "/dev/$(basename "$(dirname "${s}")")"
return 0
fi
done
return 1
}
REAL_DEV="$(find_block_dev_by_serial "${SERIAL}")" || {
echo "ERROR: no block device with serial ${SERIAL} found" >&2
exit 1
}
get_verity_param() {
local key="kata.extension.${EXTENSION_NAME}.verity_params"
local cmdline
cmdline="$(cat /proc/cmdline)"
local value=""
for param in ${cmdline}; do
case "${param}" in
"${key}="*)
value="${param#"${key}="}"
;;
esac
done
echo "${value}"
}
parse_verity_field() {
local params="$1"
local field="$2"
# Iterate without a pipeline: a `| while` loop runs in a subshell, so a
# `return` there would only exit the subshell, never this function.
local IFS=','
for pair in ${params}; do
local k="${pair%%=*}"
local v="${pair#*=}"
if [[ "${k}" == "${field}" ]]; then
echo "${v}"
return
fi
done
}
VERITY_PARAMS="$(get_verity_param)"
PART_SEP=""
[[ "${REAL_DEV}" =~ [0-9]$ ]] && PART_SEP="p"
DATA_DEV="${REAL_DEV}${PART_SEP}1"
HASH_DEV="${REAL_DEV}${PART_SEP}2"
# The image build encodes its integrity policy in the on-disk layout, and that
# layout -- not the kernel command line -- is the source of truth for whether
# this extension must be verified: a measured extension (MEASURED_ROOTFS=yes)
# is built with a dm-verity hash partition (p2) next to the data partition (p1);
# an unmeasured extension (MEASURED_ROOTFS=no, e.g. on s390x, where Secure
# Execution protects the guest through a different mechanism) has only p1.
#
# We cross-check that layout against the verity params on the kernel command
# line (which, in a confidential guest, is part of the measured/attested boot)
# so we fail closed instead of silently downgrading a measured extension to an
# unverified mount:
#
# hash device + params -> verify (normal measured extension)
# hash device + NO params -> refuse: verity was stripped/disabled (tamper)
# NO hash device + params -> refuse: params but nothing to verify (mismatch)
# NO hash device + NO params -> raw mount (genuinely unmeasured extension)
#
# See "Integrity policy: measured vs. unmeasured, and failing closed" in
# docs/design/proposals/composable-vm-images.md for the rationale.
if [[ -b "${HASH_DEV}" ]]; then
if [[ -z "${VERITY_PARAMS}" ]]; then
echo "ERROR: extension ${EXTENSION_NAME} ships a dm-verity hash device but no verity params were provided on the kernel command line; refusing to mount it unverified" >&2
exit 1
fi
ROOT_HASH="$(parse_verity_field "${VERITY_PARAMS}" "root_hash")"
SALT="$(parse_verity_field "${VERITY_PARAMS}" "salt")"
DATA_BLOCKS="$(parse_verity_field "${VERITY_PARAMS}" "data_blocks")"
HASH_BLOCK_SIZE="$(parse_verity_field "${VERITY_PARAMS}" "hash_block_size")"
DATA_BLOCK_SIZE="$(parse_verity_field "${VERITY_PARAMS}" "data_block_size")"
if [[ -z "${ROOT_HASH}" ]]; then
echo "ERROR: extension ${EXTENSION_NAME} verity params carry no root_hash; refusing to mount" >&2
exit 1
fi
DM_NAME="extension-${EXTENSION_NAME}"
veritysetup open "${DATA_DEV}" "${DM_NAME}" "${HASH_DEV}" "${ROOT_HASH}" \
--no-superblock \
--hash-block-size="${HASH_BLOCK_SIZE:-4096}" \
--data-block-size="${DATA_BLOCK_SIZE:-4096}" \
--data-blocks="${DATA_BLOCKS}" \
--salt="${SALT}"
MOUNT_SRC="/dev/mapper/${DM_NAME}"
else
if [[ -n "${VERITY_PARAMS}" ]]; then
echo "ERROR: extension ${EXTENSION_NAME} has verity params on the kernel command line but no dm-verity hash device; refusing to mount" >&2
exit 1
fi
if [[ -b "${DATA_DEV}" ]]; then
MOUNT_SRC="${DATA_DEV}"
else
MOUNT_SRC="${REAL_DEV}"
fi
fi
mkdir -p "${MOUNT_DIR}"
mount -t erofs -o ro "${MOUNT_SRC}" "${MOUNT_DIR}"
echo "Extension ${EXTENSION_NAME} mounted at ${MOUNT_DIR}"

View File

@@ -0,0 +1,22 @@
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
[Unit]
Description=Mount Kata extension image %i
DefaultDependencies=no
Before=kata-agent.service
After=local-fs-pre.target
ConditionKernelCommandLine=kata.extension.%i.verity_params
OnFailure=poweroff.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/libexec/kata-extension-mount.sh %i
ExecStop=/usr/libexec/kata-extension-umount.sh %i
[Install]
WantedBy=kata-containers.target

View File

@@ -0,0 +1,30 @@
#!/bin/bash
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Unmount a kata extension block device.
# Usage: kata-extension-umount.sh <extension-name>
set -euo pipefail
EXTENSION_NAME="${1:?extension name required}"
MOUNT_DIR="/run/kata-extensions/${EXTENSION_NAME}"
DM_NAME="extension-${EXTENSION_NAME}"
# The extension is consumed in place (the agent reads binaries/config straight from
# ${MOUNT_DIR} via the component manifest), so there are no bind mounts to undo
# here -- we only need to unmount the extension filesystem and close dm-verity.
# Unmount the extension filesystem
if mountpoint -q "${MOUNT_DIR}" 2>/dev/null; then
umount "${MOUNT_DIR}"
fi
# Close dm-verity device if present
if [[ -e "/dev/mapper/${DM_NAME}" ]]; then
veritysetup close "${DM_NAME}" 2>/dev/null || true
fi
echo "Extension ${EXTENSION_NAME} unmounted"