Skip to content

Commit 2a6194e

Browse files
pa1guptagregkh
authored andcommitted
selftest/x86/bugs: Add selftests for ITS
commit 7a9b709 upstream. Below are the tests added for Indirect Target Selection (ITS): - its_sysfs.py - Check if sysfs reflects the correct mitigation status for the mitigation selected via the kernel cmdline. - its_permutations.py - tests mitigation selection with cmdline permutations with other bugs like spectre_v2 and retbleed. - its_indirect_alignment.py - verifies that for addresses in .retpoline_sites section that belong to lower half of cacheline are patched to ITS-safe thunk. Typical output looks like below: Site 49: function symbol: __x64_sys_restart_syscall+0x1f <0xffffffffbb1509af> # vmlinux: 0xffffffff813509af: jmp 0xffffffff81f5a8e0 # kcore: 0xffffffffbb1509af: jmpq *%rax # ITS thunk NOT expected for site 49 # PASSED: Found *%rax # Site 50: function symbol: __resched_curr+0xb0 <0xffffffffbb181910> # vmlinux: 0xffffffff81381910: jmp 0xffffffff81f5a8e0 # kcore: 0xffffffffbb181910: jmp 0xffffffffc02000fc # ITS thunk expected for site 50 # PASSED: Found 0xffffffffc02000fc -> jmpq *%rax <scattered-thunk?> - its_ret_alignment.py - verifies that for addresses in .return_sites section that belong to lower half of cacheline are patched to its_return_thunk. Typical output looks like below: Site 97: function symbol: collect_event+0x48 <0xffffffffbb007f18> # vmlinux: 0xffffffff81207f18: jmp 0xffffffff81f5b500 # kcore: 0xffffffffbb007f18: jmp 0xffffffffbbd5b560 # PASSED: Found jmp 0xffffffffbbd5b560 <its_return_thunk> # Site 98: function symbol: collect_event+0xa4 <0xffffffffbb007f74> # vmlinux: 0xffffffff81207f74: jmp 0xffffffff81f5b500 # kcore: 0xffffffffbb007f74: retq # PASSED: Found retq Some of these tests have dependency on tools like virtme-ng[1] and drgn[2]. When the dependencies are not met, the test will be skipped. [1] https://github.com/arighi/virtme-ng [2] https://github.com/osandov/drgn Co-developed-by: Tao Zhang <[email protected]> Signed-off-by: Tao Zhang <[email protected]> Signed-off-by: Pawan Gupta <[email protected]> Signed-off-by: Dave Hansen <[email protected]> Signed-off-by: Greg Kroah-Hartman <[email protected]>
1 parent 88a817e commit 2a6194e

File tree

7 files changed

+631
-0
lines changed

7 files changed

+631
-0
lines changed

tools/testing/selftests/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ TARGETS += user_events
115115
TARGETS += vDSO
116116
TARGETS += mm
117117
TARGETS += x86
118+
TARGETS += x86/bugs
118119
TARGETS += zram
119120
#Please keep the TARGETS list alphabetically sorted
120121
# Run "make quicktest=1 run_tests" or
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
TEST_PROGS := its_sysfs.py its_permutations.py its_indirect_alignment.py its_ret_alignment.py
2+
TEST_FILES := common.py
3+
include ../../lib.mk
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0
3+
#
4+
# Copyright (c) 2025 Intel Corporation
5+
#
6+
# This contains kselftest framework adapted common functions for testing
7+
# mitigation for x86 bugs.
8+
9+
import os, sys, re, shutil
10+
11+
sys.path.insert(0, '../../kselftest')
12+
import ksft
13+
14+
def read_file(path):
15+
if not os.path.exists(path):
16+
return None
17+
with open(path, 'r') as file:
18+
return file.read().strip()
19+
20+
def cpuinfo_has(arg):
21+
cpuinfo = read_file('/proc/cpuinfo')
22+
if arg in cpuinfo:
23+
return True
24+
return False
25+
26+
def cmdline_has(arg):
27+
cmdline = read_file('/proc/cmdline')
28+
if arg in cmdline:
29+
return True
30+
return False
31+
32+
def cmdline_has_either(args):
33+
cmdline = read_file('/proc/cmdline')
34+
for arg in args:
35+
if arg in cmdline:
36+
return True
37+
return False
38+
39+
def cmdline_has_none(args):
40+
return not cmdline_has_either(args)
41+
42+
def cmdline_has_all(args):
43+
cmdline = read_file('/proc/cmdline')
44+
for arg in args:
45+
if arg not in cmdline:
46+
return False
47+
return True
48+
49+
def get_sysfs(bug):
50+
return read_file("/sys/devices/system/cpu/vulnerabilities/" + bug)
51+
52+
def sysfs_has(bug, mitigation):
53+
status = get_sysfs(bug)
54+
if mitigation in status:
55+
return True
56+
return False
57+
58+
def sysfs_has_either(bugs, mitigations):
59+
for bug in bugs:
60+
for mitigation in mitigations:
61+
if sysfs_has(bug, mitigation):
62+
return True
63+
return False
64+
65+
def sysfs_has_none(bugs, mitigations):
66+
return not sysfs_has_either(bugs, mitigations)
67+
68+
def sysfs_has_all(bugs, mitigations):
69+
for bug in bugs:
70+
for mitigation in mitigations:
71+
if not sysfs_has(bug, mitigation):
72+
return False
73+
return True
74+
75+
def bug_check_pass(bug, found):
76+
ksft.print_msg(f"\nFound: {found}")
77+
# ksft.print_msg(f"\ncmdline: {read_file('/proc/cmdline')}")
78+
ksft.test_result_pass(f'{bug}: {found}')
79+
80+
def bug_check_fail(bug, found, expected):
81+
ksft.print_msg(f'\nFound:\t {found}')
82+
ksft.print_msg(f'Expected:\t {expected}')
83+
ksft.print_msg(f"\ncmdline: {read_file('/proc/cmdline')}")
84+
ksft.test_result_fail(f'{bug}: {found}')
85+
86+
def bug_status_unknown(bug, found):
87+
ksft.print_msg(f'\nUnknown status: {found}')
88+
ksft.print_msg(f"\ncmdline: {read_file('/proc/cmdline')}")
89+
ksft.test_result_fail(f'{bug}: {found}')
90+
91+
def basic_checks_sufficient(bug, mitigation):
92+
if not mitigation:
93+
bug_status_unknown(bug, "None")
94+
return True
95+
elif mitigation == "Not affected":
96+
ksft.test_result_pass(bug)
97+
return True
98+
elif mitigation == "Vulnerable":
99+
if cmdline_has_either([f'{bug}=off', 'mitigations=off']):
100+
bug_check_pass(bug, mitigation)
101+
return True
102+
return False
103+
104+
def get_section_info(vmlinux, section_name):
105+
from elftools.elf.elffile import ELFFile
106+
with open(vmlinux, 'rb') as f:
107+
elffile = ELFFile(f)
108+
section = elffile.get_section_by_name(section_name)
109+
if section is None:
110+
ksft.print_msg("Available sections in vmlinux:")
111+
for sec in elffile.iter_sections():
112+
ksft.print_msg(sec.name)
113+
raise ValueError(f"Section {section_name} not found in {vmlinux}")
114+
return section['sh_addr'], section['sh_offset'], section['sh_size']
115+
116+
def get_patch_sites(vmlinux, offset, size):
117+
import struct
118+
output = []
119+
with open(vmlinux, 'rb') as f:
120+
f.seek(offset)
121+
i = 0
122+
while i < size:
123+
data = f.read(4) # s32
124+
if not data:
125+
break
126+
sym_offset = struct.unpack('<i', data)[0] + i
127+
i += 4
128+
output.append(sym_offset)
129+
return output
130+
131+
def get_instruction_from_vmlinux(elffile, section, virtual_address, target_address):
132+
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
133+
section_start = section['sh_addr']
134+
section_end = section_start + section['sh_size']
135+
136+
if not (section_start <= target_address < section_end):
137+
return None
138+
139+
offset = target_address - section_start
140+
code = section.data()[offset:offset + 16]
141+
142+
cap = init_capstone()
143+
for instruction in cap.disasm(code, target_address):
144+
if instruction.address == target_address:
145+
return instruction
146+
return None
147+
148+
def init_capstone():
149+
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_OPT_SYNTAX_ATT
150+
cap = Cs(CS_ARCH_X86, CS_MODE_64)
151+
cap.syntax = CS_OPT_SYNTAX_ATT
152+
return cap
153+
154+
def get_runtime_kernel():
155+
import drgn
156+
return drgn.program_from_kernel()
157+
158+
def check_dependencies_or_skip(modules, script_name="unknown test"):
159+
for mod in modules:
160+
try:
161+
__import__(mod)
162+
except ImportError:
163+
ksft.test_result_skip(f"Skipping {script_name}: missing module '{mod}'")
164+
ksft.finished()
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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

Comments
 (0)