From e85a19a8a660a57fa7c7fab5e96e4fcfa9fcc799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabiano=20Fid=C3=AAncio?= Date: Sun, 10 May 2026 19:03:37 +0200 Subject: [PATCH] agent: add systemd template unit for extension image mount/unmount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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-) - Reads verity parameters from kata.extension..verity_params on the kernel command line - Creates a dm-verity device and mounts the EROFS filesystem read-only at /run/kata-extensions// 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..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 Assisted-by: Cursor --- src/agent/Makefile | 13 ++ src/agent/kata-extension-mount-generator.sh | 59 ++++++++ src/agent/kata-extension-mount.sh | 141 ++++++++++++++++++++ src/agent/kata-extension-mount@.service | 22 +++ src/agent/kata-extension-umount.sh | 30 +++++ 5 files changed, 265 insertions(+) create mode 100755 src/agent/kata-extension-mount-generator.sh create mode 100755 src/agent/kata-extension-mount.sh create mode 100644 src/agent/kata-extension-mount@.service create mode 100755 src/agent/kata-extension-umount.sh diff --git a/src/agent/Makefile b/src/agent/Makefile index 6f79ab69e2..f35a46e5c2 100644 --- a/src/agent/Makefile +++ b/src/agent/Makefile @@ -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: diff --git a/src/agent/kata-extension-mount-generator.sh b/src/agent/kata-extension-mount-generator.sh new file mode 100755 index 0000000000..37ed54b260 --- /dev/null +++ b/src/agent/kata-extension-mount-generator.sh @@ -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..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 diff --git a/src/agent/kata-extension-mount.sh b/src/agent/kata-extension-mount.sh new file mode 100755 index 0000000000..1292af3f6a --- /dev/null +++ b/src/agent/kata-extension-mount.sh @@ -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 +# +# The block device is discovered by scanning /sys/block/*/serial for +# a device whose serial matches "extension-". +# Verity params are read from kernel cmdline: kata.extension..verity_params=... +# The extension is mounted read-only (erofs) at /run/kata-extensions//. + +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}" diff --git a/src/agent/kata-extension-mount@.service b/src/agent/kata-extension-mount@.service new file mode 100644 index 0000000000..ff3c649b3f --- /dev/null +++ b/src/agent/kata-extension-mount@.service @@ -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 diff --git a/src/agent/kata-extension-umount.sh b/src/agent/kata-extension-umount.sh new file mode 100755 index 0000000000..6948c9fb2f --- /dev/null +++ b/src/agent/kata-extension-umount.sh @@ -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 + +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"