diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 8f002e7..0f5ef33 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -17,24 +17,32 @@ concurrency: cancel-in-progress: true jobs: + # The image of test-e2e-qemu too: one image, one migration. lint-and-test: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # gcc's 32-bit libraries, which test-i686 links with. + - run: | + sudo apt-get update + sudo apt-get install --yes gcc-multilib + - 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 + - run: make -k lint test test-i686 + # QEMU 9.0 and newer fault unaligned loads from Device memory as arm64 does, which the + # aarch64 guests check; Ubuntu 24.04 has QEMU 8.2. test-e2e-qemu: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 120 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -57,23 +65,39 @@ jobs: # 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. + # The guests' packages and each architecture's DriverSample.efi, keyed also on the + # apt packages and the runner image (this file) that build DriverSample. 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') }} + target/qemu-test/packages + target/qemu-test/*/driver-sample + key: qemu-inputs-${{ hashFiles('tests/qemu/guest-packages-*', 'tests/qemu/build-driver-sample.sh', 'tests/qemu/ubuntu-packages', '.github/workflows/checks.yml') }} + + - run: make -k test-e2e-qemu-x86_64 test-e2e-qemu-i686 + + # DO NOT MERGE: diagnostics for the reset on AMD runners. + - if: always() + run: tests/qemu/ci-diag-reset.sh - - run: make test-e2e-qemu + - if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: reset-diag + path: | + target/qemu-test/diag + target/qemu-test/x86_64/*/*/*.log + target/qemu-test/i686/*/*/*.log + if-no-files-found: ignore + retention-days: 14 - if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: qemu-test-logs path: | - target/qemu-test/*/*/*.log - target/qemu-test/*/executables + target/qemu-test/*/*/*/*.log + target/qemu-test/*/*/executables if-no-files-found: ignore retention-days: 14 diff --git a/Cargo.lock b/Cargo.lock index 2d183ff..153a62b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + [[package]] name = "either" version = "1.9.0" @@ -1047,6 +1056,7 @@ dependencies = [ name = "uefisettings-qemu-guest" version = "0.1.0" dependencies = [ + "crc32fast", "serde", "serde_json", ] diff --git a/Makefile b/Makefile index bb4c5c1..012a031 100644 --- a/Makefile +++ b/Makefile @@ -11,13 +11,15 @@ # 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 test-e2e-qemu an x86_64 -# host with /dev/kvm and what tests/qemu/README.md lists: +# RUST_TOOLCHAIN is set, as it is by default, for test-i686 an x86 host with +# gcc's 32-bit libraries, which README.md names, 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 lint test test-i686 +# 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 @@ -27,10 +29,10 @@ # # 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 -# may keep target/qemu-test/kernel-rpms and target/qemu-test/driver-sample -# between runs: tests/qemu/fetch-kernel.sh downloads only the RPMs that are +# may keep target/qemu-test/packages and target/qemu-test/*/driver-sample +# between runs: tests/qemu/fetch-packages.sh downloads only the packages that are # missing there or fail their sha256 check, and tests/qemu/run.sh builds -# DriverSample only when it is missing, so delete target/qemu-test/driver-sample +# DriverSample only when it is missing, so delete target/qemu-test/*/driver-sample # after a change to build-driver-sample.sh. SHELL := bash @@ -58,14 +60,16 @@ export RUSTFLAGS := -D warnings unexport CARGO_ENCODED_RUSTFLAGS # The targets that the checks build for, comma-separated as rustup takes them. -TARGETS := x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,x86_64-unknown-uefi +TARGETS := x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,i686-unknown-linux-gnu,x86_64-unknown-linux-musl,x86_64-unknown-uefi,i686-unknown-linux-musl,i686-unknown-uefi,aarch64-unknown-linux-musl,aarch64-unknown-uefi comma := , -# 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 +# CI runs lint test test-i686 and test-e2e-qemu: a new check joins LINTS or +# QEMU_TESTS or CI runs it too. +LINTS := fmt clippy-x86_64 clippy-aarch64 clippy-i686 clippy-uefi doc +QEMU_TESTS := test-e2e-qemu-x86_64 test-e2e-qemu-i686 test-e2e-qemu-aarch64 +CHECKS := $(LINTS) test test-i686 test-e2e-qemu -.PHONY: check lint help setup toolchain $(CHECKS) +.PHONY: check lint help setup toolchain $(CHECKS) $(QEMU_TESTS) check: $(CHECKS) @@ -75,7 +79,8 @@ 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 test-e2e-qemu runs $(QEMU_TESTS)." + @echo "make lint test test-i686 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." @@ -101,7 +106,7 @@ toolchain: fmt: toolchain cargo fmt --check -clippy-x86_64 clippy-aarch64: clippy-%: toolchain +clippy-x86_64 clippy-aarch64 clippy-i686: clippy-%: toolchain cargo clippy --locked --workspace --all-targets --all-features --target $*-unknown-linux-gnu -- -D warnings # publish-hiidb, the one crate built for UEFI. @@ -111,10 +116,17 @@ clippy-uefi: toolchain test: toolchain scripts/assert-tests-ran.sh cargo test --locked +# The tests on 32-bit glibc, where off_t is signed and has 32 bits; this +# host runs them. +test-i686: toolchain + scripts/assert-tests-ran.sh cargo test --locked --target i686-unknown-linux-gnu + doc: toolchain 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. -test-e2e-qemu: toolchain - tests/qemu/run.sh +test-e2e-qemu: $(QEMU_TESTS) + +# Boots the scenarios of tests/qemu/scenarios.tsv on one architecture, with logs in +# target/qemu-test///; see tests/qemu/README.md. +$(QEMU_TESTS): test-e2e-qemu-%: toolchain + tests/qemu/run.sh $* diff --git a/README.md b/README.md index 0358088..fa7fb41 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: 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 `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, i686 and UEFI, and the docs), the tests (`make test`, and `make test-i686` on 32-bit glibc) 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,13 @@ 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 lint test test-i686 # every check but the QEMU tests make RUST_TOOLCHAIN= # the checks on the cargo of PATH make help # lists the checks ``` +`make test-i686` also needs an x86 host (x86_64 or i686), which runs the i686 test binaries, and gcc's 32-bit libraries (Ubuntu: `gcc-multilib`, or, next to the `gcc-aarch64-linux-gnu` of [`tests/qemu/ubuntu-packages`](tests/qemu/ubuntu-packages), which conflicts with it, `libc6-dev-i386` and `lib32gcc--dev`; Fedora: `glibc-devel.i686`). + `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. diff --git a/src/lib/chattr.rs b/src/lib/chattr.rs index 6e89405..a8d6119 100644 --- a/src/lib/chattr.rs +++ b/src/lib/chattr.rs @@ -11,6 +11,8 @@ // 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. use std::fs::OpenOptions; +use std::os::raw::c_long; +use std::os::raw::c_uint; use std::os::unix::io::AsRawFd; use std::path::Path; use std::path::PathBuf; @@ -26,24 +28,24 @@ const FS_IOC_GETFLAGS: u8 = 1; const FS_IOC_SETFLAGS: u8 = 2; // full list of flags https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h#L239 -pub const FS_IMMUTABLE_FL: i64 = 0x00000010; // Immutable file +pub const FS_IMMUTABLE_FL: c_uint = 0x00000010; // Immutable file -nix::ioctl_read!( +// The requests encode the size of a long, but the kernel reads and writes an unsigned +// int (ioctl_getflags() and ioctl_setflags() in fs/file_attr.c, Linux v6.17). +nix::ioctl_read_bad!( ioctl_fs_get_attrs, - FS_IOC_MAGIC, - FS_IOC_GETFLAGS, - ::std::os::raw::c_long + nix::request_code_read!(FS_IOC_MAGIC, FS_IOC_GETFLAGS, std::mem::size_of::()), + c_uint ); -nix::ioctl_write_ptr!( +nix::ioctl_write_ptr_bad!( ioctl_fs_set_attrs, - FS_IOC_MAGIC, - FS_IOC_SETFLAGS, - ::std::os::raw::c_long + nix::request_code_write!(FS_IOC_MAGIC, FS_IOC_SETFLAGS, std::mem::size_of::()), + c_uint ); /// returns all extended fs attributes for the given path as a set of bit flags -pub fn get_all_attrs(path: impl AsRef) -> Result { +pub fn get_all_attrs(path: impl AsRef) -> Result { let path = path.as_ref(); let file = OpenOptions::new() .write(false) @@ -54,13 +56,11 @@ pub fn get_all_attrs(path: impl AsRef) -> Result { path.to_string_lossy() ))?; let fd = file.as_raw_fd(); - let mut attrs = 0i64; - // SAFETY c_long is guaranteed to be i32 or i64 (https://doc.rust-lang.org/std/os/raw/type.c_long.html) - // This tool is linux specific and do not support 32bit systems, so we can assume that it's always i64 - let attrs_ptr = &mut attrs as *mut i64; - + let mut attrs: c_uint = 0; + // SAFETY: fd stays open while file lives, and attrs is the unsigned int that the + // kernel writes. unsafe { - ioctl_fs_get_attrs(fd, attrs_ptr).context("ioctl failed")?; + ioctl_fs_get_attrs(fd, &mut attrs).context("ioctl failed")?; } Ok(attrs) @@ -68,7 +68,7 @@ pub fn get_all_attrs(path: impl AsRef) -> Result { /// writes extended fs attributes for the given path as a set of bit flags /// all existing attributes will be overwritten -pub fn set_all_attrs(path: impl AsRef, attrs: i64) -> Result<()> { +pub fn set_all_attrs(path: impl AsRef, attrs: c_uint) -> Result<()> { let path = path.as_ref(); let file = OpenOptions::new() .write(false) @@ -79,12 +79,10 @@ pub fn set_all_attrs(path: impl AsRef, attrs: i64) -> Result<()> { path.to_string_lossy() ))?; let fd = file.as_raw_fd(); - let mut tmp = attrs; - // SAFETY c_long is guaranteed to be i32 or i64 (https://doc.rust-lang.org/std/os/raw/type.c_long.html) - // This tool is linux specific and do not support 32bit systems, so we can assume that it's always i64 - let attrs_ptr = &mut tmp as *mut i64; + // SAFETY: fd stays open while file lives, and attrs is the unsigned int that the + // kernel reads. unsafe { - ioctl_fs_set_attrs(fd, attrs_ptr).context("ioctl failed")?; + ioctl_fs_set_attrs(fd, &attrs).context("ioctl failed")?; } Ok(()) } @@ -127,7 +125,7 @@ macro_rules! clear_attrs { pub struct EfivarsImmutabilityGuard { path: PathBuf, - attrs_before_writing: i64, + attrs_before_writing: c_uint, } impl EfivarsImmutabilityGuard { @@ -180,7 +178,7 @@ pub mod tests { use super::*; //in the tests we expect that FS_NODUMP_FL is initially cleared, might be a weak assumption.. - pub const FS_NODUMP_FL: i64 = 0x00000040; // do not dump file + pub const FS_NODUMP_FL: c_uint = 0x00000040; // do not dump file #[test] fn test_get_all_attrs() -> Result<()> { let test_file = tempfile::NamedTempFile::new().expect("Failed to create test file"); @@ -230,6 +228,14 @@ pub mod tests { get_all_attrs(test_file.path()).context("No error expected from get_attrs")?; // checking that FS_NODUMP_FL is set assert_eq!(attrs_after_set & FS_NODUMP_FL, FS_NODUMP_FL); + // checking that clearing a flag that no new file has or inherits, so that clearing it + // needs no capability, keeps FS_NODUMP_FL, the only flag that the owner may set on both + // ZFS and tmpfs + clear_attrs!(test_file.path(), FS_IMMUTABLE_FL) + .context("No error expected from clear_attrs")?; + let attrs_after_other_clear = + get_all_attrs(test_file.path()).context("No error expected from get_attrs")?; + assert_eq!(attrs_after_other_clear & FS_NODUMP_FL, FS_NODUMP_FL); clear_attrs!(test_file.path(), FS_NODUMP_FL, FS_NODUMP_FL) .context("No error expected from clear_attrs")?; let attrs_after_clear = @@ -237,7 +243,7 @@ pub mod tests { // checking that FS_NODUMP_FL is cleared assert_eq!(attrs_after_clear & FS_NODUMP_FL, 0); // checking that everything else is still the same - assert_eq!(attrs_after_set & !FS_NODUMP_FL, attrs & !FS_NODUMP_FL); + assert_eq!(attrs_after_clear & !FS_NODUMP_FL, attrs & !FS_NODUMP_FL); Ok(()) } } diff --git a/src/lib/hii/extract.rs b/src/lib/hii/extract.rs index 48d50ae..2a1ed23 100644 --- a/src/lib/hii/extract.rs +++ b/src/lib/hii/extract.rs @@ -197,13 +197,14 @@ fn copy_mapped_page(file: &File, offset: u64, dst: &mut [u8]) -> Result { let offset_in_page = usize::try_from(offset % page_size as u64)?; let len = (page_size - offset_in_page).min(dst.len()); let page_base = offset - offset_in_page as u64; - let map_base = libc::off_t::try_from(page_base) - .with_context(|| format!("Mapping offset 0x{page_base:x} does not fit off_t"))?; + // mmap64(), as off_t is signed and has 32 bits on most 32-bit glibc targets. + let map_base = libc::off64_t::try_from(page_base) + .with_context(|| format!("Mapping offset 0x{page_base:x} does not fit off64_t"))?; // SAFETY: addr = NULL lets the kernel pick an unused address range, so the // new PROT_READ mapping cannot alias any Rust allocation (including dst). let mapped = unsafe { - libc::mmap( + libc::mmap64( ptr::null_mut(), page_size, libc::PROT_READ, @@ -356,7 +357,7 @@ mod tests { } } - /// The offset also fails the off_t check; the overflow error must win. + /// The offset also fails the off64_t check; the overflow error must win. #[test] fn read_mapped_rejects_overflowing_range() { let (file, _) = mapped_fixture(16); diff --git a/tests/qemu/README.md b/tests/qemu/README.md index aa90607..620350f 100644 --- a/tests/qemu/README.md +++ b/tests/qemu/README.md @@ -1,12 +1,14 @@ # QEMU tests -These tests boot QEMU guests under OVMF and run `uefisettings` inside them, so they +These tests boot x86_64, i686 and aarch64 QEMU guests under EDK2's firmware (OVMF, or +AAVMF on aarch64) and run `uefisettings` inside them, so they exercise the CLI against real UEFI firmware, efivarfs and `/dev/mem` without touching the machine that runs them. ## Running -`make test-e2e-qemu` runs every scenario, on the Rust +`make test-e2e-qemu` runs every scenario on every architecture, and +`make test-e2e-qemu-` on one of `x86_64`, `i686` and `aarch64`, on the Rust toolchain and targets that `make setup` installs (see the Checks section of the repository's README): @@ -15,36 +17,47 @@ make setup # once make test-e2e-qemu ``` -It needs an x86_64 host with `/dev/kvm` and, on Ubuntu 24.04, the packages of +It needs an x86_64 host with `/dev/kvm`, which the x86_64 and i686 guests use; QEMU +emulates the aarch64 guests, and must be 9.0 or newer, which faults unaligned loads from +Device memory as arm64 does. On Ubuntu 26.04 it also needs the packages of [`ubuntu-packages`](ubuntu-packages): ```sh xargs --arg-file=tests/qemu/ubuntu-packages sudo apt-get install --yes ``` -`run.sh` alone runs the scenarios on the active toolchain of rustup, which then needs the -musl and UEFI targets: +The guest's kernel, firmware and busybox do not come from the host: they are the packages +that `guest-packages-` ([x86_64](guest-packages-x86_64), [i686](guest-packages-i686) +or [aarch64](guest-packages-aarch64)) pins, which +[`fetch-packages.sh`](fetch-packages.sh) downloads. + +`run.sh` alone runs the scenarios of an architecture on the active toolchain of rustup, +which then needs that architecture's musl and UEFI targets: ```sh rustup target add x86_64-unknown-linux-musl x86_64-unknown-uefi # once -tests/qemu/run.sh # every scenario -tests/qemu/run.sh boot-smoke # the named scenarios +tests/qemu/run.sh x86_64 # every scenario +tests/qemu/run.sh x86_64 boot-smoke # the named scenarios ``` -Elsewhere, the environment variables in the header of [`run.sh`](run.sh) give the paths -of QEMU, the OVMF code and variable images, a static busybox and the directory of -DriverSample.efi, and these tools must be on `PATH`: `virt-fw-vars`, `jq` and `cpio` for -`run.sh`; `curl`, `rpm2cpio` and `cpio` for [`fetch-kernel.sh`](fetch-kernel.sh); and, +Elsewhere, these tools must be on `PATH`: `qemu-system-x86_64`, `qemu-system-i386`, +`qemu-system-aarch64`, `virt-fw-vars`, `jq` and `cpio` for `run.sh`; `curl`, `rpm2cpio`, +`cpio`, `ar`, `tar`, `xz` and `zstd` for `fetch-packages.sh`; and, unless DriverSample.efi is already built, `gcc`, `g++`, `make`, `git`, `nasm`, `python3` and the libuuid headers for [`build-driver-sample.sh`](build-driver-sample.sh). +The aarch64 builds also need `aarch64-linux-gnu-gcc` with the `libdl.a` of an aarch64 +glibc, or another linker that `CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER` names, and +the i686 build of `uefisettings` a 32-bit `libdl.a` (Fedora: `glibc-devel.i686`), as the +`libloading` crate links `-ldl`. -`QEMU` must name the `qemu-system-x86_64` binary or a wrapper that `exec`s it: `run.sh` +The `qemu-system-*` on `PATH` may be a wrapper that `exec`s QEMU: `run.sh` stops a boot after 120 s by signalling that program, not its children, so a wrapper that runs QEMU as a child would leave the guest running. -Each run writes to `target/qemu-test//`, with one directory per scenario. For -each boot `N`, `bootN.console.log` has the firmware, kernel and `init` output and -`bootN.test.log` the test output. +Each run writes to `target/qemu-test///`, with one directory per +scenario. For each boot `N`, `bootN.console.log` has the firmware, kernel and `init` +output, `bootN.test.log` the test output and `bootN.qemu.log` how QEMU saw the guest end +(`run.sh` explains its lines). [`scenarios.tsv`](scenarios.tsv) lists the scenarios and the boots of each; each file in [`guest/tests/`](guest/tests/) is one test binary, which a boot names, and its module @@ -55,7 +68,7 @@ command line lacks `uefisettings.test=`. They build only with the `test-vm` feat ## HII fixture -OVMF does not publish its HII database for the OS, so the HII scenarios have the firmware +EDK2's firmware does not publish its HII database for the OS, so the HII scenarios have it run two drivers from the ESP before the kernel, through `Driver####` load options: - `DriverSample.efi`, EDK2's unmodified sample HII driver (BSD-2-Clause-Patent), which @@ -63,6 +76,7 @@ run two drivers from the ESP before the kernel, through `Driver####` load option the first run; - `publish-hiidb.efi`, from [`publish-hiidb/`](publish-hiidb/), which points the `HiiDB` variable at a copy of the HII database, the way the firmware of OCP machines does. + It also puts the CRC-32 of the database in the `HiiDBCrc32` variable for the tests. [`vars/my-ifr-nv-data.json`](vars/my-ifr-nv-data.json) creates DriverSample's variable first, with runtime access, so that efivarfs shows it, and with DriverSample's defaults at diff --git a/tests/qemu/build-driver-sample.sh b/tests/qemu/build-driver-sample.sh index 45b53c8..26502f3 100755 --- a/tests/qemu/build-driver-sample.sh +++ b/tests/qemu/build-driver-sample.sh @@ -12,25 +12,34 @@ # 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. # Builds DriverSample.efi, EDK2's sample HII driver -# (MdeModulePkg/Universal/DriverSampleDxe), which the QEMU tests load into OVMF -# for its HII forms. The source is the edk2-stable202508 tag, checked against +# (MdeModulePkg/Universal/DriverSampleDxe), which the QEMU tests load into EDK2's firmware +# for its HII forms. The source is the edk2-stable202608 tag, checked against # its commit. DriverSample is BSD-2-Clause-Patent, so EDK2's License.txt is # copied next to it as DriverSample-License.txt. # Needs gcc, g++, make, nasm, python3, git and the libuuid headers; on Ubuntu # 24.04: apt-get install build-essential git nasm python3 uuid-dev. +# AARCH64 also needs the gcc that GCC_AARCH64_PREFIX names (default: aarch64-linux-gnu-, +# which gcc-aarch64-linux-gnu has). # JOBS sets the make and build job count (default: nproc). # -# Usage: [JOBS=] tests/qemu/build-driver-sample.sh +# Usage: [JOBS=] tests/qemu/build-driver-sample.sh set -euo pipefail -if (($# != 1)); then - echo "usage: $0 " >&2 +arch=${1-} +case $arch in +X64 | IA32 | AARCH64) ;; +*) arch= ;; +esac +if (($# != 2)) || [[ -z $arch ]]; then + echo "usage: $0 " >&2 exit 2 fi -out=$(realpath -m -- "$1") -tag=edk2-stable202508 -commit=d46aa46c8361194521391aa581593e556c707c6e +out=$(realpath -m -- "$2") +# EDK2's tools_def.txt reads the prefix of the AARCH64 gcc from it. +export GCC_AARCH64_PREFIX=${GCC_AARCH64_PREFIX-aarch64-linux-gnu-} +tag=edk2-stable202608 +commit=2970e5699ba6267f3384ffab20f96647578aebc8 jobs=${JOBS:-$(nproc)} work=$(mktemp -d) @@ -54,8 +63,17 @@ set -- && set +u # shellcheck source=/dev/null . ./edksetup.sh set -u -build -a X64 -t GCC -b RELEASE -n "$jobs" -p MdeModulePkg/MdeModulePkg.dsc \ +# EDK2's X64 flags name no -march, so gcc's default applies, and that varies: Ubuntu's +# amd64v3 gcc emits AVX, which faults in the firmware (CR4.OSXSAVE is clear). The DSC's +# build options append -march=x86-64, the baseline, to EDK2's flags. +cat >>MdeModulePkg/MdeModulePkg.dsc <<'EOF' + +[BuildOptions] + GCC:*_*_X64_CC_FLAGS = -march=x86-64 + GCC:*_*_X64_DLINK_FLAGS = -march=x86-64 +EOF +build -a "$arch" -t GCC -b RELEASE -n "$jobs" -p MdeModulePkg/MdeModulePkg.dsc \ -m MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf mkdir -p -- "$out" -cp Build/MdeModule/RELEASE_GCC/X64/DriverSample.efi "$out/" +cp "Build/MdeModule/RELEASE_GCC/$arch/DriverSample.efi" "$out/" cp License.txt "$out/DriverSample-License.txt" diff --git a/tests/qemu/ci-diag-attribution.py b/tests/qemu/ci-diag-attribution.py new file mode 100755 index 0000000..4c956cc --- /dev/null +++ b/tests/qemu/ci-diag-attribution.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""DO NOT MERGE: attributes each SHUTDOWN exit in a host trace to KVM (L1) or Hyper-V (L0). + +KVM flushes the nested mappings of a root it loads (Linux v7.0 arch/x86/kvm/mmu/mmu.c, +kvm_mmu_load, then svm_flush_tlb_current, arch/x86/kvm/svm/svm.c:4031-4045, which calls +hyperv_flush_guest_mapping on that root when Hyper-V's enlightened NPT TLB is on). For the +vCPU that exits with SHUTDOWN, this takes its fatal entry (its last kvm_entry before the +exit), the root that it loaded last before that entry (the kvmdiag/load_mmu_pgd +kprobe on svm_load_mmu_pgd, svm.c:4442), the last event before the entry that drops the +vCPU's root (the anchor) and the hyperv_nested_flush_guest_mapping* hypercalls between the +anchor and the entry. Two events drop roots: kvm_mmu_zap_all_fast, which makes every root +obsolete (mmu.c:6787-6825) and runs in whichever thread changed the memory map, the dying +vCPU's own after its MMIO exit or another one, so it is matched by time across all threads; +and kvm_mmu_reset_context (the kvmdiag/mmu_reset kprobe), which unloads the roots of one vCPU +(mmu.c:6043-6047, 6083-6092), e.g. when userspace writes a PAE vCPU's PDPTRs back (Linux v7.0 +arch/x86/kvm/x86.c:12449-12477), so it counts only for the dying vCPU. Either way the vCPU +loads a root before it enters again (kvm_mmu_load, mmu.c:6050-6080, which then flushes it). +A root load or a reset belongs to the vCPU of its vcpu_id field, which the BTF-typed kprobes +record, and else to the vCPU of its thread: + +- events lost in the window (ftrace's overwrite mode keeps each CPU's newest events only, so a + CPU whose oldest kept event is newer than the anchor may have lost a root load or a flush): + undetermined; +- no root observed: undetermined; +- a root loaded before the anchor: L1 (the zap's own flushes target the old roots, so they do + not count); +- no "Hyper-V enlightened NPT TLB flush enabled" in dmesg: undetermined, as KVM then flushes + without the hypercall (svm.c:4041, svm_onhyperv.h:18-24); +- the flush hypercalls' events not traced (--flush-events not-traced): undetermined, as an + absent flush then says nothing; +- a successful flush (ret 0) of that root in the window: L0, a matching flush preceded the + fault; +- no flush, or flushes of other roots, failed ones or ones after the entry only: L1. +""" + +import argparse +import gzip +import re +from dataclasses import dataclass +from pathlib import Path + +LINE = re.compile( + r"^\s*(?P.+?)-(?P\d+)\s+\[(?P\d+)\]\s+\S+\s+(?P