|
| 1 | +import os |
| 2 | +import re |
| 3 | +import shutil |
| 4 | +import time |
| 5 | + |
| 6 | +from avocado.utils import archive, build, process |
| 7 | +from virttest import error_context |
| 8 | +from virttest.utils_misc import verify_dmesg, verify_secure_guest, verify_secure_host |
| 9 | + |
| 10 | + |
| 11 | +@error_context.context_aware |
| 12 | +def run(test, params, env): |
| 13 | + """ |
| 14 | + QEMU test case to verify the Idle HLT Intercept feature and |
| 15 | + monitor idle-halt exits using ftrace. |
| 16 | +
|
| 17 | + :param test: QEMU test object for logging and test control. |
| 18 | + :param params: Dictionary with test parameters (e.g., vm_secure_guest_type, |
| 19 | + url_cpuid_tool). |
| 20 | + :param env: Dictionary with test environment, including VM configuration. |
| 21 | + """ |
| 22 | + |
| 23 | + def setup_ftrace(hlt_exit_reason): |
| 24 | + # Set up ftrace |
| 25 | + error_context.context("Configuring ftrace for kvm:kvm_exit", test.log.info) |
| 26 | + if not os.path.exists(trace_dir): |
| 27 | + test.cancel("ftrace not available at {}".format(trace_dir)) |
| 28 | + |
| 29 | + try: |
| 30 | + with open(os.path.join(trace_dir, "tracing_on"), "w") as f: |
| 31 | + f.write("0") |
| 32 | + with open(os.path.join(trace_dir, "trace"), "w") as f: |
| 33 | + f.write("") |
| 34 | + with open(os.path.join(trace_dir, "events/kvm/kvm_exit/enable"), "w") as f: |
| 35 | + f.write("1") |
| 36 | + filter_text = "exit_reason == " + hlt_exit_reason |
| 37 | + with open(os.path.join(trace_dir, "events/kvm/kvm_exit/filter"), "w") as f: |
| 38 | + f.write(filter_text) |
| 39 | + test.log.info( |
| 40 | + "ftrace configured for kvm:kvm_exit with exit_reason == %s.", |
| 41 | + hlt_exit_reason, |
| 42 | + ) |
| 43 | + except (IOError, PermissionError) as e: |
| 44 | + test.cancel("Failed to configure ftrace: {}".format(e)) |
| 45 | + |
| 46 | + def cpuid_tool_build(): |
| 47 | + """ |
| 48 | + Build and install cpuid from source tarball if not already installed. |
| 49 | + """ |
| 50 | + error_context.context("Building cpuid tool from source", test.log.info) |
| 51 | + test.log.info("Using cpuid source URL: %s", url_cpuid_tool) |
| 52 | + |
| 53 | + # Check for build tools |
| 54 | + for tool in ["make", "gcc", "tar"]: |
| 55 | + if not shutil.which(tool): |
| 56 | + test.cancel( |
| 57 | + f"Build tool {tool} not found. Please install it " |
| 58 | + f"(e.g., 'sudo apt install build-essential')." |
| 59 | + ) |
| 60 | + |
| 61 | + try: |
| 62 | + # Download the tarball |
| 63 | + tarball = test.fetch_asset(url_cpuid_tool) |
| 64 | + test.log.info("Downloaded cpuid source: %s", tarball) |
| 65 | + |
| 66 | + # Extract tarball |
| 67 | + source_dir_name = os.path.basename(tarball).split(".src.tar.")[0] |
| 68 | + sourcedir = os.path.join(test.teststmpdir, source_dir_name) |
| 69 | + archive.extract(tarball, test.teststmpdir) |
| 70 | + test.log.info("Extracted cpuid source to %s", sourcedir) |
| 71 | + |
| 72 | + # Build and install (use sudo for make install) |
| 73 | + build.make(sourcedir, extra_args="install", ignore_status=False) |
| 74 | + test.log.info("Successfully built and installed cpuid") |
| 75 | + |
| 76 | + # Verify installation |
| 77 | + cpuid_path = shutil.which("cpuid") |
| 78 | + if not cpuid_path: |
| 79 | + test.fail("cpuid binary not found in PATH after installation") |
| 80 | + |
| 81 | + # Verify cpuid works |
| 82 | + result = process.run("cpuid --version", shell=True, ignore_status=True) |
| 83 | + if result.exit_status != 0: |
| 84 | + test.fail( |
| 85 | + "Installed cpuid tool failed to execute: {}".format( |
| 86 | + result.stderr.decode() |
| 87 | + ) |
| 88 | + ) |
| 89 | + test.log.info( |
| 90 | + "cpuid tool installed and verified: %s", result.stdout.decode().strip() |
| 91 | + ) |
| 92 | + |
| 93 | + except Exception as e: |
| 94 | + test.cancel("Failed to build/install test prerequisite: {}".format(e)) |
| 95 | + |
| 96 | + if params.get("vm_secure_guest_type"): |
| 97 | + secure_guest_type = params.get("vm_secure_guest_type") |
| 98 | + supported_secureguest = ["sev", "snp"] |
| 99 | + if secure_guest_type not in supported_secureguest: |
| 100 | + test.cancel( |
| 101 | + "Testcase does not support vm_secure_guest_type %s" % secure_guest_type |
| 102 | + ) |
| 103 | + # Check host kernel sev support |
| 104 | + verify_secure_host(params) |
| 105 | + error_context.context("Setting up test environment", test.log.info) |
| 106 | + timeout = params.get_numeric("login_timeout", 240) |
| 107 | + trace_dir = params.get("trace_dir", "/sys/kernel/tracing") |
| 108 | + hlt_exit_reason = params.get("hlt_exit_reason", "0x0a6") |
| 109 | + url_cpuid_tool = params.get( |
| 110 | + "url_cpuid_tool", |
| 111 | + default="http://www.etallen.com/cpuid/cpuid-20250513.src.tar.gz", |
| 112 | + ) |
| 113 | + # Check if cpuid is installed; build if not |
| 114 | + if not shutil.which("cpuid"): |
| 115 | + test.log.info("cpuid tool not found, attempting to build from source") |
| 116 | + cpuid_tool_build() |
| 117 | + |
| 118 | + # Check Idle HLT Intercept feature |
| 119 | + error_context.context("Checking Idle HLT Intercept feature", test.log.info) |
| 120 | + try: |
| 121 | + result = process.run( |
| 122 | + "cpuid -1 -r -l 0x8000000A", shell=True, ignore_status=True |
| 123 | + ) |
| 124 | + if result.exit_status != 0: |
| 125 | + test.cancel( |
| 126 | + "Failed to execute cpuid command: {}".format(result.stderr.decode()) |
| 127 | + ) |
| 128 | + output = result.stdout.decode() |
| 129 | + edx_match = re.search(r"edx\s*=\s*0x([0-9a-fA-F]+)", output) |
| 130 | + if not edx_match: |
| 131 | + test.cancel("Could not parse EDX from cpuid output.") |
| 132 | + edx = int(edx_match.group(1), 16) |
| 133 | + if not (edx & (1 << 30)): |
| 134 | + test.cancel("Idle HLT Intercept feature is not supported on this platform.") |
| 135 | + test.log.info("Idle HLT Intercept feature is supported.") |
| 136 | + |
| 137 | + except process.CmdError as e: |
| 138 | + test.cancel("Error executing cpuid: {}".format(e)) |
| 139 | + # Set up ftrace |
| 140 | + setup_ftrace(hlt_exit_reason) |
| 141 | + try: |
| 142 | + # Enable ftrace |
| 143 | + with open(os.path.join(trace_dir, "tracing_on"), "w") as f: |
| 144 | + f.write("1") |
| 145 | + vm_name = params["main_vm"] |
| 146 | + vm = env.get_vm(vm_name) |
| 147 | + vm.create() |
| 148 | + vm.verify_alive() |
| 149 | + session = vm.wait_for_login(timeout=timeout) |
| 150 | + verify_dmesg() |
| 151 | + if "secure_guest_type" in locals() and secure_guest_type: |
| 152 | + verify_secure_guest(session, params, vm) |
| 153 | + time.sleep(5) |
| 154 | + with open(os.path.join(trace_dir, "tracing_on"), "w") as f: |
| 155 | + f.write("0") |
| 156 | + with open(os.path.join(trace_dir, "trace"), "r") as f: |
| 157 | + trace_output = f.read() |
| 158 | + if "idle-halt" not in trace_output: |
| 159 | + test.fail("No idle-halt exits detected in ftrace output.") |
| 160 | + else: |
| 161 | + test.log.info( |
| 162 | + "Idle-halt exits detected in ftrace output:\n%s", trace_output |
| 163 | + ) |
| 164 | + except Exception as e: |
| 165 | + test.fail("Test failed: %s" % str(e)) |
| 166 | + finally: |
| 167 | + try: |
| 168 | + if os.path.exists(os.path.join(trace_dir, "tracing_on")): |
| 169 | + with open(os.path.join(trace_dir, "tracing_on"), "w") as f: |
| 170 | + f.write("0") |
| 171 | + with open( |
| 172 | + os.path.join(trace_dir, "events/kvm/kvm_exit/enable"), "w" |
| 173 | + ) as f: |
| 174 | + f.write("0") |
| 175 | + with open( |
| 176 | + os.path.join(trace_dir, "events/kvm/kvm_exit/filter"), "w" |
| 177 | + ) as f: |
| 178 | + f.write("0") |
| 179 | + with open(os.path.join(trace_dir, "trace"), "w") as f: |
| 180 | + f.write("") |
| 181 | + test.log.info("ftrace cleaned up.") |
| 182 | + except (IOError, PermissionError) as e: |
| 183 | + test.log.warning("Failed to clean up ftrace: %s", e) |
| 184 | + if "session" in locals() and session: |
| 185 | + session.close() |
| 186 | + vm.destroy() |
0 commit comments