From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from linux.microsoft.com (linux.microsoft.com [13.77.154.182]) by smtp.subspace.kernel.org (Postfix) with ESMTP id E9E42430CE4; Wed, 5 Aug 2026 11:03:57 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=13.77.154.182 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785927840; cv=none; b=HiFsdx9TS1gTcRLsCKOFuv9AvUBY8Fi49pg3RmtVWxtKhq9FfIit6gCFAYQeqAr1Lset+AMip5hJbZ15Z152P4yMIHnw6UP9S3nbK1FHSvavXt0dULJMfkLczGN470p/PTepqE0AgQKtl8QBtSKFDAbpBbJbCR2+/8ezN4vpJms= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785927840; c=relaxed/simple; bh=UmUWVtSuVwvlzujYbsuuA85G+zuIQuxu2AqHP9g/B4E=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=kwwvCON8VFHI+nyb+SHLH7WL0j8Zt1JjdQep0NsyQJOykLjc5vFeDlPpPrSRX7RWBoCcbJymrKBR/CXwJXybon1BXQHJmBehsQHAW47PprMwl0ly979uf5wBuGi1971Lo9jCtgUQhqybw3iX+InU53gnKnyeAMB+VZ62XTDx+9Q= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.microsoft.com; spf=pass smtp.mailfrom=linux.microsoft.com; dkim=pass (1024-bit key) header.d=linux.microsoft.com header.i=@linux.microsoft.com header.b=LQG66M5N; arc=none smtp.client-ip=13.77.154.182 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.microsoft.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.microsoft.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.microsoft.com header.i=@linux.microsoft.com header.b="LQG66M5N" Received: from fedora.hsd1.wa.comcast.net (unknown [52.148.140.42]) by linux.microsoft.com (Postfix) with ESMTPSA id 701D020B716D; Wed, 5 Aug 2026 04:03:36 -0700 (PDT) DKIM-Filter: OpenDKIM Filter v2.11.0 linux.microsoft.com 701D020B716D DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.microsoft.com; s=default; t=1785927816; bh=qS0R4suTYYgDZhfRy1IzPe8WLVp7bZFCgZd3OyOa0lw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=LQG66M5Nzg/62vEtn9XJy+wAxSsB7Ds2It5CipJ51S1UapXxoYY1OnGdwlVvkH6Vl TMRcBlBlWNiOtiQwOQ7inOE12vxeHpzA0zoBzMI3LlNlCDXUjprfiXO4EMHK3P+aTj 3xA7WBfAkzuu1gCChGCwqUTVzZx8gkRN/THW+aOI= From: Sriram Nambakam To: kvm@vger.kernel.org Cc: linux-kernel@vger.kernel.org Subject: [RFC PATCH v1 16/42] vbs: Add HEKI kernel sealing and fix KVM plane memory attribute guards Date: Wed, 5 Aug 2026 04:02:58 -0700 Message-ID: <20260805110324.25067-17-snambakam@linux.microsoft.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260805110324.25067-1-snambakam@linux.microsoft.com> References: <20260805110324.25067-1-snambakam@linux.microsoft.com> Precedence: bulk X-Mailing-List: kvm@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 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 +#include +#include + +#ifdef CONFIG_X86_64 +#include + +/* ── 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 + +/* + * 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 #include #include +#include #include +#include + +#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