mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Anthony Harivel <aharivel@redhat.com>
To: kvm@vger.kernel.org
Cc: pbonzini@redhat.com, seanjc@google.com,
	linux-kernel@vger.kernel.org,
	Anthony Harivel <aharivel@redhat.com>
Subject: [PATCH RFC v3 3/3] KVM: selftests: add cstate_policy_test for KVM_CAP_CSTATE_POLICY
Date: Mon, 14 Sep 2026 16:37:02 +0200	[thread overview]
Message-ID: <20260914143702.915401-4-aharivel@redhat.com> (raw)
In-Reply-To: <20260914143702.915401-1-aharivel@redhat.com>

Add a KVM selftest that validates the KVM_CAP_CSTATE_POLICY capability:

  1. cap_supported: capability is advertised
  2. valid_policies: max_cstate -1 through 6 accepted
  3. invalid_policies: out-of-range values and bad flags rejected
  4. policy_change: policy can be changed at runtime on a live VM
  5. cpuidle_no_policy: control test — without policy, deep C-states
     (C6) are entered during guest HLT (confirms test methodology)
  6. cpuidle_enforcement: with policy=C1, deep C-states are blocked —
     C6 usage delta is near zero while C1 accumulates
  7. policy_c0_no_idle: with policy=C0, no idle states are entered

Tests 5-7 pin a vCPU to a specific pCPU and compare host cpuidle
usage counters before and after 1000 guest HLT cycles. The guest
uses a periodic APIC timer to wake from HLT.

Tested on Dell R640 (Intel Xeon Gold 5118, intel_idle driver with
POLL/C1/C1E/C6 states). Results:
  - No policy:    C6 entered 1003 times (control)
  - Policy=C1:    C6 entered 0 times, C1 entered 1073 times
  - Policy=C0:    POLL entered 56743 times, no real idle

Signed-off-by: Anthony Harivel <aharivel@redhat.com>
---
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/x86/cstate_policy_test.c    | 517 ++++++++++++++++++
 2 files changed, 518 insertions(+)
 create mode 100644 tools/testing/selftests/kvm/x86/cstate_policy_test.c

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 6fc34e9bf8e1..09e58d6c71e3 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -151,6 +151,7 @@ TEST_GEN_PROGS_x86 += x86/max_vcpuid_cap_test
 TEST_GEN_PROGS_x86 += x86/triple_fault_event_test
 TEST_GEN_PROGS_x86 += x86/recalc_apic_map_test
 TEST_GEN_PROGS_x86 += x86/aperfmperf_test
+TEST_GEN_PROGS_x86 += x86/cstate_policy_test
 TEST_GEN_PROGS_x86 += access_tracking_perf_test
 TEST_GEN_PROGS_x86 += coalesced_io_test
 TEST_GEN_PROGS_x86 += dirty_log_perf_test
diff --git a/tools/testing/selftests/kvm/x86/cstate_policy_test.c b/tools/testing/selftests/kvm/x86/cstate_policy_test.c
new file mode 100644
index 000000000000..cb10d5ade454
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/cstate_policy_test.c
@@ -0,0 +1,517 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Test for KVM_CAP_CSTATE_POLICY
+ *
+ * Copyright (C) 2026, Red Hat, Inc.
+ *
+ * Verify that KVM_CAP_CSTATE_POLICY correctly constrains host cpuidle
+ * state selection when a vCPU halts. The test pins a vCPU to a specific
+ * pCPU, sets a C-state policy, runs a guest that executes HLT in a loop,
+ * and checks host cpuidle usage counters to confirm that deeper C-states
+ * are not entered.
+ *
+ * Requires: isolated pCPU (isolcpus= or cgroup cpuset) for clean signal.
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
+
+#include "kvm_util.h"
+#include "processor.h"
+#include "apic.h"
+#include "kselftest.h"
+
+/*
+ * KVM_CAP_CSTATE_POLICY — defined in include/uapi/linux/kvm.h.
+ * Fallback for building against older headers.
+ */
+#ifndef KVM_CAP_CSTATE_POLICY
+#define KVM_CAP_CSTATE_POLICY	251
+#endif
+
+#define NUM_HALTS		1000
+#define MAX_CPUIDLE_STATES	10
+
+struct cpuidle_snapshot {
+	int		num_states;
+	char		name[MAX_CPUIDLE_STATES][32];
+	int		latency[MAX_CPUIDLE_STATES];
+	unsigned long	usage[MAX_CPUIDLE_STATES];
+	unsigned long long time[MAX_CPUIDLE_STATES];
+};
+
+static int read_sysfs_int(const char *path)
+{
+	FILE *f = fopen(path, "r");
+	int val = -1;
+
+	if (f) {
+		fscanf(f, "%d", &val);
+		fclose(f);
+	}
+	return val;
+}
+
+static unsigned long read_sysfs_ulong(const char *path)
+{
+	FILE *f = fopen(path, "r");
+	unsigned long val = 0;
+
+	if (f) {
+		fscanf(f, "%lu", &val);
+		fclose(f);
+	}
+	return val;
+}
+
+static unsigned long long read_sysfs_ull(const char *path)
+{
+	FILE *f = fopen(path, "r");
+	unsigned long long val = 0;
+
+	if (f) {
+		fscanf(f, "%llu", &val);
+		fclose(f);
+	}
+	return val;
+}
+
+static void read_sysfs_str(const char *path, char *buf, size_t len)
+{
+	FILE *f = fopen(path, "r");
+
+	buf[0] = '\0';
+	if (f) {
+		if (fgets(buf, len, f)) {
+			char *nl = strchr(buf, '\n');
+			if (nl)
+				*nl = '\0';
+		}
+		fclose(f);
+	}
+}
+
+static void snapshot_cpuidle(int cpu, struct cpuidle_snapshot *snap)
+{
+	char path[256];
+	int i;
+
+	snap->num_states = 0;
+	for (i = 0; i < MAX_CPUIDLE_STATES; i++) {
+		snprintf(path, sizeof(path),
+			 "/sys/devices/system/cpu/cpu%d/cpuidle/state%d/name",
+			 cpu, i);
+		if (access(path, R_OK) != 0)
+			break;
+
+		read_sysfs_str(path, snap->name[i], sizeof(snap->name[i]));
+
+		snprintf(path, sizeof(path),
+			 "/sys/devices/system/cpu/cpu%d/cpuidle/state%d/latency",
+			 cpu, i);
+		snap->latency[i] = read_sysfs_int(path);
+
+		snprintf(path, sizeof(path),
+			 "/sys/devices/system/cpu/cpu%d/cpuidle/state%d/usage",
+			 cpu, i);
+		snap->usage[i] = read_sysfs_ulong(path);
+
+		snprintf(path, sizeof(path),
+			 "/sys/devices/system/cpu/cpu%d/cpuidle/state%d/time",
+			 cpu, i);
+		snap->time[i] = read_sysfs_ull(path);
+
+		snap->num_states = i + 1;
+	}
+}
+
+#define TIMER_VECTOR	0x30
+
+static void guest_timer_handler(struct ex_regs *regs)
+{
+	xapic_write_reg(APIC_EOI, 0);
+}
+
+static void guest_hlt_loop(void)
+{
+	int i;
+
+	xapic_enable();
+
+	/* Periodic APIC timer to wake from HLT */
+	xapic_write_reg(APIC_LVTT, APIC_LVT_TIMER_PERIODIC | TIMER_VECTOR);
+	xapic_write_reg(APIC_TDCR, 0x3); /* divide by 16 */
+	xapic_write_reg(APIC_TMICT, 100000);
+
+	for (i = 0; i < NUM_HALTS; i++) {
+		asm volatile("sti; hlt; cli");
+		GUEST_SYNC(i);
+	}
+	GUEST_DONE();
+}
+
+static int enable_cstate_policy(struct kvm_vm *vm, int max_cstate, int flags)
+{
+	struct kvm_enable_cap cap = {
+		.cap = KVM_CAP_CSTATE_POLICY,
+		.args = { max_cstate, flags },
+	};
+
+	return __vm_ioctl(vm, KVM_ENABLE_CAP, &cap);
+}
+
+/*
+ * Test: capability query and basic validation
+ */
+static void test_cap_supported(void)
+{
+	struct kvm_vm *vm;
+	int ret;
+
+	vm = vm_create_barebones();
+
+	ret = vm_check_cap(vm, KVM_CAP_CSTATE_POLICY);
+	TEST_ASSERT(ret > 0, "KVM_CAP_CSTATE_POLICY not supported");
+
+	kvm_vm_free(vm);
+	ksft_test_result_pass("cap_supported\n");
+}
+
+/*
+ * Test: valid policy values accepted
+ */
+static void test_valid_policies(void)
+{
+	struct kvm_vm *vm;
+	int ret, cstate;
+
+	for (cstate = -1; cstate <= 6; cstate++) {
+		vm = vm_create_barebones();
+		ret = enable_cstate_policy(vm, cstate, 0);
+		TEST_ASSERT(ret == 0,
+			    "Setting max_cstate=%d should succeed, got %d",
+			    cstate, ret);
+		kvm_vm_free(vm);
+	}
+
+	ksft_test_result_pass("valid_policies\n");
+}
+
+/*
+ * Test: invalid policy values rejected
+ */
+static void test_invalid_policies(void)
+{
+	struct kvm_vm *vm;
+	int ret;
+
+	/* max_cstate too low */
+	vm = vm_create_barebones();
+	ret = enable_cstate_policy(vm, -2, 0);
+	TEST_ASSERT(ret < 0, "max_cstate=-2 should fail");
+	kvm_vm_free(vm);
+
+	/* max_cstate too high */
+	vm = vm_create_barebones();
+	ret = enable_cstate_policy(vm, 7, 0);
+	TEST_ASSERT(ret < 0, "max_cstate=7 should fail");
+	kvm_vm_free(vm);
+
+	/* non-zero flags */
+	vm = vm_create_barebones();
+	ret = enable_cstate_policy(vm, 1, 1);
+	TEST_ASSERT(ret < 0, "non-zero flags should fail");
+	kvm_vm_free(vm);
+
+	ksft_test_result_pass("invalid_policies\n");
+}
+
+/*
+ * Test: policy can be changed at runtime
+ */
+static void test_policy_change(void)
+{
+	struct kvm_vm *vm;
+	int ret;
+
+	vm = vm_create_barebones();
+
+	ret = enable_cstate_policy(vm, 1, 0);
+	TEST_ASSERT(ret == 0, "Initial policy set failed");
+
+	ret = enable_cstate_policy(vm, 6, 0);
+	TEST_ASSERT(ret == 0, "Policy change to 6 failed");
+
+	ret = enable_cstate_policy(vm, -1, 0);
+	TEST_ASSERT(ret == 0, "Policy change to -1 (disable) failed");
+
+	ret = enable_cstate_policy(vm, 0, 0);
+	TEST_ASSERT(ret == 0, "Policy change to 0 failed");
+
+	kvm_vm_free(vm);
+	ksft_test_result_pass("policy_change\n");
+}
+
+/*
+ * Test: cpuidle enforcement — with policy=C1, deep C-states (C6) should
+ * not accumulate additional usage during guest HLT.
+ *
+ * This test requires running on isolated CPUs for reliable results.
+ * It is marked as SKIP if the signal is too noisy.
+ */
+static void test_cpuidle_enforcement(void)
+{
+	struct cpuidle_snapshot before, after;
+	struct kvm_vcpu *vcpu;
+	struct kvm_vm *vm;
+	struct ucall uc;
+	int cpu, i, deep_state_idx;
+	unsigned long deep_usage_delta;
+	bool has_deep_state = false;
+
+	cpu = pin_self_to_any_cpu();
+
+	snapshot_cpuidle(cpu, &before);
+	TEST_REQUIRE(before.num_states >= 3);
+
+	/*
+	 * Find the deepest state (typically C6). We'll check that its usage
+	 * counter does NOT increase when policy caps at C1.
+	 */
+	deep_state_idx = before.num_states - 1;
+	if (before.latency[deep_state_idx] > 50)
+		has_deep_state = true;
+
+	TEST_REQUIRE(has_deep_state);
+
+	/* Create VM with C-state policy = C1 (state index 1) */
+	vm = vm_create(1);
+
+	i = enable_cstate_policy(vm, 1, 0);
+	TEST_ASSERT(i == 0, "Failed to set C-state policy to C1");
+
+	vcpu = vm_vcpu_add(vm, 0, guest_hlt_loop);
+	vm_install_exception_handler(vm, TIMER_VECTOR, guest_timer_handler);
+	virt_pg_map(vm, APIC_DEFAULT_GPA, APIC_DEFAULT_GPA);
+
+	snapshot_cpuidle(cpu, &before);
+
+	for (i = 0; i < NUM_HALTS; i++) {
+		vcpu_run(vcpu);
+		TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO);
+
+		switch (get_ucall(vcpu, &uc)) {
+		case UCALL_SYNC:
+			break;
+		case UCALL_DONE:
+			goto done;
+		case UCALL_ABORT:
+			REPORT_GUEST_ASSERT(uc);
+		default:
+			TEST_FAIL("Unknown ucall %lu", uc.cmd);
+		}
+	}
+done:
+	snapshot_cpuidle(cpu, &after);
+
+	deep_usage_delta = after.usage[deep_state_idx] -
+			   before.usage[deep_state_idx];
+
+	printf("  CPU %d, policy=C1, %d HLTs\n", cpu, NUM_HALTS);
+	for (i = 0; i < after.num_states; i++) {
+		unsigned long delta = after.usage[i] - before.usage[i];
+
+		printf("    %-6s (lat=%3dus): usage +%lu\n",
+		       after.name[i], after.latency[i], delta);
+	}
+
+	/*
+	 * With policy=C1, the deep C-state (C6, latency >100us) should see
+	 * zero or near-zero additional entries. Allow a small margin for
+	 * host interrupts that might briefly enter idle between our
+	 * measurements.
+	 */
+	TEST_ASSERT(deep_usage_delta < 5,
+		    "Deep C-state '%s' entered %lu times with policy=C1 "
+		    "(expected <5). Policy enforcement may not be working.",
+		    after.name[deep_state_idx], deep_usage_delta);
+
+	kvm_vm_free(vm);
+	ksft_test_result_pass("cpuidle_enforcement\n");
+}
+
+/*
+ * Test: without policy (max_cstate=-1), deep C-states ARE entered during HLT.
+ * This is the control test — confirms the test methodology works.
+ */
+static void test_cpuidle_no_policy(void)
+{
+	struct cpuidle_snapshot before, after;
+	struct kvm_vcpu *vcpu;
+	struct kvm_vm *vm;
+	struct ucall uc;
+	int cpu, i, deep_state_idx;
+	unsigned long deep_usage_delta, total_idle_delta;
+	bool has_deep_state = false;
+
+	cpu = pin_self_to_any_cpu();
+
+	snapshot_cpuidle(cpu, &before);
+	TEST_REQUIRE(before.num_states >= 3);
+
+	deep_state_idx = before.num_states - 1;
+	if (before.latency[deep_state_idx] > 50)
+		has_deep_state = true;
+
+	TEST_REQUIRE(has_deep_state);
+
+	vm = vm_create_with_one_vcpu(&vcpu, guest_hlt_loop);
+	vm_install_exception_handler(vm, TIMER_VECTOR, guest_timer_handler);
+	virt_pg_map(vm, APIC_DEFAULT_GPA, APIC_DEFAULT_GPA);
+
+	snapshot_cpuidle(cpu, &before);
+
+	for (i = 0; i < NUM_HALTS; i++) {
+		vcpu_run(vcpu);
+		TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO);
+
+		switch (get_ucall(vcpu, &uc)) {
+		case UCALL_SYNC:
+			break;
+		case UCALL_DONE:
+			goto done;
+		case UCALL_ABORT:
+			REPORT_GUEST_ASSERT(uc);
+		default:
+			TEST_FAIL("Unknown ucall %lu", uc.cmd);
+		}
+	}
+done:
+	snapshot_cpuidle(cpu, &after);
+
+	total_idle_delta = 0;
+	printf("  CPU %d, no policy (default), %d HLTs\n", cpu, NUM_HALTS);
+	for (i = 0; i < after.num_states; i++) {
+		unsigned long delta = after.usage[i] - before.usage[i];
+
+		printf("    %-6s (lat=%3dus): usage +%lu\n",
+		       after.name[i], after.latency[i], delta);
+		total_idle_delta += delta;
+	}
+
+	deep_usage_delta = after.usage[deep_state_idx] -
+			   before.usage[deep_state_idx];
+
+	/*
+	 * Without a policy, we expect SOME idle state entries. If nothing
+	 * entered idle at all, halt polling consumed everything and this
+	 * test can't validate the control case — skip.
+	 */
+	if (total_idle_delta == 0) {
+		ksft_test_result_skip("cpuidle_no_policy: halt polling "
+				      "consumed all halts, no idle entries\n");
+		kvm_vm_free(vm);
+		return;
+	}
+
+	/*
+	 * We don't strictly require deep C-state entry here (the governor
+	 * might choose shallow states for short halts), but we log it for
+	 * comparison with the enforcement test.
+	 */
+	printf("  Deep state '%s' entered %lu times (control — no cap)\n",
+	       after.name[deep_state_idx], deep_usage_delta);
+
+	kvm_vm_free(vm);
+	ksft_test_result_pass("cpuidle_no_policy\n");
+}
+
+/*
+ * Test: policy=C0 means guest HLT returns immediately (no idle at all).
+ */
+static void test_policy_c0_no_idle(void)
+{
+	struct cpuidle_snapshot before, after;
+	struct kvm_vcpu *vcpu;
+	struct kvm_vm *vm;
+	struct ucall uc;
+	int cpu, i;
+	unsigned long total_idle_delta;
+
+	cpu = pin_self_to_any_cpu();
+
+	vm = vm_create(1);
+	i = enable_cstate_policy(vm, 0, 0);
+	TEST_ASSERT(i == 0, "Failed to set C-state policy to C0");
+
+	vcpu = vm_vcpu_add(vm, 0, guest_hlt_loop);
+	vm_install_exception_handler(vm, TIMER_VECTOR, guest_timer_handler);
+	virt_pg_map(vm, APIC_DEFAULT_GPA, APIC_DEFAULT_GPA);
+
+	snapshot_cpuidle(cpu, &before);
+
+	for (i = 0; i < NUM_HALTS; i++) {
+		vcpu_run(vcpu);
+		TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO);
+
+		switch (get_ucall(vcpu, &uc)) {
+		case UCALL_SYNC:
+			break;
+		case UCALL_DONE:
+			goto done;
+		case UCALL_ABORT:
+			REPORT_GUEST_ASSERT(uc);
+		default:
+			TEST_FAIL("Unknown ucall %lu", uc.cmd);
+		}
+	}
+done:
+	snapshot_cpuidle(cpu, &after);
+
+	total_idle_delta = 0;
+	printf("  CPU %d, policy=C0, %d HLTs\n", cpu, NUM_HALTS);
+	for (i = 0; i < after.num_states; i++) {
+		unsigned long delta = after.usage[i] - before.usage[i];
+
+		printf("    %-6s (lat=%3dus): usage +%lu\n",
+		       after.name[i], after.latency[i], delta);
+		if (i > 0)
+			total_idle_delta += delta;
+	}
+
+	/*
+	 * With policy=C0, HLT should return immediately without entering
+	 * any real idle state (state > POLL). Allow small margin for
+	 * host background activity.
+	 */
+	TEST_ASSERT(total_idle_delta < 5,
+		    "Idle states entered %lu times with policy=C0 (expected <5)",
+		    total_idle_delta);
+
+	kvm_vm_free(vm);
+	ksft_test_result_pass("policy_c0_no_idle\n");
+}
+
+int main(int argc, char *argv[])
+{
+	ksft_print_header();
+	ksft_set_plan(7);
+
+	TEST_REQUIRE(kvm_has_cap(KVM_CAP_CSTATE_POLICY));
+
+	test_cap_supported();
+	test_valid_policies();
+	test_invalid_policies();
+	test_policy_change();
+	test_cpuidle_no_policy();
+	test_cpuidle_enforcement();
+	test_policy_c0_no_idle();
+
+	ksft_finished();
+	return 0;
+}
-- 
2.55.0


      parent reply	other threads:[~2026-09-14 14:37 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-14 14:36 [PATCH RFC v3 0/3] KVM: x86: per-VM C-state policy enforcement (KVM_CAP_CSTATE_POLICY) Anthony Harivel
2026-09-14 14:37 ` [PATCH RFC v3 1/3] cpuidle: export cpuidle_devices for KVM C-state policy enforcement Anthony Harivel
2026-09-14 14:37 ` [PATCH RFC v3 2/3] KVM: x86: add KVM_CAP_CSTATE_POLICY for per-VM C-state enforcement Anthony Harivel
2026-09-14 15:23   ` Sean Christopherson
2026-09-14 14:37 ` Anthony Harivel [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260914143702.915401-4-aharivel@redhat.com \
    --to=aharivel@redhat.com \
    --cc=kvm@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=pbonzini@redhat.com \
    --cc=seanjc@google.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®