diff --git a/src/runtime/pkg/katautils/config.go b/src/runtime/pkg/katautils/config.go index 0626409200..22182e81bc 100644 --- a/src/runtime/pkg/katautils/config.go +++ b/src/runtime/pkg/katautils/config.go @@ -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-) and the kata.extension..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..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 { diff --git a/src/runtime/virtcontainers/hypervisor.go b/src/runtime/virtcontainers/hypervisor.go index b2a0d0f0d6..4aa2812df3 100644 --- a/src/runtime/virtcontainers/hypervisor.go +++ b/src/runtime/virtcontainers/hypervisor.go @@ -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- and match verity params + // from kernel cmdline kata.extension..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 diff --git a/src/runtime/virtcontainers/qemu.go b/src/runtime/virtcontainers/qemu.go index cd4ef69224..240ace2c39 100644 --- a/src/runtime/virtcontainers/qemu.go +++ b/src/runtime/virtcontainers/qemu.go @@ -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..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 {