diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..8f002e7 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,79 @@ +# The checks of the Makefile; the QEMU boots, which need KVM and apt packages, in +# a job of their own. +name: ci + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +# PRs: a new push cancels the previous run. main: one group per run, as a shared +# group drops pending runs. +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - run: make setup + + # After setup: the key covers installed toolchains. + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + + # Every check but the boots. + - run: make -k lint test + + test-e2e-qemu: + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - run: | + sudo apt-get update + xargs --arg-file=tests/qemu/ubuntu-packages sudo apt-get install --yes + + # Opens /dev/kvm to the runner user, per + # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ + - run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | + sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + # No rust-cache: its cleanup takes run.sh's initramfs/tests for a cargo + # profile and logs errors; the builds take about a minute without it. + - run: make setup + + # Kernel RPMs and DriverSample.efi. Saved only on success, as run.sh reuses + # DriverSample.efi unchecked. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + target/qemu-test/kernel-rpms + target/qemu-test/driver-sample + key: qemu-inputs-${{ hashFiles('tests/qemu/kernel.sha256', 'tests/qemu/build-driver-sample.sh') }} + + - run: make test-e2e-qemu + + - if: failure() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: qemu-test-logs + path: | + target/qemu-test/*/*/*.log + target/qemu-test/*/executables + if-no-files-found: ignore + retention-days: 14 diff --git a/Makefile b/Makefile index a5a6bb3..bb4c5c1 100644 --- a/Makefile +++ b/Makefile @@ -11,18 +11,19 @@ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # The checks of uefisettings. They need GNU make and bash, rustup while -# RUST_TOOLCHAIN is set, as it is by default, and for qemu an x86_64 host with -# /dev/kvm and what tests/qemu/README.md lists: +# RUST_TOOLCHAIN is set, as it is by default, and for test-e2e-qemu an x86_64 +# host with /dev/kvm and what tests/qemu/README.md lists: # make every check of CHECKS, stopping at the first that fails # make -k every check of CHECKS, even after one fails # make ... the named checks, e.g. make clippy-aarch64 doc +# make lint the checks of LINTS +# make lint test every check but test-e2e-qemu # make RUST_TOOLCHAIN= the checks on the cargo of PATH # make setup installs, through rustup, the toolchain, components and # targets that the checks use # make help the checks # make -n prints the commands of a check without running them. The -# checks install nothing themselves. qemu needs qemu-lint, so under make -k a -# qemu-lint failure skips the qemu boots. +# checks install nothing themselves. # # A CI host runs make setup && make -k under timeout, as nothing bounds the time # that cargo takes: timeout -k 60 bash -c 'make setup && make -k'. It @@ -60,15 +61,21 @@ unexport CARGO_ENCODED_RUSTFLAGS TARGETS := x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,x86_64-unknown-uefi comma := , -CHECKS := fmt clippy-x86_64 clippy-aarch64 test doc qemu-lint qemu +# CI runs lint test and test-e2e-qemu: a new check joins LINTS or gets a CI job. +LINTS := fmt clippy-x86_64 clippy-aarch64 clippy-uefi doc +CHECKS := $(LINTS) test test-e2e-qemu -.PHONY: check help setup toolchain $(CHECKS) +.PHONY: check lint help setup toolchain $(CHECKS) check: $(CHECKS) +lint: $(LINTS) + help: @echo "checks: $(CHECKS)" @echo "make runs every check; make ... runs the named ones." + @echo "make lint runs $(LINTS)." + @echo "make lint test runs every check but test-e2e-qemu." @echo "make -k runs every check, even after one fails." @echo "make -n prints the commands of a check without running them." @echo "make RUST_TOOLCHAIN= runs the checks on the cargo of PATH." @@ -90,26 +97,24 @@ endif toolchain: @scripts/check-toolchain.sh '$(RUST_TOOLCHAIN)' '$(TARGETS)' +# Covers the workspace's QEMU test crates too. fmt: toolchain cargo fmt --check clippy-x86_64 clippy-aarch64: clippy-%: toolchain cargo clippy --locked --workspace --all-targets --all-features --target $*-unknown-linux-gnu -- -D warnings +# publish-hiidb, the one crate built for UEFI. +clippy-uefi: toolchain + cargo clippy --locked --release --target x86_64-unknown-uefi --manifest-path tests/qemu/publish-hiidb/Cargo.toml -- -D warnings + test: toolchain scripts/assert-tests-ran.sh cargo test --locked doc: toolchain - RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items --locked - -# The checks of the QEMU test crates that boot no guest. -qemu-lint: toolchain - cargo fmt --check --manifest-path tests/qemu/guest/Cargo.toml - RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items --locked --manifest-path tests/qemu/guest/Cargo.toml - cargo fmt --check --manifest-path tests/qemu/publish-hiidb/Cargo.toml - cargo clippy --locked --release --target x86_64-unknown-uefi --manifest-path tests/qemu/publish-hiidb/Cargo.toml -- -D warnings + RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps --document-private-items --locked # Boots the scenarios of tests/qemu/scenarios.tsv under KVM, with logs in # target/qemu-test//; see tests/qemu/README.md. -qemu: qemu-lint +test-e2e-qemu: toolchain tests/qemu/run.sh diff --git a/README.md b/README.md index 82363e0..0358088 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ mv lib.rs thrift/rust/uefisettings_spellings_db_thrift/uefisettings_spellings_db ## Checks -The `Makefile` in the root of the repository runs the checks that a change should pass: rustfmt, clippy for x86_64 and aarch64, the tests, the docs and the QEMU tests. +The `Makefile` in the root of the repository runs the checks that a change should pass: the lints (`make lint`: rustfmt, clippy for x86_64, aarch64 and UEFI, and the docs), the tests (`make test`) and the QEMU tests (`make test-e2e-qemu`). The checks need GNU make and bash, and rustup unless `make RUST_TOOLCHAIN=` runs them on the cargo of `PATH`. @@ -153,11 +153,14 @@ make setup # once: installs, through rustup, the Rust toolchain, compo make # every check, stopping at the first that fails make -k # every check, even after one fails make ... # the named checks, e.g. make clippy-aarch64 doc +make lint test # every check but the QEMU tests make RUST_TOOLCHAIN= # the checks on the cargo of PATH make help # lists the checks ``` -`make qemu`, the QEMU tests, also needs an x86_64 host with `/dev/kvm` and the packages that [`tests/qemu/README.md`](tests/qemu/README.md) lists. +`make test-e2e-qemu`, the QEMU tests, also needs an x86_64 host with `/dev/kvm` and the packages that [`tests/qemu/README.md`](tests/qemu/README.md) lists. + +GitHub Actions runs the checks on every pull request and every push to main, as [`.github/workflows/checks.yml`](.github/workflows/checks.yml) describes. ## QEMU tests diff --git a/tests/qemu/README.md b/tests/qemu/README.md index f0b1d25..aa90607 100644 --- a/tests/qemu/README.md +++ b/tests/qemu/README.md @@ -6,13 +6,13 @@ machine that runs them. ## Running -`make qemu` runs the checks of the QEMU test crates and then every scenario, on the Rust +`make test-e2e-qemu` runs every scenario, on the Rust toolchain and targets that `make setup` installs (see the Checks section of the repository's README): ```sh make setup # once -make qemu +make test-e2e-qemu ``` It needs an x86_64 host with `/dev/kvm` and, on Ubuntu 24.04, the packages of diff --git a/tests/qemu/init b/tests/qemu/init index bfb386a..ac175a4 100755 --- a/tests/qemu/init +++ b/tests/qemu/init @@ -39,10 +39,18 @@ done # start and exit lines begin with a newline. run.sh prints the second port from the start # line on. printf '\nuefisettings-qemu-test: start %s %s\n' "$test" "$filter" > /dev/ttyS1 +# The health checks judge only the test: the kernel log and taint of the boot depend on +# the host, e.g. a RETBleed error on Intel hosts that hide IBRS from the guest. +read -r start < /proc/sys/kernel/random/uuid +echo "uefisettings-qemu-test: start of test $start" > /dev/kmsg +# cat, not read: busybox read reads a byte at a time, and numeric sysctls answer only a +# read at offset 0, so read would get only the first digit. +taint=$(cat /proc/sys/kernel/tainted /sys/kernel/warn_count) "/tests/$test" --test-threads=1 ${filter:+"$filter"} > /dev/ttyS1 2>&1 test_status=$? -# The kernel is not tainted (TAINT_* in include/linux/panic.h), kept its whole log (the -# first /dev/kmsg record has sequence number 0) and logged no error: each /dev/kmsg record +# The test added no taint (TAINT_* in include/linux/panic.h) or warning (a repeated WARN +# adds no taint bit), and the kernel kept its log +# (/dev/kmsg still has the start record) and logged no error since: each /dev/kmsg record # starts with facility << 3 | level, and the kernel's own facility is 0. /dev/kmsg keeps # the records that a log clear hides from dmesg. A read of it returns one record, fails if # the record does not fit and waits at the end of the log, so dd copies the records into @@ -50,19 +58,20 @@ test_status=$? # opens the FIFO for dd, so awk also stops if dd cannot open /dev/kmsg. health_status=0 { - read -r taint < /proc/sys/kernel/tainted - [ "$taint" = 0 ] || { echo "the kernel is tainted: $taint"; health_status=1; } - dd if=/dev/kmsg bs=8192 count=1 2> /dev/null | grep -q '^[0-9]*,0,' || { - echo "the first kernel log record is gone, or /dev/kmsg cannot be read" + tainted=$(cat /proc/sys/kernel/tainted /sys/kernel/warn_count) + set -- $taint + [ $# = 2 ] && [ "$tainted" = "$taint" ] || { + echo "the taint or warning count changed or is missing:" $taint, then $tainted health_status=1 } read -r end < /proc/sys/kernel/random/uuid echo "uefisettings-qemu-test: end of log $end" > /dev/kmsg mkfifo /tmp/kmsg dd if=/dev/kmsg bs=8192 2> /dev/null > /tmp/kmsg & - awk -v end="$end" ' - index($0, end) { found = 1; exit } - /^[0-3],/ { print; error = 1 } + awk -v start="$start" -v end="$end" ' + index($0, end) { found = started; exit } + started && /^[0-3],/ { print; error = 1 } + index($0, start) { started = 1 } END { if (!found) print "cannot read the kernel log"; exit !found || error }' /tmp/kmsg || health_status=1 kill "$!" 2> /dev/null diff --git a/tests/qemu/run.sh b/tests/qemu/run.sh index d1bd529..c5223e7 100755 --- a/tests/qemu/run.sh +++ b/tests/qemu/run.sh @@ -62,6 +62,7 @@ mapfile -t named < <(awk -F '\t' '$1 !~ /^(#|$)/ { echo "run.sh: start $(date -u +%FT%TZ), logs in $run" mkdir -p "$run/initramfs/bin" "$run/initramfs/tests" "$run/esp" +grep --max-count=1 '^model name' /proc/cpuinfo "$qemu" --version "$busybox" --help | sed -n 1p