diff options
Diffstat (limited to 'tests')
25 files changed, 498 insertions, 115 deletions
diff --git a/tests/Makefile.include b/tests/Makefile.include index 62a4fc8..e47ef4d 100644 --- a/tests/Makefile.include +++ b/tests/Makefile.include @@ -105,11 +105,11 @@ check-venv: $(TESTS_VENV_TOKEN) FUNCTIONAL_TARGETS=$(patsubst %-softmmu,check-functional-%, $(filter %-softmmu,$(TARGETS))) .PHONY: $(FUNCTIONAL_TARGETS) -$(FUNCTIONAL_TARGETS): +$(FUNCTIONAL_TARGETS): check-venv @$(MAKE) SPEED=thorough $(subst -functional,-func,$@) .PHONY: check-functional -check-functional: +check-functional: check-venv @$(NINJA) precache-functional @QEMU_TEST_NO_DOWNLOAD=1 $(MAKE) SPEED=thorough check-func check-func-quick diff --git a/tests/data/acpi/loongarch64/virt/DSDT b/tests/data/acpi/loongarch64/virt/DSDT Binary files differindex b31841a..55aa34f 100644 --- a/tests/data/acpi/loongarch64/virt/DSDT +++ b/tests/data/acpi/loongarch64/virt/DSDT diff --git a/tests/data/acpi/loongarch64/virt/DSDT.memhp b/tests/data/acpi/loongarch64/virt/DSDT.memhp Binary files differindex e291200..c0955eb 100644 --- a/tests/data/acpi/loongarch64/virt/DSDT.memhp +++ b/tests/data/acpi/loongarch64/virt/DSDT.memhp diff --git a/tests/data/acpi/loongarch64/virt/DSDT.numamem b/tests/data/acpi/loongarch64/virt/DSDT.numamem Binary files differindex 07923ac..61e47e7 100644 --- a/tests/data/acpi/loongarch64/virt/DSDT.numamem +++ b/tests/data/acpi/loongarch64/virt/DSDT.numamem diff --git a/tests/data/acpi/loongarch64/virt/DSDT.topology b/tests/data/acpi/loongarch64/virt/DSDT.topology Binary files differindex 6dfbb49..b2afebc 100644 --- a/tests/data/acpi/loongarch64/virt/DSDT.topology +++ b/tests/data/acpi/loongarch64/virt/DSDT.topology diff --git a/tests/docker/common.rc b/tests/docker/common.rc index 752f4f3..79d533a 100755 --- a/tests/docker/common.rc +++ b/tests/docker/common.rc @@ -53,8 +53,8 @@ configure_qemu() config_opts="--enable-werror \ ${TARGET_LIST:+--target-list=${TARGET_LIST}} \ --prefix=$INSTALL_DIR \ - $QEMU_CONFIGURE_OPTS $EXTRA_CONFIGURE_OPTS \ $enable_rust \ + $QEMU_CONFIGURE_OPTS $EXTRA_CONFIGURE_OPTS \ $@" echo "Configure options:" echo $config_opts diff --git a/tests/docker/dockerfiles/debian-i686-cross.docker b/tests/docker/dockerfiles/debian-i686-cross.docker index 4e8b3a8..2998764 100644 --- a/tests/docker/dockerfiles/debian-i686-cross.docker +++ b/tests/docker/dockerfiles/debian-i686-cross.docker @@ -178,7 +178,7 @@ ENV ABI "i686-linux-gnu" ENV MESON_OPTS "--cross-file=i686-linux-gnu" ENV RUST_TARGET "i686-unknown-linux-gnu" ENV QEMU_CONFIGURE_OPTS --cross-prefix=i686-linux-gnu- -ENV DEF_TARGET_LIST x86_64-softmmu,x86_64-linux-user,i386-softmmu,i386-linux-user +ENV DEF_TARGET_LIST i386-softmmu,i386-linux-user # As a final step configure the user (if env is defined) ARG USER ARG UID diff --git a/tests/functional/aarch64/test_device_passthrough.py b/tests/functional/aarch64/test_device_passthrough.py index 1743778..05a3f52 100755 --- a/tests/functional/aarch64/test_device_passthrough.py +++ b/tests/functional/aarch64/test_device_passthrough.py @@ -85,8 +85,8 @@ class Aarch64DevicePassthrough(QemuSystemTest): # https://docs.kernel.org/driver-api/vfio.html#vfio-device-cde ASSET_DEVICE_PASSTHROUGH_STACK = Asset( ('https://github.com/pbo-linaro/qemu-linux-stack/' - 'releases/download/build/device_passthrough-c3fb84a.tar.xz'), - '15ac2b02bed0c0ea8e3e007de0bcfdaf6fd51c1ba98213f841dc7d01d6f72f04') + 'releases/download/build/device_passthrough-a9612a2.tar.xz'), + 'f7d2f70912e7231986e6e293e1a2c4786dd02bec113a7acb6bfc619e96155455') # This tests the device passthrough implementation, by booting a VM # supporting it with two nvme disks attached, and launching a nested VM diff --git a/tests/functional/aarch64/test_reverse_debug.py b/tests/functional/aarch64/test_reverse_debug.py index 8bc91cc..ec3348c 100755 --- a/tests/functional/aarch64/test_reverse_debug.py +++ b/tests/functional/aarch64/test_reverse_debug.py @@ -2,36 +2,34 @@ # # SPDX-License-Identifier: GPL-2.0-or-later # -# Reverse debugging test +# Reverse debugging test for aarch64 # # Copyright (c) 2020 ISP RAS +# Copyright (c) 2025 Linaro Limited # # Author: # Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru> +# Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado) # # This work is licensed under the terms of the GNU GPL, version 2 or # later. See the COPYING file in the top-level directory. -from qemu_test import Asset, skipIfMissingImports, skipFlakyTest +from qemu_test import Asset, skipFlakyTest from reverse_debugging import ReverseDebugging -@skipIfMissingImports('avocado.utils') class ReverseDebugging_AArch64(ReverseDebugging): - REG_PC = 32 - ASSET_KERNEL = Asset( ('https://archives.fedoraproject.org/pub/archive/fedora/linux/' 'releases/29/Everything/aarch64/os/images/pxeboot/vmlinuz'), '7e1430b81c26bdd0da025eeb8fbd77b5dc961da4364af26e771bd39f379cbbf7') - @skipFlakyTest("https://gitlab.com/qemu-project/qemu/-/issues/2921") def test_aarch64_virt(self): self.set_machine('virt') self.cpu = 'cortex-a53' kernel_path = self.ASSET_KERNEL.fetch() - self.reverse_debugging(args=('-kernel', kernel_path)) + self.reverse_debugging(gdb_arch='aarch64', args=('-kernel', kernel_path)) if __name__ == '__main__': diff --git a/tests/functional/aarch64/test_rme_sbsaref.py b/tests/functional/aarch64/test_rme_sbsaref.py index ca892e0..6f92858 100755 --- a/tests/functional/aarch64/test_rme_sbsaref.py +++ b/tests/functional/aarch64/test_rme_sbsaref.py @@ -25,8 +25,8 @@ class Aarch64RMESbsaRefMachine(QemuSystemTest): # ./build.sh && ./archive_artifacts.sh out.tar.xz ASSET_RME_STACK_SBSA = Asset( ('https://github.com/pbo-linaro/qemu-linux-stack/' - 'releases/download/build/rme_sbsa_release-a7f02cf.tar.xz'), - '27d8400b11befb828d6db0cab97e7ae102d0992c928d3dfbf38b24b6cf6c324c') + 'releases/download/build/rme_sbsa_release-6a2dfc5.tar.xz'), + '5adba482aa069912292a8da746c6b21268224d9d81c97fe7c0bed690579ebdcb') # This tests the FEAT_RME cpu implementation, by booting a VM supporting it, # and launching a nested VM using it. diff --git a/tests/functional/aarch64/test_rme_virt.py b/tests/functional/aarch64/test_rme_virt.py index bb603aa..5e23773 100755 --- a/tests/functional/aarch64/test_rme_virt.py +++ b/tests/functional/aarch64/test_rme_virt.py @@ -23,8 +23,8 @@ class Aarch64RMEVirtMachine(QemuSystemTest): # ./build.sh && ./archive_artifacts.sh out.tar.xz ASSET_RME_STACK_VIRT = Asset( ('https://github.com/pbo-linaro/qemu-linux-stack/' - 'releases/download/build/rme_release-86101e5.tar.xz'), - 'e42fef8439badb52a071ac446fc33cff4cb7d61314c7a28fdbe61a11e1faad3a') + 'releases/download/build/rme_release-56bc99e.tar.xz'), + '0e3dc6b8a4b828dbae09c951a40dcb710eded084b32432b50c69cf4173ffa4be') # This tests the FEAT_RME cpu implementation, by booting a VM supporting it, # and launching a nested VM using it. diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 2a0c5aa..725630d 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -77,6 +77,12 @@ foreach speed : ['quick', 'thorough'] test_env.set('PYTHONPATH', meson.project_source_root() / 'python:' + meson.current_source_dir()) + # Define the GDB environment variable if gdb is available. + gdb = get_option('gdb') + if gdb != '' + test_env.set('QEMU_TEST_GDB', gdb) + endif + foreach test : target_tests testname = '@0@-@1@'.format(target_base, test) if fs.exists('generic' / 'test_' + test + '.py') diff --git a/tests/functional/ppc64/test_reverse_debug.py b/tests/functional/ppc64/test_reverse_debug.py index 5931ade..69551fb 100755 --- a/tests/functional/ppc64/test_reverse_debug.py +++ b/tests/functional/ppc64/test_reverse_debug.py @@ -2,39 +2,36 @@ # # SPDX-License-Identifier: GPL-2.0-or-later # -# Reverse debugging test +# Reverse debugging test for ppc64 # # Copyright (c) 2020 ISP RAS +# Copyright (c) 2025 Linaro Limited # # Author: # Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru> +# Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado) # # This work is licensed under the terms of the GNU GPL, version 2 or # later. See the COPYING file in the top-level directory. -from qemu_test import skipIfMissingImports, skipFlakyTest +from qemu_test import skipFlakyTest from reverse_debugging import ReverseDebugging -@skipIfMissingImports('avocado.utils') class ReverseDebugging_ppc64(ReverseDebugging): - REG_PC = 0x40 - @skipFlakyTest("https://gitlab.com/qemu-project/qemu/-/issues/1992") def test_ppc64_pseries(self): self.set_machine('pseries') # SLOF branches back to its entry point, which causes this test # to take the 'hit a breakpoint again' path. That's not a problem, # just slightly different than the other machines. - self.endian_is_le = False - self.reverse_debugging() + self.reverse_debugging(gdb_arch='powerpc:common64') @skipFlakyTest("https://gitlab.com/qemu-project/qemu/-/issues/1992") def test_ppc64_powernv(self): self.set_machine('powernv') - self.endian_is_le = False - self.reverse_debugging() + self.reverse_debugging(gdb_arch='powerpc:common64') if __name__ == '__main__': diff --git a/tests/functional/qemu_test/__init__.py b/tests/functional/qemu_test/__init__.py index 6e666a0..3201935 100644 --- a/tests/functional/qemu_test/__init__.py +++ b/tests/functional/qemu_test/__init__.py @@ -15,6 +15,8 @@ from .testcase import QemuBaseTest, QemuUserTest, QemuSystemTest from .linuxkernel import LinuxKernelTest from .decorators import skipIfMissingCommands, skipIfNotMachine, \ skipFlakyTest, skipUntrustedTest, skipBigDataTest, skipSlowTest, \ - skipIfMissingImports, skipIfOperatingSystem, skipLockedMemoryTest + skipIfMissingImports, skipIfOperatingSystem, skipLockedMemoryTest, \ + skipIfMissingEnv from .archive import archive_extract from .uncompress import uncompress +from .gdb import GDB diff --git a/tests/functional/qemu_test/asset.py b/tests/functional/qemu_test/asset.py index 2971a98..f666125 100644 --- a/tests/functional/qemu_test/asset.py +++ b/tests/functional/qemu_test/asset.py @@ -225,7 +225,6 @@ class Asset: log.addHandler(handler) for name, asset in vars(test.__class__).items(): if name.startswith("ASSET_") and type(asset) == Asset: - log.info("Attempting to cache '%s'" % asset) try: asset.fetch() except AssetError as e: diff --git a/tests/functional/qemu_test/decorators.py b/tests/functional/qemu_test/decorators.py index c0d1567..b239295 100644 --- a/tests/functional/qemu_test/decorators.py +++ b/tests/functional/qemu_test/decorators.py @@ -11,6 +11,24 @@ from unittest import skipIf, skipUnless from .cmd import which ''' +Decorator to skip execution of a test if the provided +environment variables are not set. +Example: + + @skipIfMissingEnv("QEMU_ENV_VAR0", "QEMU_ENV_VAR1") +''' +def skipIfMissingEnv(*vars_): + missing_vars = [] + for var in vars_: + if os.getenv(var) == None: + missing_vars.append(var) + + has_vars = True if len(missing_vars) == 0 else False + + return skipUnless(has_vars, f"Missing env var(s): {', '.join(missing_vars)}") + +''' + Decorator to skip execution of a test if the list of command binaries is not available in $PATH. Example: diff --git a/tests/functional/qemu_test/gdb.py b/tests/functional/qemu_test/gdb.py new file mode 100644 index 0000000..558d476 --- /dev/null +++ b/tests/functional/qemu_test/gdb.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# A simple interface module built around pygdbmi for handling GDB commands. +# +# Copyright (c) 2025 Linaro Limited +# +# Author: +# Gustavo Romero <gustavo.romero@linaro.org> +# + +import re + + +class GDB: + """Provides methods to run and capture GDB command output.""" + + + def __init__(self, gdb_path, echo=True, suffix='# ', prompt="$ "): + from pygdbmi.gdbcontroller import GdbController + from pygdbmi.constants import GdbTimeoutError + type(self).TimeoutError = GdbTimeoutError + + gdb_cmd = [gdb_path, "-q", "--interpreter=mi2"] + self.gdbmi = GdbController(gdb_cmd) + self.echo = echo + self.suffix = suffix + self.prompt = prompt + self.response = None + self.cmd_output = None + + + def get_payload(self, response, kind): + output = [] + for o in response: + # Unpack payloads of the same type. + _type, _, payload, *_ = o.values() + if _type == kind: + output += [payload] + + # Some output lines do not end with \n but begin with it, + # so remove the leading \n and merge them with the next line + # that ends with \n. + lines = [line.lstrip('\n') for line in output] + lines = "".join(lines) + lines = lines.splitlines(keepends=True) + + return lines + + + def cli(self, cmd, timeout=32.0): + self.response = self.gdbmi.write(cmd, timeout_sec=timeout) + self.cmd_output = self.get_payload(self.response, kind="console") + if self.echo: + print(self.suffix + self.prompt + cmd) + + if len(self.cmd_output) > 0: + cmd_output = self.suffix.join(self.cmd_output) + print(self.suffix + cmd_output, end="") + + return self + + + def get_addr(self): + address_pattern = r"0x[0-9A-Fa-f]+" + cmd_output = "".join(self.cmd_output) # Concat output lines. + + match = re.search(address_pattern, cmd_output) + + return int(match[0], 16) if match else None + + + def get_log(self): + r = self.get_payload(self.response, kind="log") + r = "".join(r) + + return r + + + def get_console(self): + r = "".join(self.cmd_output) + + return r + + + def exit(self): + self.gdbmi.exit() diff --git a/tests/functional/reverse_debugging.py b/tests/functional/reverse_debugging.py index f9a1d39..68cfcb3 100644 --- a/tests/functional/reverse_debugging.py +++ b/tests/functional/reverse_debugging.py @@ -1,18 +1,23 @@ -# Reverse debugging test -# # SPDX-License-Identifier: GPL-2.0-or-later # +# Reverse debugging test +# # Copyright (c) 2020 ISP RAS +# Copyright (c) 2025 Linaro Limited # # Author: # Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru> +# Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado) # # This work is licensed under the terms of the GNU GPL, version 2 or # later. See the COPYING file in the top-level directory. -import os + import logging +import os +from subprocess import check_output -from qemu_test import LinuxKernelTest, get_qemu_img +from qemu_test import LinuxKernelTest, get_qemu_img, GDB, \ + skipIfMissingEnv, skipIfMissingImports from qemu_test.ports import Ports @@ -28,13 +33,9 @@ class ReverseDebugging(LinuxKernelTest): that the execution is stopped at the last of them. """ - timeout = 10 STEPS = 10 - endian_is_le = True def run_vm(self, record, shift, args, replay_path, image_path, port): - from avocado.utils import datadrainer - logger = logging.getLogger('replay') vm = self.get_vm(name='record' if record else 'replay') vm.set_console() @@ -52,55 +53,20 @@ class ReverseDebugging(LinuxKernelTest): if args: vm.add_args(*args) vm.launch() - console_drainer = datadrainer.LineLogger(vm.console_socket.fileno(), - logger=self.log.getChild('console'), - stop_check=(lambda : not vm.is_running())) - console_drainer.start() return vm @staticmethod - def get_reg_le(g, reg): - res = g.cmd(b'p%x' % reg) - num = 0 - for i in range(len(res))[-2::-2]: - num = 0x100 * num + int(res[i:i + 2], 16) - return num - - @staticmethod - def get_reg_be(g, reg): - res = g.cmd(b'p%x' % reg) - return int(res, 16) - - def get_reg(self, g, reg): - # value may be encoded in BE or LE order - if self.endian_is_le: - return self.get_reg_le(g, reg) - else: - return self.get_reg_be(g, reg) - - def get_pc(self, g): - return self.get_reg(g, self.REG_PC) - - def check_pc(self, g, addr): - pc = self.get_pc(g) - if pc != addr: - self.fail('Invalid PC (read %x instead of %x)' % (pc, addr)) - - @staticmethod - def gdb_step(g): - g.cmd(b's', b'T05thread:01;') - - @staticmethod - def gdb_bstep(g): - g.cmd(b'bs', b'T05thread:01;') + def get_pc(gdb: GDB): + return gdb.cli("print $pc").get_addr() @staticmethod def vm_get_icount(vm): return vm.qmp('query-replay')['return']['icount'] - def reverse_debugging(self, shift=7, args=None): - from avocado.utils import gdb - from avocado.utils import process + @skipIfMissingImports("pygdbmi") # Required by GDB class + @skipIfMissingEnv("QEMU_TEST_GDB") + def reverse_debugging(self, gdb_arch, shift=7, args=None): + from qemu_test import GDB logger = logging.getLogger('replay') @@ -111,8 +77,9 @@ class ReverseDebugging(LinuxKernelTest): if qemu_img is None: self.skipTest('Could not find "qemu-img", which is required to ' 'create the temporary qcow2 image') - cmd = '%s create -f qcow2 %s 128M' % (qemu_img, image_path) - process.run(cmd) + out = check_output([qemu_img, 'create', '-f', 'qcow2', image_path, '128M'], + encoding='utf8') + logger.info("qemu-img: %s" % out) replay_path = os.path.join(self.workdir, 'replay.bin') @@ -129,68 +96,107 @@ class ReverseDebugging(LinuxKernelTest): with Ports() as ports: port = ports.find_free_port() vm = self.run_vm(False, shift, args, replay_path, image_path, port) - logger.info('connecting to gdbstub') - g = gdb.GDBRemote('127.0.0.1', port, False, False) - g.connect() - r = g.cmd(b'qSupported') - if b'qXfer:features:read+' in r: - g.cmd(b'qXfer:features:read:target.xml:0,ffb') - if b'ReverseStep+' not in r: + + try: + logger.info('Connecting to gdbstub...') + self.reverse_debugging_run(vm, port, gdb_arch, last_icount) + logger.info('Test passed.') + except GDB.TimeoutError: + # Convert a GDB timeout exception into a unittest failure exception. + raise self.failureException("Timeout while connecting to or " + "communicating with gdbstub...") from None + except Exception: + # Re-throw exceptions from unittest, like the ones caused by fail(), + # skipTest(), etc. + raise + + def reverse_debugging_run(self, vm, port, gdb_arch, last_icount): + logger = logging.getLogger('replay') + + gdb_cmd = os.getenv('QEMU_TEST_GDB') + gdb = GDB(gdb_cmd) + + r = gdb.cli("set architecture").get_log() + if gdb_arch not in r: + self.skipTest(f"GDB does not support arch '{gdb_arch}'") + + gdb.cli("set debug remote 1") + + c = gdb.cli(f"target remote localhost:{port}").get_console() + if not f"Remote debugging using localhost:{port}" in c: + self.fail("Could not connect to gdbstub!") + + # Remote debug messages are in 'log' payloads. + r = gdb.get_log() + if 'ReverseStep+' not in r: self.fail('Reverse step is not supported by QEMU') - if b'ReverseContinue+' not in r: + if 'ReverseContinue+' not in r: self.fail('Reverse continue is not supported by QEMU') + gdb.cli("set debug remote 0") + logger.info('stepping forward') steps = [] # record first instruction addresses for _ in range(self.STEPS): - pc = self.get_pc(g) + pc = self.get_pc(gdb) logger.info('saving position %x' % pc) steps.append(pc) - self.gdb_step(g) + gdb.cli("stepi") # visit the recorded instruction in reverse order logger.info('stepping backward') for addr in steps[::-1]: - self.gdb_bstep(g) - self.check_pc(g, addr) logger.info('found position %x' % addr) + gdb.cli("reverse-stepi") + pc = self.get_pc(gdb) + if pc != addr: + logger.info('Invalid PC (read %x instead of %x)' % (pc, addr)) + self.fail('Reverse stepping failed!') # visit the recorded instruction in forward order logger.info('stepping forward') for addr in steps: - self.check_pc(g, addr) - self.gdb_step(g) logger.info('found position %x' % addr) + pc = self.get_pc(gdb) + if pc != addr: + logger.info('Invalid PC (read %x instead of %x)' % (pc, addr)) + self.fail('Forward stepping failed!') + gdb.cli("stepi") # set breakpoints for the instructions just stepped over logger.info('setting breakpoints') for addr in steps: - # hardware breakpoint at addr with len=1 - g.cmd(b'Z1,%x,1' % addr, b'OK') + gdb.cli(f"break *{hex(addr)}") # this may hit a breakpoint if first instructions are executed # again logger.info('continuing execution') vm.qmp('replay-break', icount=last_icount - 1) # continue - will return after pausing - # This could stop at the end and get a T02 return, or by - # re-executing one of the breakpoints and get a T05 return. - g.cmd(b'c') + # This can stop at the end of the replay-break and gdb gets a SIGINT, + # or by re-executing one of the breakpoints and gdb stops at a + # breakpoint. + gdb.cli("continue") + if self.vm_get_icount(vm) == last_icount - 1: logger.info('reached the end (icount %s)' % (last_icount - 1)) else: logger.info('hit a breakpoint again at %x (icount %s)' % - (self.get_pc(g), self.vm_get_icount(vm))) + (self.get_pc(gdb), self.vm_get_icount(vm))) logger.info('running reverse continue to reach %x' % steps[-1]) # reverse continue - will return after stopping at the breakpoint - g.cmd(b'bc', b'T05thread:01;') + gdb.cli("reverse-continue") # assume that none of the first instructions is executed again # breaking the order of the breakpoints - self.check_pc(g, steps[-1]) + pc = self.get_pc(gdb) + if pc != steps[-1]: + self.fail("'reverse-continue' did not hit the first PC in reverse order!") + logger.info('successfully reached %x' % steps[-1]) logger.info('exiting gdb and qemu') + gdb.exit() vm.shutdown() diff --git a/tests/functional/x86_64/test_reverse_debug.py b/tests/functional/x86_64/test_reverse_debug.py index d713e91..2b31ae8 100755 --- a/tests/functional/x86_64/test_reverse_debug.py +++ b/tests/functional/x86_64/test_reverse_debug.py @@ -2,34 +2,29 @@ # # SPDX-License-Identifier: GPL-2.0-or-later # -# Reverse debugging test +# Reverse debugging test for x86_64 # # Copyright (c) 2020 ISP RAS +# Copyright (c) 2025 Linaro Limited # # Author: # Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru> +# Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado) # # This work is licensed under the terms of the GNU GPL, version 2 or # later. See the COPYING file in the top-level directory. -from qemu_test import skipIfMissingImports, skipFlakyTest +from qemu_test import skipFlakyTest from reverse_debugging import ReverseDebugging -@skipIfMissingImports('avocado.utils') class ReverseDebugging_X86_64(ReverseDebugging): - REG_PC = 0x10 - REG_CS = 0x12 - def get_pc(self, g): - return self.get_reg_le(g, self.REG_PC) \ - + self.get_reg_le(g, self.REG_CS) * 0x10 - @skipFlakyTest("https://gitlab.com/qemu-project/qemu/-/issues/2922") def test_x86_64_pc(self): self.set_machine('pc') # start with BIOS only - self.reverse_debugging() + self.reverse_debugging(gdb_arch='x86-64') if __name__ == '__main__': diff --git a/tests/lcitool/refresh b/tests/lcitool/refresh index 6459593..056cfb6 100755 --- a/tests/lcitool/refresh +++ b/tests/lcitool/refresh @@ -216,8 +216,6 @@ try: generate_dockerfile("debian-i686-cross", "debian-13", cross="i686", trailer=cross_build("i686-linux-gnu-", - "x86_64-softmmu," - "x86_64-linux-user," "i386-softmmu,i386-linux-user")) # mips no longer supported in debian-13 @@ -272,8 +270,8 @@ try: # # Ansible package lists # - generate_yaml("ubuntu", "ubuntu-2204", "aarch64") - generate_yaml("ubuntu", "ubuntu-2204", "s390x") + generate_yaml("ubuntu", "ubuntu-2404", "aarch64") + generate_yaml("ubuntu", "ubuntu-2404", "s390x") sys.exit(0) diff --git a/tests/tcg/aarch64/Makefile.target b/tests/tcg/aarch64/Makefile.target index 1755874..55ce34e 100644 --- a/tests/tcg/aarch64/Makefile.target +++ b/tests/tcg/aarch64/Makefile.target @@ -75,6 +75,11 @@ AARCH64_TESTS += $(SME_TESTS) $(SME_TESTS): CFLAGS += $(CROSS_AS_HAS_ARMV9_SME) endif +# GCS Tests +GCS_TESTS += gcsstr gcspushm gcsss +AARCH64_TESTS += $(GCS_TESTS) +$(GCS_TESTS): gcs.h + # System Registers Tests AARCH64_TESTS += sysregs diff --git a/tests/tcg/aarch64/gcs.h b/tests/tcg/aarch64/gcs.h new file mode 100644 index 0000000..6f013d0 --- /dev/null +++ b/tests/tcg/aarch64/gcs.h @@ -0,0 +1,80 @@ +/* + * Linux kernel fallback API definitions for GCS and test helpers. + * + * Copyright (c) 2025 Linaro Ltd + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include <assert.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <unistd.h> +#include <errno.h> +#include <signal.h> +#include <sys/mman.h> +#include <sys/prctl.h> +#include <sys/syscall.h> + +#ifndef PR_GET_SHADOW_STACK_STATUS +#define PR_GET_SHADOW_STACK_STATUS 74 +#endif +#ifndef PR_SET_SHADOW_STACK_STATUS +#define PR_SET_SHADOW_STACK_STATUS 75 +#endif +#ifndef PR_LOCK_SHADOW_STACK_STATUS +#define PR_LOCK_SHADOW_STACK_STATUS 76 +#endif +#ifndef PR_SHADOW_STACK_ENABLE +# define PR_SHADOW_STACK_ENABLE (1 << 0) +# define PR_SHADOW_STACK_WRITE (1 << 1) +# define PR_SHADOW_STACK_PUSH (1 << 2) +#endif +#ifndef SHADOW_STACK_SET_TOKEN +#define SHADOW_STACK_SET_TOKEN (1 << 0) +#endif +#ifndef SHADOW_STACK_SET_MARKER +#define SHADOW_STACK_SET_MARKER (1 << 1) +#endif +#ifndef SEGV_CPERR +#define SEGV_CPERR 10 +#endif +#ifndef __NR_map_shadow_stack +#define __NR_map_shadow_stack 453 +#endif + +/* + * Macros, and implement the syscall inline, lest we fail + * the checked return from any function call. + */ +#define enable_gcs(flags) \ + do { \ + register long num __asm__ ("x8") = __NR_prctl; \ + register long arg1 __asm__ ("x0") = PR_SET_SHADOW_STACK_STATUS; \ + register long arg2 __asm__ ("x1") = PR_SHADOW_STACK_ENABLE | flags; \ + register long arg3 __asm__ ("x2") = 0; \ + register long arg4 __asm__ ("x3") = 0; \ + register long arg5 __asm__ ("x4") = 0; \ + asm volatile("svc #0" \ + : "+r"(arg1) \ + : "r"(arg2), "r"(arg3), "r"(arg4), "r"(arg5), "r"(num) \ + : "memory", "cc"); \ + if (arg1) { \ + errno = -arg1; \ + perror("PR_SET_SHADOW_STACK_STATUS"); \ + exit(2); \ + } \ + } while (0) + +#define gcspr() \ + ({ uint64_t *r; asm volatile("mrs %0, s3_3_c2_c5_1" : "=r"(r)); r; }) + +#define gcsss1(val) \ + do { \ + asm volatile("sys #3, c7, c7, #2, %0" : : "r"(val) : "memory"); \ + } while (0) + +#define gcsss2() \ + ({ uint64_t *r; \ + asm volatile("sysl %0, #3, c7, c7, #3" : "=r"(r) : : "memory"); r; }) diff --git a/tests/tcg/aarch64/gcspushm.c b/tests/tcg/aarch64/gcspushm.c new file mode 100644 index 0000000..c330417 --- /dev/null +++ b/tests/tcg/aarch64/gcspushm.c @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "gcs.h" + + +#define GCSPUSHM "sys #3, c7, c7, #0, %[push]" +#define GCSPOPM "sysl %[pop], #3, c7, c7, #1" + +static void test_sigsegv(int sig, siginfo_t *info, void *vuc) +{ + ucontext_t *uc = vuc; + uint64_t inst_sigsegv; + + __asm__("adr %0, inst_sigsegv" : "=r"(inst_sigsegv)); + assert(uc->uc_mcontext.pc == inst_sigsegv); + assert(info->si_code == SEGV_CPERR); + /* TODO: Dig for ESR and verify syndrome. */ + uc->uc_mcontext.pc += 4; +} + +static void test_sigill(int sig, siginfo_t *info, void *vuc) +{ + ucontext_t *uc = vuc; + uint64_t inst_sigill; + + __asm__("adr %0, inst_sigill" : "=r"(inst_sigill)); + assert(uc->uc_mcontext.pc == inst_sigill); + assert(info->si_code == ILL_ILLOPC); + uc->uc_mcontext.pc += 4; +} + +int main() +{ + struct sigaction sa = { .sa_flags = SA_SIGINFO }; + uint64_t old, new; + + sa.sa_sigaction = test_sigsegv; + if (sigaction(SIGSEGV, &sa, NULL) < 0) { + perror("sigaction"); + exit(1); + } + + sa.sa_sigaction = test_sigill; + if (sigaction(SIGILL, &sa, NULL) < 0) { + perror("sigaction"); + exit(1); + } + + /* Pushm is disabled -- SIGILL via EC_SYSTEMREGISTERTRAP */ + asm volatile("inst_sigill:\t" GCSPUSHM + : : [push] "r" (1)); + + enable_gcs(PR_SHADOW_STACK_PUSH); + + /* Valid value -- low 2 bits clear */ + old = 0xdeadbeeffeedcaec; + asm volatile(GCSPUSHM "\n\t" GCSPOPM + : [pop] "=r" (new) + : [push] "r" (old) + : "memory"); + assert(old == new); + + /* Invalid value -- SIGSEGV via EC_GCS */ + asm volatile(GCSPUSHM "\n" + "inst_sigsegv:\t" GCSPOPM + : [pop] "=r" (new) + : [push] "r" (1) + : "memory"); + + exit(0); +} diff --git a/tests/tcg/aarch64/gcsss.c b/tests/tcg/aarch64/gcsss.c new file mode 100644 index 0000000..9550c68 --- /dev/null +++ b/tests/tcg/aarch64/gcsss.c @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "gcs.h" + +#define IN_PROGRESS(X) ((uint64_t)(X) | 5) +#define CAP(X) (((uint64_t)(X) & ~0xfff) + 1) + +static uint64_t * __attribute__((noinline)) recurse(size_t index) +{ + if (index == 0) { + return gcspr(); + } + return recurse(index - 1); +} + +int main() +{ + void *tmp; + uint64_t *alt_stack, *alt_cap; + uint64_t *orig_pr, *orig_cap; + uint64_t *bottom; + size_t pagesize = getpagesize(); + size_t words; + + enable_gcs(0); + orig_pr = gcspr(); + + /* Allocate a guard page before and after. */ + tmp = mmap(0, 3 * pagesize, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0); + assert(tmp != MAP_FAILED); + + /* map_shadow_stack won't replace existing mappings */ + munmap(tmp + pagesize, pagesize); + + /* Allocate a new stack between the guards. */ + alt_stack = (uint64_t *) + syscall(__NR_map_shadow_stack, tmp + pagesize, pagesize, + SHADOW_STACK_SET_TOKEN); + assert(alt_stack == tmp + pagesize); + + words = pagesize / 8; + alt_cap = alt_stack + words - 1; + + /* SHADOW_STACK_SET_TOKEN set the cap. */ + assert(*alt_cap == CAP(alt_cap)); + + /* Swap to the alt stack, one step at a time. */ + gcsss1(alt_cap); + + assert(gcspr() == alt_cap); + assert(*alt_cap == IN_PROGRESS(orig_pr)); + + orig_cap = gcsss2(); + + assert(orig_cap == orig_pr - 1); + assert(*orig_cap == CAP(orig_cap)); + assert(gcspr() == alt_stack + words); + + /* We should be able to use the whole stack. */ + bottom = recurse(words - 1); + assert(bottom == alt_stack); + + /* We should be back where we started. */ + assert(gcspr() == alt_stack + words); + + /* Swap back to the original stack. */ + gcsss1(orig_cap); + tmp = gcsss2(); + + assert(gcspr() == orig_pr); + assert(tmp == alt_cap); + + exit(0); +} diff --git a/tests/tcg/aarch64/gcsstr.c b/tests/tcg/aarch64/gcsstr.c new file mode 100644 index 0000000..b045aee --- /dev/null +++ b/tests/tcg/aarch64/gcsstr.c @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "gcs.h" + +/* + * A single garbage store to the gcs stack. + * The asm inside must be unique, so disallow inlining. + */ +void __attribute__((noinline)) +test_gcsstr(void) +{ + register uint64_t *ptr __asm__("x0") = gcspr(); + /* GCSSTR x1, x0 */ + __asm__("inst_gcsstr: .inst 0xd91f1c01" : : "r"(--ptr)); +} + +static void test_sigsegv(int sig, siginfo_t *info, void *vuc) +{ + ucontext_t *uc = vuc; + uint64_t inst_gcsstr; + + __asm__("adr %0, inst_gcsstr" : "=r"(inst_gcsstr)); + assert(uc->uc_mcontext.pc == inst_gcsstr); + assert(info->si_code == SEGV_CPERR); + /* TODO: Dig for ESR and verify syndrome. */ + exit(0); +} + +int main() +{ + struct sigaction sa = { + .sa_sigaction = test_sigsegv, + .sa_flags = SA_SIGINFO, + }; + + /* Enable GCSSTR and test the store succeeds. */ + enable_gcs(PR_SHADOW_STACK_WRITE); + test_gcsstr(); + + /* Disable GCSSTR and test the resulting sigsegv. */ + enable_gcs(0); + if (sigaction(SIGSEGV, &sa, NULL) < 0) { + perror("sigaction"); + exit(1); + } + test_gcsstr(); + abort(); +} |