diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..e44b8108 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E501 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..24b51c7c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,50 @@ +# Repository Instructions for GitHub Copilot + +## Building and Testing + +Rules: +- Run "make" in the repository root to build all code, documentation, + and tests. +- Never attempt to compile only a subset of the repository. + Always build the entire repository. +- If the build does not rebuild the files you changed, you can assume that + another user already ran a build that produced the same output. You can skip the build and run tests directly. +- Test your changes by actually running the test using: + + ./xtf-runner + + Example: When testing the `tests/nested-svm-clgi-stgi` test, run: + + ./xtf-runner nested-svm-clgi-stgi + +- Only after building and running tests should you run the final + validation command for Copilot-authored changes. + +## CI validation + +This repository uses `pre-commit` in CI. + +It checks for code formatting, linting, and other issues in code, +build system, documentation, and tests. It is configured to run on +all files in the repository. + +Among other checks, it runs: +- Adds missing trailing newlines to all files (add a trailing newline yourself) +- Removes trailing whitespace from all files. +- `isort`, `black`, and `flake8` for Python code. + +The full list of checks is in the `.pre-commit-config.yaml` file in the +repository. + +After making code, build-system, documentation, or test changes in this +repository, run: + +```sh +SKIP=git-diff pre-commit run -av +``` + +Treat this as the final validation command for Copilot-authored changes. +If the command is unavailable or cannot complete in the current environment, +report that clearly along with any narrower checks that were run instead +and tell a human reviewer to install pre-commit using `pip install pre-commit` +and run the full command before merging. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 3041fc0d..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: build - -on: [push, pull_request] - -jobs: - python: - name: "Python Tests" - - runs-on: ubuntu-22.04 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: pre-commit checks - setup cache - uses: actions/cache@v4 - with: - path: ~/.cache/pre-commit - key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - - - name: pre-commit checks - run checks - uses: pre-commit/action@v3.0.1 - - C: - name: "C Builds" - - strategy: - matrix: - arch: [x86] - compiler: [llvm-12, llvm-13, llvm-14] - - include: - - arch: x86 - compiler: gcc-9 - - arch: x86 - compiler: gcc-10 - - arch: x86 - compiler: gcc-11 - - arch: x86 - compiler: gcc-12 - - - arch: x86 - compiler: clang-12 - - arch: x86 - compiler: clang-13 - - arch: x86 - compiler: clang-14 - - runs-on: ubuntu-22.04 - - steps: - - name: Install - run: | - c=${{matrix.compiler}} - v=${c##llvm-} - case $c in - # Need all {llvm,clang,lld}-$v packages - llvm-*) EXTRA="clang-${v} lld-${v}" ;; - esac - - sudo apt-get update -q - sudo apt-get install -y build-essential python3 ${{matrix.compiler}} ${EXTRA} - - - uses: actions/checkout@v4 - - - name: Build - run: | - # Select appropriate LLVM= or CC= - c=${{matrix.compiler}} - case $c in - llvm-*) COMP="LLVM=${c#llvm}" ;; - *) COMP="CC=$c" ;; - esac - - make -j`nproc` ARCH=${{matrix.arch}} $CROSS $COMP diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f754b55d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +# Some organisations do not allow workflows to run on push events, +# so we only run on pull requests. +on: + - pull_request + +jobs: + python: + name: "Pre-commit Checks" + runs-on: ubuntu-24.04 + steps: + # Some organisations require pinning of actions to a specific commit SHA. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # Some organisations allow only verified actions to be used in workflows. + # The owner of this pre-commit action is verified: + - uses: cloudposse/github-action-pre-commit@ed9906221c8a4ee1dcb1e665da895998e0f7b396 # v4.1.0 + + C: + name: "C Builds" + + strategy: + matrix: + arch: [x86] + compiler: [llvm-18, llvm-19, gcc-12, gcc-13, gcc-14] + runs-on: ubuntu-24.04 + + steps: + - name: Install + run: | + c=${{matrix.compiler}} + v=${c##llvm-} + case $c in + # Need all {clang,lld}-$v packages + llvm-*) EXTRA="clang-${v} lld-${v}" ;; + esac + + sudo apt-get update -q + sudo apt-get install -y ${{matrix.compiler}} ${EXTRA} + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Build + run: | + # Select appropriate LLVM= or CC= + c=${{matrix.compiler}} + case $c in + llvm-*) COMP="LLVM=${c#llvm}" ;; + *) COMP="CC=$c" ;; + esac + + make -j`nproc` ARCH=${{matrix.arch}} $CROSS $COMP + + - name: scan-build + if: matrix.compiler == 'llvm-19' + run: | + sudo apt-get install -y clang-tools-19 + scan-build-19 --status-bugs -analyze-headers make -j`nproc` diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ac4cf03a..f480ca16 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,6 +9,12 @@ on: jobs: analyse: + # Only run CodeQL analysis on the main repository, not forks + # because forks may not have CodeQL analysis enabled and we + # don't want to run CodeQL analysis on forks to avoid wasting + # GitHub Actions minutes on forks. For test code, CodeQL analysis + # is not required, so we only run it on the main repository. + if: ${{ github.repository_owner == 'andyhhp' }} strategy: matrix: @@ -17,13 +23,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: github/codeql-action/init@v3 + - uses: github/codeql-action/init@v4 with: languages: ${{matrix.lang}} queries: security-and-quality - - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/autobuild@v4 - - uses: github/codeql-action/analyze@v3 + - uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml deleted file mode 100644 index 2e232e82..00000000 --- a/.github/workflows/scan-build.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: scan-build - -on: [push, pull_request] - -jobs: - scan-build: - - runs-on: ubuntu-22.04 - - steps: - - name: Install - run: | - sudo apt-get update -q - sudo apt-get install clang-tools-14 - - - uses: actions/checkout@v4 - - - name: Scan-build - run: | - scan-build-14 --status-bugs -analyze-headers make -j`nproc` diff --git a/.gitignore b/.gitignore index dcacdf3b..5402e643 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,12 @@ *.pyc *.pyo *.swp +/.ninja_deps +/.ninja_log /arch/*/*.lds +/build/.xtf.ninja.*.context +/build/xtf.ninja +/build/xtf.ninja.vars /cscope.* /dist/ /docs/autogenerated/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f1c3ce4..6b9d4d24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,14 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks # +# Installation notes: +# - Install pre-commit, e.g. via pip: +# pip install pre-commit +# - Enable it as git pre-commit hook for the current repository: +# pre-commit install fail_fast: false -default_stages: [commit, push] +default_stages: [pre-commit, pre-push] repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -23,16 +28,61 @@ repos: - id: destroyed-symlinks - id: end-of-file-fixer - id: fix-byte-order-marker - - id: fix-encoding-pragma - id: mixed-line-ending args: ['--fix=lf'] - id: trailing-whitespace -- repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + +# This is the pre-commit hook to run black, the Python code formatter. +- repo: https://github.com/psf/black + rev: 26.5.1 + hooks: + - id: black + # We use the --skip-string-normalization option to preserve the + # minimise churn initially, leaving string normalization for + # later cleanup commit to not mix with the formatting changes. + args: ['--skip-string-normalization'] + + +# This is the isort hook recommended for use with black +- repo: https://github.com/pycqa/isort + rev: 8.0.1 + hooks: + - id: isort + args: ['--profile', 'black'] + + +# This is the flake8 hook used by black itself: +- repo: https://github.com/pycqa/flake8 + rev: 7.3.0 hooks: - - id: reorder-python-imports + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify + + +# This is the mypy hook: +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v2.1.0 + hooks: + - id: mypy + args: [--strict] + exclude: ^docs/gdb-test-fw/reference/ + additional_dependencies: + - pytest + - types-gdb + + +# This is the pylint hook: +- repo: https://github.com/PyCQA/pylint + rev: v4.0.5 + hooks: + - id: pylint + +# Custom hook to show not staged changes, which may be caused by fixup commits. - repo: local hooks: - id: git-diff # https://github.com/pre-commit/pre-commit/issues/1712 diff --git a/.pylintrc b/.pylintrc index 7c7e50c4..08bfae3a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -8,9 +8,6 @@ # pygtk.require(). #init-hook= -# Profiled execution. -profile=no - # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS @@ -37,7 +34,7 @@ extension-pkg-whitelist= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. -#enable= +enable=unneeded-not,consider-using-set-comprehension # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this @@ -48,7 +45,7 @@ extension-pkg-whitelist= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=bad-whitespace, bad-continuation, global-statement, star-args +disable=import-error,global-statement,unspecified-encoding [REPORTS] @@ -58,11 +55,6 @@ disable=bad-whitespace, bad-continuation, global-statement, star-args # mypackage.mymodule.MyReporterClass. output-format=text -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - # Tells whether to display a full report or only the messages reports=no @@ -73,10 +65,6 @@ reports=no # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (RP0004). -comment=no - # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= @@ -118,10 +106,6 @@ ignored-modules= # (useful for classes with attributes dynamically set). ignored-classes=SQLObject -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no - # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. @@ -138,7 +122,7 @@ logging-modules=logging [FORMAT] # Maximum number of characters on a single line. -max-line-length=80 +max-line-length=88 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ @@ -147,9 +131,6 @@ ignore-long-lines=^\s*(# )??$ # else. single-line-if-stmt=no -# List of optional constructs for which whitespace checking is disabled -no-space-check=trailing-comma,dict-separator - # Maximum number of lines in a module max-module-lines=1000 @@ -163,12 +144,6 @@ indent-after-paren=4 [BASIC] -# Required attributes for module, separated by a comma -required-attributes= - -# List of builtins function names that should not be used, separated by a comma -bad-functions=apply,input - # Good variable names which should always be accepted, separated by a comma good-names=e,i,j,k,ex,Run,_ @@ -179,68 +154,35 @@ bad-names=foo,bar,baz,toto,tutu,tata # the name regexes allow several styles. name-group= -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - # Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{1,35}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ +function-rgx=[a-z_][a-z0-9_]{1,45}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{1,35}$ -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct constant names const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{1,35}$ -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{1,35}$ -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_-]*)|([A-Z][a-zA-Z0-9-]+))$ -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - # Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{1,35}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ +method-rgx=[a-z_][a-z0-9_]{1,45}$ # Regular expression which should only match function or class names that do # not require a docstring. @@ -285,10 +227,6 @@ int-import-graph= [CLASSES] -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp @@ -302,14 +240,15 @@ valid-metaclass-classmethod-first-arg=mcs [DESIGN] # Maximum number of arguments for function / method -max-args=5 +max-args=7 +max-positional-arguments=7 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body -max-locals=50 +max-locals=60 # Maximum number of return / yield for function / method body max-returns=250 @@ -337,4 +276,4 @@ max-public-methods=20 # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/Makefile b/Makefile index d98a4af8..47b179ef 100644 --- a/Makefile +++ b/Makefile @@ -59,21 +59,83 @@ export CC LD CPP INSTALL INSTALL_DATA INSTALL_DIR INSTALL_PROGRAM OBJCOPY PYTHON # By default enable all the tests TESTS ?= $(wildcard $(ROOT)/tests/*) -.PHONY: all -all: - @set -e; for D in $(TESTS); do \ - [ ! -e $$D/Makefile ] && continue; \ - $(MAKE) -C $$D build; \ - done +# Prefer Ninja when it is available, but keep the recursive make path as the +# fallback and as an explicit override via USE_MAKE=1. +NINJA_AVAILABLE := $(if $(shell command -v ninja 2>/dev/null),1,0) +USE_MAKE ?= $(if $(NINJA_AVAILABLE),0,1) + +ACTIVE_GOALS := $(if $(MAKECMDGOALS),$(MAKECMDGOALS),all) +NINJA_GOALS := ninja-vars ninja-file ninja-build ninja-install +METADATA_GOALS := $(NINJA_GOALS) +COMMON_GOALS := $(NINJA_GOALS) + +ifeq ($(USE_MAKE),0) +METADATA_GOALS += all install +COMMON_GOALS += all install +endif + +ifneq ($(filter $(METADATA_GOALS),$(ACTIVE_GOALS)),) +include $(ROOT)/build/load-tests.mk +endif + +ifneq ($(filter $(COMMON_GOALS),$(ACTIVE_GOALS)),) +include $(ROOT)/build/common.mk +endif + +# Convert the selected test directories into explicit top-level targets so GNU +# make can schedule independent tests in parallel, rather than hiding the work +# behind one shell loop. + +TEST_MAKEFILES := $(wildcard $(TESTS:%=%/Makefile)) +BUILD_TARGETS := $(patsubst %/Makefile,%/.build,$(TEST_MAKEFILES)) +INSTALL_TARGETS := $(patsubst %/Makefile,%/.install,$(TEST_MAKEFILES)) + +# Multiple test sub-makes rebuild the same objects under common/ and arch/. +# Seed those shared artefacts once before the parallel fan-out, but skip the +# bootstrap entirely when only one test was selected so TESTS filtering keeps +# its expected no-op behaviour. + +ifneq ($(word 2,$(BUILD_TARGETS)),) +SHARED_BOOTSTRAP_TARGET := $(firstword $(BUILD_TARGETS:.build=.shared-ready)) +endif + +ifeq ($(USE_MAKE),0) + +.PHONY: all install +all: ninja-build + +install: ninja-install + +else + +.PHONY: all $(BUILD_TARGETS) $(INSTALL_TARGETS) +all: $(SHARED_BOOTSTRAP_TARGET) $(BUILD_TARGETS) + +# Leading '+' preserves jobserver recursion when the parent was run with -j. +$(SHARED_BOOTSTRAP_TARGET): + +$(MAKE) -C $(@D) build + +# Each selected test directory now appears as a first-class prerequisite. +$(BUILD_TARGETS): | $(SHARED_BOOTSTRAP_TARGET) + +$(MAKE) -C $(@D) build .PHONY: install install: @$(INSTALL_DIR) $(DESTDIR)$(xtfdir) $(INSTALL_PROGRAM) xtf-runner $(DESTDIR)$(xtfdir) - @set -e; for D in $(TESTS); do \ - [ ! -e $$D/Makefile ] && continue; \ - $(MAKE) -C $$D install; \ - done + @find xtf -path '*/__pycache__' -prune -o -name '*.py' -print | \ + while read -r f; do \ + d="$(DESTDIR)$(xtfdir)/$$(dirname "$$f")"; \ + $(INSTALL_DIR) "$$d"; \ + $(INSTALL_DATA) "$$f" "$$d"; \ + done + +install: $(SHARED_BOOTSTRAP_TARGET) $(INSTALL_TARGETS) + +$(INSTALL_TARGETS): | $(SHARED_BOOTSTRAP_TARGET) + +$(MAKE) -C $(@D) install + +endif define all_sources find include/ arch/ common/ tests/ -name "*.[hcsS]" @@ -84,6 +146,104 @@ cscope: $(all_sources) > cscope.files cscope -b -q -k +NINJA_CONTEXT_HASH := $(shell \ + printf '%s\n' \ + '$(sort $(TESTS))' \ + '$(CC)' \ + '$(CPP)' \ + '$(LD)' \ + '$(OBJCOPY)' \ + '$(PYTHON)' \ + '$(LLVM)' \ + '$(CROSS_COMPILE)' \ + | sha1sum | cut -d' ' -f1) +NINJA_CONTEXT_STAMP := $(ROOT)/build/.xtf.ninja.$(NINJA_CONTEXT_HASH).context +NINJA_VARS_FILE := $(ROOT)/build/xtf.ninja.vars +NINJA_FILE := $(ROOT)/build/xtf.ninja +HVM64_FORMAT := $(firstword \ + $(filter elf32-x86-64,$(shell $(OBJCOPY) --help)) \ + elf32-i386) +NINJA_METADATA_INPUTS := \ + $(ROOT)/Makefile \ + $(ROOT)/build/common.mk \ + $(ROOT)/build/core.mk \ + $(ROOT)/build/files.mk \ + $(ROOT)/build/gen.mk \ + $(ROOT)/build/load-tests.mk \ + $(ROOT)/build/gen-ninja.py \ + $(TEST_MAKEFILES) \ + $(wildcard $(ROOT)/Makefile.local) + +.PHONY: ninja-vars ninja-file ninja-build ninja-install + +ninja-vars: + @printf 'global\t%s\t%s\n' ROOT '$(ROOT)' + @printf 'global\t%s\t%s\n' CC '$(CC)' + @printf 'global\t%s\t%s\n' CPP '$(CPP)' + @printf 'global\t%s\t%s\n' LD '$(LD)' + @printf 'global\t%s\t%s\n' OBJCOPY '$(OBJCOPY)' + @printf 'global\t%s\t%s\n' PYTHON '$(PYTHON)' + @printf 'global\t%s\t%s\n' DESTDIR '$(DESTDIR)' + @printf 'global\t%s\t%s\n' xtfdir '$(xtfdir)' + @printf 'global\t%s\t%s\n' xtftestdir '$(xtftestdir)' + @printf 'global\t%s\t%s\n' INSTALL '$(INSTALL)' + @printf 'global\t%s\t%s\n' INSTALL_DATA '$(INSTALL_DATA)' + @printf 'global\t%s\t%s\n' INSTALL_DIR '$(INSTALL_DIR)' + @printf 'global\t%s\t%s\n' INSTALL_PROGRAM '$(INSTALL_PROGRAM)' + @printf 'global\t%s\t%s\n' HVM64_FORMAT '$(HVM64_FORMAT)' + @printf 'objects\t%s\t%s\n' perbits '$(obj-perbits)' + @printf 'objects\t%s\t%s\n' perenv '$(obj-perenv)' + @$(foreach env,$(ALL_ENVIRONMENTS), \ + printf 'env\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + '$(env)' \ + '$($(env)_guest)' \ + '$($(env)_arch)' \ + '$(AFLAGS_$($(env)_arch))' \ + '$(CFLAGS_$($(env)_arch))' \ + '$(AFLAGS_$(env))' \ + '$(CFLAGS_$(env))' \ + '$(link-$(env))' \ + '$(LDFLAGS_$(env))' \ + '$(defcfg-$($(env)_guest))'; \ + ) + @$(foreach env,$(ALL_ENVIRONMENTS), \ + printf 'env_objects\t%s\t%s\n' \ + '$(env)' \ + '$(obj-$(env))'; \ + ) + @$(foreach key,$(REGISTERED_TESTS), \ + printf 'test\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + '$(key)' \ + '$(TEST_DIR_$(key))' \ + '$(TEST_NAME_$(key))' \ + '$(TEST_CATEGORY_$(key))' \ + '$(TEST_ENVS_$(key))' \ + '$(TEST_EXTRA_CFG_$(key))' \ + '$(TEST_VARY_CFG_$(key))' \ + '$(TEST_VCPUS_$(key))' \ + '$(TEST_LOCAL_OBJ_PERENV_$(key))'; \ + ) + +$(NINJA_CONTEXT_STAMP): + @mkdir -p $(dir $@) + @: > $@ + +$(NINJA_VARS_FILE): $(NINJA_CONTEXT_STAMP) $(NINJA_METADATA_INPUTS) + @$(MAKE) -s ninja-vars TESTS='$(TESTS)' > $@.tmp + @if ! cmp -s $@.tmp $@ 2>/dev/null; then mv -f $@.tmp $@; else rm -f $@.tmp; fi + +$(NINJA_FILE): $(NINJA_VARS_FILE) $(ROOT)/build/gen-ninja.py + @cd $(ROOT) && $(PYTHON) build/gen-ninja.py $(NINJA_VARS_FILE) $@.tmp + @if ! cmp -s $@.tmp $@ 2>/dev/null; then mv -f $@.tmp $@; else rm -f $@.tmp; fi + +ninja-file: $(NINJA_FILE) + +ninja-build: ninja-file + cd $(ROOT) && ninja -f $(NINJA_FILE) + +ninja-install: ninja-file + cd $(ROOT) && ninja -f $(NINJA_FILE) install + .PHONY: gtags gtags: $(all_sources) | gtags -f - @@ -93,6 +253,8 @@ clean: find . \( -name "*.o" -o -name "*.d" -o -name "*.lds" \) -delete find tests/ \( -perm -a=x -name "test-*" -o -name "test-*.cfg" \ -o -name "info.json" \) -delete + rm -f $(ROOT)/build/{.,}xtf.ninja* .ninja_deps .ninja_log \ + $(NINJA_VARS_FILE) $(NINJA_FILE) .PHONY: distclean distclean: clean @@ -105,4 +267,8 @@ doxygen: Doxyfile .PHONY: pylint pylint: - -pylint --rcfile=.pylintrc xtf-runner + -pylint --rcfile=.pylintrc xtf-runner xtf + +.PHONY: runner-selftest +runner-selftest: + $(PYTHON) -m xtf.runner.selftest diff --git a/build/common.mk b/build/common.mk index a6e270f5..9da0dbe6 100644 --- a/build/common.mk +++ b/build/common.mk @@ -1,4 +1,12 @@ -ALL_CATEGORIES := special functional xsa utility in-development +ifeq ($(XTF_METADATA_ONLY),1) + +# In metadata-loading mode, the top-level loader already provided the shared +# environment constants. This include stays as a compatibility shim so current +# test Makefiles can be parsed without emitting build rules. + +else + +ALL_CATEGORIES := special functional xsa utility in-development nested-svm ALL_ENVIRONMENTS := pv64 pv32pae hvm64 hvm32pae hvm32pse hvm32 @@ -6,6 +14,7 @@ PV_ENVIRONMENTS := $(filter pv%,$(ALL_ENVIRONMENTS)) HVM_ENVIRONMENTS := $(filter hvm%,$(ALL_ENVIRONMENTS)) 32BIT_ENVIRONMENTS := $(filter pv32% hvm32%,$(ALL_ENVIRONMENTS)) 64BIT_ENVIRONMENTS := $(filter pv64% hvm64%,$(ALL_ENVIRONMENTS)) +SVM_ENVIRONMENTS := hvm64 # $(env)_guest => pv or hvm mapping $(foreach env,$(PV_ENVIRONMENTS),$(eval $(env)_guest := pv)) @@ -17,7 +26,22 @@ $(foreach env,$(64BIT_ENVIRONMENTS),$(eval $(env)_arch := x86_64)) comma := , -COMMON_FLAGS := -pipe -nostdinc -I$(ROOT)/include -I$(ROOT)/arch/x86/include -MMD -MP +COMMON_FLAGS := -pipe -nostdinc -Iinclude -Iarch/x86/include -MMD -MP + +# Compile from the repository root so diagnostics use workspace-relative paths +# even when the owning makefile lives in a test subdirectory. +CURDIR_REL := $(patsubst $(ROOT)/%,%,$(CURDIR)) +VPATH := $(ROOT) + +# Convert make's per-test view of a path into the root-relative spelling used +# on the compiler command line after `cd $(ROOT)`. +root-path = $(patsubst $(ROOT)/%,%,$(if $(filter /%,$(1)),$(1),$(if $(CURDIR_REL),$(CURDIR_REL)/$(1),$(1)))) + +# Dependency files still live next to their outputs so the existing `-include` +# sites keep working from recursive sub-makes, but we address them via an +# absolute path while generating them from the repository root. +dep-path = $(call root-path,$(patsubst %.o,%.d,$(patsubst %.lds,%.d,$(1)))) +dep-file = $(if $(filter /%,$(1)),$(patsubst %.o,%.d,$(patsubst %.lds,%.d,$(1))),$(ROOT)/$(call dep-path,$(1))) cc-option = $(shell if [ -z "`echo 'int p=1;' | $(CC) $(1) -c -o /dev/null -x c - 2>&1`" ]; \ then echo y; else echo n; fi) @@ -73,23 +97,32 @@ DEPS-$(1) = \ # Generate .lds with appropriate flags %/link-$(1).lds: $(ROOT)/common/link.lds.S - $$(CPP) $$(AFLAGS_$(1)) -P $$< -o $$@ + # Run the preprocessor from $(ROOT) so diagnostics mention e.g. arch/... + # and tests/... instead of paths relative to the current test directory. + cd $(ROOT) && $$(CPP) $$(AFLAGS_$(1)) -MT $$@ -MF $$(call dep-file,$$@) -P \ + $$(call root-path,$$<) -o $$(call root-path,$$@) # Generate a per-arch .o from .S %-$($(1)_arch).o: %.S - $$(CC) $$(AFLAGS_$($(1)_arch)) -c $$< -o $$@ + # The command runs from $(ROOT), but the target name remains the original + # object path so the rest of the dependency graph does not change. + cd $(ROOT) && $$(CC) $$(AFLAGS_$($(1)_arch)) -MT $$@ -MF $$(call dep-file,$$@) -c \ + $$(call root-path,$$<) -o $$(call root-path,$$@) # Generate a per-arch .o from .c %-$($(1)_arch).o: %.c - $$(CC) $$(CFLAGS_$($(1)_arch)) -c $$< -o $$@ + cd $(ROOT) && $$(CC) $$(CFLAGS_$($(1)_arch)) -MT $$@ -MF $$(call dep-file,$$@) -c \ + $$(call root-path,$$<) -o $$(call root-path,$$@) # Generate a per-env .o from .S %-$(1).o: %.S - $$(CC) $$(AFLAGS_$(1)) -c $$< -o $$@ + cd $(ROOT) && $$(CC) $$(AFLAGS_$(1)) -MT $$@ -MF $$(call dep-file,$$@) -c \ + $$(call root-path,$$<) -o $$(call root-path,$$@) # Generate a per-env .o from .c %-$(1).o: %.c - $$(CC) $$(CFLAGS_$(1)) -c $$< -o $$@ + cd $(ROOT) && $$(CC) $$(CFLAGS_$(1)) -MT $$@ -MF $$(call dep-file,$$@) -c \ + $$(call root-path,$$<) -o $$(call root-path,$$@) endef @@ -98,3 +131,5 @@ $(foreach env,$(ALL_ENVIRONMENTS),$(eval $(call PERENV_setup,$(env)))) define move-if-changed if ! cmp -s $(1) $(2); then mv -f $(1) $(2); else rm -f $(1); fi endef + +endif diff --git a/build/core.mk b/build/core.mk new file mode 100644 index 00000000..8315d201 --- /dev/null +++ b/build/core.mk @@ -0,0 +1,17 @@ +ALL_CATEGORIES := special functional xsa utility in-development nested-svm + +ALL_ENVIRONMENTS := pv64 pv32pae hvm64 hvm32pae hvm32pse hvm32 + +PV_ENVIRONMENTS := $(filter pv%,$(ALL_ENVIRONMENTS)) +HVM_ENVIRONMENTS := $(filter hvm%,$(ALL_ENVIRONMENTS)) +32BIT_ENVIRONMENTS := $(filter pv32% hvm32%,$(ALL_ENVIRONMENTS)) +64BIT_ENVIRONMENTS := $(filter pv64% hvm64%,$(ALL_ENVIRONMENTS)) +SVM_ENVIRONMENTS := hvm64 + +# $(env)_guest => pv or hvm mapping +$(foreach env,$(PV_ENVIRONMENTS),$(eval $(env)_guest := pv)) +$(foreach env,$(HVM_ENVIRONMENTS),$(eval $(env)_guest := hvm)) + +# $(env)_arch => x86_32/64 mapping +$(foreach env,$(32BIT_ENVIRONMENTS),$(eval $(env)_arch := x86_32)) +$(foreach env,$(64BIT_ENVIRONMENTS),$(eval $(env)_arch := x86_64)) diff --git a/build/gen-ninja.py b/build/gen-ninja.py new file mode 100755 index 00000000..3b3eac74 --- /dev/null +++ b/build/gen-ninja.py @@ -0,0 +1,717 @@ +#!/usr/bin/env python3 +"""Generate build/xtf.ninja from the metadata manifest emitted by Make. + +The top-level Makefile first reduces the current build configuration to a small +tab-separated manifest. This script turns that manifest into the concrete +Ninja file used to build the selected tests and install their outputs. + +At a high level, the script follows the same structure as the recursive make +path: read the build configuration, resolve the per-environment and per-test +data, then emit the concrete commands needed for objects, linker scripts, +binaries, cfg files, metadata files, and install targets. +""" + +import os +import sys +from dataclasses import dataclass + + +@dataclass +class EnvInfo: + """Describe one concrete build environment loaded from the manifest. + + Each instance contains the already-expanded flag strings and file paths + needed to emit build statements for one environment such as pv64 or hvm32pae. + """ + + guest: str + arch: str + aflags_arch: str + cflags_arch: str + aflags_env: str + cflags_env: str + link: str + ldflags: str + defcfg: str + + +@dataclass +class TestInfo: + """Hold all manifest data needed to emit one test directory. + + The generator uses this record to create the per-test outputs that would + otherwise be expanded by the recursive make path: binaries, cfg files, + variation cfg files, metadata, and install copies. + """ + + key: str + directory: str + name: str + category: str + envs: list[str] + extra_cfg: str + vary_cfg: list[str] + vcpus: str + local_objs: list[str] + + +def split_words(value: str) -> list[str]: + """Split one manifest field into whitespace-separated words. + + The manifest stores make list values in plain text fields, so this helper + recreates the corresponding Python list representation. + """ + + return [word for word in value.split() if word] + + +def parse_manifest(path: str) -> tuple[ + dict[str, str], + dict[str, EnvInfo], + dict[str, list[str]], + list[TestInfo], + list[str], + list[str], +]: + """Parse the Make-generated manifest into typed Python structures. + + The manifest is line-oriented and tab-separated. Each record starts with a + kind tag such as global, env, or test followed by the fields for that + record type. + + The parsing work is deliberately kept simple and explicit because the input + format is produced by the Makefile rather than by a schema-aware tool. The + result mirrors the manifest's own structure: + + * globals_map holds scalar tool and path settings. + * envs holds one EnvInfo per named runtime environment. + * env_objects holds per-environment object lists. + * tests holds one TestInfo per selected test directory. + * objects_perbits and objects_perenv keep the shared object lists. + + Keeping those groups separate makes the later emission code easier to read. + The generator can iterate over the same conceptual pieces that exist in the + build system instead of repeatedly unpacking raw tab-separated strings. + """ + + globals_map: dict[str, str] = {} + envs: dict[str, EnvInfo] = {} + env_objects: dict[str, list[str]] = {} + tests: list[TestInfo] = [] + objects_perbits: list[str] = [] + objects_perenv: list[str] = [] + + with open(path, encoding="utf-8") as handle: + for raw_line in handle: + line = raw_line.rstrip("\n") + if not line: + continue + fields = line.split("\t") + kind = fields[0] + + if kind == "global": + _, key, value = fields + globals_map[key] = value + elif kind == "objects": + _, scope, value = fields + if scope == "perbits": + objects_perbits = split_words(value) + elif scope == "perenv": + objects_perenv = split_words(value) + elif kind == "env": + ( + _, + name, + guest, + arch, + aflags_arch, + cflags_arch, + aflags_env, + cflags_env, + link, + ldflags, + defcfg, + ) = fields + envs[name] = EnvInfo( + guest=guest, + arch=arch, + aflags_arch=aflags_arch, + cflags_arch=cflags_arch, + aflags_env=aflags_env, + cflags_env=cflags_env, + link=link, + ldflags=ldflags, + defcfg=defcfg, + ) + elif kind == "env_objects": + _, name, value = fields + env_objects[name] = split_words(value) + elif kind == "test": + ( + _, + key, + directory, + name, + category, + env_list, + extra_cfg, + vary_cfg, + vcpus, + local_objs, + ) = fields + tests.append( + TestInfo( + key=key, + directory=directory, + name=name, + category=category, + envs=split_words(env_list), + extra_cfg=extra_cfg, + vary_cfg=split_words(vary_cfg), + vcpus=vcpus, + local_objs=split_words(local_objs), + ) + ) + else: + raise ValueError(f"Unexpected manifest line: {line}") + + return globals_map, envs, env_objects, tests, objects_perbits, objects_perenv + + +def to_rel(root: str, path: str) -> str: + """Return a path in the form expected by the generated Ninja file. + + The manifest can contain either absolute or already-relative paths. Ninja + files are generated relative to the repository root, so absolute paths are + normalised back to that form here. + """ + + if not path: + return "" + if os.path.isabs(path): + return os.path.relpath(path, root) + return path + + +def source_for_obj(root: str, obj: str) -> tuple[str, str]: + """Resolve one object name to its source path and compile mode. + + XTF object lists name the target object, not the original source. This + helper probes for .c and .S siblings and returns both the chosen + source file and the Ninja rule name that should compile it. + """ + + rel_obj = to_rel(root, obj) + c_src = rel_obj[:-2] + ".c" + s_src = rel_obj[:-2] + ".S" + + if os.path.exists(os.path.join(root, c_src)): + return c_src, "cc" + if os.path.exists(os.path.join(root, s_src)): + return s_src, "as" + + raise FileNotFoundError(f"No source found for object {obj}") + + +def depfile_for(output: str) -> str: + """Map a generated output file to its dependency file name.""" + + if output.endswith(".lds"): + return output[:-4] + ".d" + if output.endswith(".o"): + return output[:-2] + ".d" + raise ValueError(f"No depfile mapping for {output}") + + +def emit_rule( + lines: list[str], + name: str, + command: str, + *, + depfile: str | None = None, + deps: str | None = None, +) -> None: + """Append a reusable Ninja command definition. + + In Ninja terminology, a rule names the command template, while the real + per-file work is described later by build lines that reference that rule. + """ + + lines.append(f"rule {name}") + lines.append(f" command = {command}") + if depfile is not None: + lines.append(f" depfile = {depfile}") + if deps is not None: + lines.append(f" deps = {deps}") + lines.append("") + + +def emit_phony(lines: list[str], output: str, inputs: list[str]) -> None: + """Append a phony target. + + This is the Ninja equivalent of a grouping target with no recipe of its own + whose purpose is to collect other concrete outputs behind a stable name. + """ + + emit_build(lines, output, "phony", inputs) + + +def emit_build( + lines: list[str], + output: str, + rule: str, + inputs: list[str], + variables: dict[str, str] | None = None, + implicit_inputs: list[str] | None = None, +) -> None: + """Append one concrete Ninja build statement. + + For maintainers used to GNU make, this is the closest equivalent to writing + out one fully expanded target rule after all variables have been resolved. + implicit_inputs are emitted after | so Ninja tracks them as + dependencies without adding them to the command line itself. + """ + + line = f"build {output}: {rule}" + if inputs: + line += " " + " ".join(inputs) + if implicit_inputs: + line += " | " + " ".join(implicit_inputs) + lines.append(line) + if variables: + for key, value in variables.items(): + lines.append(f" {key} = {value}") + lines.append("") + + +def python_modules(root: str, package: str) -> list[str]: + """Return Python source files under package, excluding cache directories.""" + + modules: list[str] = [] + package_root = os.path.join(root, package) + + for dirpath, dirnames, filenames in os.walk(package_root): + dirnames[:] = [name for name in dirnames if name != "__pycache__"] + + for filename in filenames: + if not filename.endswith(".py"): + continue + modules.append(os.path.relpath(os.path.join(dirpath, filename), root)) + + return sorted(modules) + + +def build_ninja( + root: str, + globals_map: dict[str, str], + envs: dict[str, EnvInfo], + env_objects: dict[str, list[str]], + tests: list[TestInfo], + objects_perbits: list[str], + objects_perenv: list[str], +) -> list[str]: + """Construct the full Ninja file as a list of text lines. + + The implementation works in two stages. First it emits the small set of + reusable command definitions shared by the whole file. Then it walks every + selected test and every enabled environment for that test, emitting the + concrete statements needed to build the required outputs. + + Ninja often calls those concrete statements edges in the dependency + graph. In GNU make terms, you can read them as explicit instantiated rules + connecting a specific output file to the exact inputs and command variables + needed to rebuild it. + + The returned list is written directly to build/xtf.ninja by main(). + """ + + cc = globals_map["CC"] + cpp = globals_map["CPP"] + ld = globals_map["LD"] + objcopy = globals_map["OBJCOPY"] + python = globals_map["PYTHON"] + destdir = globals_map["DESTDIR"] + hvm64_format = globals_map["HVM64_FORMAT"] + install_data = globals_map["INSTALL_DATA"] + install_program = globals_map["INSTALL_PROGRAM"] + xtfdir = globals_map["xtfdir"] + xtftestdir = globals_map["xtftestdir"] + + install_xtfdir = f"{destdir}{xtfdir}" if destdir else xtfdir + install_xtftestdir = f"{destdir}{xtftestdir}" if destdir else xtftestdir + + lines = [ + "# Autogenerated by build/gen-ninja.py. Do not edit.", + "ninja_required_version = 1.3", + "", + ] + + # Define the reusable command blocks referenced later by concrete build + # statements. Ninja separates the command definition from each individual + # output. If you think in make terms, this is similar to writing down the + # shared recipe form once and then reusing it with different file-specific + # variables for each concrete target. + emit_rule( + lines, + "cc", + f"{cc} $cflags -MT $out -MF $depfile -c $in -o $out", + depfile="$depfile", + deps="gcc", + ) + emit_rule( + lines, + "as", + f"{cc} $aflags -MT $out -MF $depfile -c $in -o $out", + depfile="$depfile", + deps="gcc", + ) + emit_rule( + lines, + "cpp_lds", + f"{cpp} $aflags -MT $out -MF $depfile -P $in -o $out", + depfile="$depfile", + deps="gcc", + ) + emit_rule(lines, "link", f"{ld} $ldflags $in -o $out") + emit_rule( + lines, + "link_hvm64", + f"{ld} $ldflags $in -o $tmpout && {objcopy} $tmpout -O {hvm64_format} $out" + " && rm -f $tmpout", + ) + emit_rule( + lines, + "mkcfg", + f'{python} build/mkcfg.py $out "$defcfg" "$vcpus" "$extracfg" "$varycfg"', + ) + emit_rule( + lines, + "mkinfo", + f'{python} build/mkinfo.py $out "$name" "$category" "$envs" "$variations"', + ) + emit_rule(lines, "install_data", f"mkdir -p $outdir && {install_data} $in $out") + emit_rule( + lines, "install_program", f"mkdir -p $outdir && {install_program} $in $out" + ) + + seen_outputs: set[str] = set() + build_targets: list[str] = [] + install_targets: list[str] = [] + + def emit_object(original_obj: str, output_obj: str, flags: str) -> None: + """Emit one object-file build statement if it is not already present. + + Multiple tests can depend on the same shared object, so the generator + must deduplicate these outputs while still discovering whether the input + source is C or assembly. + """ + + if output_obj in seen_outputs: + return + source, rule = source_for_obj(root, original_obj) + emit_build( + lines, + output_obj, + rule, + [source], + { + "cflags" if rule == "cc" else "aflags": flags, + "depfile": depfile_for(output_obj), + }, + ) + seen_outputs.add(output_obj) + + def emit_link_script(env_name: str) -> str: + """Emit one generated linker script when an environment first needs it. + + The recursive make path treats these linker scripts as generated files, + so the Ninja path mirrors that behaviour and emits them lazily. + """ + + output = to_rel(root, envs[env_name].link) + if output in seen_outputs: + return output + emit_build( + lines, + output, + "cpp_lds", + ["common/link.lds.S"], + { + "aflags": envs[env_name].aflags_env, + "depfile": depfile_for(output), + }, + ) + seen_outputs.add(output) + return output + + for test in tests: + info_output = os.path.join(test.directory, "info.json") + + # Start with the descriptive metadata for the test directory itself. + # This is not part of the compiled binary, but it travels with the test + # outputs and is installed alongside them for consumers that need to + # know the test name, category, supported environments, and variations. + # Emit the metadata file describing what this test is, which + # environments it supports, and which config variations exist. + emit_build( + lines, + info_output, + "mkinfo", + [os.path.join(test.directory, "Makefile"), "build/mkinfo.py"], + { + "name": test.name, + "category": test.category, + "envs": " ".join(test.envs), + "variations": " ".join(test.vary_cfg), + }, + ) + build_targets.append(info_output) + + install_info = os.path.join( + to_rel(root, install_xtftestdir), test.name, "info.json" + ) + emit_build( + lines, + install_info, + "install_data", + [info_output], + {"outdir": os.path.dirname(install_info)}, + ) + install_targets.append(install_info) + + for env_name in test.envs: + env = envs[env_name] + + dep_outputs: list[str] = [] + + # First emit the objects that are shared by all tests of the same + # bitness. These correspond to the common object lists in the make + # build and are reused across many later link steps. + for obj in objects_perbits: + rel_obj = to_rel(root, obj) + output_obj = rel_obj[:-2] + f"-{env.arch}.o" + emit_object( + obj, + output_obj, + ( + env.cflags_arch + if source_for_obj(root, obj)[1] == "cc" + else env.aflags_arch + ), + ) + dep_outputs.append(output_obj) + + # Collect the object files that are specific to this exact + # environment. This combines shared per-environment sources with + # the test directory's own object list. Together with the per-bits + # objects above, this produces the full set of link inputs for one + # test binary in one environment. + for obj in env_objects.get(env_name, []) + objects_perenv + test.local_objs: + rel_obj = to_rel(root, obj) + output_obj = rel_obj[:-2] + f"-{env_name}.o" + emit_object( + obj, + output_obj, + ( + env.cflags_env + if source_for_obj(root, obj)[1] == "cc" + else env.aflags_env + ), + ) + dep_outputs.append(output_obj) + + # Emit the final link step for one test binary. The linker script + # is tracked as an implicit dependency: Ninja rebuilds when it + # changes, but it is not appended to the link command as a normal + # input file. This keeps the command line aligned with what the + # linker actually consumes while still preserving correct rebuilds. + link_script = emit_link_script(env_name) + bin_output = os.path.join(test.directory, f"test-{env_name}-{test.name}") + link_inputs = dep_outputs + link_vars = {"ldflags": env.ldflags} + link_rule = "link" + if env_name == "hvm64": + link_rule = "link_hvm64" + link_vars["tmpout"] = bin_output + ".tmp" + emit_build( + lines, + bin_output, + link_rule, + link_inputs, + link_vars, + implicit_inputs=[link_script], + ) + build_targets.append(bin_output) + + install_bin = os.path.join( + to_rel(root, install_xtftestdir), + test.name, + os.path.basename(bin_output), + ) + emit_build( + lines, + install_bin, + "install_program", + [bin_output], + {"outdir": os.path.dirname(install_bin)}, + ) + install_targets.append(install_bin) + + # Emit the default xl cfg file for this test/environment pair. + # In practice this is the generated runtime configuration derived + # from the environment default plus any test-local extras. + cfg_output = os.path.join( + test.directory, f"test-{env_name}-{test.name}.cfg" + ) + cfg_inputs = [ + "build/mkcfg.py", + to_rel(root, env.defcfg), + os.path.join(test.directory, "Makefile"), + ] + if test.extra_cfg: + cfg_inputs.append(to_rel(root, test.extra_cfg)) + emit_build( + lines, + cfg_output, + "mkcfg", + cfg_inputs, + { + "defcfg": to_rel(root, env.defcfg), + "vcpus": test.vcpus, + "extracfg": to_rel(root, test.extra_cfg), + "varycfg": "", + }, + ) + build_targets.append(cfg_output) + + install_cfg = os.path.join( + to_rel(root, install_xtftestdir), + test.name, + os.path.basename(cfg_output), + ) + emit_build( + lines, + install_cfg, + "install_data", + [cfg_output], + {"outdir": os.path.dirname(install_cfg)}, + ) + install_targets.append(install_cfg) + + for variation in test.vary_cfg: + local_vary = os.path.join(test.directory, f"{variation}.cfg.in") + + # Variation fragments can either be private to the test or come + # from the shared config/ directory, so resolve the input path + # before emitting the cfg-generation statement. Resolving that + # choice here keeps the build rule emission below straightforward + # because it only has to work with the already-selected input. + vary_input = ( + local_vary + if os.path.exists(os.path.join(root, local_vary)) + else os.path.join("config", f"{variation}.cfg.in") + ) + vary_output = os.path.join( + test.directory, f"test-{env_name}-{test.name}~{variation}.cfg" + ) + vary_inputs = [ + "build/mkcfg.py", + to_rel(root, env.defcfg), + os.path.join(test.directory, "Makefile"), + vary_input, + ] + if test.extra_cfg: + vary_inputs.append(to_rel(root, test.extra_cfg)) + emit_build( + lines, + vary_output, + "mkcfg", + vary_inputs, + { + "defcfg": to_rel(root, env.defcfg), + "vcpus": test.vcpus, + "extracfg": to_rel(root, test.extra_cfg), + "varycfg": vary_input, + }, + ) + build_targets.append(vary_output) + + install_vary = os.path.join( + to_rel(root, install_xtftestdir), + test.name, + os.path.basename(vary_output), + ) + emit_build( + lines, + install_vary, + "install_data", + [vary_output], + {"outdir": os.path.dirname(install_vary)}, + ) + install_targets.append(install_vary) + + # The install side of the graph also needs the top-level xtf-runner helper, + # which is not associated with any one test directory. It is emitted last + # because unlike the per-test install targets above, it depends only on the + # top-level repository layout and not on any manifest test record. + runner_install = os.path.join(to_rel(root, install_xtfdir), "xtf-runner") + emit_build( + lines, + runner_install, + "install_program", + ["xtf-runner"], + {"outdir": os.path.dirname(runner_install)}, + ) + install_targets.append(runner_install) + + for module in python_modules(root, "xtf"): + module_install = os.path.join(to_rel(root, install_xtfdir), module) + emit_build( + lines, + module_install, + "install_data", + [module], + {"outdir": os.path.dirname(module_install)}, + ) + install_targets.append(module_install) + + emit_phony(lines, "build", build_targets) + emit_phony(lines, "install", install_targets) + lines.append("default build") + lines.append("") + return lines + + +def main() -> int: + """Load the manifest, generate Ninja lines, and write the output file.""" + + if len(sys.argv) != 3: + print("Usage: gen-ninja.py MANIFEST OUT", file=sys.stderr) + return 2 + + manifest_path, output_path = sys.argv[1:3] + + # Decode the Make-generated manifest first so the rest of the file can work + # in terms of environments, tests, and object lists rather than raw text + # records. This keeps the parsing concerns local to one place and lets the + # emitter logic below read more like the structure of the build itself. + globals_map, envs, env_objects, tests, objects_perbits, objects_perenv = ( + parse_manifest(manifest_path) + ) + root = globals_map["ROOT"] + + # Build the full Ninja file in memory first, then let the Makefile's + # temporary-output convention decide whether the generated file changed. + # That separation keeps this script focused purely on content generation. + lines = build_ninja( + root, globals_map, envs, env_objects, tests, objects_perbits, objects_perenv + ) + + with open(output_path, "w", encoding="utf-8") as handle: + handle.write("\n".join(lines)) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build/gen.mk b/build/gen.mk index df474a12..efe96e68 100644 --- a/build/gen.mk +++ b/build/gen.mk @@ -1,3 +1,8 @@ +ifeq ($(XTF_METADATA_ONLY),1) + +# In metadata-loading mode, this file is a compatibility shim only. + +else # Sanity checking of expected parameters @@ -48,15 +53,30 @@ hvm64-format := $(firstword $(filter elf32-x86-64,$(shell $(OBJCOPY) --help)) el define PERENV_build ifneq ($(1),hvm64) +# # Generic link line for most environments +# +# Link from $(ROOT) for the same reason as compilation: preserve +# diagnostic paths relative to the workspace root while keeping +# the make target names unchanged for recursive callers. +# test-$(1)-$(NAME): $$(DEPS-$(1)) $$(link-$(1)) - $(LD) $$(LDFLAGS_$(1)) $$(DEPS-$(1)) -o $$@ + cd $(ROOT) && $(LD) $$(LDFLAGS_$(1)) \ + $$(foreach dep,$$(DEPS-$(1)),$$(call root-path,$$(dep))) \ + -o $$(call root-path,$$@) else +# # hvm64 needs linking normally, then converting to elf32-x86-64 or elf32-i386 +# +# Keep the temporary file root-relative as well so objcopy sees +# the same path spelling that the linker emitted diagnostics for. +# test-$(1)-$(NAME): $$(DEPS-$(1)) $$(link-$(1)) - $(LD) $$(LDFLAGS_$(1)) $$(DEPS-$(1)) -o $$@.tmp - $(OBJCOPY) $$@.tmp -O $(hvm64-format) $$@ - rm -f $$@.tmp + cd $(ROOT) && $(LD) $$(LDFLAGS_$(1)) \ + $$(foreach dep,$$(DEPS-$(1)),$$(call root-path,$$(dep))) \ + -o $$(call root-path,$$@).tmp + cd $(ROOT) && $(OBJCOPY) $$(call root-path,$$@).tmp -O $(hvm64-format) $$(call root-path,$$@) + rm -f $$(call root-path,$$@).tmp endif cfg-$(1) ?= $(defcfg-$($(1)_guest)) @@ -100,3 +120,5 @@ clean: .PHONY: FORCE FORCE: + +endif diff --git a/build/load-tests.mk b/build/load-tests.mk new file mode 100644 index 00000000..42b29798 --- /dev/null +++ b/build/load-tests.mk @@ -0,0 +1,48 @@ +include $(ROOT)/build/core.mk + +TEST_MAKEFILES := $(wildcard $(TESTS:%=%/Makefile)) +REGISTERED_TESTS := + +define xtf_metadata_key +$(subst -,_,$(subst /,_,$(1))) +endef + +define xtf_reset_test_metadata +NAME := +CATEGORY := +TEST-ENVS := +TEST-EXTRA-CFG := +VARY-CFG := +VCPUS := +obj-perenv := +endef + +define xtf_canonicalise_test_obj +$(if $(filter $(ROOT)/% /%,$(1)),$(1),$(CURRENT_TEST_DIR)/$(1)) +endef + +define xtf_load_test +$$(eval $$(call xtf_reset_test_metadata)) +CURRENT_TEST_DIR := $$(patsubst %/Makefile,%,$(1)) +XTF_METADATA_ONLY := 1 +include $(1) +TEST_KEY := $$(call xtf_metadata_key,$$(CURRENT_TEST_DIR)) +REGISTERED_TESTS += $$(TEST_KEY) +TEST_DIR_$$(TEST_KEY) := $$(CURRENT_TEST_DIR) +TEST_NAME_$$(TEST_KEY) := $$(NAME) +TEST_CATEGORY_$$(TEST_KEY) := $$(CATEGORY) +TEST_ENVS_$$(TEST_KEY) := $$(TEST-ENVS) +TEST_EXTRA_CFG_$$(TEST_KEY) := $$(if $$(TEST-EXTRA-CFG), \ + $$(CURRENT_TEST_DIR)/$$(TEST-EXTRA-CFG)) +TEST_VARY_CFG_$$(TEST_KEY) := $$(VARY-CFG) +TEST_VCPUS_$$(TEST_KEY) := $$(if $$(VCPUS),$$(VCPUS),1) +TEST_LOCAL_OBJ_PERENV_$$(TEST_KEY) := \ + $$(foreach obj,$$(obj-perenv), \ + $$(call xtf_canonicalise_test_obj,$$(obj))) +undefine TEST_KEY +undefine XTF_METADATA_ONLY +undefine CURRENT_TEST_DIR +endef + +$(foreach test_makefile,$(TEST_MAKEFILES), \ + $(eval $(call xtf_load_test,$(test_makefile)))) diff --git a/build/mkcfg.py b/build/mkcfg.py index 65d5d03a..e9e1c418 100755 --- a/build/mkcfg.py +++ b/build/mkcfg.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Construct an xl configuration file for a test (from various fragments), and substitue variables appropriately. """ + import os import sys @@ -11,7 +11,7 @@ _, out, defcfg, vcpus, extracfg, varycfg = sys.argv # Evaluate environment and name from $OUT -_, env, name = out.split('.')[0].split('-', 2) +_, env, name = os.path.basename(out).split('.')[0].split('-', 2) # Possibly split apart the variation suffix variation = '' @@ -19,26 +19,32 @@ parts = name.split('~', 1) name, variation = parts[0], '~' + parts[1] -def expand(text): - """ Expand certain variables in text """ - return (text - .replace("@@NAME@@", name) - .replace("@@ENV@@", env) - .replace("@@VCPUS@@", vcpus) - .replace("@@XTFDIR@@", os.environ["xtfdir"]) - .replace("@@VARIATION@@", variation) - ) -config = open(defcfg).read() +def expand(text: str) -> str: + """Expand certain variables in text""" + return ( + text.replace("@@NAME@@", name) + .replace("@@ENV@@", env) + .replace("@@VCPUS@@", vcpus) + .replace("@@XTFDIR@@", os.environ["xtfdir"]) + .replace("@@VARIATION@@", variation) + ) + + +with open(defcfg) as f: + config = f.read() if extracfg: config += "\n# Test Extra Configuration:\n" - config += open(extracfg).read() + with open(extracfg) as f: + config += f.read() if varycfg: config += "\n# Test Variation Configuration:\n" - config += open(varycfg).read() + with open(varycfg) as f: + config += f.read() cfg = expand(config) -open(out, "w").write(cfg) +with open(out, "w") as f: + f.write(cfg) diff --git a/build/mkinfo.py b/build/mkinfo.py index 30f99832..84695916 100755 --- a/build/mkinfo.py +++ b/build/mkinfo.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +"""Generate test info JSON files from command-line arguments.""" + import json import sys @@ -11,14 +12,12 @@ "category": cat, "environments": [], "variations": [], - } +} if envs: template["environments"] = envs.split(" ") if variations: template["variations"] = variations.split(" ") -open(out, "w").write( - json.dumps(template, indent=4, separators=(',', ': ')) - + "\n" - ) +with open(out, "w") as f: + f.write(json.dumps(template, indent=4, separators=(",", ": ")) + "\n") diff --git a/common/nested-svm/setup-l2.c b/common/nested-svm/setup-l2.c new file mode 100644 index 00000000..568addd7 --- /dev/null +++ b/common/nested-svm/setup-l2.c @@ -0,0 +1,188 @@ +#include + +/* AMD MSRs. */ + +/* The VMRUN host-save area */ +#define MSR_VM_HSAVE_PA 0xc0010117U + +/* + * The L2 VMCB lives here. VMRUN auto-saves and restores the bulk of + * L1's state via the host-save area pointed to by MSR_VM_HSAVE_PA. + */ +struct vmcb vmcb12 __page_aligned_bss; + +/* Backing store for the VMRUN host-save area (MSR_VM_HSAVE_PA). */ +uint8_t hsave[PAGE_SIZE] __page_aligned_bss; + +/* Stack used by L2. Two pages of backing store. */ +uint8_t l2_stack[2 * PAGE_SIZE] __page_aligned_bss; + +/* Private IDT used by L2 guests in nested-SVM tests. */ +static env_gate l2_idt[256] __page_aligned_bss; + +/* Build a segment descriptor for the VMCB from a user descriptor. */ +static uint16_t user_desc_vmcb_attr(const user_desc *desc) +{ + return desc->type | + (desc->s << 4) | + (desc->dpl << 5) | + (desc->p << 7) | + (desc->limit1 << 8) | + (desc->avl << 12) | + (desc->l << 13) | + (desc->d << 14) | + (desc->g << 15); +} + +/* Check if a selector is null. */ +static bool selector_is_null(uint16_t sel) +{ + return !(sel & ~(X86_SEL_TI | X86_SEL_RPL_MASK)); +} + +/* Mark a segment as unusable in the VMCB. */ +static void vmcb_set_seg_unusable(struct vmcb_seg *seg, uint16_t sel) +{ + seg->sel = sel; + seg->attr = 0; + seg->limit = 0; + seg->base = 0; +} + +/* Build a segment descriptor for the VMCB from a user descriptor. */ +static void vmcb_set_seg_desc(struct vmcb_seg *seg, const user_desc *gdt, + uint16_t gdt_limit, uint16_t sel) +{ + uint16_t sel_offset = sel & ~(X86_SEL_TI | X86_SEL_RPL_MASK); + unsigned int gdt_desc_bytes = sizeof(*gdt); + const user_desc *desc; + + if ( selector_is_null(sel) ) + { + vmcb_set_seg_unusable(seg, sel); + return; + } + + if ( (sel & X86_SEL_TI) || + (sel_offset + gdt_desc_bytes - 1 > gdt_limit) ) + { + vmcb_set_seg_unusable(seg, 0); + return; + } + + desc = (const user_desc *)((const char *)gdt + sel_offset); + + if ( !desc->s ) + gdt_desc_bytes *= 2; + + if ( sel_offset + gdt_desc_bytes - 1 > gdt_limit ) + { + vmcb_set_seg_unusable(seg, 0); + return; + } + + seg->sel = sel; + seg->attr = user_desc_vmcb_attr(desc); + seg->limit = user_desc_limit(desc); + seg->base = user_desc_base(desc); +} + +/* set or clear EFER.SVME and return the original EFER value */ +uint64_t update_efer_svme(bool enable) +{ + uint64_t efer = rdmsr(MSR_EFER); + uint64_t new_efer = enable ? (efer | EFER_SVME) : (efer & ~EFER_SVME); + + wrmsr(MSR_EFER, new_efer); + return efer; +} + +/* Enable SVM in L1 and program the host-save area used by VMRUN. */ +bool svm_l1_enable_svm(void) +{ + if ( !cpu_has_svm ) { + xtf_skip("Skip: SVM not available\n"); + return false; + } + + update_efer_svme(true); + wrmsr(MSR_VM_HSAVE_PA, _u(hsave)); + return true; +} + +/* Build a minimal long-mode L2 VMCB that reuses the current L1 environment. */ +void svm_l2_build_vmcb(struct vmcb *vmcb, const struct svm_l2_config *cfg) +{ + struct svm_l2_config default_l2 = { + .efer = rdmsr(MSR_EFER), + .intercept_insns_vec_00c.fields.hlt = 1, + .intercept_insns_vec_010.fields.vmrun = 1, + .rsp = _u(&l2_stack[sizeof(l2_stack)]), + }; + desc_ptr gdt_desc, idt_desc; + const user_desc *gdt; + + memset(vmcb, 0, sizeof(*vmcb)); + + if ( !cfg ) + cfg = &default_l2; + vmcb->intercept_insns_vec_00c = cfg->intercept_insns_vec_00c; + vmcb->intercept_insns_vec_010 = cfg->intercept_insns_vec_010; + vmcb->intercept_insns_vec_014 = cfg->intercept_insns_vec_014; + vmcb->asid = cfg->asid ? cfg->asid : 1; + + vmcb->cr0 = read_cr0(); + vmcb->cr3 = read_cr3(); + vmcb->cr4 = read_cr4(); + vmcb->efer = cfg->efer; + vmcb->rflags = read_flags(); + + vmcb->rsp = cfg->rsp; + vmcb->rip = cfg->rip; + + sgdt(&gdt_desc); + sidt(&idt_desc); + vmcb->v_intr_ctrl.fields.vIRQ_prio = 2; /* vIRQ priority */ + vmcb->gdtr.base = gdt_desc.base; + vmcb->gdtr.limit = gdt_desc.limit; + vmcb->idtr.base = idt_desc.base; + vmcb->idtr.limit = idt_desc.limit; + gdt = (const user_desc *)gdt_desc.base; + + vmcb_set_seg_desc(&vmcb->ldtr, gdt, gdt_desc.limit, sldt()); + vmcb_set_seg_desc(&vmcb->tr, gdt, gdt_desc.limit, str()); + + vmcb->cs.sel = __KERN_CS; + vmcb->cs.attr = 0xa9b; + vmcb->cs.limit = ~0u; + + vmcb->ds.sel = __USER_DS; + vmcb->ds.attr = 0xcf3; + vmcb->ds.limit = ~0u; + vmcb->es = vmcb->fs = vmcb->gs = vmcb->ds; + + vmcb->ss.sel = __KERN_DS; + vmcb->ss.attr = 0; + vmcb->ss.limit = 0; +} + +/* Build an L2 IDT with a single interrupt gate for the supplied vector. */ +void setup_l2_idt(struct vmcb *vmcb, unsigned int vector, + void (*handler)(void)) +{ + pack_intr_gate(&l2_idt[vector], __KERN_CS, _u(handler), 0, 0); + + vmcb->idtr.base = _u(l2_idt); + vmcb->idtr.limit = sizeof(l2_idt) - 1; +} + +void print_efer(const char *prefix, uint64_t efer) +{ + printk("%s: EFER: 0x%lx, set:", prefix, efer); + if (efer & (1u << 0)) printk(" SCE"); + if (efer & (1u << 8)) printk(" LME"); + if (efer & (1u << 10)) printk(" LMA"); + if (efer & (1u << 11)) printk(" NXE"); + if (efer & (1u << 12)) printk(" SVME"); + printk("\n"); +} diff --git a/docs/all-tests.dox b/docs/all-tests.dox index ff387476..3495a43b 100644 --- a/docs/all-tests.dox +++ b/docs/all-tests.dox @@ -203,7 +203,7 @@ states. @subpage test-debug-regs - Debugging facility tests. -@subpage test-nested-svm - Nested SVM tests. +@subpage test-nested-svm-run - Nested SVM VMRUN tests. @subpage test-nested-vmx - Nested VT-x tests. */ diff --git a/include/nested-svm/setup-l2.h b/include/nested-svm/setup-l2.h new file mode 100644 index 00000000..68c6adfb --- /dev/null +++ b/include/nested-svm/setup-l2.h @@ -0,0 +1,49 @@ +#ifndef XTF_NESTED_SVM_SETUP_L2_H +#define XTF_NESTED_SVM_SETUP_L2_H + +#include + +#include "vmcb.h" + +/* Minimal caller-supplied state for building an L2 VMCB from L1. */ +struct svm_l2_config { + unsigned long rip; + unsigned long rsp; + uint32_t asid; + intercept_insns_vec_00c_t intercept_insns_vec_00c; + intercept_insns_vec_010_t intercept_insns_vec_010; + intercept_insns_vec_014_t intercept_insns_vec_014; + uint64_t efer; +}; + +/* The VMCB of L1 for L2, commonly called vmcb12 in nested SVM documentation */ +extern struct vmcb vmcb12 __page_aligned_bss; + +/* Backing store for the VMRUN host-save area (MSR_VM_HSAVE_PA). */ +extern uint8_t hsave[PAGE_SIZE] __page_aligned_bss; + +/* Stack used by L2. Two pages of backing store. */ +extern uint8_t l2_stack[2 * PAGE_SIZE] __page_aligned_bss; + +uint64_t update_efer_svme(bool enable); + +/* Enable SVM in L1 and program the host-save area used by VMRUN. */ +bool svm_l1_enable_svm(void); + +/* Leave L1 out of nested-hypervisor mode before guest shutdown. */ +void svm_l1_finish_vmrun(void); + +/* Build a minimal long-mode L2 VMCB that reuses the current L1 environment. */ +void svm_l2_build_vmcb(struct vmcb *vmcb, const struct svm_l2_config *cfg); + +/* Build an L2 IDT with a single interrupt gate for the supplied vector. */ +void setup_l2_idt(struct vmcb *vmcb, unsigned int vector, + void (*handler)(void)); + +/* Enter an L2 guest via the shared VMLOAD/VMRUN/VMSAVE trampoline. */ +void svm_vmrun(unsigned long l2_vmcb_pa); + +/* Print the EFER bits which are set in the supplied value. */ +void print_efer(const char *prefix, uint64_t efer); + +#endif /* XTF_NESTED_SVM_SETUP_L2_H */ diff --git a/include/nested-svm/vmcb.h b/include/nested-svm/vmcb.h new file mode 100644 index 00000000..2e8458bf --- /dev/null +++ b/include/nested-svm/vmcb.h @@ -0,0 +1,519 @@ +/* Shared minimal VMCB definitions for nested-SVM tests. */ +#ifndef XTF_NESTED_SVM_VMCB_H +#define XTF_NESTED_SVM_VMCB_H + +#include + +struct vmcb_seg { + uint16_t sel; + uint16_t attr; + uint32_t limit; + uint64_t base; +}; + +/* VMCB 0x060: Virtual Interrupt Control to inject virtual (INTR) interrupts */ +typedef union v_intr_ctrl { + uint64_t bytes; + struct v_intr_ctrl_fields { /* V_INTR_CTRL */ + uint64_t v_tpr : 8; /* 0:7 - virt Task Priority Register */ + uint64_t vIRQ_pending : 1; /* 8 - vIRQ pending */ + uint64_t vGIF : 1; /* 9 - vGIF value */ + uint64_t reserved_1 : 1; /* 10 - Reserved */ + uint64_t vNMI_pending : 1; /* 11 - vNMI pending */ + uint64_t vNMI_blocking : 1; /* 12 - vNMI blocking */ + uint64_t reserved_2 : 3; /* Reserved */ + uint64_t vIRQ_prio : 4; /* 16:19 - vIRQ Priority */ + uint64_t v_ign_tpr : 1; /* 20 - vIRQ ignore TPR */ + uint64_t reserved_3 : 3; /* Reserved */ + uint64_t vIRQ_masking : 1; /* 24 - vIRQ Masking */ + uint64_t vGIF_enabled : 1; /* 25 - vGIF enable/disable */ + uint64_t vNMI_enabled : 1; /* 26 - vNMI enable/disable */ + uint64_t reserved_4 : 3; /* Reserved */ + uint64_t x2AVIC_enable : 1; /* 30 - x2AVIC enable/disable */ + uint64_t AVIC_enable : 1; /* 31 - AVIC enable/disable */ + uint64_t vector : 8; /* 32:39 - Virtual Interrupt Vector */ + uint64_t reserved_5 : 24; /* 40:63 - Reserved */ + } __attribute__((packed)) fields; +} v_intr_ctrl_t; + +typedef union intercept_insns_00c { + uint32_t bytes; + struct intercept_insns_vec_00c_fields { + uint32_t intr : 1; /* 0 - INTR (physical maskable interrupt) */ + uint32_t nmi : 1; /* 1 - NMI */ + uint32_t smi : 1; /* 2 - SMI */ + uint32_t init : 1; /* 3 - INIT */ + uint32_t vintr : 1; /* 4 - VINTR (virtual maskable interrupt) */ + uint32_t cr0 : 1; /* 5 - CR0 bit writes other than TS or MP */ + uint32_t rd_idtr : 1; /* 6 - IDTR read */ + uint32_t rd_gdtr : 1; /* 7 - GDTR read */ + uint32_t rd_ldtr : 1; /* 8 - LDTR read */ + uint32_t rd_tr : 1; /* 9 - TR read */ + uint32_t wr_idtr : 1; /* 10 - IDTR write */ + uint32_t wr_gdtr : 1; /* 11 - GDTR write */ + uint32_t wr_ldtr : 1; /* 12 - LDTR write */ + uint32_t wr_tr : 1; /* 13 - TR write */ + uint32_t rdtsc : 1; /* 14 - RDTSC */ + uint32_t rdpmc : 1; /* 15 - RDPMC */ + uint32_t pushf : 1; /* 16 - PUSHF */ + uint32_t popf : 1; /* 17 - POPF */ + uint32_t cpuid : 1; /* 18 - CPUID */ + uint32_t rsm : 1; /* 19 - RSM */ + uint32_t iret : 1; /* 20 - IRET */ + uint32_t intn : 1; /* 21 - INTn */ + uint32_t invd : 1; /* 22 - INVD */ + uint32_t pause : 1; /* 23 - PAUSE */ + uint32_t hlt : 1; /* 24 - HLT */ + uint32_t invlpg : 1; /* 25 - INVLPG */ + uint32_t invlpga : 1; /* 26 - INVLPGA */ + uint32_t ioio : 1; /* 27 - IOIO_PROT */ + uint32_t msr : 1; /* 28 - MSR_PROT */ + uint32_t task_switch : 1; /* 29 - TASK_SWITCH */ + uint32_t ferr_freeze : 1; /* 30 - FERR_FREEZE */ + uint32_t shutdown : 1; /* 31 - Shutdown events */ + } __attribute__((packed)) fields; +} intercept_insns_vec_00c_t; + +typedef union intercept_insns_vec_010 { + uint32_t bytes; + struct intercept_insns_vec_010_fields { + uint32_t vmrun : 1; /* 0 - VMRUN */ + uint32_t vmmcall : 1; /* 1 - VMMCALL */ + uint32_t vmload : 1; /* 2 - VMLOAD */ + uint32_t vmsave : 1; /* 3 - VMSAVE */ + uint32_t stgi : 1; /* 4 - STGI */ + uint32_t clgi : 1; /* 5 - CLGI */ + uint32_t skinit : 1; /* 6 - SKINIT */ + uint32_t rdtscp : 1; /* 7 - RDTSCP */ + uint32_t icebp : 1; /* 8 - ICEBP */ + uint32_t wbinvd : 1; /* 9 - WBINVD/WBNOINVD */ + uint32_t monitor : 1; /* 10 - MONITOR/MONITORX */ + uint32_t mwait : 1; /* 11 - MWAIT/MWAITX unconditionally */ + uint32_t mwait_mon : 1; /* 12 - MWAIT/MWAITX monitor conditional */ + uint32_t xsetbv : 1; /* 13 - XSETBV */ + uint32_t rdpru : 1; /* 14 - RDPRU */ + uint32_t efer : 1; /* 15 - EFER write */ + uint32_t cr0 : 1; /* 16 - CR0 write */ + uint32_t cr1 : 1; /* 17 - CR1 write */ + uint32_t cr2 : 1; /* 18 - CR2 write */ + uint32_t cr3 : 1; /* 19 - CR3 write */ + uint32_t cr4 : 1; /* 20 - CR4 write */ + uint32_t cr5 : 1; /* 21 - CR5 write */ + uint32_t cr6 : 1; /* 22 - CR6 write */ + uint32_t cr7 : 1; /* 23 - CR7 write */ + uint32_t cr8 : 1; /* 24 - CR8 write */ + uint32_t cr9 : 1; /* 25 - CR9 write */ + uint32_t cr10 : 1; /* 26 - CR10 write */ + uint32_t cr11 : 1; /* 27 - CR11 write */ + uint32_t cr12 : 1; /* 28 - CR12 write */ + uint32_t cr13 : 1; /* 29 - CR13 write */ + uint32_t cr14 : 1; /* 30 - CR14 write */ + uint32_t cr15 : 1; /* 31 - CR15 write */ + } __attribute__((packed)) fields; +} intercept_insns_vec_010_t; + + +typedef union intercept_insns_vec_014 { + uint32_t bytes; + struct intercept_insns_vec_014_fields { + uint32_t invlpgb_all : 1; /* 0 - All INVLPGB insns */ + uint32_t invlpgb_ill : 1; /* 1 - Illegally specified INVLPGBs */ + uint32_t invpciid : 1; /* 2 - INVPCID */ + uint32_t mcommit : 1; /* 3 - MCOMMIT */ + uint32_t tlbsync : 1; /* 4 - TLB SYNC */ + uint32_t buslock : 1; /* 5 - BUSLOCK */ + uint32_t hlt_no_pending : 1; /* 6 - HLT with no pending virq */ + } __attribute__((packed)) fields; +} intercept_insns_vec_014_t; + +struct vmcb { + uint16_t intercept_read_cr; + uint16_t intercept_write_cr; + uint16_t intercept_read_dr; + uint16_t intercept_write_dr; + uint32_t intercept_exceptions; + intercept_insns_vec_00c_t intercept_insns_vec_00c; + intercept_insns_vec_010_t intercept_insns_vec_010; + intercept_insns_vec_014_t intercept_insns_vec_014; + uint8_t _pad_018[0x03C - 0x018]; + uint16_t pause_filter_threshold; + uint16_t pause_filter_count; + uint64_t iopm_base_pa; + uint64_t msrpm_base_pa; + uint64_t tsc_offset; + uint32_t asid; + uint8_t tlb_control; + uint8_t _pad_05d[3]; + /* 0x060: Virtual Interrupt Control to inject virtual interrupts */ + v_intr_ctrl_t v_intr_ctrl; + uint64_t int_state; + uint64_t exitcode; + uint64_t exitinfo1; + uint64_t exitinfo2; + uint64_t exit_int_info; + uint64_t np_enable; + uint8_t _pad_098[0x0a8 - 0x098]; + /* 0x0a8: Event Injection */ + uint64_t event_inj; + uint64_t h_cr3; + uint8_t _pad_0b8[0x400 - 0x0b8]; + struct vmcb_seg es; + struct vmcb_seg cs; + struct vmcb_seg ss; + struct vmcb_seg ds; + struct vmcb_seg fs; + struct vmcb_seg gs; + struct vmcb_seg gdtr; + struct vmcb_seg ldtr; + struct vmcb_seg idtr; + struct vmcb_seg tr; + uint8_t _pad_4a0[0x4cb - 0x4a0]; + uint8_t cpl; + uint32_t _pad_4cc; + uint64_t efer; + uint8_t _pad_4d8[0x548 - 0x4d8]; + uint64_t cr4; + uint64_t cr3; + uint64_t cr0; + uint64_t dr7; + uint64_t dr6; + uint64_t rflags; + uint64_t rip; + uint8_t _pad_580[0x5d8 - 0x580]; + uint64_t rsp; + uint8_t _pad_5e0[0x5f8 - 0x5e0]; + uint64_t rax; + uint8_t _pad_tail[0x1000 - 0x600]; +}; + +#define VMCB_CHECK(field, offset) \ + _Static_assert(__builtin_offsetof(struct vmcb, field) == (offset), \ + "VMCB layout mismatch: " #field) +VMCB_CHECK(intercept_insns_vec_00c, 0x000c); +VMCB_CHECK(intercept_insns_vec_010, 0x0010); +VMCB_CHECK(intercept_insns_vec_014, 0x0014); +VMCB_CHECK(asid, 0x0058); +VMCB_CHECK(v_intr_ctrl, 0x0060); +VMCB_CHECK(exitcode, 0x0070); +VMCB_CHECK(es, 0x0400); +VMCB_CHECK(gdtr, 0x0460); +VMCB_CHECK(idtr, 0x0480); +VMCB_CHECK(tr, 0x0490); +VMCB_CHECK(efer, 0x04d0); +VMCB_CHECK(cr4, 0x0548); +VMCB_CHECK(cr3, 0x0550); +VMCB_CHECK(cr0, 0x0558); +VMCB_CHECK(rflags, 0x0570); +VMCB_CHECK(rip, 0x0578); +VMCB_CHECK(rsp, 0x05d8); +VMCB_CHECK(rax, 0x05f8); +_Static_assert(sizeof(struct vmcb) == 0x1000, "VMCB size != 4 KiB"); +#undef VMCB_CHECK + +/* VMCB exit codes (Exception-vector exit-code slots) */ +#define VMEXIT_READ_CR0 0x000 +#define VMEXIT_READ_CR2 0x002 +#define VMEXIT_READ_CR3 0x003 +#define VMEXIT_READ_CR4 0x004 +#define VMEXIT_READ_CR8 0x008 +#define VMEXIT_WRITE_CR0 0x010 +#define VMEXIT_WRITE_CR2 0x012 +#define VMEXIT_WRITE_CR3 0x013 +#define VMEXIT_WRITE_CR4 0x014 +#define VMEXIT_WRITE_CR8 0x018 +#define VMEXIT_READ_DR0 0x020 +#define VMEXIT_READ_DR1 0x021 +#define VMEXIT_READ_DR2 0x022 +#define VMEXIT_READ_DR3 0x023 +#define VMEXIT_READ_DR4 0x024 +#define VMEXIT_READ_DR5 0x025 +#define VMEXIT_READ_DR6 0x026 +#define VMEXIT_READ_DR7 0x027 +#define VMEXIT_WRITE_DR0 0x030 +#define VMEXIT_WRITE_DR1 0x031 +#define VMEXIT_WRITE_DR2 0x032 +#define VMEXIT_WRITE_DR3 0x033 +#define VMEXIT_WRITE_DR4 0x034 +#define VMEXIT_WRITE_DR5 0x035 +#define VMEXIT_WRITE_DR6 0x036 +#define VMEXIT_WRITE_DR7 0x037 +#define VMEXIT_EXCP_BASE 0x040 +#define VMEXIT_EXCP_DE 0x040 /* vector 0, #DE */ +#define VMEXIT_EXCP_DB 0x041 /* vector 1, #DB */ +#define VMEXIT_EXCP_BP 0x043 /* vector 3, #BP */ +#define VMEXIT_EXCP_OF 0x044 /* vector 4, #OF */ +#define VMEXIT_EXCP_BR 0x045 /* vector 5, #BR */ +#define VMEXIT_EXCP_UD 0x046 /* vector 6, #UD */ +#define VMEXIT_EXCP_NM 0x047 /* vector 7, #NM */ +#define VMEXIT_EXCP_DF 0x048 /* vector 8, #DF */ +#define VMEXIT_EXCP_TS 0x04a /* vector 10, #TS */ +#define VMEXIT_EXCP_NP 0x04b /* vector 11, #NP */ +#define VMEXIT_EXCP_SS 0x04c /* vector 12, #SS */ +#define VMEXIT_EXCP_GP 0x04d /* vector 13, #GP */ +#define VMEXIT_EXCP_PF 0x04e /* vector 14, #PF */ +#define VMEXIT_EXCP_MF 0x050 /* vector 16, #MF */ +#define VMEXIT_EXCP_AC 0x051 /* vector 17, #AC */ +#define VMEXIT_EXCP_MC 0x052 /* vector 18, #MC */ +#define VMEXIT_EXCP_XF 0x053 /* vector 19, #XF */ +#define VMEXIT_EXCP_CP 0x055 /* vector 21, #CP */ +#define VMEXIT_EXCP_HV 0x05c /* vector 28, #HV */ +#define VMEXIT_EXCP_VC 0x05d /* vector 29, #VC */ +#define VMEXIT_EXCP_SX 0x05e /* vector 30, #SX */ +#define VMEXIT_INTR 0x060 +#define VMEXIT_NMI 0x061 +#define VMEXIT_SMI 0x062 +#define VMEXIT_INIT 0x063 +#define VMEXIT_VINTR 0x064 +#define VMEXIT_CR0_SEL_WRITE 0x065 +#define VMEXIT_IDTR_READ 0x066 +#define VMEXIT_GDTR_READ 0x067 +#define VMEXIT_LDTR_READ 0x068 +#define VMEXIT_TR_READ 0x069 +#define VMEXIT_IDTR_WRITE 0x06a +#define VMEXIT_GDTR_WRITE 0x06b +#define VMEXIT_LDTR_WRITE 0x06c +#define VMEXIT_TR_WRITE 0x06d +#define VMEXIT_RDTSC 0x06e +#define VMEXIT_RDPMC 0x06f +#define VMEXIT_PUSHF 0x070 +#define VMEXIT_POPF 0x071 +#define VMEXIT_CPUID 0x072 +#define VMEXIT_RSM 0x073 +#define VMEXIT_IRET 0x074 +#define VMEXIT_SWINT 0x075 +#define VMEXIT_INVD 0x076 +#define VMEXIT_PAUSE 0x077 +#define VMEXIT_HLT 0x078 +#define VMEXIT_INVLPG 0x079 +#define VMEXIT_INVLPGA 0x07a +#define VMEXIT_IOIO 0x07b +#define VMEXIT_MSR 0x07c +#define VMEXIT_TASK_SWITCH 0x07d +#define VMEXIT_FERR_FREEZE 0x07e +#define VMEXIT_SHUTDOWN 0x07f +#define VMEXIT_VMRUN 0x080 +#define VMEXIT_VMMCALL 0x081 +#define VMEXIT_VMLOAD 0x082 +#define VMEXIT_VMSAVE 0x083 +#define VMEXIT_STGI 0x084 +#define VMEXIT_CLGI 0x085 +#define VMEXIT_SKINIT 0x086 +#define VMEXIT_RDTSCP 0x087 +#define VMEXIT_ICEBP 0x088 +#define VMEXIT_WBINVD 0x089 +#define VMEXIT_MONITOR 0x08a +#define VMEXIT_MWAIT 0x08b +#define VMEXIT_MWAIT_COND 0x08c +#define VMEXIT_XSETBV 0x08d +#define VMEXIT_RDPRU 0x08e +#define VMEXIT_EFER_WRITE_TRAP 0x08f +#define VMEXIT_CR0_WRITE_TRAP 0x090 +#define VMEXIT_CR1_WRITE_TRAP 0x091 +#define VMEXIT_CR2_WRITE_TRAP 0x092 +#define VMEXIT_CR3_WRITE_TRAP 0x093 +#define VMEXIT_CR4_WRITE_TRAP 0x094 +#define VMEXIT_CR5_WRITE_TRAP 0x095 +#define VMEXIT_CR6_WRITE_TRAP 0x096 +#define VMEXIT_CR7_WRITE_TRAP 0x097 +#define VMEXIT_CR8_WRITE_TRAP 0x098 +#define VMEXIT_CR9_WRITE_TRAP 0x099 +#define VMEXIT_CR10_WRITE_TRAP 0x09a +#define VMEXIT_CR11_WRITE_TRAP 0x09b +#define VMEXIT_CR12_WRITE_TRAP 0x09c +#define VMEXIT_CR13_WRITE_TRAP 0x09d +#define VMEXIT_CR14_WRITE_TRAP 0x09e +#define VMEXIT_CR15_WRITE_TRAP 0x09f +#define VMEXIT_INVLPGB 0x0a0 +#define VMEXIT_INVLPGB_ILLEGAL 0x0a1 +#define VMEXIT_INVPCID 0x0a2 +#define VMEXIT_MCOMMIT 0x0a3 +#define VMEXIT_TLBSYNC 0x0a4 +#define VMEXIT_BUS_LOCK 0x0a5 +#define VMEXIT_IDLE_HLT 0x0a6 + +/* Feature-specific exits. */ +#define VMEXIT_NPF 0x400 +#define VMEXIT_AVIC_INCOMPLETE_IPI 0x401 +#define VMEXIT_AVIC_NOACCEL 0x402 +#define VMEXIT_VMGEXIT 0x403 + +/* Host/software and error exits. */ +#define VMEXIT_SW 0xf0000000ull +#define VMEXIT_INVALID (~0ull) /* -1 */ +#define VMEXIT_BUSY (~1ull) /* -2 */ +#define VMEXIT_IDLE_REQUIRED (~2ull) /* -3 */ +#define VMEXIT_INVALID_PMC (~3ull) /* -4 */ + +/* Get the vmexit reason */ +static const char __used *vmexit_reason(uint64_t exitcode) +{ + switch (exitcode) { + case VMEXIT_READ_CR0: return "READ_CR0"; + case VMEXIT_READ_CR2: return "READ_CR2"; + case VMEXIT_READ_CR3: return "READ_CR3"; + case VMEXIT_READ_CR4: return "READ_CR4"; + case VMEXIT_READ_CR8: return "READ_CR8"; + case VMEXIT_WRITE_CR0: return "WRITE_CR0"; + case VMEXIT_WRITE_CR2: return "WRITE_CR2"; + case VMEXIT_WRITE_CR3: return "WRITE_CR3"; + case VMEXIT_WRITE_CR4: return "WRITE_CR4"; + case VMEXIT_WRITE_CR8: return "WRITE_CR8"; + case VMEXIT_READ_DR0: return "READ_DR0"; + case VMEXIT_READ_DR1: return "READ_DR1"; + case VMEXIT_READ_DR2: return "READ_DR2"; + case VMEXIT_READ_DR3: return "READ_DR3"; + case VMEXIT_READ_DR4: return "READ_DR4"; + case VMEXIT_READ_DR5: return "READ_DR5"; + case VMEXIT_READ_DR6: return "READ_DR6"; + case VMEXIT_READ_DR7: return "READ_DR7"; + case VMEXIT_WRITE_DR0: return "WRITE_DR0"; + case VMEXIT_WRITE_DR1: return "WRITE_DR1"; + case VMEXIT_WRITE_DR2: return "WRITE_DR2"; + case VMEXIT_WRITE_DR3: return "WRITE_DR3"; + case VMEXIT_WRITE_DR4: return "WRITE_DR4"; + case VMEXIT_WRITE_DR5: return "WRITE_DR5"; + case VMEXIT_WRITE_DR6: return "WRITE_DR6"; + case VMEXIT_WRITE_DR7: return "WRITE_DR7"; + case VMEXIT_EXCP_DE: return "EXCP_DE"; + case VMEXIT_EXCP_DB: return "EXCP_DB"; + case VMEXIT_EXCP_BP: return "EXCP_BP"; + case VMEXIT_EXCP_OF: return "EXCP_OF"; + case VMEXIT_EXCP_BR: return "EXCP_BR"; + case VMEXIT_EXCP_UD: return "EXCP_UD"; + case VMEXIT_EXCP_NM: return "EXCP_NM"; + case VMEXIT_EXCP_DF: return "EXCP_DF"; + case VMEXIT_EXCP_TS: return "EXCP_TS"; + case VMEXIT_EXCP_NP: return "EXCP_NP"; + case VMEXIT_EXCP_SS: return "EXCP_SS"; + case VMEXIT_EXCP_GP: return "EXCP_GP"; + case VMEXIT_EXCP_PF: return "EXCP_PF"; + case VMEXIT_EXCP_MF: return "EXCP_MF"; + case VMEXIT_EXCP_AC: return "EXCP_AC"; + case VMEXIT_EXCP_MC: return "EXCP_MC"; + case VMEXIT_EXCP_XF: return "EXCP_XF"; + case VMEXIT_EXCP_CP: return "EXCP_CP"; + case VMEXIT_EXCP_HV: return "EXCP_HV"; + case VMEXIT_EXCP_VC: return "EXCP_VC"; + case VMEXIT_EXCP_SX: return "EXCP_SX"; + case VMEXIT_INTR: return "INTR"; + case VMEXIT_NMI: return "NMI"; + case VMEXIT_SMI: return "SMI"; + case VMEXIT_INIT: return "INIT"; + case VMEXIT_VINTR: return "VINTR"; + case VMEXIT_CR0_SEL_WRITE: return "CR0_SEL_WRITE"; + case VMEXIT_IDTR_READ: return "IDTR_READ"; + case VMEXIT_GDTR_READ: return "GDTR_READ"; + case VMEXIT_LDTR_READ: return "LDTR_READ"; + case VMEXIT_TR_READ: return "TR_READ"; + case VMEXIT_IDTR_WRITE: return "IDTR_WRITE"; + case VMEXIT_GDTR_WRITE: return "GDTR_WRITE"; + case VMEXIT_LDTR_WRITE: return "LDTR_WRITE"; + case VMEXIT_TR_WRITE: return "TR_WRITE"; + case VMEXIT_RDTSC: return "RDTSC"; + case VMEXIT_RDPMC: return "RDPMC"; + case VMEXIT_PUSHF: return "PUSHF"; + case VMEXIT_POPF: return "POPF"; + case VMEXIT_CPUID: return "CPUID"; + case VMEXIT_RSM: return "RSM"; + case VMEXIT_IRET: return "IRET"; + case VMEXIT_SWINT: return "SWINT"; + case VMEXIT_INVD: return "INVD"; + case VMEXIT_PAUSE: return "PAUSE"; + case VMEXIT_HLT: return "HLT"; + case VMEXIT_INVLPG: return "INVLPG"; + case VMEXIT_INVLPGA: return "INVLPGA"; + case VMEXIT_IOIO: return "IOIO"; + case VMEXIT_MSR: return "MSR"; + case VMEXIT_TASK_SWITCH: return "TASK_SWITCH"; + case VMEXIT_FERR_FREEZE: return "FERR_FREEZE"; + case VMEXIT_SHUTDOWN: return "SHUTDOWN"; + case VMEXIT_VMRUN: return "VMRUN"; + case VMEXIT_VMMCALL: return "VMMCALL"; + case VMEXIT_VMLOAD: return "VMLOAD"; + case VMEXIT_VMSAVE: return "VMSAVE"; + case VMEXIT_STGI: return "STGI"; + case VMEXIT_CLGI: return "CLGI"; + case VMEXIT_SKINIT: return "SKINIT"; + case VMEXIT_RDTSCP: return "RDTSCP"; + case VMEXIT_ICEBP: return "ICEBP"; + case VMEXIT_WBINVD: return "WBINVD"; + case VMEXIT_MONITOR: return "MONITOR"; + case VMEXIT_MWAIT: return "MWAIT"; + case VMEXIT_MWAIT_COND: return "MWAIT_COND"; + case VMEXIT_XSETBV: return "XSETBV"; + case VMEXIT_RDPRU: return "RDPRU"; + case VMEXIT_EFER_WRITE_TRAP: return "EFER_WRITE_TRAP"; + case VMEXIT_CR0_WRITE_TRAP: return "CR0_WRITE_TRAP"; + case VMEXIT_CR1_WRITE_TRAP: return "CR1_WRITE_TRAP"; + case VMEXIT_CR2_WRITE_TRAP: return "CR2_WRITE_TRAP"; + case VMEXIT_CR3_WRITE_TRAP: return "CR3_WRITE_TRAP"; + case VMEXIT_CR4_WRITE_TRAP: return "CR4_WRITE_TRAP"; + case VMEXIT_CR5_WRITE_TRAP: return "CR5_WRITE_TRAP"; + case VMEXIT_CR6_WRITE_TRAP: return "CR6_WRITE_TRAP"; + case VMEXIT_CR7_WRITE_TRAP: return "CR7_WRITE_TRAP"; + case VMEXIT_CR8_WRITE_TRAP: return "CR8_WRITE_TRAP"; + case VMEXIT_CR9_WRITE_TRAP: return "CR9_WRITE_TRAP"; + case VMEXIT_CR10_WRITE_TRAP: return "CR10_WRITE_TRAP"; + case VMEXIT_CR11_WRITE_TRAP: return "CR11_WRITE_TRAP"; + case VMEXIT_CR12_WRITE_TRAP: return "CR12_WRITE_TRAP"; + case VMEXIT_CR13_WRITE_TRAP: return "CR13_WRITE_TRAP"; + case VMEXIT_CR14_WRITE_TRAP: return "CR14_WRITE_TRAP"; + case VMEXIT_CR15_WRITE_TRAP: return "CR15_WRITE_TRAP"; + case VMEXIT_INVLPGB: return "INVLPGB"; + case VMEXIT_INVLPGB_ILLEGAL: return "INVLPGB_ILLEGAL"; + case VMEXIT_INVPCID: return "INVPCID"; + case VMEXIT_MCOMMIT: return "MCOMMIT"; + case VMEXIT_TLBSYNC: return "TLBSYNC"; + case VMEXIT_BUS_LOCK: return "BUS_LOCK"; + case VMEXIT_IDLE_HLT: return "IDLE_HLT"; + case VMEXIT_NPF: return "NPF"; + case VMEXIT_AVIC_INCOMPLETE_IPI: return "AVIC_INCOMPLETE_IPI"; + case VMEXIT_AVIC_NOACCEL: return "AVIC_NOACCEL"; + case VMEXIT_VMGEXIT: return "VMGEXIT"; + case VMEXIT_SW: return "SW"; + case VMEXIT_INVALID: return "INVALID"; + case VMEXIT_BUSY: return "BUSY"; + case VMEXIT_IDLE_REQUIRED: return "IDLE_REQUIRED"; + case VMEXIT_INVALID_PMC: return "INVALID_PMC"; + default: return "UNKNOWN"; + } +} + +/* Function to print all bits of v_intr_ctrl_t */ +static inline void print_v_intr_ctrl(const char *file, int line, + const struct vmcb *vmcb, + const char *prefix) +{ + struct v_intr_ctrl_fields ctrl = vmcb->v_intr_ctrl.fields; + + printk("%s:%d: rax:%lx IRQ-ctrl", file, line, vmcb->rax); + if (prefix) + printk(" %s", prefix); + printk(":"); + + /* Print a list of the bits which are set: */ + if (ctrl.vIRQ_prio) + printk(" prio:%u", ctrl.vIRQ_prio); + if (ctrl.vector) + printk(" vec:%x", ctrl.vector); + if (ctrl.vGIF_enabled) + printk(" vGIF:%u", ctrl.vGIF); + if (ctrl.vIRQ_pending) + printk(" IRQ:pending"); + if (ctrl.vNMI_enabled) + printk(" NMI:enabled"); + if (ctrl.vNMI_pending) + printk(" NMI:pending"); + if (ctrl.vNMI_blocking) + printk(" NMI:blocking"); + + if (ctrl.v_tpr) + printk(" tpr=%u", ctrl.v_tpr); + if (ctrl.v_ign_tpr) + printk(" ign_tpr"); + if (ctrl.vIRQ_masking) + printk(" IRQ:masking"); + printk("\n"); +} + +#endif /* XTF_NESTED_SVM_VMCB_H */ diff --git a/pipeline.yaml b/pipeline.yaml new file mode 100644 index 00000000..b310e8dd --- /dev/null +++ b/pipeline.yaml @@ -0,0 +1,3 @@ +# Pipeline configuration to not run copyright checks on pull requests. +copyright: + enablePRCheck: false diff --git a/tests/nested-svm-clgi-stgi/Makefile b/tests/nested-svm-clgi-stgi/Makefile new file mode 100644 index 00000000..a6f6f1cf --- /dev/null +++ b/tests/nested-svm-clgi-stgi/Makefile @@ -0,0 +1,12 @@ +include $(ROOT)/build/common.mk + +NAME := nested-svm-clgi-stgi +CATEGORY := nested-svm +TEST-ENVS := $(SVM_ENVIRONMENTS) + +TEST-EXTRA-CFG := extra.cfg.in + +obj-perenv += main.o +obj-perenv += $(ROOT)/common/nested-svm/setup-l2.o + +include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm/extra.cfg.in b/tests/nested-svm-clgi-stgi/extra.cfg.in similarity index 100% rename from tests/nested-svm/extra.cfg.in rename to tests/nested-svm-clgi-stgi/extra.cfg.in diff --git a/tests/nested-svm-clgi-stgi/main.c b/tests/nested-svm-clgi-stgi/main.c new file mode 100644 index 00000000..72d52351 --- /dev/null +++ b/tests/nested-svm-clgi-stgi/main.c @@ -0,0 +1,355 @@ +/** + * @file tests/nested-svm-clgi-stgi/main.c + * @ref test-nested-svm-clgi-stgi + * + * @page test-nested-svm-clgi-stgi Smoke-test STGI/CLGI in AMD SVM + * + * An L1 guest: + * 1. enables SVM, + * 2. builds a minimal L2 VMCB that re-uses L1's address space, and + * 3. prepares a pending virtual hardware interrupt to be taken in L2. + * 4. uses CLGI/STGI to manage interrupts in L2. + * + * L2: + * 1. tests receiving the pending virtual hardware interrupt + * 2. increments or decrements %rax to signal + * whether the interrupt was taken or not, and + * 3. signals completion with HLT, which causes a #VMEXIT back to L1. + * + * @see tests/nested-svm-clgi-stgi/main.c + */ +#include + +const char test_title[] = "Nested SVM CLGI/STGI Smoke Test"; + +/* The VMCB's Virtual Interrupt Control field for by-field access. */ +static struct v_intr_ctrl_fields *intr_ctrl = &vmcb12.v_intr_ctrl.fields; + +/** + * L2 Interrupt Service Routine for Vector 0x30. + * It is used to prove the interrupt was actually taken. + */ +static void __used l2_isr_0x30(void) +{ + /* Decrement rax before exiting to indicate that an interrupt was taken */ + asm volatile ("dec %rax;hlt"); +} + +/** + * Check if the ISR for 0x30 was called by checking if %rax was decremented. + * L2 should have decremented %rax from 2 to 1 if the interrupt was taken. + */ +static bool l2_isr_0x30_called(void) +{ + return vmcb12.rax == 1; /* L2 should have decremented %rax from 2 to 1 */ +} + +static bool l2_isr_0x30_not_called(void) +{ + return vmcb12.rax == 3; /* L2 should have incremented %rax from 2 to 3 */ +} + +static void __used l2_clgi_hlt(void) +{ + asm volatile ("clgi\n" + "hlt"); +} + +static void __used l2_stgi_hlt(void) +{ + asm volatile ("stgi\n" + "hlt"); +} + +/** + * L2 payload with STI to take pending interrupts. In case of a pending + * interrupt, it will be taken after the instruction following STI. + * If interrupts are disabled, the interrupt will not be taken, inc %rax + * will increment %rax to signal that exit L2 with HLT. + */ +static void __used l2_sti_nop_inc_rax_hlt(void) +{ + /* STI to allow interrupts, then increment %rax */ + asm volatile ("sti\n" + "nop\n" /* Ensures STI takes effect before inc %rax */ + "inc %rax\n" /* Signal that no IRQ or ISR returned */ + "hlt"); /* Exit L2 */ +} + +static void __used l2_clgi_sti_nop_inc_rax_hlt(void) +{ + asm volatile ("clgi\n" + "sti\n" + "nop\n" + "inc %rax\n" + "hlt"); +} + +static void __used l2_stgi_sti_nop_inc_rax_hlt(void) +{ + asm volatile ("stgi\n" + "sti\n" + "nop\n" + "inc %rax\n" + "hlt"); +} + +static void run_l2_probe_exit(const char *file, int line, uint64_t rip, + bool virq_pending) +{ + vmcb12.rax = 2; /* Starting value for L2 to inc/decrement before HLT */ + vmcb12.rip = rip; + vmcb12.rflags &= ~X86_EFLAGS_IF; + + /* Inject a pending virtual hardware interrupt using the VMCB */ + intr_ctrl->vIRQ_pending = virq_pending; + + print_v_intr_ctrl(file, line, &vmcb12, "pre-VMRUN "); + asm volatile("mov %0, %%rax\n" + "vmload %%rax\n" + "vmrun %%rax\n" + "vmsave %%rax\n" + : + : "r" (_u(&vmcb12)) + : "%rax", "memory"); + print_v_intr_ctrl(file, line, &vmcb12, "post-VMRUN"); +} + +static bool run_l2_expect_exit(const char *file, int line, uint64_t rip, + uint64_t expected_exitcode, bool virq_pending) +{ + run_l2_probe_exit(file, line, rip, virq_pending); + + if ( vmcb12.exitcode != expected_exitcode ) + { + xtf_failure("%s:%d: unexpected L2 exit: 0x%lx (%s), expected 0x%lx (%s)\n", + file, line, vmcb12.exitcode, + vmexit_reason(vmcb12.exitcode), expected_exitcode, + vmexit_reason(expected_exitcode)); + return false; + } + return true; +} + +static bool expect_xfail_l2_clgi_stgi_intercepts_without_vgif(void) +{ + uint64_t clgi_exitcode, stgi_exitcode; + + printk("L1: XFAIL testing CLGI/STGI intercepts with vGIF disabled\n"); + + intr_ctrl->vGIF_enabled = 0; + intr_ctrl->vGIF = 0; + vmcb12.intercept_insns_vec_010.fields.clgi = 1; + run_l2_probe_exit(__FILE__, __LINE__, _u(l2_clgi_hlt), false); + clgi_exitcode = vmcb12.exitcode; + vmcb12.intercept_insns_vec_010.fields.clgi = 0; + + intr_ctrl->vGIF_enabled = 0; + intr_ctrl->vGIF = 0; + vmcb12.intercept_insns_vec_010.fields.stgi = 1; + run_l2_probe_exit(__FILE__, __LINE__, _u(l2_stgi_hlt), false); + stgi_exitcode = vmcb12.exitcode; + vmcb12.intercept_insns_vec_010.fields.stgi = 0; + + if ( clgi_exitcode == VMEXIT_HLT && stgi_exitcode == VMEXIT_HLT ) + { + xtf_warning("XFAIL: Xen did not intercept CLGI/STGI when L1 disabled vGIF\n"); + return true; + } + + if ( clgi_exitcode == VMEXIT_CLGI && stgi_exitcode == VMEXIT_STGI ) + { + xtf_failure("XPASS: Xen intercepted CLGI/STGI with vGIF disabled; " + "update this test to expect the bug fixed\n"); + return false; + } + + xtf_failure("Fail: unexpected CLGI/STGI exits with vGIF disabled: " + "CLGI 0x%lx (%s), STGI 0x%lx (%s)\n", + clgi_exitcode, vmexit_reason(clgi_exitcode), + stgi_exitcode, vmexit_reason(stgi_exitcode)); + return false; +} + +static bool run_l2(const char *file, int line, uint64_t rip) +{ + return run_l2_expect_exit(file, line, rip, VMEXIT_HLT, true); +} + +#define FAIL(fmt, ...) { \ + xtf_failure("%s:%d: " fmt, __FILE__, __LINE__, ##__VA_ARGS__); \ + return false; \ +} + +/** + * Test that a pending virtual IRQ is not taken when vGIF is disabled. + */ +bool test_virq_without_vgif(void) +{ + printk("L1: testing pending virtual IRQ without vGIF\n"); + intr_ctrl->vGIF_enabled = 0; /* Disable the vGIF feature */ + + if ( !run_l2(__FILE__, __LINE__, _u(l2_sti_nop_inc_rax_hlt)) ) + return false; + + if ( intr_ctrl->vIRQ_pending ) + FAIL("L2 should have cleared the pending virtual IRQ\n"); + if ( !l2_isr_0x30_called() ) + FAIL("L2 0x30 isr was not called as expected\n"); + return true; +} + +/** + * Test that a pending virtual IRQ is not taken when vGIF is enabled but + * interrupts are disabled in L2. + */ +bool test_virq_with_vgif_irqs_disabled(void) +{ + printk("L1: testing pending virtual IRQ with vGIF disabled\n"); + intr_ctrl->vGIF_enabled = 1; /* Enable vGIF */ + intr_ctrl->vGIF = 0; /* disable interrupts */ + + /* Starting value for L2 to inc/decrement before HLT */ + if ( !run_l2(__FILE__, __LINE__, _u(l2_sti_nop_inc_rax_hlt)) ) + return false; + + if ( !intr_ctrl->vIRQ_pending ) + FAIL("L2 should have kept the pending virtual IRQ\n"); + if ( l2_isr_0x30_called() ) + FAIL("L2 0x30 isr called when vGIF:0 should have blocked it\n"); + return true; +} + +/** + * Test that a pending virtual IRQ is taken in L2 when vGIF is enabled and + * interrupts are enabled. + */ +bool test_virq_with_vgif_irqs_enabled(void) +{ + printk("L1: testing pending virtual IRQ with vGIF enabled\n"); + intr_ctrl->vGIF_enabled = 1; /* Enable vGIF */ + intr_ctrl->vGIF = 1; /* enable interrupts */ + + /* Starting value for L2 to inc/decrement before HLT */ + if ( !run_l2(__FILE__, __LINE__, _u(l2_sti_nop_inc_rax_hlt)) ) + return false; + + if ( intr_ctrl->vIRQ_pending ) + FAIL("L2 should have cleared the pending virtual IRQ\n"); + if ( !l2_isr_0x30_called() ) + FAIL("L2 0x30 isr was not called as expected\n"); + return true; +} + +/** + * Test that L2 CLGI and STGI are directly interceptible instructions. + */ +bool test_l2_clgi_stgi_intercepts(void) +{ + printk("L1: testing L2 CLGI/STGI intercepts\n"); + intr_ctrl->vGIF_enabled = 1; + + intr_ctrl->vGIF = 1; + vmcb12.intercept_insns_vec_010.fields.clgi = 1; + if ( !run_l2_expect_exit(__FILE__, __LINE__, _u(l2_clgi_hlt), + VMEXIT_CLGI, false) ) + return false; + vmcb12.intercept_insns_vec_010.fields.clgi = 0; + + intr_ctrl->vGIF = 0; + vmcb12.intercept_insns_vec_010.fields.stgi = 1; + if ( !run_l2_expect_exit(__FILE__, __LINE__, _u(l2_stgi_hlt), + VMEXIT_STGI, false) ) + return false; + vmcb12.intercept_insns_vec_010.fields.stgi = 0; + + return true; +} + +/** + * Test that L2 CLGI clears vGIF and blocks a pending virtual IRQ. + */ +bool test_l2_clgi_blocks_virq(void) +{ + printk("L1: testing L2 CLGI blocks a pending virtual IRQ\n"); + intr_ctrl->vGIF_enabled = 1; + intr_ctrl->vGIF = 1; + + if ( !run_l2(__FILE__, __LINE__, _u(l2_clgi_sti_nop_inc_rax_hlt)) ) + return false; + + if ( !intr_ctrl->vIRQ_pending ) + FAIL("L2 CLGI should have kept the pending virtual IRQ\n"); + if ( intr_ctrl->vGIF ) + FAIL("L2 CLGI should have cleared vGIF\n"); + if ( !l2_isr_0x30_not_called() ) + FAIL("L2 0x30 isr called when CLGI should have blocked it\n"); + return true; +} + +/** + * Test that L2 STGI sets vGIF and allows a pending virtual IRQ. + */ +bool test_l2_stgi_allows_virq(void) +{ + printk("L1: testing L2 STGI allows a pending virtual IRQ\n"); + intr_ctrl->vGIF_enabled = 1; + intr_ctrl->vGIF = 0; + + if ( !run_l2(__FILE__, __LINE__, _u(l2_stgi_sti_nop_inc_rax_hlt)) ) + return false; + + if ( intr_ctrl->vIRQ_pending ) + FAIL("L2 STGI should have cleared the pending virtual IRQ\n"); + if ( !intr_ctrl->vGIF ) + FAIL("L2 STGI should have set vGIF\n"); + if ( !l2_isr_0x30_called() ) + FAIL("L2 0x30 isr was not called after STGI\n"); + return true; +} + +/** + * Execute the nested-SVM CLGI/STGI smoke test. + * + * L1 enables SVM, prepares a minimal L2 VMCB, enters L2 once with VMRUN + * and verifies that L2 reports success before exiting with HLT. + */ +void test_main(void) +{ + /* Enable SVM, arm the host-save area and build the L2 VMCB. */ + + if ( !svm_l1_enable_svm() ) + return; + + /* Setup the vmcb for the test */ + svm_l2_build_vmcb(&vmcb12, NULL); + + /* Set up L2's IDTR and install the interrupt gate used by the test. */ + setup_l2_idt(&vmcb12, 0x30, l2_isr_0x30); + + /* Set the injected virtual hardware interrupt to vector 0x30 */ + intr_ctrl->vector = 0x30; + + if ( !test_virq_without_vgif() ) + return; + + if ( !test_virq_with_vgif_irqs_disabled() ) + return; + + if ( !test_virq_with_vgif_irqs_enabled() ) + return; + + if ( !test_l2_clgi_stgi_intercepts() ) + return; + + if ( !test_l2_clgi_blocks_virq() ) + return; + + if ( !test_l2_stgi_allows_virq() ) + return; + + if ( !expect_xfail_l2_clgi_stgi_intercepts_without_vgif() ) + return; + + xtf_success(NULL); +} diff --git a/tests/nested-svm-loadsave/Makefile b/tests/nested-svm-loadsave/Makefile new file mode 100644 index 00000000..8883bec5 --- /dev/null +++ b/tests/nested-svm-loadsave/Makefile @@ -0,0 +1,11 @@ +include $(ROOT)/build/common.mk + +NAME := nested-svm-loadsave +CATEGORY := nested-svm +TEST-ENVS := $(SVM_ENVIRONMENTS) + +TEST-EXTRA-CFG := extra.cfg.in + +obj-perenv += main.o $(ROOT)/common/nested-svm/setup-l2.o + +include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm-loadsave/extra.cfg.in b/tests/nested-svm-loadsave/extra.cfg.in new file mode 100644 index 00000000..ae494f84 --- /dev/null +++ b/tests/nested-svm-loadsave/extra.cfg.in @@ -0,0 +1 @@ +nestedhvm = 1 diff --git a/tests/nested-svm-loadsave/index.rst b/tests/nested-svm-loadsave/index.rst new file mode 100644 index 00000000..a0355fcc --- /dev/null +++ b/tests/nested-svm-loadsave/index.rst @@ -0,0 +1,31 @@ +Nested SVM VMLOAD/VMSAVE Test +============================= + +This test exercises the architectural VMLOAD and VMSAVE failure cases +that are reachable from an hvm64 L1 guest using Xen nested SVM. + +What It Verifies +---------------- + +The test verifies the distinct VMLOAD and VMSAVE error classes that are +reachable from this hvm64 harness: + +* VMLOAD/VMSAVE with EFER.SVME clear. +* VMLOAD/VMSAVE executed at CPL > 0. +* VMLOAD/VMSAVE executed with malformed VMCB physical addresses in RAX. + +How The Verification Functions Work +----------------------------------- + +``test_main()`` supplies the VMLOAD and VMSAVE-specific matrices +to the runner and checks that Xen reports the same exceptions that +the AMD architecture defines for the same preconditions, within the +limits of an hvm64 long-mode harness. + +``svm_negative_check_cases()`` toggles EFER.SVME as required +by each subtest, dispatches the instruction in kernel or user +context, and restores the original EFER value afterwards. + +``stub_vmload()`` and ``stub_vmsave()`` execute the instructions +directly in L1 with caller-supplied RAX values and record any +faults via the XTF exception-table helpers. diff --git a/tests/nested-svm-loadsave/main.c b/tests/nested-svm-loadsave/main.c new file mode 100644 index 00000000..a9437863 --- /dev/null +++ b/tests/nested-svm-loadsave/main.c @@ -0,0 +1,425 @@ +/** + * @file tests/nested-svm-vmloadsave/main.c + * @ref test-nested-svm-vmloadsave + * + * @page test-nested-svm-vmloadsave nested-svm-vmloadsave + * + * Testing of AMD SVM VMLOAD and VMSAVE from an hvm64 L1 guest. + * + * The test exercises the reachable architectural VMLOAD and VMSAVE + * failure cases in this harness: + * 1. SVM disabled via EFER.SVME clear. + * 2. Instructions executed at CPL > 0. + * 3. Instructions executed with malformed VMCB physical addresses in RAX. + * + * The test passes if it receives the expected exception class for each case. + * + * @include tests/nested-svm-vmloadsave/index.rst + * @see tests/nested-svm-vmloadsave/main.c + */ +#include + +const char test_title[] = "Nested SVM VMLOAD/VMSAVE"; + +enum svm_negative_mode { + SVM_NEGATIVE_KERNEL, + SVM_NEGATIVE_USER, +}; + +enum svm_negative_paddr_kind { + SVM_NEGATIVE_PADDR_ALIGNED, + SVM_NEGATIVE_PADDR_UNALIGNED, + SVM_NEGATIVE_PADDR_TOO_WIDE, +}; + +struct svm_negative_case { + const char *name; + bool svme; + enum svm_negative_mode mode; + enum svm_negative_paddr_kind paddr_kind; + exinfo_t expected; +}; + +struct svm_negative_ops { + exinfo_t (*kernel)(uint64_t paddr); + unsigned long (*user)(unsigned long paddr); +}; + +/* Return false when setup already reported a skip or failure for the case. */ +typedef bool (*svm_negative_setup_fn)(const struct svm_negative_case *t, + uint64_t paddr, void *data); +typedef void (*svm_negative_teardown_fn)(const struct svm_negative_case *t, + uint64_t paddr, void *data); + +/* + * Execution context for a family of SVM negative cases. + * + * Optional setup/teardown hooks let higher-level tests prepare nested + * state such as L2 or L3 control structures outside the fixed case matrix + * before the helper dispatches the actual instruction. + */ +struct svm_negative_runner { + const void *vmcb_page; + const struct svm_negative_ops *ops; + void *data; + svm_negative_setup_fn setup; + svm_negative_teardown_fn teardown; +}; + +/** + * Execute VMLOAD at CPL0 with a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static exinfo_t stub_vmload(uint64_t paddr) +{ + exinfo_t fault = 0; + + asm volatile ("1: vmload %%rax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+D" (fault) + : "a" (paddr), [rec] "p" (ex_record_fault_edi) + : "memory"); + + return fault; +} + +/** + * Execute VMLOAD at CPL3 using a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static unsigned long __user_text user_vmload(unsigned long paddr) +{ + unsigned long fault = 0; + + asm volatile ("mov %[paddr], %%rax;" + "1: vmload %%rax; xor %%eax, %%eax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+a" (fault) + : [paddr] "r" (paddr), + [rec] "p" (ex_record_fault_eax) + : "memory"); + + return fault; +} + +static const struct svm_negative_ops vmload_ops = { + .kernel = stub_vmload, + .user = user_vmload, +}; + +/* Aligned scratch page used to form candidate VMCB physical addresses. */ +static uint8_t vmcb_page[PAGE_SIZE] __page_aligned_bss; + +static const struct svm_negative_runner vmload_runner = { + .vmcb_page = vmcb_page, + .ops = &vmload_ops, +}; + +/** + * Execute VMSAVE at CPL0 with a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static exinfo_t stub_vmsave(uint64_t paddr) +{ + exinfo_t fault = 0; + + asm volatile ("1: vmsave %%rax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+D" (fault) + : "a" (paddr), [rec] "p" (ex_record_fault_edi) + : "memory"); + + return fault; +} + +/** + * Execute VMSAVE at CPL3 using a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static unsigned long __user_text user_vmsave(unsigned long paddr) +{ + unsigned long fault = 0; + + asm volatile ("mov %[paddr], %%rax;" + "1: vmsave %%rax; xor %%eax, %%eax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+a" (fault) + : [paddr] "r" (paddr), + [rec] "p" (ex_record_fault_eax) + : "memory"); + + return fault; +} + +static const struct svm_negative_ops vmsave_ops = { + .kernel = stub_vmsave, + .user = user_vmsave, +}; + +static const struct svm_negative_runner vmsave_runner = { + .vmcb_page = vmcb_page, + .ops = &vmsave_ops, +}; + +static uint64_t svm_negative_paddr(const void *vmcb_page, + enum svm_negative_paddr_kind kind) +{ + switch ( kind ) + { + case SVM_NEGATIVE_PADDR_ALIGNED: + return _u(vmcb_page); + + case SVM_NEGATIVE_PADDR_UNALIGNED: + return _u(vmcb_page) | 1ul; + + case SVM_NEGATIVE_PADDR_TOO_WIDE: + return 1ull << maxphysaddr; + } + + unreachable(); +} + +static bool svm_negative_run(const struct svm_negative_case *t, + const struct svm_negative_runner *runner, + exinfo_t *res) +{ + uint64_t paddr = svm_negative_paddr(runner->vmcb_page, t->paddr_kind); + uint64_t orig_efer = 0; + + *res = 0; + + if ( runner->setup && !runner->setup(t, paddr, runner->data) ) + return false; + + orig_efer = update_efer_svme(t->svme); + + switch ( t->mode ) + { + case SVM_NEGATIVE_KERNEL: + *res = runner->ops->kernel(paddr); + break; + + case SVM_NEGATIVE_USER: + *res = exec_user_param(runner->ops->user, paddr); + break; + } + + if ( runner->teardown ) + runner->teardown(t, paddr, runner->data); + + wrmsr(MSR_EFER, orig_efer); + return *res == t->expected; +} + +bool svm_negative_check_cases(const struct svm_negative_case *cases, + unsigned int nr_cases, + const struct svm_negative_runner *runner) +{ + bool result = true; + + for ( unsigned int i = 0; i < nr_cases; ++i ) + { + exinfo_t res; + + printk("Running test %u: %s\n", i + 1, cases[i].name); + + if ( !svm_negative_run(&cases[i], runner, &res) ) + { + xtf_failure("Fail: %s, got %pe, expected %pe\n", + cases[i].name, _p(res), _p(cases[i].expected)); + result = false; + } + } + + return result; +} + +/** + * Execute the reachable VMLOAD and VMSAVE negative-case matrices. + * + * The matrix covers the distinct bare-metal failure classes that can be + * observed from an hvm64 L1 harness: EFER.SVME clear, CPL > 0, and malformed + * VMCB physical addresses in RAX. + */ +void test_main(void) +{ + static const struct svm_negative_case vmload_cases[] = { + { + .name = "vmload with SVME clear at CPL0", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmload with SVME clear at CPL0 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME clear at CPL0 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmload with SVME clear at CPL3", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME clear at CPL3 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME clear at CPL3 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL3", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL3 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL3 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL0 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL0 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + }; + + static const struct svm_negative_case vmsave_cases[] = { + { + .name = "vmsave with SVME clear at CPL0", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmsave with SVME clear at CPL0 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME clear at CPL0 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmsave with SVME clear at CPL3", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME clear at CPL3 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME clear at CPL3 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL3", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL3 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL3 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL0 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL0 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + }; + bool res1, res2; + + if ( !cpu_has_svm ) + { + xtf_skip("Skip: SVM not available\n"); + return; + } + + res1 = svm_negative_check_cases(vmload_cases, ARRAY_SIZE(vmload_cases), + &vmload_runner); + res2 = svm_negative_check_cases(vmsave_cases, ARRAY_SIZE(vmsave_cases), + &vmsave_runner); + if (res1 && res2) + xtf_success(NULL); + else + xtf_failure("One or more negative cases failed\n"); +} diff --git a/tests/nested-svm-run/Makefile b/tests/nested-svm-run/Makefile new file mode 100644 index 00000000..eb1fe7b6 --- /dev/null +++ b/tests/nested-svm-run/Makefile @@ -0,0 +1,12 @@ +include $(ROOT)/build/common.mk + +NAME := nested-svm-run +CATEGORY := nested-svm +TEST-ENVS := $(SVM_ENVIRONMENTS) + +TEST-EXTRA-CFG := extra.cfg.in + +obj-perenv += main.o +obj-perenv += $(ROOT)/common/nested-svm/setup-l2.o + +include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm-run/extra.cfg.in b/tests/nested-svm-run/extra.cfg.in new file mode 100644 index 00000000..ae494f84 --- /dev/null +++ b/tests/nested-svm-run/extra.cfg.in @@ -0,0 +1 @@ +nestedhvm = 1 diff --git a/tests/nested-svm-run/index.rst b/tests/nested-svm-run/index.rst new file mode 100644 index 00000000..eccb6ef7 --- /dev/null +++ b/tests/nested-svm-run/index.rst @@ -0,0 +1,60 @@ +Nested SVM VMRUN +================ + +This test exercises a minimal AMD nested-SVM guest entry path on hvm64. + +An L1 guest enables SVM, builds an L2 VMCB that reuses L1's page tables and +descriptor tables, mirrors the active TR and LDTR state into the VMCB, and +enters L2 with VMRUN. The L2 payload runs on its own stack, writes a sentinel +into shared memory, and halts. + +The test passes when L1 observes a HLT VMEXIT from L2 and the expected +sentinel value. + +What It Verifies +---------------- + +The test verifies that Xen's nested-SVM implementation accepts a minimal but +architecturally valid L2 VMCB built by an L1 guest running in long mode. + +More specifically, it verifies that: + +* L1 can enable SVM and program the host-save area needed by VMRUN. +* L1 can populate an L2 VMCB with inherited control state, descriptor-table + state, and system-segment state taken from the live L1 environment. +* VMRUN succeeds in entering L2 rather than failing immediately because of + malformed guest state. +* L2 executes the supplied payload on its own stack, updates shared memory, + and exits through HLT. +* L1 receives a VMEXIT with exit code VMEXIT_HLT and observes the sentinel + value written by L2. + +How The Verification Functions Work +----------------------------------- + +The verification logic is split between the helpers that build a +valid VMCB and the final checks performed after returning from VMRUN. + +``test_main()`` performs the end-to-end verification. It enables SVM, +writes the host-save area address to MSR_VM_HSAVE_PA, builds the VMCB, +and enters L2 through ``svm_vmrun()``. + +When execution returns to L1, the test checks two conditions: the VMEXIT +reason must be ``VMEXIT_HLT``, and the shared handshake value must match +``L2_SENTINEL``. Both checks must pass for the test to report success. + +``l2_entry()`` is the L2 payload. It avoids using VMMCALL, because +in Xen's nested-SVM model that would unconditionally cause a VMEXIT +to L1. Instead, it writes a known sentinel value into shared memory +and halts in a loop so that L1 sees a clean HLT exit reason. + +``build_l2_vmcb()`` prepares the nested guest state. It programs the +required intercepts, reuses L1's paging and descriptor-table state, +assigns the L2 RIP and stack, and copies LDTR and TR from the current +GDT into the VMCB. + +The helper ``vmcb_set_seg_desc()`` translates an L1 selector into the +VMCB segment format, while rejecting selectors that are null, LDT-based, +or out of bounds for the current GDT limit. This matters because VMRUN +consumes the VMCB's segment state directly, including the system +descriptors needed for long-mode execution. diff --git a/tests/nested-svm-run/main.c b/tests/nested-svm-run/main.c new file mode 100644 index 00000000..b23eaa06 --- /dev/null +++ b/tests/nested-svm-run/main.c @@ -0,0 +1,142 @@ +/** + * @file tests/nested-svm-run/main.c + * @ref test-nested-svm-run + * @page test-nested-svm-run Nested SVM VMRUN Smoke Test + * + * An L1 guest: + * 1. enables SVM, + * 2. builds a minimal L2 VMCB that re-uses L1's address space, and + * 3. uses VMRUN to enter an L2 callback. + * + * L2: + * 1. increments %rax + * 2. signals completion with HLT, which causes a #VMEXIT back to L1. + * 3. checks that HLT with L2 IF clear doesn't power off L1 when L1 has the + * INTR intercept armed. + * + * @see tests/nested-svm-run/main.c + */ +#include + +const char test_title[] = "Nested SVM VMRUN Smoke Test"; + +/** + * Run a minimal L2 payload and report success back to L1. + */ +static void __used l2_entry(void) +{ + asm volatile ("inc %rax;hlt"); /* Signal success by incrementing %rax */ +} + +static void __used l2_cli_hlt(void) +{ + asm volatile ("cli;hlt"); +} + +static void run_l2(void) +{ + asm volatile("mov %0, %%rax\n" + "vmload %%rax\n" + "vmrun %%rax\n" + "vmsave %%rax\n" + : + : "r" (_u(&vmcb12)) + : "%rax", "memory"); +} + +/* + * Test that L2 CLI;HLT with L1 INTR intercept armed does not power off L1. + * This is a known Xen bug that is expected to be fixed in the future: + * + * Issue to be fixed: L1 killed if L2 halts while not intercepted: + * (XEN) arch/x86/hvm/hvm.c:1735:d13v0 All CPUs offline -- powering off. + * (XEN) vcpu_runstate_change: d13 has no online vcpus! + * + * Possible cause: + * This code may be wrong since L1 called vmrun with the IF flag + * set and the INTR intercept enabled. It could come from this + * check which only checks eflags from L2: + * + * > * If we halt with interrupts disabled, that's a pretty sure sign that we + * > * want to shut down. In a real processor, NMIs are the only way to break + * > * out of this. + * > + * > if ( unlikely(!(eflags & X86_EFLAGS_IF)) ) + * > return hvm_vcpu_down(curr); + */ +static bool expect_xfail_l2_cli_hlt_with_l1_intr_intercept(void) +{ + unsigned long l1_rflags = read_flags(); + + printk("L1: XFAIL testing L2 CLI;HLT with L1 INTR intercept armed\n"); + + svm_l2_build_vmcb(&vmcb12, NULL); + vmcb12.intercept_insns_vec_00c.fields.hlt = 0; + vmcb12.intercept_insns_vec_00c.fields.intr = 1; + vmcb12.rflags |= X86_EFLAGS_IF; + vmcb12.rip = _u(l2_cli_hlt); + + write_flags(l1_rflags | X86_EFLAGS_IF); + if ( !(read_flags() & X86_EFLAGS_IF) ) + { + xtf_error("L1 IF is clear before VMRUN\n"); + write_flags(l1_rflags); + return false; + } + + xtf_warning("XFAIL: broken Xen powers off L1 after L2 CLI;HLT with " + "HLT intercept clear and INTR intercept set\n"); + printk("L1: entering L2 via VMRUN with HLT intercept clear, INTR intercept set\n"); + run_l2(); + write_flags(l1_rflags); + printk("L1: returned from L2 (exit 0x%lx %s)\n", + vmcb12.exitcode, vmexit_reason(vmcb12.exitcode)); + + if ( vmcb12.exitcode == VMEXIT_INTR || vmcb12.exitcode == VMEXIT_VINTR ) + { + xtf_failure("XPASS: L2 CLI;HLT exited to L1 via %s; " + "update this test to expect the bug fixed\n", + vmexit_reason(vmcb12.exitcode)); + return false; + } + + xtf_warning("XFAIL: L2 CLI;HLT did not exit to L1 via the INTR intercept\n"); + return true; +} + +/** + * Execute the nested-SVM VMRUN smoke test. + * + * L1 enables SVM, prepares a minimal L2 VMCB, enters L2 once with VMRUN + * and verifies that L2 reports success before exiting with HLT. + */ +void test_main(void) +{ + /* Enable SVM, arm the host-save area and build the L2 VMCB. */ + if (!svm_l1_enable_svm()) + return; + + svm_l2_build_vmcb(&vmcb12, NULL); + + /* Set the L2 entry point to this test's l2_entry function. */ + vmcb12.rip = _u(l2_entry); + + print_efer("VMCB12", vmcb12.efer); + printk("L1: entering L2 via VMRUN\n"); + run_l2(); + printk("L1: returned from L2 (rax 0x%lx)\n", vmcb12.rax); + + if ( vmcb12.exitcode != VMEXIT_HLT ) + { + printk("Exit reason: %s\n", vmexit_reason(vmcb12.exitcode)); + return xtf_failure("unexpected L2 exit: 0x%lx\n", vmcb12.exitcode); + } + + if ( vmcb12.rax != 1 ) /* L2 should have incremented %rax from 0 to 1 */ + return xtf_failure("unexpected L2 %%rax: 0x%lx\n", vmcb12.rax); + + if ( !expect_xfail_l2_cli_hlt_with_l1_intr_intercept() ) + return; + + xtf_success(NULL); +} diff --git a/tests/nested-svm/Makefile b/tests/nested-svm/Makefile deleted file mode 100644 index a457d8a1..00000000 --- a/tests/nested-svm/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -include $(ROOT)/build/common.mk - -NAME := nested-svm -CATEGORY := in-development -TEST-ENVS := $(HVM_ENVIRONMENTS) - -TEST-EXTRA-CFG := extra.cfg.in - -obj-perenv += main.o - -include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm/main.c b/tests/nested-svm/main.c deleted file mode 100644 index 3dc0ff1f..00000000 --- a/tests/nested-svm/main.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file tests/nested-svm/main.c - * @ref test-nested-svm - * - * @page test-nested-svm Nested SVM - * - * Functional testing of the SVM features in a nested-virt environment. - * - * @see tests/nested-svm/main.c - */ -#include - -const char test_title[] = "Nested SVM testing"; - -void test_main(void) -{ - if ( !cpu_has_svm ) - return xtf_skip("Skip: SVM not available\n"); - - if ( !vendor_is_amd ) - xtf_warning("Warning: SVM found on non-AMD processor\n"); - - xtf_success(NULL); -} - -/* - * Local variables: - * mode: C - * c-file-style: "BSD" - * c-basic-offset: 4 - * tab-width: 4 - * indent-tabs-mode: nil - * End: - */ diff --git a/xtf-runner b/xtf-runner index 8e27a484..98c63788 100755 --- a/xtf-runner +++ b/xtf-runner @@ -1,748 +1,9 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -xtf-runner - A utility for enumerating and running XTF tests. +"""Compatibility wrapper for the importable xtf-runner implementation.""" -Currently assumes the presence and availability of the `xl` toolstack. -""" -from __future__ import print_function -from __future__ import unicode_literals - -import json -import os -import subprocess import sys -from functools import partial -from optparse import OptionParser -from os import path -from subprocess import PIPE - - -# Python 2/3 compatibility -if sys.version_info >= (3, ): - basestring = str - - -# Wrap Subprocess functions to use universal_newlines by default -Popen = partial(subprocess.Popen, universal_newlines = True) -subproc_call = partial(subprocess.call, universal_newlines = True) -check_output = partial(subprocess.check_output, universal_newlines = True) - - -# All results of a test, keep in sync with C code report.h. -# Notes: -# - WARNING is not a result on its own. -# - CRASH isn't known to the C code, but covers all cases where a valid -# result was not found. -all_results = ('SUCCESS', 'SKIP', 'ERROR', 'FAILURE', 'CRASH') - -# Return the exit code for different states. Avoid using 1 and 2 because -# python interpreter uses them -- see document for sys.exit. -def exit_code(state): - """ Convert a test result to an xtf-runner exit code. """ - return { "SUCCESS": 0, - "SKIP": 3, - "ERROR": 4, - "FAILURE": 5, - "CRASH": 6, - }[state] - -# All test categories -default_categories = {"functional", "xsa"} -non_default_categories = {"special", "utility", "in-development"} -all_categories = default_categories | non_default_categories - -# All test environments -pv_environments = {"pv64", "pv32pae"} -hvm_environments = {"hvm64", "hvm32pae", "hvm32pse", "hvm32"} -all_environments = pv_environments | hvm_environments - - -class RunnerError(Exception): - """ Errors relating to xtf-runner itself """ - - -def env_to_virt_caps(env): - """ Identify which virt cap(s) are needed for an environment """ - if env in hvm_environments: - return {"hvm"} - caps = {"pv"} - if env == "pv32pae": - caps |= {"pv32"} - return caps - - -class TestInstance(object): - """ Object representing a single test. """ - - def __init__(self, arg): - """ Parse and verify 'arg' as a test instance. """ - self.env, self.name, self.variation = parse_test_instance_string(arg) - - if self.env is None: - raise RunnerError("No environment for '{0}'".format(arg)) - - if self.variation is None and get_all_test_info()[self.name].variations: - raise RunnerError("Test '{0}' has variations, but none specified" - .format(self.name)) - - self.req_caps = env_to_virt_caps(self.env) - self.req_caps |= {"hap", "shadow"} & set((self.variation, )) - - def vm_name(self): - """ Return the VM name as `xl` expects it. """ - return repr(self) - - def cfg_path(self): - """ Return the path to the `xl` config file for this test. """ - return path.join("tests", self.name, repr(self) + ".cfg") - - def __repr__(self): - if not self.variation: - return "test-{0}-{1}".format(self.env, self.name) - else: - return "test-{0}-{1}~{2}".format(self.env, self.name, self.variation) - - def __hash__(self): - return hash(repr(self)) - - def __eq__(self, other): - return repr(self) == repr(other) - - -class TestInfo(object): - """ Object representing a tests info.json, in a more convenient form. """ - - def __init__(self, test_json): - """Parse and verify 'test_json'. - - May raise KeyError, TypeError or ValueError. - """ - - name = test_json["name"] - if not isinstance(name, basestring): - raise TypeError("Expected string for 'name', got '{0}'" - .format(type(name))) - self.name = name - - cat = test_json["category"] - if not isinstance(cat, basestring): - raise TypeError("Expected string for 'category', got '{0}'" - .format(type(cat))) - if not cat in all_categories: - raise ValueError("Unknown category '{0}'".format(cat)) - self.cat = cat - - envs = test_json["environments"] - if not isinstance(envs, list): - raise TypeError("Expected list for 'environments', got '{0}'" - .format(type(envs))) - if not envs: - raise ValueError("Expected at least one environment") - for env in envs: - if not env in all_environments: - raise ValueError("Unknown environments '{0}'".format(env)) - self.envs = envs - - variations = test_json["variations"] - if not isinstance(variations, list): - raise TypeError("Expected list for 'variations', got '{0}'" - .format(type(variations))) - self.variations = variations - - def all_instances(self, env_filter = None, vary_filter = None): - """Return a list of TestInstances, for each supported environment. - Optionally filtered by env_filter. May return an empty list if - the filter doesn't match any supported environment. - """ - - if env_filter: - envs = set(env_filter).intersection(self.envs) - else: - envs = self.envs - - if vary_filter: - variations = set(vary_filter).intersection(self.variations) - else: - variations = self.variations - - res = [] - if variations: - for env in envs: - for vary in variations: - res.append(TestInstance("test-{0}-{1}~{2}" - .format(env, self.name, vary))) - else: - res = [ TestInstance("test-{0}-{1}".format(env, self.name)) - for env in envs ] - return res - - def __repr__(self): - return "TestInfo({0})".format(self.name) - - -def parse_test_instance_string(arg): - """Parse a test instance string. - - Has the form: '[[test-]$ENV-]$NAME[~$VARIATION]' - - Optional 'test-' prefix - Optional $ENV environment part - Mandatory $NAME - Optional ~$VARIATION suffix - - Verifies: - - $NAME is valid - - if $ENV, it is valid for $NAME - - if $VARIATION, it is valid for $NAME - - Returns: tuple($ENV or None, $NAME, $VARIATION or None) - """ - - all_tests = get_all_test_info() - - variation = None - if '~' in arg: - arg, variation = arg.split('~', 1) - - parts = arg.split('-', 2) - parts_len = len(parts) - - # If arg =~ test-$ENV-$NAME - if parts_len == 3 and parts[0] == "test" and parts[1] in all_environments: - _, env, name = parts - - # If arg =~ $ENV-$NAME - elif parts_len > 0 and parts[0] in all_environments: - env, name = parts[0], "-".join(parts[1:]) - - # If arg =~ $NAME - elif arg in all_tests: - env, name = None, arg - - # Otherwise, give up - else: - raise RunnerError("Unrecognised test '{0}'".format(arg)) - - # At this point, 'env' has always been checked for plausibility. 'name' - # might not be - - if name not in all_tests: - raise RunnerError("Unrecognised test name '{0}' for '{1}'" - .format(name, arg)) - - info = all_tests[name] - - if env and env not in info.envs: - raise RunnerError("Test '{0}' has no environment '{1}'" - .format(name, env)) - - # If a variation has been given, check it is valid - if variation is not None: - if not info.variations: - raise RunnerError("Test '{0}' has no variations".format(name)) - elif not variation in info.variations: - raise RunnerError("No variation '{0}' for test '{1}'" - .format(variation, name)) - - return env, name, variation - - -# Cached data from tests/*/info.json -_all_test_info = {} - -def get_all_test_info(): - """ Open and collate each info.json """ - if not _all_test_info: # Cache on first request - - for test in os.listdir("tests"): - try: - with open(path.join("tests", test, "info.json")) as f: - - info = TestInfo(json.load(f)) - - if info.name != test: - raise ValueError # JSON also looks bad - - _all_test_info[test] = info - - except (IOError, # Ignore directories without a info.json - ValueError, KeyError, TypeError): # Ingore bad JSON - continue - - return _all_test_info - - -# Cached virt caps -_virt_caps = set() - -def get_virt_caps(): - """ Query Xen for the virt capabilities of the host """ - global _virt_caps - - if not _virt_caps: # Cache on first request - - # Filter down to caps we're happy for tests to use - caps = {"pv", "hvm", "hap", "shadow"} - caps &= set(check_output(["xl", "info", "virt_caps"]).split()) - - # Synthesize a pv32 virt cap by looking at xen_caps - if ("pv" in caps and - "xen-3.0-x86_32p" in check_output(["xl", "info", "xen_caps"])): - caps |= {"pv32"} - - _virt_caps = caps - - return _virt_caps - - -def tests_from_selection(cats, envs, tests, caps): - """Given a selection of possible categories, environment and tests, return - all tests within the provided parameters. - - Multiple entries for each individual parameter are combined or-wise. - e.g. cats=['special', 'functional'] chooses all tests which are either - special or functional. envs=['hvm64', 'pv64'] chooses all tests which are - either pv64 or hvm64. - - Multiple parameter are combined and-wise, taking the intersection rather - than the union. e.g. cats=['functional'], envs=['pv64'] gets the tests - which are both part of the functional category and the pv64 environment. - - By default, not all categories are available. Selecting envs=['pv64'] - alone does not include the non-default categories, as this is most likely - not what the author intended. Any reference to non-default categories in - cats[] or tests[] turns them all back on, so non-default categories are - available when explicitly referenced. - """ - - all_tests = get_all_test_info() - all_test_info = all_tests.values() - res = [] - - if cats: - # If a selection of categories have been requested, start with all test - # instances in any of the requested categories. - for info in all_test_info: - if info.cat in cats: - res.extend(info.all_instances()) - - if envs: - # If a selection of environments have been requested, reduce the - # category selection to requested environments, or pick all suitable - # tests matching the environments request. - if res: - res = [ x for x in res if x.env in envs ] - else: - # Work out whether to include non-default categories or not. - categories = default_categories - if non_default_categories & set(cats): - categories = all_categories - - elif tests: - sel_test_names = set(x.name for x in tests) - sel_test_cats = set(all_tests[x].cat for x in sel_test_names) - - if non_default_categories & sel_test_cats: - categories = all_categories - - for info in all_test_info: - if info.cat in categories: - res.extend(info.all_instances(env_filter = envs)) - - if tests: - # If a selection of tests has been requested, reduce the results so - # far to the requested tests (this is meaningful in the case that - # tests[] has been specified without a specific environment), or just - # take the tests verbatim. - if res: - res = [ x for x in res if x in tests ] - else: - res = tests - - if caps: - res = [ x for x in res if x.req_caps.issubset(caps) ] - - # Sort the results. Variation third, Env second and Name fist. - res = sorted(res, key = lambda test: test.variation or "") - res = sorted(res, key = lambda test: test.env) - res = sorted(res, key = lambda test: test.name) - return res - - -def interpret_selection(opts): - """Interpret the argument list as a collection of categories, environments, - pseduo-environments and partial and complete test names. - - Returns a list of all test instances within the selection. - """ - - args = set(opts.args) - - # First, filter into large buckets - cats = all_categories & args - envs = all_environments & args - others = args - cats - envs - - # Add all categories if --all or --non-default is passed - if opts.all: - cats |= default_categories - if opts.non_default: - cats |= non_default_categories - - # Allow "pv" and "hvm" as a combination of environments - if "pv" in others: - envs |= pv_environments - others -= {"pv"} - - if "hvm" in others: - envs |= hvm_environments - others -= {"hvm"} - - # No input? No selection. - if not cats and not envs and not others: - return [] - - all_tests = get_all_test_info() - tests = [] - - # Second, sanity check others as full or partial test names - for arg in others: - env, name, vary = parse_test_instance_string(arg) - - instances = all_tests[name].all_instances( - env_filter = env and [env] or None, - vary_filter = vary and [vary] or None, - ) - - if not instances: - raise RunnerError("No appropriate instances for '{0}' (env {1})" - .format(arg, env)) - - tests.extend(instances) - - # Third, if --host is passed, also filter by capabilities - caps = None - if opts.host: - caps = get_virt_caps() - - return tests_from_selection(cats, envs, set(tests), caps) - - -def list_tests(opts): - """ List tests """ - - if opts.environments: - # The caller only wants the environment list - for env in sorted(all_environments): - print(env) - return - - if not opts.selection: - raise RunnerError("No tests selected") - - for sel in opts.selection: - print(sel) - - -def interpret_result(logline): - """ Interpret the final log line of a guest for a result """ - - if not "Test result:" in logline: - return "CRASH" - - for res in all_results: - if res in logline: - return res - - return "CRASH" - - -def run_test_console(opts, test): - """ Run a specific, obtaining results via xenconsole """ - - cmd = ['xl', 'create', '-p', test.cfg_path()] - if not opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - - create = Popen(cmd, stdout = PIPE, stderr = PIPE) - _, stderr = create.communicate() - - if create.returncode: - if opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - print(stderr) - raise RunnerError("Failed to create VM") - - cmd = ['xl', 'console', test.vm_name()] - if not opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - - console = Popen(cmd, stdout = PIPE) - - cmd = ['xl', 'unpause', test.vm_name()] - if not opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - - rc = subproc_call(cmd) - if rc: - if opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - raise RunnerError("Failed to unpause VM") - - stdout, _ = console.communicate() - - if console.returncode: - raise RunnerError("Failed to obtain VM console") - - lines = stdout.splitlines() - - if lines: - if not opts.quiet: - print("\n".join(lines)) - print("") - - else: - return "CRASH" - - return interpret_result(lines[-1]) - - -def run_test_logfile(opts, test): - """ Run a specific test, obtaining results from a logfile """ - - logpath = path.join(opts.logfile_dir, - opts.logfile_pattern.replace("%s", str(test))) - - if not opts.quiet: - print("Using logfile '{0}'".format(logpath)) - - fd = os.open(logpath, os.O_CREAT | os.O_RDONLY, 0o644) - logfile = os.fdopen(fd) - logfile.seek(0, os.SEEK_END) - - cmd = ['xl', 'create', '-F', test.cfg_path()] - if not opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - - guest = Popen(cmd, stdout = PIPE, stderr = PIPE) - - _, stderr = guest.communicate() - - if guest.returncode: - if opts.quiet: - print("Executing '{0}'".format(" ".join(cmd))) - print(stderr) - raise RunnerError("Failed to run test") - - line = "" - for line in logfile.readlines(): - - line = line.rstrip() - if not opts.quiet: - print(line) - - if "Test result:" in line: - print("") - break - - logfile.close() - - return interpret_result(line) - - -def run_test(opts, test): - """ Run a single test instance """ - - # If caps say the test can't run, short circuit to SKIP - if not test.req_caps.issubset(get_virt_caps()): - return "SKIP" - - fn = { - "console": run_test_console, - "logfile": run_test_logfile, - }[opts.results_mode] - - return fn(opts, test) - - -def run_tests(opts): - """ Run tests """ - - tests = opts.selection - if not tests: - raise RunnerError("No tests to run") - - rc = all_results.index('SUCCESS') - results = [] - - for test in tests: - - res = run_test(opts, test) - res_idx = all_results.index(res) - if res_idx > rc: - rc = res_idx - - results.append(res) - - print("Combined test results:") - - for test, res in zip(tests, results): - - if res == "SUCCESS" and opts.quiet >= 2: - continue - - print("{0:<40} {1}".format(str(test), res)) - - return exit_code(all_results[rc]) - - -def main(): - """ Main entrypoint """ - - # Change stdout to be line-buffered. - sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1) - - # Normalise $CWD to the directory this script is in - os.chdir(path.dirname(path.abspath(sys.argv[0]))) - - # Avoid wrapping the epilog text - OptionParser.format_epilog = lambda self, formatter: self.epilog - - parser = OptionParser( - usage = "%prog [--list] [options]", - description = "Xen Test Framework enumeration and running tool", - epilog = ( - "\n" - "Overview:\n" - " Running with --list will print the entire selection\n" - " to the console. Running without --list will execute\n" - " all tests in the selection, printing a summary of their\n" - " results at the end.\n" - "\n" - " To determine how runner should get output from Xen, use\n" - ' --results-mode option. The default value is "console", \n' - " which means using xenconsole program to extract output.\n" - ' The other supported value is "logfile", which\n' - " means to get output from log file.\n" - "\n" - ' The "logfile" mode requires users to configure\n' - " xenconsoled to log guest console output. This mode\n" - " is useful for Xen version < 4.8. Also see --logfile-dir\n" - " and --logfile-pattern options.\n" - "\n" - "Selections:\n" - " A selection is zero or more of any of the following\n" - " parameters: Categories, Environments and Tests.\n" - " Multiple instances of the same type of parameter are\n" - " unioned while the end result in intersected across\n" - " types. e.g.\n" - "\n" - " 'functional xsa'\n" - " All tests in the functional and xsa categories\n" - "\n" - " 'functional xsa hvm32'\n" - " All tests in the functional and xsa categories\n" - " which are implemented for the hvm32 environment\n" - "\n" - " 'invlpg example'\n" - " The invlpg and example tests in all implemented\n" - " environments\n" - "\n" - " 'invlpg example pv'\n" - " The pv environments of the invlpg and example tests\n" - "\n" - " 'pv32pae-pv-iopl'\n" - " The pv32pae environment of the pv-iopl test only\n" - "\n" - " Additionally, --host may be passed to restrict the\n" - " selection to tests applicable to the current host.\n" - " --all may be passed to choose all default categories\n" - " without needing to explicitly name them. --non-default\n" - " is available to obtain the non-default categories.\n" - "\n" - " The special parameter --environments may be passed to\n" - " get the full list of environments. This option does not\n" - " make sense combined with a selection.\n" - "\n" - "Examples:\n" - " Listing all tests implemented for hvm32 environment:\n" - " ./xtf-runner --list hvm32\n" - "\n" - " Listing all functional tests appropriate for this host:\n" - " ./xtf-runner --list functional --host\n" - "\n" - " Running all the pv-iopl tests:\n" - " ./xtf-runner pv-iopl\n" - " \n" - " Combined test results:\n" - " test-pv64-pv-iopl SUCCESS\n" - " test-pv32pae-pv-iopl SUCCESS\n" - "\n" - " Exit code for this script:\n" - " 0: everything is ok\n" - " 1,2: reserved for python interpreter\n" - " 3: test(s) are skipped\n" - " 4: test(s) report error\n" - " 5: test(s) report failure\n" - " 6: test(s) crashed\n" - "\n" - ), - ) - - parser.add_option("-l", "--list", action = "store_true", - dest = "list_tests", - help = "List tests in the selection", - ) - parser.add_option("-a", "--all", action = "store_true", - dest = "all", - help = "Select all default categories", - ) - parser.add_option("--non-default", action = "store_true", - dest = "non_default", - help = "Select all non default categories", - ) - parser.add_option("--environments", action = "store_true", - dest = "environments", - help = "List all the known environments", - ) - parser.add_option("--host", action = "store_true", - dest = "host", help = "Restrict selection to applicable" - " tests for the current host", - ) - parser.add_option("-m", "--results-mode", action = "store", - dest = "results_mode", default = "console", - type = "choice", choices = ("console", "logfile"), - help = "Control how xtf-runner gets its test results") - parser.add_option("--logfile-dir", action = "store", - dest = "logfile_dir", default = "/var/log/xen/console/", - type = "string", - help = ('Specify the directory to look for console logs, ' - 'defaults to "/var/log/xen/console/"'), - ) - parser.add_option("--logfile-pattern", action = "store", - dest = "logfile_pattern", default = "guest-%s.log", - type = "string", - help = ('Specify the log file name pattern, ' - 'defaults to "guest-%s.log"'), - ) - parser.add_option("-q", "--quiet", action = "count", - dest = "quiet", default = 0, - help = ("Progressively make the output less verbose. " - "1) No console logs, only test results. " - "2) Not even SUCCESS results."), - ) - - opts, args = parser.parse_args() - opts.args = args - - opts.selection = interpret_selection(opts) - - if opts.list_tests: - return list_tests(opts) - else: - return run_tests(opts) +from xtf.runner.cli import RunnerError, main if __name__ == "__main__": try: diff --git a/xtf/__init__.py b/xtf/__init__.py new file mode 100644 index 00000000..8324a00f --- /dev/null +++ b/xtf/__init__.py @@ -0,0 +1 @@ +"""Python support code for the Xen Test Framework.""" diff --git a/xtf/runner/__init__.py b/xtf/runner/__init__.py new file mode 100644 index 00000000..88a65d6b --- /dev/null +++ b/xtf/runner/__init__.py @@ -0,0 +1 @@ +"""Importable implementation of xtf-runner.""" diff --git a/xtf/runner/cli.py b/xtf/runner/cli.py new file mode 100755 index 00000000..adac4ed0 --- /dev/null +++ b/xtf/runner/cli.py @@ -0,0 +1,805 @@ +#!/usr/bin/env python3 +""" +xtf-runner - A utility for enumerating and running XTF tests. + +Currently assumes the presence and availability of the `xl` toolstack. +""" + +import json +import os +import subprocess +import sys +from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter +from functools import partial +from os import path +from subprocess import PIPE +from typing import Any, Dict, List, Optional, Set, Tuple + +# Wrap Subprocess functions to use universal_newlines by default +Popen = partial(subprocess.Popen, universal_newlines=True) +subproc_call = partial(subprocess.call, universal_newlines=True) +check_output = partial(subprocess.check_output, universal_newlines=True) + + +# All results of a test, keep in sync with C code report.h where applicable. +# Notes: +# - WARNING is not a result on its own. +# - CRASH isn't known to the C code, but covers all cases where a valid +# result was not found. +# - XFAIL isn't known to the C code, but covers marked expected crashes. +all_results = ("SUCCESS", "XFAIL", "SKIP", "ERROR", "FAILURE", "CRASH") + + +# Return the exit code for different states. Avoid using 1 and 2 because +# python interpreter uses them -- see document for sys.exit. +def exit_code(state: str) -> int: + """Convert a test result to an xtf-runner exit code.""" + return { + "SUCCESS": 0, + "XFAIL": 0, + "SKIP": 3, + "ERROR": 4, + "FAILURE": 5, + "CRASH": 6, + }[state] + + +# All test categories +default_categories = {"functional", "xsa", "nested-svm"} +non_default_categories = {"special", "utility", "in-development"} +all_categories = default_categories | non_default_categories + +# All test environments +pv_environments = {"pv64", "pv32pae"} +hvm_environments = {"hvm64", "hvm32pae", "hvm32pse", "hvm32"} +all_environments = pv_environments | hvm_environments + + +class RunnerError(Exception): + """Errors relating to xtf-runner itself""" + + +def env_to_virt_caps(env: str) -> Set[str]: + """Identify which virt cap(s) are needed for an environment""" + if env in hvm_environments: + return {"hvm"} + caps = {"pv"} + if env == "pv32pae": + caps |= {"pv32"} + return caps + + +class TestInstance: + """Object representing a single test.""" + + def __init__(self, arg: str) -> None: + """Parse and verify 'arg' as a test instance.""" + env_result, name_result, variation_result = parse_test_instance_string(arg) + self.env: Optional[str] = env_result + self.name: str = name_result + self.variation: Optional[str] = variation_result + + if self.env is None: + raise RunnerError(f"No environment for '{arg}'") + + if self.variation is None and get_all_test_info()[self.name].variations: + raise RunnerError(f"Test '{self.name}' has variations, but none specified") + + self.req_caps: Set[str] = env_to_virt_caps(self.env) + self.req_caps |= {"hap", "shadow"} & {self.variation} + + def vm_name(self) -> str: + """Return the VM name as `xl` expects it.""" + return repr(self) + + def cfg_path(self) -> str: + """Return the path to the `xl` config file for this test.""" + return path.join("tests", self.name, repr(self) + ".cfg") + + def __repr__(self) -> str: + if not self.variation: + return f"test-{self.env}-{self.name}" + return f"test-{self.env}-{self.name}~{self.variation}" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __eq__(self, other: Any) -> bool: + return repr(self) == repr(other) + + +class TestInfo: + """Object representing a tests info.json, in a more convenient form.""" + + def __init__(self, test_json: Any) -> None: + """Parse and verify 'test_json'. + + May raise KeyError, TypeError or ValueError. + """ + + name: Any = test_json["name"] + if not isinstance(name, str): + raise TypeError(f"Expected string for 'name', got '{type(name)}'") + self.name: str = name + + cat: Any = test_json["category"] + if not isinstance(cat, str): + raise TypeError(f"Expected string for 'category', got '{type(cat)}'") + if cat not in all_categories: + raise ValueError(f"Unknown category '{cat}'") + self.cat: str = cat + + envs: Any = test_json["environments"] + if not isinstance(envs, list): + raise TypeError(f"Expected list for 'environments', got '{type(envs)}'") + if not envs: + raise ValueError("Expected at least one environment") + for env in envs: + if env not in all_environments: + raise ValueError(f"Unknown environments '{env}'") + self.envs: List[str] = envs + + variations: Any = test_json["variations"] + if not isinstance(variations, list): + raise TypeError(f"Expected list for 'variations', got '{type(variations)}'") + self.variations: List[str] = variations + + def all_instances( + self, + env_filter: Optional[Set[str]] = None, + vary_filter: Optional[List[str]] = None, + ) -> List[TestInstance]: + """Return a list of TestInstances, for each supported environment. + Optionally filtered by env_filter. May return an empty list if + the filter doesn't match any supported environment. + """ + + if env_filter: + envs_list = list(set(env_filter).intersection(self.envs)) + else: + envs_list = self.envs + + if vary_filter: + variations_list = list(set(vary_filter).intersection(self.variations)) + else: + variations_list = self.variations + + res = [] + if variations_list: + for env in envs_list: + for vary in variations_list: + res.append(TestInstance(f"test-{env}-{self.name}~{vary}")) + else: + res = [TestInstance(f"test-{env}-{self.name}") for env in envs_list] + return res + + def __repr__(self) -> str: + return f"TestInfo({self.name})" + + +def parse_test_instance_string(arg: str) -> Tuple[Optional[str], str, Optional[str]]: + """Parse a test instance string. + + Has the form: '[[test-]$ENV-]$NAME[~$VARIATION]' + + Optional 'test-' prefix + Optional $ENV environment part + Mandatory $NAME + Optional ~$VARIATION suffix + + Verifies: + - $NAME is valid + - if $ENV, it is valid for $NAME + - if $VARIATION, it is valid for $NAME + + Returns: tuple($ENV or None, $NAME, $VARIATION or None) + """ + + all_tests = get_all_test_info() + + variation = None + if "~" in arg: + arg, variation = arg.split("~", 1) + + parts = arg.split("-", 2) + parts_len = len(parts) + + env = None + name = "" + + # If arg =~ test-$ENV-$NAME + if parts_len == 3 and parts[0] == "test" and parts[1] in all_environments: + _, env, name = parts + + # If arg =~ $ENV-$NAME + elif parts_len > 0 and parts[0] in all_environments: + env, name = parts[0], "-".join(parts[1:]) + + # If arg =~ $NAME + elif arg in all_tests: + env, name = None, arg + + # Otherwise, give up + else: + raise RunnerError(f"Unrecognised test '{arg}'") + + # At this point, 'env' has always been checked for plausibility. 'name' + # might not be + + if name not in all_tests: + raise RunnerError(f"Unrecognised test name '{name}' for '{arg}'") + + info = all_tests[name] + + if env and env not in info.envs: + raise RunnerError(f"Test '{name}' has no environment '{env}'") + + # If a variation has been given, check it is valid + if variation is not None: + if not info.variations: + raise RunnerError(f"Test '{name}' has no variations") + if variation not in info.variations: + raise RunnerError(f"No variation '{variation}' for test '{name}'") + + return env, name, variation + + +# Cached data from tests/*/info.json +_all_test_info: Dict[str, TestInfo] = {} + + +def get_all_test_info() -> Dict[str, TestInfo]: + """Open and collate each info.json""" + if not _all_test_info: # Cache on first request + + for test in os.listdir("tests"): + try: + with open(path.join("tests", test, "info.json")) as file: + info = TestInfo(json.load(file)) + + if info.name != test: + raise ValueError # JSON also looks bad + + _all_test_info[test] = info + + except ( + IOError, # Ignore directories without a info.json + ValueError, + KeyError, + TypeError, + ): # Ignore bad JSON + continue + + return _all_test_info + + +# Cached virt caps +_virt_caps: Set[str] = set() + + +def get_virt_caps() -> Set[str]: + """Query Xen for the virt capabilities of the host""" + global _virt_caps + + if not _virt_caps: # Cache on first request + + # Filter down to caps we're happy for tests to use + caps = {"pv", "hvm", "hap", "shadow"} + caps &= set(check_output(["xl", "info", "virt_caps"]).split()) + + # Synthesize a pv32 virt cap by looking at xen_caps + if "pv" in caps and "xen-3.0-x86_32p" in check_output( + ["xl", "info", "xen_caps"] + ): + caps |= {"pv32"} + + _virt_caps = caps + + return _virt_caps + + +def reset_all_test_info() -> None: + """Reset the cached test info (mainly for testing).""" + global _all_test_info + _all_test_info = {} + + +def reset_virt_caps() -> None: + """Reset the cached virt caps (mainly for testing).""" + global _virt_caps + _virt_caps = set() + + +def tests_from_selection( + cats: Optional[Set[str]], + envs: Optional[Set[str]], + tests: Set[TestInstance], + caps: Optional[Set[str]], +) -> List[TestInstance]: + """Given a selection of possible categories, environment and tests, return + all tests within the provided parameters. + + Multiple entries for each individual parameter are combined or-wise. + e.g. cats=['special', 'functional'] chooses all tests which are either + special or functional. envs=['hvm64', 'pv64'] chooses all tests which are + either pv64 or hvm64. + + Multiple parameter are combined and-wise, taking the intersection rather + than the union. e.g. cats=['functional'], envs=['pv64'] gets the tests + which are both part of the functional category and the pv64 environment. + + By default, not all categories are available. Selecting envs=['pv64'] + alone does not include the non-default categories, as this is most likely + not what the author intended. Any reference to non-default categories in + cats[] or tests[] turns them all back on, so non-default categories are + available when explicitly referenced. + """ + + all_tests = get_all_test_info() + all_test_info = all_tests.values() + res = [] + + if cats: + # If a selection of categories have been requested, start with all test + # instances in any of the requested categories. + for info in all_test_info: + if info.cat in cats: + res.extend(info.all_instances()) + + if envs: + # If a selection of environments have been requested, reduce the + # category selection to requested environments, or pick all suitable + # tests matching the environments request. + if res: + res = [x for x in res if x.env in envs] + else: + # Work out whether to include non-default categories or not. + categories = default_categories + if cats and non_default_categories & set(cats): + categories = all_categories + + elif tests: + sel_test_names = {x.name for x in tests} + sel_test_cats = {all_tests[x].cat for x in sel_test_names} + + if non_default_categories & sel_test_cats: + categories = all_categories + + for info in all_test_info: + if info.cat in categories: + res.extend(info.all_instances(env_filter=envs)) + + if tests: + # If a selection of tests has been requested, reduce the results so + # far to the requested tests (this is meaningful in the case that + # tests[] has been specified without a specific environment), or just + # take the tests verbatim. + if res: + res = [x for x in res if x in tests] + else: + res = list(tests) + + if caps: + res = [x for x in res if x.req_caps.issubset(caps)] + + # Sort the results. Variation third, Env second and Name fist. + res = sorted(res, key=lambda test: test.variation if test.variation else "") + res = sorted(res, key=lambda test: test.env if test.env else "") + res = sorted(res, key=lambda test: test.name) + return res + + +def interpret_selection(opts: Namespace) -> List[TestInstance]: + """Interpret the argument list as a collection of categories, environments, + pseduo-environments and partial and complete test names. + + Returns a list of all test instances within the selection. + """ + + args = set(opts.args) + + # First, filter into large buckets + cats = all_categories & args + envs = all_environments & args + others = args - cats - envs + + # Add all categories if --all or --non-default is passed + if opts.all: + cats |= default_categories + if opts.non_default: + cats |= non_default_categories + + # Allow "pv" and "hvm" as a combination of environments + if "pv" in others: + envs |= pv_environments + others -= {"pv"} + + if "hvm" in others: + envs |= hvm_environments + others -= {"hvm"} + + # No input? No selection. + if not cats and not envs and not others: + return [] + + all_tests = get_all_test_info() + tests = [] + + # Second, sanity check others as full or partial test names + for arg in others: + env, name, vary = parse_test_instance_string(arg) + + instances = all_tests[name].all_instances( + env_filter={env} if env else None, + vary_filter=[vary] if vary else None, + ) + + if not instances: + raise RunnerError(f"No appropriate instances for '{arg}' (env {env})") + + tests.extend(instances) + + # Third, if --host is passed, also filter by capabilities + caps = None + if opts.host: + caps = get_virt_caps() + + return tests_from_selection(cats, envs, set(tests), caps) + + +def list_tests(opts: Namespace) -> None: + """List tests""" + + if opts.environments: + # The caller only wants the environment list + for env in sorted(all_environments): + print(env) + return + + if not opts.selection: + raise RunnerError("No tests selected") + + for sel in opts.selection: + print(sel) + + +def interpret_result(logline: str) -> str: + """Interpret the final log line of a guest for a result""" + + if "Test result:" not in logline: + return "CRASH" + + for res in all_results: + if res in logline: + return res + + return "CRASH" + + +def interpret_console_lines(lines: List[str]) -> str: + """Interpret a guest console log for a result.""" + + res = interpret_result(lines[-1]) + if res == "CRASH" and any("XFAIL:" in line for line in lines): + return "XFAIL" + + return res + + +def run_test_console(opts: Namespace, test: TestInstance) -> str: + """Run a specific, obtaining results via xenconsole""" + + cmd = ["xl", "create", "-p", test.cfg_path()] + if not opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + + create = Popen(cmd, stdout=PIPE, stderr=PIPE) + _, stderr = create.communicate() + + if create.returncode: + if opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + print(stderr) + raise RunnerError("Failed to create VM") + + cmd = ["xl", "console", test.vm_name()] + if not opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + + console = Popen(cmd, stdout=PIPE) + + cmd = ["xl", "unpause", test.vm_name()] + if not opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + + rc = subproc_call(cmd) + if rc: + if opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + raise RunnerError("Failed to unpause VM") + + stdout, _ = console.communicate() + + if console.returncode: + raise RunnerError("Failed to obtain VM console") + + lines = stdout.splitlines() + + if lines: + if not opts.quiet: + print("\n".join(lines)) + print("") + + else: + return "CRASH" + + return interpret_console_lines(lines) + + +def run_test_logfile(opts: Namespace, test: TestInstance) -> str: + """Run a specific test, obtaining results from a logfile""" + + logpath = path.join(opts.logfile_dir, opts.logfile_pattern.replace("%s", str(test))) + + if not opts.quiet: + print(f"Using logfile '{logpath}'") + + fd = os.open(logpath, os.O_CREAT | os.O_RDONLY, 0o644) + logfile = os.fdopen(fd) + logfile.seek(0, os.SEEK_END) + + cmd = ["xl", "create", "-F", test.cfg_path()] + if not opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + + guest = Popen(cmd, stdout=PIPE, stderr=PIPE) + + _, stderr = guest.communicate() + + if guest.returncode: + if opts.quiet: + print(f"Executing '{' '.join(cmd)}'") + print(stderr) + raise RunnerError("Failed to run test") + + line = "" + saw_xfail = False + for line in logfile.readlines(): + + line = line.rstrip() + if not opts.quiet: + print(line) + + saw_xfail |= "XFAIL:" in line + + if "Test result:" in line: + print("") + break + + logfile.close() + + res = interpret_result(line) + if res == "CRASH" and saw_xfail: + return "XFAIL" + + return res + + +def run_test(opts: Namespace, test: TestInstance) -> str: + """Run a single test instance""" + + # If caps say the test can't run, short circuit to SKIP + if not test.req_caps.issubset(get_virt_caps()): + return "SKIP" + + fn: Any = { + "console": run_test_console, + "logfile": run_test_logfile, + }[opts.results_mode] + + return fn(opts, test) # type: ignore[no-any-return] + + +def run_tests(opts: Namespace) -> int: + """Run tests""" + + tests = opts.selection + if not tests: + raise RunnerError("No tests to run") + + rc = all_results.index("SUCCESS") + results = [] + + for test in tests: + + res = run_test(opts, test) + res_idx = all_results.index(res) + rc = max(rc, res_idx) + + results.append(res) + + print("Combined test results:") + + for test, res in zip(tests, results): + + if res == "SUCCESS" and opts.quiet >= 2: + continue + + print(f"{str(test):<40} {res}") + + return exit_code(all_results[rc]) + + +def main(root: Optional[str] = None, line_buffer_stdout: bool = True) -> int: + """Main entrypoint""" + + # Change stdout to be line-buffered. + if line_buffer_stdout: + sys.stdout = os.fdopen(sys.stdout.fileno(), "w", 1) + + # Normalise $CWD to the directory this script is in + if root is None: + root = path.dirname(path.abspath(sys.argv[0])) + os.chdir(root) + + parser = ArgumentParser( + usage="%(prog)s [--list] [options]", + description="Xen Test Framework enumeration and running tool", + formatter_class=RawDescriptionHelpFormatter, + epilog=( + "\n" + "Overview:\n" + " Running with --list will print the entire selection\n" + " to the console. Running without --list will execute\n" + " all tests in the selection, printing a summary of their\n" + " results at the end.\n" + "\n" + " To determine how runner should get output from Xen, use\n" + ' --results-mode option. The default value is "console", \n' + " which means using xenconsole program to extract output.\n" + ' The other supported value is "logfile", which\n' + " means to get output from log file.\n" + "\n" + ' The "logfile" mode requires users to configure\n' + " xenconsoled to log guest console output. This mode\n" + " is useful for Xen version < 4.8. Also see --logfile-dir\n" + " and --logfile-pattern options.\n" + "\n" + "Selections:\n" + " A selection is zero or more of any of the following\n" + " parameters: Categories, Environments and Tests.\n" + " Multiple instances of the same type of parameter are\n" + " unioned while the end result in intersected across\n" + " types. e.g.\n" + "\n" + " 'functional xsa'\n" + " All tests in the functional and xsa categories\n" + "\n" + " 'functional xsa hvm32'\n" + " All tests in the functional and xsa categories\n" + " which are implemented for the hvm32 environment\n" + "\n" + " 'invlpg example'\n" + " The invlpg and example tests in all implemented\n" + " environments\n" + "\n" + " 'invlpg example pv'\n" + " The pv environments of the invlpg and example tests\n" + "\n" + " 'pv32pae-pv-iopl'\n" + " The pv32pae environment of the pv-iopl test only\n" + "\n" + " Additionally, --host may be passed to restrict the\n" + " selection to tests applicable to the current host.\n" + " --all may be passed to choose all default categories\n" + " without needing to explicitly name them. --non-default\n" + " is available to obtain the non-default categories.\n" + "\n" + " The special parameter --environments may be passed to\n" + " get the full list of environments. This option does not\n" + " make sense combined with a selection.\n" + "\n" + "Examples:\n" + " Listing all tests implemented for hvm32 environment:\n" + " ./xtf-runner --list hvm32\n" + "\n" + " Listing all functional tests appropriate for this host:\n" + " ./xtf-runner --list functional --host\n" + "\n" + " Running all the pv-iopl tests:\n" + " ./xtf-runner pv-iopl\n" + " \n" + " Combined test results:\n" + " test-pv64-pv-iopl SUCCESS\n" + " test-pv32pae-pv-iopl SUCCESS\n" + "\n" + " Exit code for this script:\n" + " 0: everything is ok\n" + " 1,2: reserved for python interpreter\n" + " 3: test(s) are skipped\n" + " 4: test(s) report error\n" + " 5: test(s) report failure\n" + " 6: test(s) crashed\n" + "\n" + ), + ) + + parser.add_argument( + "-l", + "--list", + action="store_true", + dest="list_tests", + help="List tests in the selection", + ) + parser.add_argument( + "-a", + "--all", + action="store_true", + dest="all", + help="Select all default categories", + ) + parser.add_argument( + "--non-default", + action="store_true", + dest="non_default", + help="Select all non default categories", + ) + parser.add_argument( + "--environments", + action="store_true", + dest="environments", + help="List all the known environments", + ) + parser.add_argument( + "--host", + action="store_true", + dest="host", + help="Restrict selection to applicable" " tests for the current host", + ) + parser.add_argument( + "-m", + "--results-mode", + action="store", + dest="results_mode", + default="console", + choices=["console", "logfile"], + help="Control how xtf-runner gets its test results", + ) + parser.add_argument( + "--logfile-dir", + action="store", + dest="logfile_dir", + default="/var/log/xen/console/", + help=( + "Specify the directory to look for console logs, " + 'defaults to "/var/log/xen/console/"' + ), + ) + parser.add_argument( + "--logfile-pattern", + action="store", + dest="logfile_pattern", + default="guest-%s.log", + help=("Specify the log file name pattern, " 'defaults to "guest-%%s.log"'), + ) + parser.add_argument( + "-q", + "--quiet", + action="count", + dest="quiet", + default=0, + help=( + "Progressively make the output less verbose. " + "1) No console logs, only test results. " + "2) Not even SUCCESS results." + ), + ) + + opts, args = parser.parse_known_args() + opts.args = args + opts.selection = interpret_selection(opts) + + if opts.list_tests: + list_tests(opts) + return 0 + return run_tests(opts) diff --git a/xtf/runner/selftest.py b/xtf/runner/selftest.py new file mode 100644 index 00000000..bf274388 --- /dev/null +++ b/xtf/runner/selftest.py @@ -0,0 +1,161 @@ +"""Selftests for the importable xtf-runner implementation.""" + +import contextlib +import io +import json +import os +import sys +import tempfile +import unittest +from os import path +from types import SimpleNamespace +from typing import Generator, List, Optional, Tuple +from unittest import mock + +from xtf.runner import cli as runner + + +def _write_test_info( + root: str, name: str, envs: Optional[List[str]] = None, category: str = "functional" +) -> None: + """Create a minimal XTF test metadata file under a temporary root.""" + if envs is None: + envs = ["hvm64"] + + test_dir = path.join(root, "tests", name) + os.makedirs(test_dir) + + with open(path.join(test_dir, "info.json"), "w", encoding="utf-8") as info: + json.dump( + { + "name": name, + "category": category, + "environments": envs, + "variations": [], + }, + info, + ) + + +@contextlib.contextmanager +def _temporary_runner_root() -> Generator[str, None, None]: + """Use a temporary directory as the runner's repository root.""" + cwd = os.getcwd() + argv = sys.argv[:] + + with tempfile.TemporaryDirectory() as tmpdir: + os.makedirs(path.join(tmpdir, "tests")) + + runner.reset_all_test_info() + runner.reset_virt_caps() + + try: + yield tmpdir + finally: + os.chdir(cwd) + sys.argv = argv + runner.reset_all_test_info() + runner.reset_virt_caps() + + +class RunnerCliTests(unittest.TestCase): + """Tests for legacy CLI behaviour through xtf.runner.cli.""" + + def test_list_prints_selected_test_instances(self) -> None: + """`xtf-runner --list NAME` prints matching test instances.""" + with _temporary_runner_root() as tmpdir: + _write_test_info(tmpdir, "alpha", ["hvm64", "hvm32"]) + sys.argv = [path.join(tmpdir, "xtf-runner"), "--list", "alpha"] + + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + rc = runner.main(root=tmpdir, line_buffer_stdout=False) + + self.assertIsNone(rc) + self.assertEqual( + stdout.getvalue().splitlines(), + [ + "test-hvm32-alpha", + "test-hvm64-alpha", + ], + ) + + def test_run_prints_combined_results_and_exit_code(self) -> None: + """Running a selected test preserves summary and exit-code mapping.""" + with _temporary_runner_root() as tmpdir: + _write_test_info(tmpdir, "alpha") + sys.argv = [path.join(tmpdir, "xtf-runner"), "hvm64-alpha", "-q"] + + stdout = io.StringIO() + with mock.patch.object( + runner, "get_virt_caps", return_value={"hvm"} + ), mock.patch.object( + runner, "run_test_console", return_value="SUCCESS" + ) as run_console, contextlib.redirect_stdout( + stdout + ): + rc = runner.main(root=tmpdir, line_buffer_stdout=False) + + self.assertEqual(rc, runner.exit_code("SUCCESS")) + run_console.assert_called_once() + self.assertEqual(str(run_console.call_args[0][1]), "test-hvm64-alpha") + self.assertIn("Combined test results:", stdout.getvalue()) + self.assertIn("test-hvm64-alpha", stdout.getvalue()) + self.assertIn("SUCCESS", stdout.getvalue()) + + def test_run_test_console_uses_xl_lifecycle(self) -> None: + """Console-mode execution still uses create-paused/console/unpause.""" + opts = SimpleNamespace(quiet=1) + test = SimpleNamespace( + cfg_path=lambda: "tests/alpha/test-hvm64-alpha.cfg", + vm_name=lambda: "test-hvm64-alpha", + ) + + popen_calls = [] + + class FakeProcess: + """Small fake for subprocess.Popen return objects.""" + + def __init__( + self, + cmd: List[str], + stdout: Optional[object] = None, + stderr: Optional[object] = None, + ) -> None: + del stdout, stderr + self.cmd = cmd + self.returncode = 0 + popen_calls.append(cmd) + + def communicate(self) -> Tuple[str, Optional[str]]: + """Simulate a successful console session with a test result.""" + if self.cmd[:2] == ["xl", "console"]: + return "boot\nTest result: SUCCESS\n", None + return "", "" + + with mock.patch.object( + runner, "Popen", side_effect=FakeProcess + ), mock.patch.object(runner, "subproc_call", return_value=0) as call: + result = runner.run_test_console(opts, test) # type: ignore[arg-type] + + self.assertEqual(result, "SUCCESS") + self.assertEqual( + popen_calls, + [ + ["xl", "create", "-p", "tests/alpha/test-hvm64-alpha.cfg"], + ["xl", "console", "test-hvm64-alpha"], + ], + ) + call.assert_called_once_with(["xl", "unpause", "test-hvm64-alpha"]) + + def test_marked_crash_is_xfail(self) -> None: + """Marked crashes are reported as expected failures.""" + self.assertEqual( + runner.interpret_console_lines(["boot", "XFAIL: expected crash"]), + "XFAIL", + ) + self.assertEqual(runner.interpret_console_lines(["boot"]), "CRASH") + + +if __name__ == "__main__": + unittest.main()