runtime: add guest_extension_images cold-plug support for Go runtime

Mirror the Rust runtime's guest_extension_images support in the Go runtime:

- hypervisor.go: add GuestExtensionImage struct and GuestExtensionImages field to
  HypervisorConfig.
- config.go: parse [[hypervisor.qemu.guest_extension_images]] TOML sections
  via toGuestExtensionImages(), validating that every extension has a non-empty
  name and valid dm-verity parameters.
- qemu.go: in buildDevices(), attach each extra image as a virtio-blk
  drive with ID extension-<name>; in kernelParameters(), append
  kata.extension.<name>.verity_params=... for each extra image.

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:09 +02:00
committed by Fabiano Fidêncio
parent e96b6c8b17
commit 968d6e9431
3 changed files with 108 additions and 0 deletions

View File

@@ -177,6 +177,13 @@ type hypervisor struct {
DisableGuestSeLinux bool `toml:"disable_guest_selinux"`
LegacySerial bool `toml:"use_legacy_serial"`
ExtraMonitorSocket govmmQemu.MonitorProtocol `toml:"extra_monitor_socket"`
GuestExtensionImages []guestExtensionImage `toml:"guest_extension_images"`
}
type guestExtensionImage struct {
Name string `toml:"name"`
Path string `toml:"path"`
VerityParams string `toml:"verity_params"`
}
type runtime struct {
@@ -1033,6 +1040,11 @@ func newQemuHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
return vc.HypervisorConfig{}, err
}
guestExtensionImages, err := toGuestExtensionImages(h.GuestExtensionImages)
if err != nil {
return vc.HypervisorConfig{}, err
}
return vc.HypervisorConfig{
HypervisorPath: hypervisor,
HypervisorPathList: h.HypervisorPathList,
@@ -1116,9 +1128,57 @@ func newQemuHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
SnpIdAuth: h.SnpIdAuth,
SnpGuestPolicy: h.SnpGuestPolicy,
MeasurementAlgo: h.GetMeasurementAlgo(),
GuestExtensionImages: guestExtensionImages,
}, nil
}
// isValidExtensionName reports whether name is safe to embed in the virtio-blk
// serial (extension-<name>) and the kata.extension.<name>.verity_params kernel
// parameter. A stray space or '=' would corrupt the kernel command line, so the
// name is restricted to ASCII alphanumerics, '-' and '_'.
func isValidExtensionName(name string) bool {
if name == "" {
return false
}
for _, r := range name {
switch {
case r >= 'a' && r <= 'z':
case r >= 'A' && r <= 'Z':
case r >= '0' && r <= '9':
case r == '-' || r == '_':
default:
return false
}
}
return true
}
func toGuestExtensionImages(imgs []guestExtensionImage) ([]vc.GuestExtensionImage, error) {
if len(imgs) == 0 {
return nil, nil
}
result := make([]vc.GuestExtensionImage, len(imgs))
for i, img := range imgs {
if img.Name == "" {
return nil, fmt.Errorf("guest_extension_images entry %d is missing required 'name' field", i)
}
if !isValidExtensionName(img.Name) {
return nil, fmt.Errorf("guest_extension_images '%s' has an invalid name: only ASCII alphanumerics, '-' and '_' are allowed (the name is used in the virtio-blk serial and in the kata.extension.<name>.verity_params kernel parameter)", img.Name)
}
if strings.TrimSpace(img.VerityParams) != "" {
if _, err := vc.ParseKernelVerityParams(img.VerityParams); err != nil {
return nil, fmt.Errorf("guest_extension_images '%s' has invalid verity_params: %w", img.Name, err)
}
}
result[i] = vc.GuestExtensionImage{
Name: img.Name,
Path: img.Path,
VerityParams: img.VerityParams,
}
}
return result, nil
}
func newClhHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
hypervisor, err := h.path()
if err != nil {

View File

@@ -463,6 +463,22 @@ type Param struct {
Value string
}
// GuestExtensionImage represents an additional block device image to attach to the VM
// (e.g. CoCo extension, GPU extension).
type GuestExtensionImage struct {
// Name is a short identifier for this extension (e.g. "coco", "gpu").
// Used as the virtio-blk serial so the guest can discover the device
// via /dev/disk/by-id/virtio-extension-<name> and match verity params
// from kernel cmdline kata.extension.<name>.verity_params=...
Name string
// Path is the host path to the extension image file.
Path string
// VerityParams contains dm-verity parameters for this extension image.
VerityParams string
}
// HypervisorConfig is the hypervisor configuration.
// nolint: govet
type HypervisorConfig struct {
@@ -485,6 +501,9 @@ type HypervisorConfig struct {
// ImagePath and InitrdPath cannot be set at the same time.
InitrdPath string
// GuestExtensionImages lists additional block device images to attach to the VM.
GuestExtensionImages []GuestExtensionImage
// RootfsType is filesystem type of rootfs.
RootfsType string

View File

@@ -214,6 +214,18 @@ func (q *qemu) kernelParameters() string {
// params are added here, they will take priority over the defaults.
params = append(params, q.config.KernelParams...)
// Emit one kata.extension.<name>.verity_params entry per configured
// extension. This doubles as the guest-side activation signal (the systemd
// generator and mount unit key on it), so it is emitted even when
// VerityParams is empty (e.g. an unmeasured extension on s390x); the mount
// helper then mounts the extension off its raw partition.
for _, extra := range q.config.GuestExtensionImages {
params = append(params, Param{
Key: fmt.Sprintf("kata.extension.%s.verity_params", extra.Name),
Value: extra.VerityParams,
})
}
paramsStr := SerializeParams(params, "=")
return strings.Join(paramsStr, " ")
@@ -865,6 +877,23 @@ func (q *qemu) buildDevices(ctx context.Context, kernelPath string) ([]govmmQemu
kernel.InitrdPath = ""
}
for _, extra := range q.config.GuestExtensionImages {
if extra.Path == "" {
continue
}
drive := config.BlockDrive{
File: extra.Path,
Format: "raw",
ID: fmt.Sprintf("extension-%s", extra.Name),
ShareRW: true,
ReadOnly: true,
}
devices, err = q.arch.appendBlockDevice(ctx, devices, drive)
if err != nil {
return nil, nil, nil, err
}
}
if q.config.IOMMU {
devices, err = q.arch.appendIOMMU(devices)
if err != nil {