[RFC PATCH v1 17/42] vbs: Add module authentication via VBS/HEKI

From: Sriram Nambakam

Date: Wed Aug 05 2026 - 07:14:51 EST


Hook the kernel module loader to send module validation requests to the
secure kernel (plane-1 / QEMU) before allowing modules to load, and to
set per-section EPT permissions after module formation.

kernel/module/main.c:
- After add_unformed_module(): call vbs_validate_module() with the
module ELF blob GPA and the kernel's own sig_ok result from
module_sig_check(). If the secure side rejects, loading is aborted.
- After complete_formation(): call vbs_set_module_perms() to apply
EPT permissions per section (text=R+X, rodata=R, data=R+W).
Failure is non-fatal to avoid breaking module loading on ioctl
errors.
- In free_module(): call vbs_unload_module() so the secure side can
release EPT overrides for the freed module.
- All hooks are guarded by vbs_available() and are no-ops when VBS
is not active.

security/vbs/heki.h:
- Add vbs_validate_module_req with module name, ELF GPA/size, and
sig_ok flag (kernel's signature verification result).
- Add vbs_module_section and vbs_set_module_perms_req for per-section
GPA + permissions.
- Add vbs_unload_module_req for module unload notification.

security/vbs/kvm_planes.c:
- Implement kvm_planes_validate_module(): converts vmalloc ELF
pointer to GPA, sends sig_ok flag via VBS_CALL_VALIDATE_MODULE.
- Implement kvm_planes_set_module_perms(): iterates mod->mem[]
array, maps each section type to VBS_MEM_* permissions (TEXT→R+X,
RODATA→R, DATA→R+W), sends via VBS_CALL_SET_MODULE_PERMS.
- Implement kvm_planes_unload_module(): sends module name via
VBS_CALL_UNLOAD_MODULE.

Signed-off-by: Sriram Nambakam <snambakam@xxxxxxxxxxxxxxxxxxx>
---
kernel/module/main.c | 37 ++++++++++++++
security/vbs/heki.h | 46 +++++++++++++++++
security/vbs/kvm_planes.c | 102 +++++++++++++++++++++++++++++++++++---
3 files changed, 178 insertions(+), 7 deletions(-)

diff --git a/kernel/module/main.c b/kernel/module/main.c
index 46dd8d25a605..2d0232fccf18 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -39,6 +39,7 @@
#include <linux/string.h>
#include <linux/mutex.h>
#include <linux/rculist.h>
+#include <linux/vbs.h>
#include <linux/uaccess.h>
#include <asm/cacheflush.h>
#include <linux/set_memory.h>
@@ -1418,6 +1419,11 @@ static void free_module(struct module *mod)
{
trace_module_free(mod);

+ /* Notify the secure kernel that this module is being unloaded
+ * so it can release any EPT permission overrides. */
+ if (vbs_available())
+ vbs_unload_module(mod);
+
codetag_unload_module(mod);

mod_sysfs_teardown(mod);
@@ -3472,6 +3478,22 @@ static int load_module(struct load_info *info, const char __user *uargs,
if (err)
goto free_module;

+ /*
+ * If VBS is available, ask the secure kernel (plane-1) to
+ * validate this module. We pass the module name and the
+ * sig_ok flag from the kernel's own signature check.
+ * Plane-1 can enforce additional policy (e.g., allowlist).
+ */
+ if (vbs_available()) {
+ err = vbs_validate_module(info->hdr, info->len,
+ NULL, info->sig_ok ? 1 : 0);
+ if (err) {
+ pr_warn("vbs: module '%s' rejected by secure kernel (%ld)\n",
+ mod->name, err);
+ goto unlink_mod;
+ }
+ }
+
/*
* We are tainting your kernel if your module gets into
* the modules linked list somehow.
@@ -3539,6 +3561,21 @@ static int load_module(struct load_info *info, const char __user *uargs,
if (err)
goto ddebug_cleanup;

+ /*
+ * If VBS is available, send the module's per-section layout
+ * to the secure kernel so it can enforce EPT permissions:
+ * text → R+X (NO_WRITE), rodata → R (NO_WRITE|NO_EXEC),
+ * data → R+W (no restrictions).
+ */
+ if (vbs_available()) {
+ err = vbs_set_module_perms(mod);
+ if (err)
+ pr_warn("vbs: set_module_perms for %s failed (%ld)\n",
+ mod->name, err);
+ /* Non-fatal: continue loading even if protection fails */
+ err = 0;
+ }
+
err = prepare_coming_module(mod);
if (err)
goto bug_cleanup;
diff --git a/security/vbs/heki.h b/security/vbs/heki.h
index fee986de351a..5b7fa92bce21 100644
--- a/security/vbs/heki.h
+++ b/security/vbs/heki.h
@@ -36,6 +36,52 @@ struct vbs_seal_kernel_req {
__u64 cr3; /* plane-0 kernel CR3 for verification */
} __packed;

+/* ── Module authentication ────────────────────────────────────────────── */
+
+/*
+ * VBS_CALL_VALIDATE_MODULE payload — plane-0 sends the GPA of the module
+ * ELF blob and its appended PKCS#7 signature for plane-1 verification.
+ * The module blob is in guest physical memory; the secure side reads it
+ * directly via the GPA (no copy through the CAA page).
+ */
+struct vbs_validate_module_req {
+ char name[56]; /* module name (null-terminated) */
+ __u64 elf_gpa; /* GPA of the module ELF data */
+ __u64 elf_size; /* size of the ELF data (excl. signature) */
+ __u32 sig_ok; /* 1 if kernel's sig check passed */
+ __u32 reserved; /* padding */
+} __packed;
+
+/*
+ * Per-section descriptor for VBS_CALL_SET_MODULE_PERMS.
+ * Sent as an array in the CAA buffer after the module name.
+ */
+struct vbs_module_section {
+ __u64 gpa; /* section GPA (page-aligned) */
+ __u64 size; /* section size (page-aligned) */
+ __u32 perms; /* VBS_MEM_* permission flags */
+ __u32 type; /* enum mod_mem_type */
+} __packed;
+
+/*
+ * VBS_CALL_SET_MODULE_PERMS payload — after relocation, plane-0 sends
+ * the per-section layout so plane-1 can set EPT permissions.
+ * Sections follow immediately after this header in the buffer.
+ */
+struct vbs_set_module_perms_req {
+ char name[56]; /* module name (null-terminated) */
+ __u32 nr_sections; /* number of vbs_module_section entries */
+ __u32 flags; /* reserved, must be 0 */
+ /* struct vbs_module_section sections[]; follows in buffer */
+} __packed;
+
+/*
+ * VBS_CALL_UNLOAD_MODULE payload — module is being freed.
+ */
+struct vbs_unload_module_req {
+ char name[56]; /* module name (null-terminated) */
+} __packed;
+
/* ── x86-64 page table walker (for plane-1 auditing) ─────────────────── */

/* Classification of a guest-physical page based on page table walk */
diff --git a/security/vbs/kvm_planes.c b/security/vbs/kvm_planes.c
index 293c960c0968..1114adfbd46c 100644
--- a/security/vbs/kvm_planes.c
+++ b/security/vbs/kvm_planes.c
@@ -23,6 +23,9 @@
#include <linux/mm.h>
#include <linux/io.h>
#include <linux/kvm_para.h>
+#include <linux/module.h>
+#include <linux/string.h>
+#include <linux/elf.h>
#include <asm/sections.h>
#include <asm/kvm_para.h>
#include <asm/processor.h>
@@ -147,26 +150,111 @@ static int kvm_planes_seal_kernel(void)
static int kvm_planes_validate_module(const void *elf, size_t elf_size,
const void *sig, size_t sig_size)
{
+ struct vbs_validate_module_req req = {};
+ struct page *elf_page;
+ const Elf64_Ehdr *ehdr;
+
+ if (!elf || !elf_size)
+ return -EINVAL;
+
/*
- * Module blobs can be large — for the KVM planes backend we pass
- * the physical address and size to plane-1 via the VTL call and
- * let plane-1 map/read the pages directly from its EPT view.
- * For now, a stub that signals "not yet implemented".
+ * sig_size is repurposed: 1 = kernel's own sig check passed,
+ * 0 = module is unsigned or sig check failed.
*/
+ req.sig_ok = sig_size ? 1 : 0;
+
+ /* Try to extract the module name from the ELF .modinfo section.
+ * For now, just use a placeholder — the name is available at
+ * the call site in load_module() but not passed through the
+ * vbs_ops interface which takes (elf, elf_size, sig, sig_size).
+ */
+ ehdr = elf;
+ if (elf_size >= sizeof(*ehdr) && ehdr->e_ident[0] == 0x7f)
+ strscpy(req.name, "module", sizeof(req.name));
+ else
+ strscpy(req.name, "unknown", sizeof(req.name));
+
+ /* Get GPA of the ELF blob */
+ elf_page = vmalloc_to_page(elf);
+ if (elf_page) {
+ req.elf_gpa = page_to_phys(elf_page) +
+ offset_in_page(elf);
+ req.elf_size = elf_size;
+ }
+
+ pr_debug("vbs-kvm: validate_module elf_gpa=0x%llx size=0x%llx sig_ok=%u\n",
+ req.elf_gpa, req.elf_size, req.sig_ok);
+
return kvm_planes_vtl_call(VBS_CALL_VALIDATE_MODULE,
- NULL, 0, NULL, 0);
+ &req, sizeof(req), NULL, 0);
}

static int kvm_planes_set_module_perms(const struct module *mod)
{
+ struct {
+ struct vbs_set_module_perms_req hdr;
+ struct vbs_module_section sections[MOD_MEM_NUM_TYPES];
+ } __packed req = {};
+ int i, n = 0;
+
+ strscpy(req.hdr.name, mod->name, sizeof(req.hdr.name));
+
+ for (i = 0; i < MOD_MEM_NUM_TYPES; i++) {
+ const struct module_memory *mem = &mod->mem[i];
+ struct vbs_module_section *sec;
+ unsigned long gpa;
+ struct page *p;
+
+ if (!mem->base || !mem->size)
+ continue;
+
+ p = vmalloc_to_page(mem->base);
+ if (!p)
+ continue;
+
+ gpa = page_to_phys(p) + offset_in_page(mem->base);
+ sec = &req.sections[n];
+ sec->gpa = gpa;
+ sec->size = PAGE_ALIGN(mem->size);
+ sec->type = i;
+
+ /* Set permissions based on section type */
+ switch (i) {
+ case MOD_TEXT:
+ case MOD_INIT_TEXT:
+ sec->perms = VBS_MEM_READ | VBS_MEM_EXEC;
+ break;
+ case MOD_RODATA:
+ case MOD_RO_AFTER_INIT:
+ case MOD_INIT_RODATA:
+ sec->perms = VBS_MEM_READ;
+ break;
+ default: /* MOD_DATA, MOD_INIT_DATA */
+ sec->perms = VBS_MEM_READ | VBS_MEM_WRITE;
+ break;
+ }
+ n++;
+ }
+
+ req.hdr.nr_sections = n;
+
+ pr_debug("vbs-kvm: set_module_perms %s: %d sections\n",
+ mod->name, n);
+
return kvm_planes_vtl_call(VBS_CALL_SET_MODULE_PERMS,
- NULL, 0, NULL, 0);
+ &req,
+ sizeof(req.hdr) + n * sizeof(req.sections[0]),
+ NULL, 0);
}

static int kvm_planes_unload_module(const struct module *mod)
{
+ struct vbs_unload_module_req req = {};
+
+ strscpy(req.name, mod->name, sizeof(req.name));
+
return kvm_planes_vtl_call(VBS_CALL_UNLOAD_MODULE,
- NULL, 0, NULL, 0);
+ &req, sizeof(req), NULL, 0);
}

/* ── key / certificate management ─────────────────────────────────────── */
--
2.55.0