[RFC PATCH v1 16/42] vbs: Add HEKI kernel sealing and fix KVM plane memory attribute guards

Sriram Nambakam <[email protected]> Wed, 5 Aug 2026 04:02:58 -0700
Newsgroups org.kernel.vger.kvm,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Implement Hypervisor-Enforced Kernel Integrity (HEKI) — the plane-0
guest kernel automatically seals its text and rodata sections at
late_initcall time by sending their GPAs to QEMU via the VBS VTL
call mechanism.

Guest-side changes:
- security/vbs/core.c: Add vbs_heki_late_init() as a late_initcall
  that calls ops->init() to set up the VBS backend (allocate the
  shared CAA page, send VBS_CALL_INIT), then calls vbs_seal_kernel()
  to request kernel text/rodata protection.

- security/vbs/kvm_planes.c: Implement kvm_planes_seal_kernel() to
  build a vbs_seal_kernel_req with page-aligned text/rodata GPAs
  and CR3, sent via VBS_CALL_SEAL_KERNEL to QEMU.

- security/vbs/heki.h (new): Shared HEKI data structures
  (vbs_seal_kernel_req, vbs_protect_memory_req) and x86-64 page
  table walker callback interface.

- security/vbs/heki.c (new): x86-64 4-level page table walker
  for plane-1 auditing of plane-0 mappings. Classifies pages as
  TEXT/RODATA/DATA_RW/DATA_RX based on PTE permission bits.

- security/vbs/Kconfig: Add CONFIG_VBS_HEKI option.
- security/vbs/Makefile: Build heki.o when CONFIG_VBS_HEKI=y.

Host-side fix:
- virt/kvm/kvm_main.c: Replace CONFIG_KVM_MAX_NR_VCPU_PLANES
  (which had no Kconfig definition and was never set) with
  CONFIG_VM_PLANES in the three #ifdef guards protecting
  KVM_SET_PLANE_MEMORY_ATTRIBUTES ioctl and NO_WRITE/NO_EXEC
  attribute support. Without this fix the ioctl returned -ENOTTY.

Signed-off-by: Sriram Nambakam <[email protected]>
---
 security/vbs/Kconfig      |  14 ++
 security/vbs/Makefile     |   1 +
 security/vbs/core.c       |  32 +++++
 security/vbs/heki.c       | 287 ++++++++++++++++++++++++++++++++++++++
 security/vbs/heki.h       |  83 +++++++++++
 security/vbs/kvm_planes.c |  19 ++-
 6 files changed, 435 insertions(+), 1 deletion(-)
 create mode 100644 security/vbs/heki.c
 create mode 100644 security/vbs/heki.h

diff --git a/security/vbs/Kconfig b/security/vbs/Kconfig
index 3d9fb104b1fc..0fdbfbc7a795 100644
--- a/security/vbs/Kconfig
+++ b/security/vbs/Kconfig
@@ -15,6 +15,20 @@ config VBS
 
 	  If unsure, say N.
 
+config VBS_HEKI
+	bool "HEKI: Hypervisor-Enforced Kernel Integrity"
+	depends on VBS && X86_64
+	help
+	  Enable the HEKI subsystem which provides:
+	  - x86-64 page table walker for auditing guest kernel mappings
+	  - Kernel seal support (make kernel text/rodata immutable via
+	    EPT permission enforcement)
+
+	  This code runs in plane-1 (secure kernel) to inspect and
+	  protect plane-0's address space.
+
+	  If unsure, say N.
+
 config VBS_KVM_PLANES
 	bool "VBS backend: KVM software planes"
 	depends on VBS && KVM_GUEST
diff --git a/security/vbs/Makefile b/security/vbs/Makefile
index 4f0f26ef4f71..e33052ccde2d 100644
--- a/security/vbs/Makefile
+++ b/security/vbs/Makefile
@@ -2,6 +2,7 @@
 obj-$(CONFIG_VBS) += vbs.o
 vbs-y := core.o probe.o
 
+vbs-$(CONFIG_VBS_HEKI)		+= heki.o
 obj-$(CONFIG_VBS_KVM_PLANES)	+= kvm_planes.o
 obj-$(CONFIG_VBS_SEV_SNP)	+= sev_snp.o
 obj-$(CONFIG_VBS_TDX)		+= tdx.o
diff --git a/security/vbs/core.c b/security/vbs/core.c
index 352590d88136..16b5329964f9 100644
--- a/security/vbs/core.c
+++ b/security/vbs/core.c
@@ -164,3 +164,35 @@ int vbs_kexec_invalidate(void)
 	return ops->kexec_invalidate();
 }
 EXPORT_SYMBOL_GPL(vbs_kexec_invalidate);
+
+/* ── HEKI: automatic kernel sealing at late init ──────────────────────── */
+
+static int __init vbs_heki_late_init(void)
+{
+	const struct vbs_ops *ops = READ_ONCE(vbs_backend);
+	int ret;
+
+	if (!ops) {
+		pr_debug("vbs: HEKI: no backend, skipping kernel seal\n");
+		return 0;
+	}
+
+	/* Initialize the backend (allocates shared memory, etc.) */
+	if (ops->init) {
+		ret = ops->init();
+		if (ret) {
+			pr_warn("vbs: HEKI: backend init failed (%d)\n", ret);
+			return 0;
+		}
+	}
+
+	pr_info("vbs: HEKI: sealing kernel text and rodata\n");
+	ret = vbs_seal_kernel();
+	if (ret)
+		pr_warn("vbs: HEKI: seal_kernel failed (%d)\n", ret);
+	else
+		pr_info("vbs: HEKI: kernel sealed successfully\n");
+
+	return 0;
+}
+late_initcall(vbs_heki_late_init);
diff --git a/security/vbs/heki.c b/security/vbs/heki.c
new file mode 100644
index 000000000000..8b4c4e3b170b
--- /dev/null
+++ b/security/vbs/heki.c
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * HEKI — Hypervisor-Enforced Kernel Integrity
+ *
+ * x86-64 page table walker and kernel protection logic.
+ *
+ * The page table walker is designed to be called from plane-1 (the secure
+ * kernel) to audit plane-0's page tables.  It is parameterised with a
+ * read_gpa() callback so it can work both in-kernel (for plane-1 with
+ * direct GPA access) and from QEMU (future, for host-side auditing).
+ *
+ * The seal_kernel helper runs in plane-0 and sends the kernel text/rodata
+ * GPA ranges to the secure side via the VBS VTL call mechanism.
+ */
+
+#include "heki.h"
+#include "internal.h"
+
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <asm/sections.h>
+
+#ifdef CONFIG_X86_64
+#include <asm/page.h>
+
+/* ── x86-64 page table constants ──────────────────────────────────────── */
+
+#define PT_ENTRIES		512
+#define PT_ENTRY_SIZE		8
+
+/* PTE bit positions */
+#define PTE_PRESENT		BIT_ULL(0)
+#define PTE_WRITABLE		BIT_ULL(1)
+#define PTE_USER		BIT_ULL(2)
+#define PTE_PS			BIT_ULL(7)	/* page size (huge page) */
+#define PTE_NX			BIT_ULL(63)	/* no-execute */
+
+/* Physical address mask for 4-level paging (bits 12..51) */
+#define PTE_ADDR_MASK		0x000FFFFFFFFFF000ULL
+
+/* Page sizes */
+#define PAGE_SIZE_4K		(1UL << 12)
+#define PAGE_SIZE_2M		(1UL << 21)
+#define PAGE_SIZE_1G		(1UL << 30)
+
+/* Virtual address extraction helpers */
+static inline unsigned int pml4_index(unsigned long va)
+{
+	return (va >> 39) & 0x1FF;
+}
+
+static inline unsigned int pdpt_index(unsigned long va)
+{
+	return (va >> 30) & 0x1FF;
+}
+
+static inline unsigned int pd_index(unsigned long va)
+{
+	return (va >> 21) & 0x1FF;
+}
+
+static inline unsigned int pt_index(unsigned long va)
+{
+	return (va >> 12) & 0x1FF;
+}
+
+/*
+ * Classify a page based on its PTE permission bits.
+ */
+static enum heki_page_class classify_pte(u64 pte)
+{
+	bool writable = !!(pte & PTE_WRITABLE);
+	bool executable = !(pte & PTE_NX);
+
+	if (executable && !writable)
+		return HEKI_PAGE_TEXT;
+	if (!executable && !writable)
+		return HEKI_PAGE_RODATA;
+	if (!executable && writable)
+		return HEKI_PAGE_DATA_RW;
+	/* executable + writable — W^X violation */
+	return HEKI_PAGE_DATA_RX;
+}
+
+/*
+ * Read a single page table entry from guest physical memory.
+ */
+static int read_pte(u64 table_gpa, unsigned int index,
+		    int (*read_gpa)(u64, void *, size_t, void *),
+		    void *ctx, u64 *pte_out)
+{
+	u64 entry_gpa = table_gpa + (u64)index * PT_ENTRY_SIZE;
+
+	return read_gpa(entry_gpa, pte_out, sizeof(*pte_out), ctx);
+}
+
+/*
+ * Walk a page table (PT) level — 4K pages.
+ */
+static int walk_pt(u64 pt_gpa, unsigned long va_base,
+		   int (*read_gpa)(u64, void *, size_t, void *), void *ctx,
+		   unsigned long va_start, unsigned long va_end,
+		   heki_walk_cb cb, void *priv)
+{
+	unsigned int start_idx, end_idx, i;
+	int ret;
+
+	start_idx = (va_start > va_base) ? pt_index(va_start) : 0;
+	end_idx   = (va_end && va_end < va_base + PT_ENTRIES * PAGE_SIZE_4K)
+		    ? pt_index(va_end - 1) : PT_ENTRIES - 1;
+
+	for (i = start_idx; i <= end_idx; i++) {
+		u64 pte;
+		unsigned long va = va_base + (unsigned long)i * PAGE_SIZE_4K;
+
+		ret = read_pte(pt_gpa, i, read_gpa, ctx, &pte);
+		if (ret)
+			return ret;
+		if (!(pte & PTE_PRESENT))
+			continue;
+
+		ret = cb(va, pte & PTE_ADDR_MASK, PAGE_SIZE_4K,
+			 classify_pte(pte), priv);
+		if (ret)
+			return ret;
+	}
+	return 0;
+}
+
+/*
+ * Walk a page directory (PD) level — 2M huge pages or recurse into PT.
+ */
+static int walk_pd(u64 pd_gpa, unsigned long va_base,
+		   int (*read_gpa)(u64, void *, size_t, void *), void *ctx,
+		   unsigned long va_start, unsigned long va_end,
+		   heki_walk_cb cb, void *priv)
+{
+	unsigned int start_idx, end_idx, i;
+	int ret;
+
+	start_idx = (va_start > va_base) ? pd_index(va_start) : 0;
+	end_idx   = (va_end && va_end < va_base + (unsigned long)PT_ENTRIES * PAGE_SIZE_2M)
+		    ? pd_index(va_end - 1) : PT_ENTRIES - 1;
+
+	for (i = start_idx; i <= end_idx; i++) {
+		u64 pde;
+		unsigned long va = va_base + (unsigned long)i * PAGE_SIZE_2M;
+
+		ret = read_pte(pd_gpa, i, read_gpa, ctx, &pde);
+		if (ret)
+			return ret;
+		if (!(pde & PTE_PRESENT))
+			continue;
+
+		if (pde & PTE_PS) {
+			/* 2M huge page */
+			ret = cb(va, pde & PTE_ADDR_MASK, PAGE_SIZE_2M,
+				 classify_pte(pde), priv);
+			if (ret)
+				return ret;
+		} else {
+			ret = walk_pt(pde & PTE_ADDR_MASK, va,
+				      read_gpa, ctx, va_start, va_end,
+				      cb, priv);
+			if (ret)
+				return ret;
+		}
+	}
+	return 0;
+}
+
+/*
+ * Walk a page directory pointer table (PDPT) — 1G huge pages or recurse.
+ */
+static int walk_pdpt(u64 pdpt_gpa, unsigned long va_base,
+		     int (*read_gpa)(u64, void *, size_t, void *), void *ctx,
+		     unsigned long va_start, unsigned long va_end,
+		     heki_walk_cb cb, void *priv)
+{
+	unsigned int start_idx, end_idx, i;
+	int ret;
+
+	start_idx = (va_start > va_base) ? pdpt_index(va_start) : 0;
+	end_idx   = (va_end && va_end < va_base + (unsigned long)PT_ENTRIES * PAGE_SIZE_1G)
+		    ? pdpt_index(va_end - 1) : PT_ENTRIES - 1;
+
+	for (i = start_idx; i <= end_idx; i++) {
+		u64 pdpte;
+		unsigned long va = va_base + (unsigned long)i * PAGE_SIZE_1G;
+
+		ret = read_pte(pdpt_gpa, i, read_gpa, ctx, &pdpte);
+		if (ret)
+			return ret;
+		if (!(pdpte & PTE_PRESENT))
+			continue;
+
+		if (pdpte & PTE_PS) {
+			/* 1G huge page */
+			ret = cb(va, pdpte & PTE_ADDR_MASK, PAGE_SIZE_1G,
+				 classify_pte(pdpte), priv);
+			if (ret)
+				return ret;
+		} else {
+			ret = walk_pd(pdpte & PTE_ADDR_MASK, va,
+				      read_gpa, ctx, va_start, va_end,
+				      cb, priv);
+			if (ret)
+				return ret;
+		}
+	}
+	return 0;
+}
+
+/**
+ * heki_walk_x86_tables - walk x86-64 4-level page tables
+ * @cr3:        value of CR3 (page table root physical address)
+ * @read_gpa:   callback to read bytes from a guest physical address
+ * @read_ctx:   opaque context passed to read_gpa
+ * @va_start:   start of virtual address range (0 = from beginning)
+ * @va_end:     end of virtual address range (0 = to end)
+ * @cb:         callback invoked for each present page
+ * @priv:       opaque context passed to cb
+ *
+ * Walks the full PML4 → PDPT → PD → PT hierarchy, invoking @cb for
+ * every present page (4K, 2M, or 1G) within [va_start, va_end).
+ *
+ * Returns 0 on success, or the first non-zero return from @cb / @read_gpa.
+ */
+int heki_walk_x86_tables(unsigned long cr3,
+			 int (*read_gpa)(u64 gpa, void *buf, size_t len,
+					 void *ctx),
+			 void *read_ctx,
+			 unsigned long va_start, unsigned long va_end,
+			 heki_walk_cb cb, void *priv)
+{
+	u64 pml4_gpa = cr3 & PTE_ADDR_MASK;
+	unsigned int i;
+	int ret;
+
+	if (!read_gpa || !cb)
+		return -EINVAL;
+
+	/*
+	 * Walk PML4 entries.  Each PML4 entry covers 512 GB.
+	 * For the kernel half of the address space on x86-64,
+	 * entries 256..511 map the kernel virtual addresses
+	 * (0xffff800000000000 and above).
+	 */
+	for (i = 0; i < PT_ENTRIES; i++) {
+		u64 pml4e;
+		/* Each PML4 entry covers 512 GiB */
+		unsigned long va_base = (unsigned long)i << 39;
+
+		/*
+		 * Sign-extend for canonical addresses: entries 256..511
+		 * map the upper half (kernel space).
+		 */
+		if (i >= 256)
+			va_base |= 0xFFFF000000000000UL;
+
+		/* Skip entries outside the requested range */
+		if (va_end && va_base >= va_end)
+			break;
+		if (va_start) {
+			unsigned long entry_end = va_base +
+				(1UL << 39) - 1;
+			if (entry_end < va_start)
+				continue;
+		}
+
+		ret = read_pte(pml4_gpa, i, read_gpa, read_ctx, &pml4e);
+		if (ret)
+			return ret;
+		if (!(pml4e & PTE_PRESENT))
+			continue;
+
+		ret = walk_pdpt(pml4e & PTE_ADDR_MASK, va_base,
+				read_gpa, read_ctx, va_start, va_end,
+				cb, priv);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+#endif /* CONFIG_X86_64 */
diff --git a/security/vbs/heki.h b/security/vbs/heki.h
new file mode 100644
index 000000000000..fee986de351a
--- /dev/null
+++ b/security/vbs/heki.h
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * HEKI — Hypervisor-Enforced Kernel Integrity
+ *
+ * Shared data structures between the guest kernel (plane-0) and the
+ * VBS secure kernel / QEMU dispatcher.  These structs are placed in
+ * the VBS CAA page buffer and must be kept in sync with the QEMU-side
+ * definitions.
+ */
+#ifndef _VBS_HEKI_H
+#define _VBS_HEKI_H
+
+#include <linux/types.h>
+
+/*
+ * VBS_CALL_PROTECT_MEMORY payload — request EPT permission changes on
+ * a contiguous GPA range from the perspective of the calling plane.
+ */
+struct vbs_protect_memory_req {
+	__u64	gpa;		/* guest-physical address (page-aligned)  */
+	__u64	size;		/* region size in bytes (page-aligned)    */
+	__u32	perms;		/* desired permissions: VBS_MEM_* flags   */
+	__u32	flags;		/* reserved, must be 0                   */
+} __packed;
+
+/*
+ * VBS_CALL_SEAL_KERNEL payload — plane-0 sends the GPAs of its kernel
+ * text and rodata sections so that the secure side can make them
+ * immutable (NO_WRITE in the lower plane's EPT).
+ */
+struct vbs_seal_kernel_req {
+	__u64	text_gpa;	/* _stext physical address               */
+	__u64	text_size;	/* _etext - _stext                       */
+	__u64	rodata_gpa;	/* __start_rodata physical address        */
+	__u64	rodata_size;	/* __end_rodata - __start_rodata          */
+	__u64	cr3;		/* plane-0 kernel CR3 for verification    */
+} __packed;
+
+/* ── x86-64 page table walker (for plane-1 auditing) ─────────────────── */
+
+/* Classification of a guest-physical page based on page table walk */
+enum heki_page_class {
+	HEKI_PAGE_UNMAPPED	= 0,
+	HEKI_PAGE_TEXT		= 1,	/* executable, read-only  (kernel text) */
+	HEKI_PAGE_RODATA	= 2,	/* non-executable, read-only            */
+	HEKI_PAGE_DATA_RW	= 3,	/* non-executable, read-write           */
+	HEKI_PAGE_DATA_RX	= 4,	/* executable, read-write (DANGEROUS)   */
+};
+
+/*
+ * Callback invoked for each mapped page during a page table walk.
+ * @va:    virtual address of the page
+ * @pa:    guest-physical address of the page
+ * @size:  page size (4K, 2M, or 1G)
+ * @pclass: classification based on PTE permission bits
+ * @priv:  opaque context from the caller
+ *
+ * Return 0 to continue walking, non-zero to stop.
+ */
+typedef int (*heki_walk_cb)(unsigned long va, unsigned long pa,
+			    unsigned long size, enum heki_page_class pclass,
+			    void *priv);
+
+#ifdef CONFIG_X86_64
+/*
+ * Walk x86-64 4-level page tables starting from @cr3.
+ * @read_gpa: function to read @len bytes from guest physical address @gpa
+ *            into @buf.  Returns 0 on success.
+ * @va_start, @va_end: virtual address range to walk (0 for full walk)
+ * @cb:     callback invoked for each mapped page
+ * @priv:   opaque context passed to the callback
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int heki_walk_x86_tables(unsigned long cr3,
+			 int (*read_gpa)(u64 gpa, void *buf, size_t len,
+					 void *ctx),
+			 void *read_ctx,
+			 unsigned long va_start, unsigned long va_end,
+			 heki_walk_cb cb, void *priv);
+#endif /* CONFIG_X86_64 */
+
+#endif /* _VBS_HEKI_H */
diff --git a/security/vbs/kvm_planes.c b/security/vbs/kvm_planes.c
index 07a004712e9f..293c960c0968 100644
--- a/security/vbs/kvm_planes.c
+++ b/security/vbs/kvm_planes.c
@@ -23,7 +23,11 @@
 #include <linux/mm.h>
 #include <linux/io.h>
 #include <linux/kvm_para.h>
+#include <asm/sections.h>
 #include <asm/kvm_para.h>
+#include <asm/processor.h>
+
+#include "heki.h"
 
 /* ── shared-memory calling area (modelled after the SVSM CAA) ─────── */
 
@@ -122,7 +126,20 @@ static int kvm_planes_protect_memory(unsigned long pfn,
 
 static int kvm_planes_seal_kernel(void)
 {
-	return kvm_planes_vtl_call(VBS_CALL_SEAL_KERNEL, NULL, 0, NULL, 0);
+	struct vbs_seal_kernel_req req = {
+		.text_gpa    = __pa_symbol(_stext),
+		.text_size   = PAGE_ALIGN((u64)(_etext - _stext)),
+		.rodata_gpa  = __pa_symbol(__start_rodata),
+		.rodata_size = PAGE_ALIGN((u64)(__end_rodata - __start_rodata)),
+		.cr3         = read_cr3_pa(),
+	};
+
+	pr_info("vbs-kvm: seal_kernel text=[0x%llx+0x%llx] rodata=[0x%llx+0x%llx] cr3=0x%llx\n",
+		req.text_gpa, req.text_size,
+		req.rodata_gpa, req.rodata_size, req.cr3);
+
+	return kvm_planes_vtl_call(VBS_CALL_SEAL_KERNEL,
+				   &req, sizeof(req), NULL, 0);
 }
 
 /* ── module authentication ────────────────────────────────────────────── */
-- 
2.55.0