|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# SPDX-License-Identifier: GPL-2.0 |
| 3 | +# |
| 4 | +# Copyright (c) 2025 Intel Corporation |
| 5 | +# |
| 6 | +# Test for indirect target selection (ITS) mitigation. |
| 7 | +# |
| 8 | +# Test if indirect CALL/JMP are correctly patched by evaluating |
| 9 | +# the vmlinux .retpoline_sites in /proc/kcore. |
| 10 | + |
| 11 | +# Install dependencies |
| 12 | +# add-apt-repository ppa:michel-slm/kernel-utils |
| 13 | +# apt update |
| 14 | +# apt install -y python3-drgn python3-pyelftools python3-capstone |
| 15 | +# |
| 16 | +# Best to copy the vmlinux at a standard location: |
| 17 | +# mkdir -p /usr/lib/debug/lib/modules/$(uname -r) |
| 18 | +# cp $VMLINUX /usr/lib/debug/lib/modules/$(uname -r)/vmlinux |
| 19 | +# |
| 20 | +# Usage: ./its_indirect_alignment.py [vmlinux] |
| 21 | + |
| 22 | +import os, sys, argparse |
| 23 | +from pathlib import Path |
| 24 | + |
| 25 | +this_dir = os.path.dirname(os.path.realpath(__file__)) |
| 26 | +sys.path.insert(0, this_dir + '/../../kselftest') |
| 27 | +import ksft |
| 28 | +import common as c |
| 29 | + |
| 30 | +bug = "indirect_target_selection" |
| 31 | + |
| 32 | +mitigation = c.get_sysfs(bug) |
| 33 | +if not mitigation or "Aligned branch/return thunks" not in mitigation: |
| 34 | + ksft.test_result_skip("Skipping its_indirect_alignment.py: Aligned branch/return thunks not enabled") |
| 35 | + ksft.finished() |
| 36 | + |
| 37 | +if c.sysfs_has("spectre_v2", "Retpolines"): |
| 38 | + ksft.test_result_skip("Skipping its_indirect_alignment.py: Retpolines deployed") |
| 39 | + ksft.finished() |
| 40 | + |
| 41 | +c.check_dependencies_or_skip(['drgn', 'elftools', 'capstone'], script_name="its_indirect_alignment.py") |
| 42 | + |
| 43 | +from elftools.elf.elffile import ELFFile |
| 44 | +from drgn.helpers.common.memory import identify_address |
| 45 | + |
| 46 | +cap = c.init_capstone() |
| 47 | + |
| 48 | +if len(os.sys.argv) > 1: |
| 49 | + arg_vmlinux = os.sys.argv[1] |
| 50 | + if not os.path.exists(arg_vmlinux): |
| 51 | + ksft.test_result_fail(f"its_indirect_alignment.py: vmlinux not found at argument path: {arg_vmlinux}") |
| 52 | + ksft.exit_fail() |
| 53 | + os.makedirs(f"/usr/lib/debug/lib/modules/{os.uname().release}", exist_ok=True) |
| 54 | + os.system(f'cp {arg_vmlinux} /usr/lib/debug/lib/modules/$(uname -r)/vmlinux') |
| 55 | + |
| 56 | +vmlinux = f"/usr/lib/debug/lib/modules/{os.uname().release}/vmlinux" |
| 57 | +if not os.path.exists(vmlinux): |
| 58 | + ksft.test_result_fail(f"its_indirect_alignment.py: vmlinux not found at {vmlinux}") |
| 59 | + ksft.exit_fail() |
| 60 | + |
| 61 | +ksft.print_msg(f"Using vmlinux: {vmlinux}") |
| 62 | + |
| 63 | +retpolines_start_vmlinux, retpolines_sec_offset, size = c.get_section_info(vmlinux, '.retpoline_sites') |
| 64 | +ksft.print_msg(f"vmlinux: Section .retpoline_sites (0x{retpolines_start_vmlinux:x}) found at 0x{retpolines_sec_offset:x} with size 0x{size:x}") |
| 65 | + |
| 66 | +sites_offset = c.get_patch_sites(vmlinux, retpolines_sec_offset, size) |
| 67 | +total_retpoline_tests = len(sites_offset) |
| 68 | +ksft.print_msg(f"Found {total_retpoline_tests} retpoline sites") |
| 69 | + |
| 70 | +prog = c.get_runtime_kernel() |
| 71 | +retpolines_start_kcore = prog.symbol('__retpoline_sites').address |
| 72 | +ksft.print_msg(f'kcore: __retpoline_sites: 0x{retpolines_start_kcore:x}') |
| 73 | + |
| 74 | +x86_indirect_its_thunk_r15 = prog.symbol('__x86_indirect_its_thunk_r15').address |
| 75 | +ksft.print_msg(f'kcore: __x86_indirect_its_thunk_r15: 0x{x86_indirect_its_thunk_r15:x}') |
| 76 | + |
| 77 | +tests_passed = 0 |
| 78 | +tests_failed = 0 |
| 79 | +tests_unknown = 0 |
| 80 | + |
| 81 | +with open(vmlinux, 'rb') as f: |
| 82 | + elffile = ELFFile(f) |
| 83 | + text_section = elffile.get_section_by_name('.text') |
| 84 | + |
| 85 | + for i in range(0, len(sites_offset)): |
| 86 | + site = retpolines_start_kcore + sites_offset[i] |
| 87 | + vmlinux_site = retpolines_start_vmlinux + sites_offset[i] |
| 88 | + passed = unknown = failed = False |
| 89 | + try: |
| 90 | + vmlinux_insn = c.get_instruction_from_vmlinux(elffile, text_section, text_section['sh_addr'], vmlinux_site) |
| 91 | + kcore_insn = list(cap.disasm(prog.read(site, 16), site))[0] |
| 92 | + operand = kcore_insn.op_str |
| 93 | + insn_end = site + kcore_insn.size - 1 # TODO handle Jcc.32 __x86_indirect_thunk_\reg |
| 94 | + safe_site = insn_end & 0x20 |
| 95 | + site_status = "" if safe_site else "(unsafe)" |
| 96 | + |
| 97 | + ksft.print_msg(f"\nSite {i}: {identify_address(prog, site)} <0x{site:x}> {site_status}") |
| 98 | + ksft.print_msg(f"\tvmlinux: 0x{vmlinux_insn.address:x}:\t{vmlinux_insn.mnemonic}\t{vmlinux_insn.op_str}") |
| 99 | + ksft.print_msg(f"\tkcore: 0x{kcore_insn.address:x}:\t{kcore_insn.mnemonic}\t{kcore_insn.op_str}") |
| 100 | + |
| 101 | + if (site & 0x20) ^ (insn_end & 0x20): |
| 102 | + ksft.print_msg(f"\tSite at safe/unsafe boundary: {str(kcore_insn.bytes)} {kcore_insn.mnemonic} {operand}") |
| 103 | + if safe_site: |
| 104 | + tests_passed += 1 |
| 105 | + passed = True |
| 106 | + ksft.print_msg(f"\tPASSED: At safe address") |
| 107 | + continue |
| 108 | + |
| 109 | + if operand.startswith('0xffffffff'): |
| 110 | + thunk = int(operand, 16) |
| 111 | + if thunk > x86_indirect_its_thunk_r15: |
| 112 | + insn_at_thunk = list(cap.disasm(prog.read(thunk, 16), thunk))[0] |
| 113 | + operand += ' -> ' + insn_at_thunk.mnemonic + ' ' + insn_at_thunk.op_str + ' <dynamic-thunk?>' |
| 114 | + if 'jmp' in insn_at_thunk.mnemonic and thunk & 0x20: |
| 115 | + ksft.print_msg(f"\tPASSED: Found {operand} at safe address") |
| 116 | + passed = True |
| 117 | + if not passed: |
| 118 | + if kcore_insn.operands[0].type == capstone.CS_OP_IMM: |
| 119 | + operand += ' <' + prog.symbol(int(operand, 16)) + '>' |
| 120 | + if '__x86_indirect_its_thunk_' in operand: |
| 121 | + ksft.print_msg(f"\tPASSED: Found {operand}") |
| 122 | + else: |
| 123 | + ksft.print_msg(f"\tPASSED: Found direct branch: {kcore_insn}, ITS thunk not required.") |
| 124 | + passed = True |
| 125 | + else: |
| 126 | + unknown = True |
| 127 | + if passed: |
| 128 | + tests_passed += 1 |
| 129 | + elif unknown: |
| 130 | + ksft.print_msg(f"UNKNOWN: unexpected operand: {kcore_insn}") |
| 131 | + tests_unknown += 1 |
| 132 | + else: |
| 133 | + ksft.print_msg(f'\t************* FAILED *************') |
| 134 | + ksft.print_msg(f"\tFound {kcore_insn.bytes} {kcore_insn.mnemonic} {operand}") |
| 135 | + ksft.print_msg(f'\t**********************************') |
| 136 | + tests_failed += 1 |
| 137 | + except Exception as e: |
| 138 | + ksft.print_msg(f"UNKNOWN: An unexpected error occurred: {e}") |
| 139 | + tests_unknown += 1 |
| 140 | + |
| 141 | +ksft.print_msg(f"\n\nSummary:") |
| 142 | +ksft.print_msg(f"PASS: \t{tests_passed} \t/ {total_retpoline_tests}") |
| 143 | +ksft.print_msg(f"FAIL: \t{tests_failed} \t/ {total_retpoline_tests}") |
| 144 | +ksft.print_msg(f"UNKNOWN: \t{tests_unknown} \t/ {total_retpoline_tests}") |
| 145 | + |
| 146 | +if tests_failed == 0: |
| 147 | + ksft.test_result_pass("All ITS return thunk sites passed") |
| 148 | +else: |
| 149 | + ksft.test_result_fail(f"{tests_failed} ITS return thunk sites failed") |
| 150 | +ksft.finished() |
0 commit comments