diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a0cddada720..e09e03cc118 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -108,7 +108,7 @@ jobs: for attempt in 1 2 3; do apt-get -o Acquire::Check-Valid-Until=false update -qq > /tmp/apt.log 2>&1 \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git curl ca-certificates \ - zstd xz-utils build-essential qemu-system-x86 >> /tmp/apt.log 2>&1 \ + zstd xz-utils build-essential qemu-system-x86 ovmf-generic >> /tmp/apt.log 2>&1 \ && break [ "$attempt" = 3 ] && { cat /tmp/apt.log; exit 1; } sleep 20 diff --git a/NOTICE b/NOTICE index e40c536f047..63fdd1df8bf 100644 --- a/NOTICE +++ b/NOTICE @@ -15,11 +15,6 @@ Each section names its terms on one `SPDX-License-Identifier:` line, which agrees with the licence column of `COMMITTED_FILES` in `src/licence.rs` for every file both name, and that file judges what an image ships by them. -Provenance below was established by inspecting content, not by trusting -memory: `assets/` and `ovmf/` both entered in the initial squashed commit and -carry no attribution of their own. Every hash is `shasum -a 256`, taken -2026-08-08. - **If you redistribute a ToyOS image, read the DOOM1.WAD entry.** It is the one that constrains what you may do with a build. @@ -236,63 +231,6 @@ tests/iced-counter/src/bin/iced-counter.rs — iced's own counter example, MIT `system.toml` builds ships it. -ovmf/*.fd — EDK II firmware, BSD-2-Clause-Patent ------------------------------------------------- - - OVMF_CODE-pure-efi.fd 1,966,080 bytes - sha256 9de33971d47958f42af86584b502f83256120b2482e4f7ed14db32fd68e92922 - OVMF_VARS-pure-efi.fd 131,072 bytes - sha256 c653de93db67e4f2213a35598efb379a13ef4a12c241e003699d4d7afd193635 - DEBUGX64_OVMF.fd 4,194,304 bytes - sha256 800ff5af1220d1232d4da7173ccddbb74a9217600bd8935903d9d534801778b4 - - Copyright (c) 2019, TianoCore and contributors - Licence text: licenses/BSD-2-Clause-Patent-EDK2.txt - SPDX-License-Identifier: BSD-2-Clause-Patent - -Third-party builds of Tianocore EDK II, read out of the binaries' own build -paths: the two `-pure-efi` files were built on a Jenkins worker from -`edk2-gf0064ac3af`, and `DEBUGX64_OVMF.fd` from a different tree (`/src/edk2`, -`DEBUG_GCC5`). No `LICENSE`, version record or build recipe came with them. - -`OVMF_CODE`/`OVMF_VARS` are load-bearing — `src/qemu.rs:101` and -`tests/common/qemu.rs:2207` point QEMU's pflash at them, so every boot on the -dev host uses them. **`DEBUGX64_OVMF.fd` is referenced by nothing**: it is -4,194,304 bytes of firmware no code path reaches. The audit missed it, calling -all three load-bearing; whether to keep it is the owner's. - -Two open questions for the owner: whether to record a real upstream and version -for these, and whether to take the firmware from QEMU's own installation -instead — which would put it inside the "comes with QEMU" allowance and delete -6,291,456 bytes from the repository. - - - -aavmf/*.fd — EDK II firmware for QEMU `virt`, BSD-2-Clause-Patent AND Apache-2.0 ------------------------------------------------------------- - - AAVMF_CODE.fd 67,108,864 bytes - sha256 47765fe344818cbc464b1c14ae658fb4b854f5c2ceffa982411731eb4865594d - AAVMF_VARS.fd 67,108,864 bytes - sha256 b3b855c5a80310168051164986855692d1bdb06e67619856177965cd87c6774f - - Copyright (c) 2019, TianoCore and contributors - Licence text: licenses/BSD-2-Clause-Patent-EDK2.txt - Copyright 1995-2023 The OpenSSL Project Authors - Licence text: licenses/Apache-2.0-OpenSSL.txt - SPDX-License-Identifier: BSD-2-Clause-Patent AND Apache-2.0 - -QEMU's own prebuilt ArmVirtQemu firmware, unmodified: `edk2-aarch64-code.fd` -and `edk2-arm-vars.fd` as QEMU 11.1.1 installs them under `share/qemu/`, -whose version string reads `edk2-stable202408-prebuilt.qemu.org` (a -`DEBUG_GCC5` build of 2024-09-12). The variable store is the template: every -boot gives it a writable snapshot QEMU discards, because this `DEBUG` build -asserts on a read-only store. QEMU builds it with `NETWORK_TLS_ENABLE` -(`roms/edk2-build.config`, `[opts.common]`), and edk2-stable202408's -ArmVirtQemu links edk2's bundled OpenSSL either way (`OpensslLib` with TLS, -`OpensslLibCrypto` without): OpenSSL 3.0.9, whose `LICENSE.txt` is the -Apache-2.0 text above and which carries no `NOTICE`. - tests/fixtures/gbae-v0.2.0-* — gbae, MIT ----------------------------------------- diff --git a/README.md b/README.md index ae9207f9d9a..04b3eadddf0 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ at your option. Some of what the repository carries is not ours and is under other terms: `userland/doom` is GPL-2.0, `assets/` holds a font, a set of icons, a wallpaper -and id Software's Doom shareware IWAD, `ovmf/` holds EDK II firmware builds, +and id Software's Doom shareware IWAD, and `tests/testcases/` holds TinyCC's test corpus. Third-party crates keep their own upstream licenses. **[NOTICE](NOTICE) is the list**, item by item, with the licence texts in [licenses/](licenses). diff --git a/aavmf/AAVMF_CODE.fd b/aavmf/AAVMF_CODE.fd deleted file mode 100644 index 89924f4b509..00000000000 Binary files a/aavmf/AAVMF_CODE.fd and /dev/null differ diff --git a/aavmf/AAVMF_VARS.fd b/aavmf/AAVMF_VARS.fd deleted file mode 100644 index a71658f9882..00000000000 Binary files a/aavmf/AAVMF_VARS.fd and /dev/null differ diff --git a/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md b/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md new file mode 100644 index 00000000000..8f6dd54b1eb --- /dev/null +++ b/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md @@ -0,0 +1,58 @@ +--- +status: expected-red +kind: defect +opened: 2026-09-28 +--- + +# An unreadable sector on a USB boot stick hangs the loader past the firmware watchdog + +On stock edk2, a ROOT sector the USB boot stick fails with EIO stops the boot +inside the loader's read of ROOT. The read did not return in 147 s, and the 60 s +watchdog the loader arms at entry did not reset the machine. +`bootloader/src/rootimage.rs`'s `read_root` leans on that watchdog "if the +firmware honours it". This firmware did not. + +Measured on the dev host with Homebrew QEMU 11.1.1's own +`edk2-x86_64-code.fd` under TCG. The stimulus is `root_chunk_refused`'s as it +stood on the Headless profile: `blkdebug` failing every `read_aio` covering +the sector seven past ROOT's middle with errno 5, under the boot image on a +`usb-storage` on `nec-usb-xhci`. The harness's deadline was lifted to 150 s, +and each 16550 line was stamped with the seconds since QEMU's spawn: + +- `Firmware watchdog: 60 s, until ExitBootServices disables it` at 2.0 s; +- `Slot A: signed header … verifies under this loader's key` at 3.0 s. This + is the last byte on the 16550. The loader's next step is the chunked read + of ROOT that covers the failing sector; +- no further byte in the 147 s to the deadline, no `the read of … failed` + line, and no reset. `-no-reboot` would have turned a reset into QEMU's + exit, which the harness reports as `QEMU died before`. It reported + `Boot timed out` instead. + +On the tree's former `ovmf/` image the same stimulus got further and then +also went silent. The failed read returned, and its line reached the console, +but the stick answered the loader's next write to `loader.log` with nothing, +so the read's line was the last the boot said (the ready-marker comment in +`4444076c`'s `tests/common/volumes.rs`). The stick went silent after the +EIO under both firmwares. + +This measurement does not show which side does not finish: edk2's USB +mass-storage and xHCI stack, or QEMU's `usb-storage`. It also does not show +why the watchdog's timer event did not run. `usb_pcap` records only the first +data disk and not the boot stick, so no existing instrument sees the bus here. + +Every measurement here is QEMU under TCG. Whether a stick with an unreadable +sector hangs the loader on a real machine is unmeasured. + +`root_chunk_refused` stages the same EIO on the `InternalDisk` profile's NVMe +boot disk. `root_chunk_refused_on_a_usb_stick` is the same body on the +Headless profile's stick, and `src/redlist.rs` disables it on this file. + +## Exit condition + +`root_chunk_refused_on_a_usb_stick` is green on stock edk2 and its +`src/redlist.rs` row is lifted. Then this file is deleted. + +## Owner + +`bootloader/src/rootimage.rs`'s `read_root` and the loader's watchdog in +`bootloader/src/main.rs`; unheld. diff --git a/issues/build/ftruncate-flush-race-reds-intermittently-and-nothing-says-why.md b/issues/build/ftruncate-flush-race-reds-intermittently-and-nothing-says-why.md index e0905d05e12..995fff63ed2 100644 --- a/issues/build/ftruncate-flush-race-reds-intermittently-and-nothing-says-why.md +++ b/issues/build/ftruncate-flush-race-reds-intermittently-and-nothing-says-why.md @@ -20,12 +20,18 @@ shrink-mark work and against the same tree with that kernel change reverted: | kernel change reverted | alone | 3 | 1 fail, then 2 pass (attempt 0, 358.0 and 356.3 ms) | | branch | full nightly tier | 3 | 2 fail, 1 pass | | kernel change reverted | full nightly tier | 1 | pass | +| stock edk2, #572 at `9082d6b4` | alone | 5 | 2 fail (runs 1 and 3), 3 pass | +| `cd2e6307`, the committed `ovmf/` | alone | 5 | 1 fail (run 5), 4 pass | Both arms fail and both pass, and the only same-configuration comparison with more than one sample per side is the *alone* one, where the branch is 6 for 6 and the reverted arm failed once. So the numbers say intermittent; they do not name a cause and they do not point at a diff. +In each of the two red runs at `9082d6b4` all 10 attempts missed, so each +failed all-or-nothing within its boot: ten `STALLED WINDOW HELD` lines about +410 ms apart, each followed by its flusher thread's exit, then the panic. + **No mechanism is established, and one plausible reading is already refuted.** `STALLED WINDOW HELD` is not evidence that the truncate arrived after the window: `SYS_FTRUNCATE` and `SYS_FSYNC` take the same VFS lock and the stall diff --git a/issues/build/no-harness-test-boots-qemus-own-edk2.md b/issues/build/no-harness-test-boots-qemus-own-edk2.md deleted file mode 100644 index d71800896a9..00000000000 --- a/issues/build/no-harness-test-boots-qemus-own-edk2.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-28 ---- - -# No harness test boots QEMU's own edk2 - -Every harness boot uses the repository's `ovmf/`. QEMU's own -`edk2-x86_64-code.fd` hands an AMD vCPU a reserved range at -`0xfd00000000..0x10000000000`, which `ovmf/` never names, and a kernel whose -direct map reached every range in the map died there after `pmm:`, and a -survey that offered a 64-bit BAR only the space above that range handed the -NIC over nowhere. The host tests `toyos-bootmap/tests/direct_map.rs` and -`toyos-pci`'s `placement` hold the two rules; nothing boots the -firmware that broke them, so a regression outside those rules is seen by the -first person who boots QEMU's firmware and nobody else. - -Owner: orchestrator. Exit condition: a harness test boots QEMU's own edk2 on -the command line the release probe uses and reaches `compositor: ready` and `netd: DHCP: lease`. diff --git a/issues/build/ovmf-s-licence-record-names-no-openssl.md b/issues/build/ovmf-s-licence-record-names-no-openssl.md deleted file mode 100644 index dd99fbafc47..00000000000 --- a/issues/build/ovmf-s-licence-record-names-no-openssl.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-26 ---- - -# OVMF's licence record names no OpenSSL - -`NOTICE`'s `ovmf/*.fd` section and its three `src/licence.rs` rows say -`BSD-2-Clause-Patent`. EDK II's platforms link its bundled OpenSSL into the -firmware through `CryptoPkg` (`BaseCryptLib`, and `TlsLib` when -`NETWORK_TLS_ENABLE` is set), and OpenSSL 3 is Apache-2.0. For the AArch64 -firmware this is established and recorded (PR #524: edk2-stable202408's -ArmVirtQemu links OpenSSL 3.0.9 whether TLS is on or off, and QEMU builds it -with TLS on). For OVMF nothing is: the section itself says the files came -with no version record or build recipe, so which CryptoPkg libraries they -link is unknown, and the record claims a single licence nobody has checked. - -**Exit condition**: the OVMF images' crypto content is read out of the -binaries or their upstream build, and the section and rows state every -licence it carries, or the images are replaced by ones whose build is known -(the section's own open question to the owner). diff --git a/issues/build/the-kernel-console-split-does-not-re-arm-across-a-guest-reset.md b/issues/build/the-kernel-console-split-does-not-re-arm-across-a-guest-reset.md new file mode 100644 index 00000000000..bbbc3d7dbb9 --- /dev/null +++ b/issues/build/the-kernel-console-split-does-not-re-arm-across-a-guest-reset.md @@ -0,0 +1,42 @@ +--- +status: open +kind: tooling +opened: 2026-09-28 +--- + +# The kernel console split does not re-arm across a guest reset + +`src/kernelconsole.rs`'s `KernelConsole` withholds the virtio port's bytes +until the first `[kernel ` head and passes every byte after it: `held` is +`None` from then on. A guest reset inside one QEMU process does not re-arm it. +`tests/common/update.rs`'s `Rig::boot` boots the Headless profile, whose +stdio is that port, with `takes_the_reset`, and each `reboot` resets that +machine in place. Whatever the next boot's firmware and loader write on the +port reaches every stdio reader as kernel console. + +What the next boot writes there has never been captured. The first boot's +stream is measured. On QEMU 11.1.1's own edk2 it opens with +`ESC[2J ESC[01;01H ESC[=3h ESC[2J ESC[01;01H` twice, then +`BdsDxe: loading Boot0001 …`, as a failing test's console dump in the +orchestrator's #572 round-2 nightly shows. No log from #572's runs shows the +port after an in-process reset: each carries that prelude only where a stream +starts. So no signal is known to be in the stream at a reset. Re-arming on the +first boot's prelude would rest on the guess that the firmware repeats it. + +Nothing reds on it. The update tests look for a needle past an offset in the +console (`owed`, `reboot_until`), and firmware lines beside the needle do not +stop it matching. Every update test passed in #572's round-2 and round-3 +nightlies. If the firmware does write its handoff line there, +`Loader log: … so [kernel …` is a line `Serial::interleaved` reports as a torn +kernel line. + +## Exit condition + +A capture of stdio across an in-process reset on a virtio profile. If the +firmware writes on the port there, the split re-arms on a signal that capture +shows, with a host test over the captured bytes. If it writes nothing, this +file is deleted with the capture in the commit. + +## Owner + +`src/kernelconsole.rs` and `tests/common/qemu.rs`'s stdio reader; unheld. diff --git a/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md b/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md index fdb6fa5f2d2..1f67c93134f 100644 --- a/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md +++ b/issues/kernel/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md @@ -11,9 +11,9 @@ three extents: the firmware map, every BAR firmware assigned (`taken.push` of `memory.address()..end`), and every range a bridge forwards (`taken.push(forwarded)`). The rule is host-tested; the two kernel pushes are not. Deleting the BAR push still places the NIC at `0xc000200000` on QEMU's -edk2 and at `0x800200000` on `ovmf/`, because the 2 MiB alignment steps over -firmware's BARs there, and `alone_in_its_page` checks only BARs, not the ranges -bridges forward. No machine in reach puts a BAR it hands over behind a bridge. +edk2, because the 2 MiB alignment steps over firmware's BARs there, and +`alone_in_its_page` checks only BARs, not the ranges bridges forward. No +machine in reach puts a BAR it hands over behind a bridge. Owner: orchestrator. Exit condition: a guest test goes red when either push is deleted — a machine whose firmware places a BAR, or a bridge's forwarded range, diff --git a/licenses/Apache-2.0-OpenSSL.txt b/licenses/Apache-2.0-OpenSSL.txt deleted file mode 100644 index 49cc83d2ee2..00000000000 --- a/licenses/Apache-2.0-OpenSSL.txt +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/licenses/BSD-2-Clause-Patent-EDK2.txt b/licenses/BSD-2-Clause-Patent-EDK2.txt deleted file mode 100644 index ee840505cb0..00000000000 --- a/licenses/BSD-2-Clause-Patent-EDK2.txt +++ /dev/null @@ -1,51 +0,0 @@ -Copyright (c) 2019, TianoCore and contributors. All rights reserved. - -SPDX-License-Identifier: BSD-2-Clause-Patent - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -Subject to the terms and conditions of this license, each copyright holder -and contributor hereby grants to those receiving rights under this license -a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except for failure to satisfy the conditions of this license) patent -license to make, have made, use, offer to sell, sell, import, and otherwise -transfer this software, where such license applies only to those patent -claims, already acquired or hereafter acquired, licensable by such copyright -holder or contributor that are necessarily infringed by: - -(a) their Contribution(s) (the licensed copyrights of copyright holders and - non-copyrightable additions of contributors, in source or binary form) - alone; or - -(b) combination of their Contribution(s) with the work of authorship to - which such Contribution(s) was added by such copyright holder or - contributor, if, at the time the Contribution is added, such addition - causes such combination to be necessarily infringed. The patent license - shall not apply to any other combinations which include the - Contribution. - -Except as expressly stated above, no rights or licenses from any copyright -holder or contributor is granted under this license, whether expressly, by -implication, estoppel or otherwise. - -DISCLAIMER - -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 HOLDERS 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. diff --git a/ovmf/DEBUGX64_OVMF.fd b/ovmf/DEBUGX64_OVMF.fd deleted file mode 100644 index bc15ddb8b97..00000000000 Binary files a/ovmf/DEBUGX64_OVMF.fd and /dev/null differ diff --git a/ovmf/OVMF_CODE-pure-efi.fd b/ovmf/OVMF_CODE-pure-efi.fd deleted file mode 100644 index 30480564c21..00000000000 Binary files a/ovmf/OVMF_CODE-pure-efi.fd and /dev/null differ diff --git a/ovmf/OVMF_VARS-pure-efi.fd b/ovmf/OVMF_VARS-pure-efi.fd deleted file mode 100644 index 0a695a54ed5..00000000000 Binary files a/ovmf/OVMF_VARS-pure-efi.fd and /dev/null differ diff --git a/src/arch.rs b/src/arch.rs index 63f75df0d49..d93bc9fd6c8 100644 --- a/src/arch.rs +++ b/src/arch.rs @@ -1,7 +1,7 @@ //! The machines ToyOS runs on. //! -//! Every target triple, QEMU binary, firmware image, guest CPU and accelerator -//! the build system and the harness name is a function of one [`Arch`]. Neither +//! Every target triple, QEMU binary, guest CPU and accelerator the build +//! system and the harness name is a function of one [`Arch`]. Neither //! architecture is the reference: a new question about a machine is a new //! method here that both variants answer, or it does not compile. @@ -110,29 +110,12 @@ impl Arch { } } - /// The firmware's code and variable-store images, relative to the - /// repository root, pinned and hashed in `NOTICE`. - pub const fn firmware(self) -> (&'static str, &'static str) { + /// The `-machine` alias this architecture's guests boot on. + pub const fn machine(self) -> &'static str { match self { - Arch::X86_64 => ("ovmf/OVMF_CODE-pure-efi.fd", "ovmf/OVMF_VARS-pure-efi.fd"), - Arch::Aarch64 => ("aavmf/AAVMF_CODE.fd", "aavmf/AAVMF_VARS.fd"), - } - } - - /// The two `-drive` values that give a guest its firmware, from the - /// repository at `root`. The store is never written back: OVMF boots from a - /// read-only one, and AAVMF's `DEBUG` build asserts on one, so it writes a - /// snapshot QEMU discards. - pub fn pflash(self, root: &Path) -> [String; 2] { - let (code, vars) = self.firmware(); - let store = match self { - Arch::X86_64 => "readonly=on", - Arch::Aarch64 => "snapshot=on", - }; - [ - format!("if=pflash,format=raw,unit=0,file={},readonly=on", root.join(code).display()), - format!("if=pflash,format=raw,unit=1,file={},{store}", root.join(vars).display()), - ] + Arch::X86_64 => "q35", + Arch::Aarch64 => "virt", + } } /// QEMU's `-boot` for this machine's firmware, if it needs one. AAVMF diff --git a/src/ci.rs b/src/ci.rs index 4abafde7532..4c101815ac1 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -721,17 +721,13 @@ fn verdicts(log: &str) -> String { } } -/// The QEMU on `PATH` against `.github/qemu-version`, and whether `/dev/kvm` -/// opens where it is present — the two things a guest verdict must be read -/// against. +/// The QEMU on `PATH` against `.github/qemu-version`, the firmware it declares, +/// and whether `/dev/kvm` opens where it is present — the three things a guest +/// verdict must be read against. fn instrument(root: &Path, arch: Arch) -> Result { let want = declared_qemu_version(root).ok_or(".github/qemu-version declares no version")?; - let out = Command::new(arch.qemu()) - .arg("--version") - .output() - .map_err(|e| format!("{}: {e}", arch.qemu()))?; - let said = String::from_utf8_lossy(&out.stdout).into_owned(); - let have = parse_qemu_version(&said).ok_or_else(|| format!("QEMU said {said:?}"))?; + let have = qemu_version(arch)?; + let firmware = crate::firmware::of(arch)?; let node = Path::new("/dev/kvm").exists(); let accelerated = arch.accel().is_hardware(); let accel = match (node, accelerated) { @@ -748,7 +744,10 @@ fn instrument(root: &Path, arch: Arch) -> Result { }) .unwrap_or_else(|| "an unnamed CPU".to_string()); let cores = std::thread::available_parallelism().map_or(0, |n| n.get()); - let line = format!("QEMU {have}, {accel}, {cpu}, {cores} core(s)"); + let line = format!( + "QEMU {have}, firmware {}, {accel}, {cpu}, {cores} core(s)", + firmware.code.display() + ); if have != want { return Err(format!( "{line}: this runs QEMU {have} and .github/qemu-version declares {want}. The \ @@ -776,6 +775,16 @@ pub fn declared_qemu_version(root: &Path) -> Option { (!version.is_empty()).then_some(version) } +/// The version the QEMU on `PATH` that boots `arch` says it is. +pub fn qemu_version(arch: Arch) -> Result { + let out = Command::new(arch.qemu()) + .arg("--version") + .output() + .map_err(|e| format!("{}: {e}", arch.qemu()))?; + let said = String::from_utf8_lossy(&out.stdout).into_owned(); + parse_qemu_version(&said).ok_or_else(|| format!("QEMU said {said:?}")) +} + /// `QEMU emulator version 11.0.3 (Debian 1:11.0.3+ds-1)` → `11.0.3`. fn parse_qemu_version(text: &str) -> Option { let first = text.lines().next()?; @@ -788,8 +797,7 @@ fn parse_qemu_version(text: &str) -> Option { /// `.github/qemu-version` declares, and nothing at all when it is. pub fn qemu_version_note(root: &Path, arch: Arch) -> Option { let want = declared_qemu_version(root)?; - let out = Command::new(arch.qemu()).arg("--version").output().ok()?; - let have = parse_qemu_version(&String::from_utf8_lossy(&out.stdout))?; + let have = qemu_version(arch).ok()?; (have != want).then(|| { format!( "Note: this host runs QEMU {have} and .github/qemu-version declares {want} — \ diff --git a/src/firmware.rs b/src/firmware.rs new file mode 100644 index 00000000000..d605c8dfbc2 --- /dev/null +++ b/src/firmware.rs @@ -0,0 +1,351 @@ +//! The UEFI firmware a guest boots: whichever the host's QEMU installation +//! declares, never a file this tree carries or a path read off one machine. +//! +//! Found the way QEMU's interop spec (`docs/interop/firmware.json`) tells +//! management software to: the `firmware/*.json` descriptors under the user's +//! override directory, then the system's, then every data directory QEMU +//! reports (`-L help`), taken in that order, a name in an earlier directory +//! hiding the same name in a later one, and the first that fits the machine +//! wins. Nothing fits, and the boot is refused by name. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; + +use serde::Deserialize; + +use crate::arch::Arch; + +/// One installation's firmware for one machine. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Firmware { + /// The code image, which a guest only ever reads. + pub code: PathBuf, + /// The variable-store template, which no guest is handed: each boot writes + /// a copy of its own ([`Firmware::fresh_vars`]). + pub vars: PathBuf, +} + +impl Firmware { + /// A fresh variable store at `to`, from the template. + pub fn fresh_vars(&self, to: &Path) -> Result<(), String> { + std::fs::copy(&self.vars, to) + .map_err(|e| format!("copy the firmware's variable store {} to {}: {e}", self.vars.display(), to.display()))?; + // `fs::copy` carries the template's mode. A read-only template (a 0444 + // store, as on Nix) would leave the boot's own copy unwritable to QEMU + // and the next boot's copy over it failing the same way. + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(to, std::fs::Permissions::from_mode(0o644)) + .map_err(|e| format!("make the firmware variable store copy {} writable: {e}", to.display())) + } + + /// The two `-drive` values that give a guest this firmware, with `vars` as + /// its writable variable store. + pub fn drives(&self, vars: &Path) -> [String; 2] { + [ + format!("if=pflash,format=raw,unit=0,file={},readonly=on", self.code.display()), + format!("if=pflash,format=raw,unit=1,file={},readonly=off", vars.display()), + ] + } +} + +/// The firmware the host's QEMU declares for `arch`'s machine, asked once per +/// process. +pub fn of(arch: Arch) -> Result<&'static Firmware, String> { + static FOUND: [OnceLock>; 2] = [OnceLock::new(), OnceLock::new()]; + let slot = &FOUND[Arch::ALL.iter().position(|a| *a == arch).expect("every Arch is in ALL")]; + slot.get_or_init(|| find(arch)).as_ref().map_err(Clone::clone) +} + +fn find(arch: Arch) -> Result { + let version = crate::ci::qemu_version(arch)?; + let out = Command::new(arch.qemu()) + .args(["-L", "help"]) + .output() + .map_err(|e| format!("{} -L help: {e}", arch.qemu()))?; + if !out.status.success() { + return Err(format!("{} -L help: {}: {}", arch.qemu(), out.status, String::from_utf8_lossy(&out.stderr))); + } + let datadirs = String::from_utf8_lossy(&out.stdout).lines().filter(|l| !l.is_empty()).map(|l| Path::new(l).join("firmware")).collect(); + let dirs = search_dirs( + std::env::var_os("XDG_CONFIG_HOME").as_deref().map(Path::new), + std::env::var_os("HOME").as_deref().map(Path::new), + datadirs, + ); + select(arch, &machine_type(arch, &version), &descriptors(&dirs)?).map_err(|why| { + let searched: Vec = dirs.iter().map(|d| d.display().to_string()).collect(); + format!("{why}; searched {} (QEMU {version}'s data directories among them)", searched.join(", ")) + }) +} + +/// The directories `descriptors` reads, in the precedence +/// `docs/interop/firmware.json` gives: the user's override +/// (`$XDG_CONFIG_HOME`, else `$HOME/.config`), then the system's, then every +/// data directory QEMU itself reports (`-L help`, `datadirs`). +fn search_dirs(xdg_config_home: Option<&Path>, home: Option<&Path>, datadirs: Vec) -> Vec { + let user_config = xdg_config_home.map(Path::to_path_buf).or_else(|| home.map(|h| h.join(".config"))); + let mut dirs: Vec = user_config.map(|c| c.join("qemu/firmware")).into_iter().collect(); + dirs.push(PathBuf::from("/etc/qemu/firmware")); + dirs.extend(datadirs); + dirs +} + +/// Every `*.json` under `dirs`, read, in file-name order, a name in an earlier +/// directory hiding the same name in a later one. A directory that does not +/// exist holds none. +fn descriptors(dirs: &[PathBuf]) -> Result)>, String> { + let mut named: BTreeMap = BTreeMap::new(); + for dir in dirs { + let entries = match std::fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, + Err(e) => return Err(format!("{}: {e}", dir.display())), + }; + for entry in entries { + let path = entry.map_err(|e| format!("{}: {e}", dir.display()))?.path(); + if path.extension().is_some_and(|x| x == "json") { + let name = path.file_name().expect("a listed file has a name").to_string_lossy().into_owned(); + named.entry(name).or_insert(path); + } + } + } + named + .into_values() + .map(|path| std::fs::read(&path).map(|bytes| (path.clone(), bytes)).map_err(|e| format!("{}: {e}", path.display()))) + .collect() +} + +/// The versioned machine type `arch`'s machine resolves to under QEMU +/// `version`: what a descriptor's `machines` globs are matched against. +/// x86_64's PC lineage versions its type under a `pc-` prefix the `q35` alias +/// itself does not carry; aarch64's `virt` carries none. +fn machine_type(arch: Arch, version: &str) -> String { + let release: Vec<&str> = version.split('.').take(2).collect(); + let prefix = match arch { + Arch::X86_64 => "pc-", + Arch::Aarch64 => "", + }; + format!("{prefix}{}-{}", arch.machine(), release.join(".")) +} + +/// The first of `descriptors`, in the order given, that declares UEFI firmware +/// for `machine` on `arch` as a raw code image beside a raw variable-store +/// template, with neither secure boot nor SMM, which no guest here is set up +/// for. An empty file is one the spec says hides its name, and one that does +/// not parse is refused. +fn select(arch: Arch, machine: &str, descriptors: &[(PathBuf, Vec)]) -> Result { + for (path, bytes) in descriptors { + if bytes.is_empty() { + continue; + } + let d: Descriptor = serde_json::from_slice(bytes).map_err(|e| format!("{}: {e}", path.display()))?; + let Mapping::Flash(flash) = d.mapping else { continue }; + let targets = d.targets.iter().filter(|t| t.architecture == arch.name()); + let fits_machine = targets.flat_map(|t| &t.machines).map(|g| glob(g, machine)).collect::, _>>(); + let fits = d.interface_types.iter().any(|i| i == "uefi") + && flash.mode.as_deref().is_none_or(|m| m == "split") + && flash.executable.format == "raw" + && flash.nvram_template.as_ref().is_some_and(|t| t.format == "raw") + && !d.features.iter().any(|f| f == "secure-boot" || f == "requires-smm") + && fits_machine.map_err(|why| format!("{}: {why}", path.display()))?.contains(&true); + if fits { + return Ok(Firmware { + code: flash.executable.filename, + vars: flash.nvram_template.expect("a fitting descriptor names its template").filename, + }); + } + } + let read: Vec = descriptors.iter().map(|(p, _)| p.display().to_string()).collect(); + Err(format!( + "no firmware descriptor declares UEFI flash firmware without secure boot for {} `{machine}`; read [{}]", + arch.name(), + read.join(", ") + )) +} + +/// Whether `pattern` matches `name`. Anything else — a non-trailing `*`, or +/// another fnmatch metacharacter — is refused by name rather than misread. +fn glob(pattern: &str, name: &str) -> Result { + match pattern.strip_suffix('*') { + Some(prefix) if !prefix.contains(['*', '?', '[', '\\']) => Ok(name.starts_with(prefix)), + None if !pattern.contains(['?', '[', '\\']) => Ok(pattern == name), + _ => Err(format!("the machine glob {pattern:?} is not a prefix with at most one trailing `*`, which this reader does not match")), + } +} + +/// The fields of `docs/interop/firmware.json`'s `Firmware` this reader decides by. +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct Descriptor { + interface_types: Vec, + mapping: Mapping, + targets: Vec, + features: Vec, +} + +#[derive(Deserialize)] +#[serde(tag = "device", rename_all = "kebab-case")] +enum Mapping { + Flash(Flash), + #[serde(other)] + Other, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct Flash { + mode: Option, + executable: File, + nvram_template: Option, +} + +#[derive(Deserialize)] +struct File { + filename: PathBuf, + format: String, +} + +#[derive(Deserialize)] +struct Target { + architecture: String, + machines: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn descriptor(arch: &str, machines: &str, features: &str, code: &str) -> Vec { + format!( + r#"{{"description":"t","interface-types":["uefi"], + "mapping":{{"device":"flash","executable":{{"filename":"{code}","format":"raw"}}, + "nvram-template":{{"filename":"{code}.vars","format":"raw"}}}}, + "targets":[{{"architecture":"{arch}","machines":[{machines}]}}], + "features":[{features}],"tags":[]}}"# + ) + .into_bytes() + } + + fn named(files: &[(&str, Vec)]) -> Vec<(PathBuf, Vec)> { + files.iter().map(|(n, b)| (PathBuf::from(*n), b.clone())).collect() + } + + #[test] + fn the_first_non_secure_uefi_flash_descriptor_for_the_machine_wins() { + let files = named(&[ + ("40-memory.json", br#"{"interface-types":["uefi"],"mapping":{"device":"memory","filename":"/sev.fd"},"targets":[{"architecture":"x86_64","machines":["pc-q35-*"]}],"features":["amd-sev"]}"#.to_vec()), + ("50-secure.json", descriptor("x86_64", r#""pc-q35-*""#, r#""requires-smm","secure-boot""#, "/secure.fd")), + ("55-i440fx.json", descriptor("x86_64", r#""pc-i440fx-*""#, "", "/i440fx.fd")), + ("56-arm.json", descriptor("aarch64", r#""virt-*""#, "", "/arm.fd")), + ("58-hidden.json", Vec::new()), + // Fedora-style non-secure descriptors ahead of the raw ones: each + // fits every rule but the one it is named for, and must stay + // refused by that rule alone. + ( + "58a-qcow2.json", + br#"{"interface-types":["uefi"],"mapping":{"device":"flash","executable":{"filename":"/qcow2.fd","format":"qcow2"},"nvram-template":{"filename":"/qcow2.fd.vars","format":"raw"}},"targets":[{"architecture":"x86_64","machines":["pc-q35-*"]}],"features":[]}"#.to_vec(), + ), + ( + "58b-smm.json", + descriptor("x86_64", r#""pc-q35-*""#, r#""requires-smm""#, "/smm.fd"), + ), + ( + "59-combined.json", + br#"{"interface-types":["uefi"],"mapping":{"device":"flash","mode":"combined","executable":{"filename":"/combined.fd","format":"raw"},"nvram-template":{"filename":"/combined.fd.vars","format":"raw"}},"targets":[{"architecture":"x86_64","machines":["pc-q35-*"]}],"features":[]}"#.to_vec(), + ), + ("60-plain.json", descriptor("x86_64", r#""pc-i440fx-*","pc-q35-*""#, r#""acpi-s3","amd-sev""#, "/plain.fd")), + ("70-later.json", descriptor("x86_64", r#""pc-q35-*""#, "", "/later.fd")), + ]); + assert_eq!( + select(Arch::X86_64, "pc-q35-11.1", &files), + Ok(Firmware { + code: PathBuf::from("/plain.fd"), + vars: PathBuf::from("/plain.fd.vars"), + }) + ); + assert_eq!(select(Arch::Aarch64, "virt-11.1", &files).map(|f| f.code), Ok(PathBuf::from("/arm.fd"))); + } + + #[test] + fn nothing_fitting_is_refused_naming_what_was_read() { + let files = named(&[("50-secure.json", descriptor("x86_64", r#""pc-q35-*""#, r#""secure-boot""#, "/s.fd"))]); + let why = select(Arch::X86_64, "pc-q35-11.1", &files).unwrap_err(); + assert!(why.contains("x86_64 `pc-q35-11.1`") && why.contains("50-secure.json"), "{why}"); + assert!(select(Arch::X86_64, "pc-q35-11.1", &[]).is_err()); + } + + #[test] + fn a_descriptor_that_does_not_parse_is_refused_by_name() { + let files = named(&[("10-broken.json", b"{".to_vec()), ("60-plain.json", descriptor("x86_64", r#""pc-q35-*""#, "", "/p.fd"))]); + let why = select(Arch::X86_64, "pc-q35-11.1", &files).unwrap_err(); + assert!(why.starts_with("10-broken.json: "), "{why}"); + } + + #[test] + fn an_earlier_directory_hides_a_later_ones_name_and_a_missing_one_holds_nothing() { + let tmp = toyos_tmpdir::TempDir::new("firmware-descriptors"); + let [first, second] = ["first", "second"].map(|d| tmp.path().join(d)); + for (dir, files) in [(&first, &["60-b.json"][..]), (&second, &["60-b.json", "50-a.json", "README"][..])] { + std::fs::create_dir(dir).unwrap(); + for file in files { + std::fs::write(dir.join(file), dir.display().to_string()).unwrap(); + } + } + let read = descriptors(&[tmp.path().join("absent"), first.clone(), second.clone()]).unwrap(); + assert_eq!( + read, + vec![ + (second.join("50-a.json"), second.display().to_string().into_bytes()), + (first.join("60-b.json"), first.display().to_string().into_bytes()), + ] + ); + } + + #[test] + fn the_machine_is_the_versioned_type_its_alias_resolves_to() { + assert_eq!(machine_type(Arch::X86_64, "11.1.1"), "pc-q35-11.1"); + assert_eq!(machine_type(Arch::Aarch64, "11.1.1"), "virt-11.1"); + } + + #[test] + fn a_glob_matches_a_trailing_star_as_a_prefix_and_refuses_anything_else() { + assert_eq!(glob("pc-q35-*", "pc-q35-11.1"), Ok(true)); + assert_eq!(glob("pc-q35-*", "pc-i440fx-11.1"), Ok(false)); + assert_eq!(glob("pc-q35-11.0", "pc-q35-11.1"), Ok(false)); + assert_eq!(glob("pc-q35-11.1", "pc-q35-11.1"), Ok(true)); + assert_eq!(glob("*", "virt-11.1"), Ok(true)); + assert!(glob("pc-*-11.*", "pc-q35-11.1").is_err()); + assert!(glob("pc-q35-1?.*", "pc-q35-11.1").is_err()); + } + + #[test] + fn the_users_and_the_systems_directories_come_before_qemus_own() { + let dirs = search_dirs(Some(Path::new("/x/cfg")), Some(Path::new("/x/home")), vec![PathBuf::from("/data/firmware")]); + assert_eq!( + dirs, + vec![PathBuf::from("/x/cfg/qemu/firmware"), PathBuf::from("/etc/qemu/firmware"), PathBuf::from("/data/firmware")] + ); + let dirs = search_dirs(None, Some(Path::new("/x/home")), vec![]); + assert_eq!(dirs[0], PathBuf::from("/x/home/.config/qemu/firmware")); + let dirs = search_dirs(None, None, vec![PathBuf::from("/data/firmware")]); + assert_eq!(dirs, vec![PathBuf::from("/etc/qemu/firmware"), PathBuf::from("/data/firmware")]); + } + + #[test] + fn a_read_only_templates_copy_is_still_writable() { + use std::os::unix::fs::PermissionsExt; + let tmp = toyos_tmpdir::TempDir::new("firmware-vars"); + let template = tmp.path().join("template.fd"); + std::fs::write(&template, b"vars").unwrap(); + std::fs::set_permissions(&template, std::fs::Permissions::from_mode(0o444)).unwrap(); + let firmware = Firmware { code: PathBuf::new(), vars: template }; + let to = tmp.path().join("copy.fd"); + firmware.fresh_vars(&to).unwrap(); + let mode = std::fs::metadata(&to).unwrap().permissions().mode() & 0o777; + assert_ne!(mode & 0o200, 0, "the copy must be owner-writable: {mode:04o}"); + // A second boot copies over the same file: still possible only because + // the first copy did not inherit the template's read-only mode. + firmware.fresh_vars(&to).unwrap(); + } +} diff --git a/src/kernelconsole.rs b/src/kernelconsole.rs new file mode 100644 index 00000000000..f3c15fbc1bd --- /dev/null +++ b/src/kernelconsole.rs @@ -0,0 +1,108 @@ +//! The kernel's console as the host reads it off the virtio port: every byte +//! from the kernel's first record on, and none before it. +//! +//! A firmware whose UEFI console drives that port writes its own lines and the +//! loader's there first, the last of them cut off where boot services end and +//! the kernel's first record written onto its tail. Every one of them is on the +//! 16550 as well, and a loader line is read there. Where the kernel begins is +//! the whole rule, so a firmware that writes nothing on the port reads the same. + +use std::borrow::Cow; + +/// What every kernel record's console line opens with: `write_line` in +/// `kernel/src/log/console.rs` tags each record `kernel`, and nothing before +/// the kernel writes it. +pub const HEAD: &str = "[kernel "; + +/// A console's bytes as they arrive, withheld until the kernel's first record. +/// +/// Of a QEMU process's first boot only: a guest reset does not re-arm it +/// (`issues/build/the-kernel-console-split-does-not-re-arm-across-a-guest-reset.md`). +pub struct KernelConsole { + /// The withheld tail that could still begin [`HEAD`]; `None` once the + /// kernel has begun. + held: Option>, +} + +impl Default for KernelConsole { + fn default() -> Self { + Self { held: Some(Vec::new()) } + } +} + +impl KernelConsole { + /// What of the next `chunk` is the kernel's. + pub fn pass<'a>(&mut self, chunk: &'a [u8]) -> Cow<'a, [u8]> { + let Some(held) = &mut self.held else { return Cow::Borrowed(chunk) }; + held.extend_from_slice(chunk); + let head = HEAD.as_bytes(); + if let Some(at) = held.windows(head.len()).position(|w| w == head) { + let from = held.split_off(at); + self.held = None; + return Cow::Owned(from); + } + held.drain(..held.len().saturating_sub(head.len() - 1)); + Cow::Borrowed(&[]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// QEMU 11.1.1's own edk2 on the virtio port, as a boot put it there: the + /// screen clears, `BdsDxe` and the loader, and the loader's last line cut + /// off by the handoff. + const FIRMWARE: &str = "\x1b[2J\x1b[01;01H\x1b[=3h\x1b[2J\x1b[01;01HBdsDxe: loading Boot0001 \ + \"UEFI QEMU QEMU USB HARDDRIVE TOYOS0BOOTSTICK1\" from PciRoot(0x0)/Pci(0x1,0x0)/USB(0x0,0x0)\n\ + ToyOS Bootloader 1.0\n\ + ROOT: read into memory at 0x7c894000+0x800000 from LBA 212992+16384, 1048576 bytes a request \ + (optimal granularity: not reported), in 22519000 TSC cycles\n\ + Loader log: the kernel handoff begins, so "; + + const KERNEL: &str = "[kernel 0.000 cpu0 boot] black box: 0x8000000 is this boot's, 16344 bytes \ + for the next boot's loader\n\ + [kernel 0.001 cpu0 boot] pmm: the firmware map calls 4288393216 bytes usable\n"; + + /// Everything `stream` passes, fed in pieces cut at each of `cuts`. + fn passed(stream: &str, cuts: &[usize]) -> String { + let mut console = KernelConsole::default(); + let mut out = Vec::new(); + let mut from = 0; + for &to in cuts.iter().chain([stream.len()].iter()) { + out.extend_from_slice(&console.pass(&stream.as_bytes()[from..to])); + from = to; + } + String::from_utf8(out).expect("the kernel's bytes are UTF-8") + } + + #[test] + fn the_fused_line_is_the_kernels_first_record_wherever_the_chunks_fall() { + let stream = format!("{FIRMWARE}{KERNEL}"); + for cut in 0..=stream.len() { + assert_eq!(passed(&stream, &[cut]), KERNEL, "cut at byte {cut}"); + } + let every_byte: Vec = (1..stream.len()).collect(); + let kernel = passed(&stream, &every_byte); + let first = kernel.lines().next().expect("a first line"); + assert_eq!( + crate::bootlog::message(first), + Some("black box: 0x8000000 is this boot's, 16344 bytes for the next boot's loader") + ); + } + + #[test] + fn a_port_the_firmware_left_alone_passes_whole() { + let later = format!("{KERNEL}{{1.002 init}} init: a program's line\n"); + assert_eq!(passed(&later, &[3, 40]), later); + } + + #[test] + fn nothing_before_the_kernel_passes() { + assert_eq!(passed(FIRMWARE, &[5, 200]), ""); + let mut console = KernelConsole::default(); + assert!(console.pass(FIRMWARE.as_bytes()).is_empty()); + let held = console.held.as_ref().expect("the kernel has not begun").len(); + assert!(held < HEAD.len(), "{held} bytes withheld, more than could begin the head"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3bce5ecd05b..b10a2a5f703 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod clippy; pub mod compiler; /// What the untouched-disk gate compares a device against, in `tests/`. pub mod fingerprint; +pub mod firmware; pub mod flags; pub mod forkcheck; pub mod heartbeat; @@ -19,6 +20,7 @@ pub mod hostws; pub mod icmp; pub mod identity; pub mod image; +pub mod kernelconsole; pub mod signing; /// Which kernel containers may be hashed, and by whose keys; read by nothing /// but its own tests. diff --git a/src/licence.rs b/src/licence.rs index a8dc1c73b9f..437c9ac1924 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -412,36 +412,6 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Font("OFL-1.1"), ), - ( - "aavmf/AAVMF_CODE.fd", - "47765fe344818cbc464b1c14ae658fb4b854f5c2ceffa982411731eb4865594d", - "NOTICE", - Terms::Spdx("BSD-2-Clause-Patent AND Apache-2.0"), - ), - ( - "aavmf/AAVMF_VARS.fd", - "b3b855c5a80310168051164986855692d1bdb06e67619856177965cd87c6774f", - "NOTICE", - Terms::Spdx("BSD-2-Clause-Patent AND Apache-2.0"), - ), - ( - "ovmf/DEBUGX64_OVMF.fd", - "800ff5af1220d1232d4da7173ccddbb74a9217600bd8935903d9d534801778b4", - "NOTICE", - Terms::Spdx("BSD-2-Clause-Patent"), - ), - ( - "ovmf/OVMF_CODE-pure-efi.fd", - "9de33971d47958f42af86584b502f83256120b2482e4f7ed14db32fd68e92922", - "NOTICE", - Terms::Spdx("BSD-2-Clause-Patent"), - ), - ( - "ovmf/OVMF_VARS-pure-efi.fd", - "c653de93db67e4f2213a35598efb379a13ef4a12c241e003699d4d7afd193635", - "NOTICE", - Terms::Spdx("BSD-2-Clause-Patent"), - ), ( "tests/fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz", "99fcd8a7263b5c25cd90cead1baaa7200ef272100fc2226e008a4e8205ba2916", diff --git a/src/qemu.rs b/src/qemu.rs index a9b799e743f..c0da3a831c6 100644 --- a/src/qemu.rs +++ b/src/qemu.rs @@ -141,7 +141,15 @@ pub fn launch(opts: &Options) { } qemu.arg("-cpu").arg(arch.cpu(accel)); - let [code, vars] = arch.pflash(std::path::Path::new(".")); + // Remade every launch, beside the image it boots: what one session's + // firmware wrote is never the next one's premise. + let vars = std::path::Path::new("target/firmware-vars.fd"); + let [code, vars] = toyos_build::firmware::of(arch) + .and_then(|firmware| firmware.fresh_vars(vars).map(|()| firmware.drives(vars))) + .unwrap_or_else(|why| { + eprintln!("Error: {why}"); + std::process::exit(1) + }); if let Some(boot) = arch.boot() { qemu.arg("-boot").arg(boot); } @@ -281,12 +289,13 @@ pub fn launch(opts: &Options) { /// The machine a profile runs on, with its IOMMU where the machine carries one /// as a property rather than a device. -fn machine(arch: Arch, iommu: bool) -> &'static str { +fn machine(arch: Arch, iommu: bool) -> String { + let base = arch.machine(); match (arch, iommu) { - (Arch::X86_64, true) => "q35,kernel-irqchip=split", - (Arch::X86_64, false) => "q35", - (Arch::Aarch64, true) => "virt,gic-version=3,iommu=smmuv3", - (Arch::Aarch64, false) => "virt,gic-version=3", + (Arch::X86_64, true) => format!("{base},kernel-irqchip=split"), + (Arch::X86_64, false) => base.to_string(), + (Arch::Aarch64, true) => format!("{base},gic-version=3,iommu=smmuv3"), + (Arch::Aarch64, false) => format!("{base},gic-version=3"), } } diff --git a/src/redlist.rs b/src/redlist.rs index 88d243fd6fc..6216fd68bad 100644 --- a/src/redlist.rs +++ b/src/redlist.rs @@ -68,6 +68,10 @@ pub const DISABLED: &[Disabled] = &[ test: "quiesce_wakes_on_the_last_park", issue: "issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md", }, + Disabled { + test: "root_chunk_refused_on_a_usb_stick", + issue: "issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md", + }, Disabled { test: "sched_check_build", issue: "issues/build/the-pass-cost-gates-ci-sample-is-eight-days-stale-twice.md", diff --git a/src/sourcegate.rs b/src/sourcegate.rs index a323d80c63b..21e9f5e583f 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -554,7 +554,7 @@ const HOST_SPAWNS: &[Spawn] = &[ }, Spawn { arg: "arch.qemu()", - sites: &[("src/qemu.rs", 1), ("src/ci.rs", 2), ("tests/common/qemu.rs", 1)], + sites: &[("src/qemu.rs", 1), ("src/ci.rs", 1), ("src/firmware.rs", 1), ("tests/common/qemu.rs", 1)], why: "QEMU, the other half of the bar: `Arch::qemu` names `qemu-system-x86_64` and \ `qemu-system-aarch64`, and `check_prerequisites` requires the one being booted", }, @@ -743,6 +743,13 @@ const CI_PACKAGES: &[Package] = &[ why: "the same Ninja, pinned to the version Ubuntu 24.04 released, on the nightly's \ toolchain runner", }, + Package { + name: "ovmf-generic", + why: "the package that carries the descriptor Debian's QEMU declares for q35 \ + (src/firmware.rs): Debian's edk2 build, named rather than left to \ + `qemu-system-x86`'s Recommends or pulled in by the `ovmf` metapackage, whose \ + amdsev/inteltdx siblings contribute only memory-mapped descriptors this reader skips", + }, Package { name: "python3", why: "the Python standing failure CLAUDE.md:56 declares, wearing a package name — \ diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 09e7bf0eeb9..c52ca646588 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -2347,10 +2347,10 @@ pub struct BootOptions { /// before it left — and a guest with it set runs until the harness kills it. pub takes_the_reset: bool, /// Keep the firmware's variables in this file, writable, instead of the - /// shared read-only template: a copy the test made, so what one boot's - /// loader writes — the anti-rollback floor, `BootNext` — is what the next - /// boot of the same machine reads. `None` is every other boot, whose - /// variables live in firmware memory and die with the guest. + /// boot's own fresh copy of the template: a copy the test made, so what one + /// boot's loader writes — the anti-rollback floor, `BootNext` — is what the + /// next boot of the same machine reads. `None` is every other boot, whose + /// copy dies with the guest. pub firmware_vars: Option, /// The console line that means the boot reached the state under test. /// Anything other than [`DEFAULT_READY`] also declares that a panic is the @@ -2391,10 +2391,11 @@ pub struct BootOptions { /// a driver put on it, which no line the guest prints can be. Refused by /// name on a profile with no data disk, where it would record nothing. pub usb_pcap: Option, - /// Fail with EIO every read of the boot stick that covers this 512-byte - /// sector, through QEMU's `blkdebug` under the stick's raw format: a disk - /// error at a place the test chose, which no well-formed image can stage. - pub stick_read_error: Option, + /// Fail with EIO every read of the boot disk that covers this 512-byte + /// sector, through QEMU's `blkdebug` under the boot image's raw format, on + /// whichever bus the profile puts that disk: a disk error at a place the + /// test chose, which no well-formed image can stage. + pub boot_read_error: Option, /// What the emulated RTC reads when the machine starts, as /// `YYYY-MM-DDTHH:MM:SS`. /// @@ -2521,7 +2522,7 @@ impl Default for BootOptions { boot_image: None, usb_images: Vec::new(), usb_pcap: None, - stick_read_error: None, + boot_read_error: None, rtc_base: None, extra_root_files: Vec::new(), log_port: None, @@ -2642,6 +2643,9 @@ pub struct QemuInstance { /// delete: a [`BootOptions::boot_image`] belongs to the test that staged it /// and is often read back after the guest is gone. own_boot_image: Option, + /// The variable store this boot copied for itself, on the same terms as + /// `own_boot_image`: a [`BootOptions::firmware_vars`] is the test's. + own_vars: Option, boot_log: String, /// Whether this boot armed `i8042-trace`, which is the only channel a /// windowed shell has for saying it took a burst out of the device. @@ -2973,7 +2977,7 @@ pub fn build_toyos_bins(crate_path: &Path) -> Vec<(String, Vec)> { /// and nothing else writes it — a program's line reaches the console only /// through `logd`, under the program's own head. pub fn is_kernel_line(line: &str) -> bool { - line.starts_with("[kernel ") + line.starts_with(toyos_build::kernelconsole::HEAD) } /// A console line's text as its program wrote it: a program's line without @@ -3155,6 +3159,17 @@ impl QemuInstance { let _ = fs::remove_file(&uart_log); let console_file = options.console_file.then(|| ConsoleFile::of(&uart_log).made()); + let (firmware_vars, own_vars) = match &options.firmware_vars { + Some(vars) => (vars.clone(), None), + None => { + let vars = test_dir.join(format!("vars-{seq}.fd")); + toyos_build::firmware::of(options.profile.arch()) + .and_then(|firmware| firmware.fresh_vars(&vars)) + .unwrap_or_else(|why| panic!("[qemu] {why}")); + (vars.clone(), Some(vars)) + } + }; + let qemu = qemu_command( &boot_image, nvme.path(), @@ -3162,6 +3177,7 @@ impl QemuInstance { &audio_wav, &uart_log, &sockets.dir, + &firmware_vars, &options, ); spawn_and_wait_ready( @@ -3176,6 +3192,7 @@ impl QemuInstance { sockets, screendump, own_boot_image, + own_vars, carried, console_file, }, @@ -3789,11 +3806,7 @@ impl Drop for QemuInstance { let _ = self.child.wait(); let _ = fs::remove_file(&self.audio_wav); // **The 16550's log outlives the guest, because it is the one channel - // that exists before the console does.** Firmware, the bootloader and - // the kernel up to the backend switch write here and nowhere else, so a - // boot that dies before virtio-console comes up leaves this file and an - // empty capture — which is exactly the shape `issues/diagnostics/` - // records as looking like a kernel that never started. 1.4 KB on a + // that exists before the console does.** 1.4 KB on a // healthy `tests/testcases` boot, measured, against the hundreds of // megabytes of per-boot image beside it. // @@ -3805,8 +3818,8 @@ impl Drop for QemuInstance { let _ = fs::remove_file(&self.screendump); // A per-boot image is hundreds of megabytes and a full run makes ~76 of // them; the shared name used to make that one file. - if let Some(image) = &self.own_boot_image { - let _ = fs::remove_file(image); + for own in [&self.own_boot_image, &self.own_vars].into_iter().flatten() { + let _ = fs::remove_file(own); } // `sockets` goes with the fields, after QEMU is reaped. LIVE.fetch_sub(1, Ordering::SeqCst); @@ -4314,7 +4327,7 @@ impl QmpDevices { pub fn profile_argv(options: &BootOptions) -> Vec { let p = Path::new("/nonexistent"); let usb: Vec = options.profile.usb_disks().iter().map(|_| p.to_path_buf()).collect(); - qemu_command(p, p, &usb, p, p, p, options) + qemu_command(p, p, &usb, p, p, p, p, options) .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect() @@ -4334,6 +4347,7 @@ fn stick_file(image: &Path, read_error: Option) -> String { } } +#[allow(clippy::too_many_arguments)] fn qemu_command( boot_image: &Path, nvme_image: &Path, @@ -4341,6 +4355,7 @@ fn qemu_command( audio_wav: &Path, uart_log: &Path, socket_dir: &Path, + firmware_vars: &Path, options: &BootOptions, ) -> Command { let (qmp_socket, segment) = socket_names(socket_dir, options); @@ -4356,8 +4371,9 @@ fn qemu_command( ); let arch = options.profile.arch(); - let repo = compile::repo_root(); - let [firmware_code, firmware_vars] = arch.pflash(&repo); + let [firmware_code, firmware_vars] = toyos_build::firmware::of(arch) + .unwrap_or_else(|why| panic!("[qemu] {why}")) + .drives(firmware_vars); let mut qemu = Command::new(arch.qemu()); if let Some(boot) = arch.boot() { @@ -4383,15 +4399,15 @@ fn qemu_command( // needs the userspace half of the irqchip, and a machine with no unit has // no reason to be built differently from the one it has always been. let mut machine = match arch { - Arch::X86_64 => String::from("q35"), + Arch::X86_64 => arch.machine().to_string(), Arch::Aarch64 => { // `virt` has no i8042 to take away, and the unit a profile declares // is VT-d, which it has none of either. assert!(options.i8042 && shape.iommu.is_none(), "`virt` has neither an i8042 nor VT-d"); - String::from(match options.profile { - Profile::VirtEl2 => "virt,gic-version=3,virtualization=on", - _ => "virt,gic-version=3", - }) + match options.profile { + Profile::VirtEl2 => format!("{},gic-version=3,virtualization=on", arch.machine()), + _ => format!("{},gic-version=3", arch.machine()), + } } }; if !options.i8042 { @@ -4423,14 +4439,11 @@ fn qemu_command( .arg("-drive") .arg(firmware_code) .arg("-drive") - .arg(match &options.firmware_vars { - Some(vars) => format!("if=pflash,format=raw,unit=1,file={},readonly=off", vars.display()), - None => firmware_vars, - }) + .arg(firmware_vars) .arg("-drive") .arg(format!( "if=none,id=stick,{}{}", - stick_file(boot_image, options.stick_read_error), + stick_file(boot_image, options.boot_read_error), // **What a `Staged::Pristine` boot is made of.** QEMU keeps this // drive's writes in a temporary file and drops it when the guest // exits, so the staged image is never written and the boot after it @@ -4820,6 +4833,7 @@ struct Files { sockets: Sockets, screendump: PathBuf, own_boot_image: Option, + own_vars: Option, carried: Option>, console_file: Option, } @@ -4894,6 +4908,7 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) sockets, screendump, own_boot_image, + own_vars, carried, console_file, } = files; @@ -4932,6 +4947,14 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) let (tx, rx) = mpsc::channel::(); let console = ConsoleStream::new(); let reader_console = console.clone(); + // The virtio port starts at the kernel's first record; a 16550 on stdio has + // no other file, so it is read whole. + let mut kernel_console = options + .profile + .shape() + .virtio + .present() + .then(toyos_build::kernelconsole::KernelConsole::default); let reader_thread = thread::spawn(move || { let mut reader = BufReader::new(stdout); let mut full_log = String::new(); @@ -4951,12 +4974,16 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) } return full_log; } + let read = match &mut kernel_console { + Some(console) => console.pass(&chunk[..read]), + None => std::borrow::Cow::Borrowed(&chunk[..read]), + }; reader_console .0 .lock() .expect("the console stream lock is never held across a panic") - .extend_from_slice(&chunk[..read]); - pending.extend_from_slice(&chunk[..read]); + .extend_from_slice(&read); + pending.extend_from_slice(&read); while let Some(at) = pending.iter().position(|&b| b == b'\n') { let mut line: Vec = pending.drain(..=at).collect(); line.pop(); @@ -4994,6 +5021,7 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) sockets, screendump, own_boot_image, + own_vars, boot_log, console, i8042_trace: options.kernel_params.contains(&"i8042-trace"), diff --git a/tests/common/serial.rs b/tests/common/serial.rs index 29ee146e44a..94d65a9c1ab 100644 --- a/tests/common/serial.rs +++ b/tests/common/serial.rs @@ -252,7 +252,7 @@ impl Serial { pub fn interleaved(&self) -> Option<&str> { self.text .lines() - .find(|l| !is_kernel_line(l) && l.contains("[kernel ")) + .find(|l| !is_kernel_line(l) && l.contains(toyos_build::kernelconsole::HEAD)) } /// The channel carried something the kernel wrote. diff --git a/tests/common/update.rs b/tests/common/update.rs index 5ce91a9d5e5..a2944254c1d 100644 --- a/tests/common/update.rs +++ b/tests/common/update.rs @@ -108,9 +108,8 @@ impl Rig { ); let image = scratch.join("machine.img"); std::fs::write(&image, disk).map_err(|e| format!("write {}: {e}", image.display()))?; - let vars = scratch.join("OVMF_VARS.fd"); - std::fs::copy(root.join("ovmf/OVMF_VARS-pure-efi.fd"), &vars) - .map_err(|e| format!("copy the firmware's variable store: {e}"))?; + let vars = scratch.join("vars.fd"); + toyos_build::firmware::of(toyos_build::arch::Arch::X86_64)?.fresh_vars(&vars)?; Ok(Self { scratch, image, vars, identity, port: qemu::free_host_port(), base_kernel: parts.kernel.len() }) } @@ -282,7 +281,7 @@ fn loader_said(guest: &QemuInstance, from: usize, what: &str) -> Result<(), Stri if since.contains(what) { return Ok(()); } - let loader: Vec<&str> = since.lines().filter(|l| !l.starts_with("[kernel ")).collect(); + let loader: Vec<&str> = since.lines().filter(|l| !qemu::is_kernel_line(l)).collect(); Err(format!("the loader never said {what:?}; it said:\n{}", loader.join("\n"))) } @@ -613,8 +612,7 @@ pub fn update_floor_is_the_images_own(_: &Path, _: &[(String, Vec)], _: &[(S let own = rig.floor_name()?; let owner = floors::name(Scope::Machine, &key.public(), &[0; 16]).as_str().to_string(); let other = floors::name(Scope::Image, &key.public(), &[0x55; 16]).as_str().to_string(); - let template = super::compile::repo_root().join("ovmf/OVMF_VARS-pure-efi.fd"); - let fresh = || std::fs::copy(&template, &rig.vars).map(|_| ()).map_err(|e| format!("{}: {e}", rig.vars.display())); + let fresh = || toyos_build::firmware::of(toyos_build::arch::Arch::X86_64)?.fresh_vars(&rig.vars); fresh()?; vars::plant(&rig.vars, &owner, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; diff --git a/tests/common/volumes.rs b/tests/common/volumes.rs index 89c369a97ab..ace7fd983a0 100644 --- a/tests/common/volumes.rs +++ b/tests/common/volumes.rs @@ -3249,7 +3249,7 @@ pub fn root_named_twice( } /// **A chunk of ROOT the disk will not read refuses the boot, naming that -/// chunk.** The boot stick fails with EIO every read covering the sector +/// chunk.** The boot disk fails with EIO every read covering the sector /// seven past ROOT's middle, so the chunk that fails is not the first. The /// loader reads ROOT in chunks, so the refusal names a chunk that holds the /// sector and starts where the bytes read before it end. @@ -3257,6 +3257,27 @@ pub fn root_chunk_refused( test_config: &Path, c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)], +) -> Result<(), String> { + root_chunk_refused_on(qemu::Profile::InternalDisk, test_config, c_bins, rust_bins) +} + +/// [`root_chunk_refused`] with the boot image on a USB stick: stock edk2's +/// read of that sector does not return, and its watchdog does not reset the +/// machine +/// (`issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md`). +pub fn root_chunk_refused_on_a_usb_stick( + test_config: &Path, + c_bins: &[(String, Vec)], + rust_bins: &[(String, Vec)], +) -> Result<(), String> { + root_chunk_refused_on(qemu::Profile::Headless, test_config, c_bins, rust_bins) +} + +fn root_chunk_refused_on( + profile: qemu::Profile, + test_config: &Path, + c_bins: &[(String, Vec)], + rust_bins: &[(String, Vec)], ) -> Result<(), String> { let image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); let (at, len) = root_extent(&image)?; @@ -3269,12 +3290,9 @@ pub fn root_chunk_refused( c_bins, rust_bins, BootOptions { + profile, boot_image: Some(qemu::Staged::Written(path.clone())), - stick_read_error: Some(bad), - // The read's own line and not the refusal after it: the stick - // QEMU fails a read on answers the loader's next write to - // `loader.log` with nothing, and the console line before that - // write is the last this boot says. + boot_read_error: Some(bad), ready_marker: CHUNK_REFUSED, ..Default::default() }, @@ -3400,7 +3418,7 @@ pub fn root_named_twice_on_the_boot_disk( rust_bins, BootOptions { boot_image: Some(qemu::Staged::Written(path.clone())), ..Default::default() }, ); - let log = format!("{}{}", qemu.uart_log(), qemu.boot_log()); + let log = qemu.uart_log(); drop(qemu); let _ = std::fs::remove_file(&path); diff --git a/tests/toyos.rs b/tests/toyos.rs index 493f00dbb1f..5c6aaeded2a 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1362,6 +1362,7 @@ const MACHINE_TESTS: &[(&str, Sched, Tier)] = &[ ("root_candidate_malformed", Sched::Serial, Tier::Weekly), ("root_named_but_absent", Sched::Serial, Tier::Weekly), ("root_chunk_refused", Sched::Serial, Tier::Nightly), + ("root_chunk_refused_on_a_usb_stick", Sched::Serial, Tier::Fast), ("root_candidate_overlaps", Sched::Serial, Tier::Nightly), ("root_named_twice_on_the_boot_disk", Sched::Serial, Tier::Nightly), ("root_named_twice", Sched::Serial, Tier::Weekly), @@ -11625,6 +11626,9 @@ fn run_machine_test( common::volumes::root_named_but_absent(test_config, c_bins, rust_bins) } "root_chunk_refused" => common::volumes::root_chunk_refused(test_config, c_bins, rust_bins), + "root_chunk_refused_on_a_usb_stick" => { + common::volumes::root_chunk_refused_on_a_usb_stick(test_config, c_bins, rust_bins) + } "root_candidate_overlaps" => common::volumes::root_candidate_overlaps(test_config, c_bins, rust_bins), "root_named_twice_on_the_boot_disk" => { common::volumes::root_named_twice_on_the_boot_disk(test_config, c_bins, rust_bins) diff --git a/toyos-acpi/fixtures/ovmf-pure-efi/SOURCE b/toyos-acpi/fixtures/ovmf-pure-efi/SOURCE index 4f76adfdd53..9d3bcd81714 100644 --- a/toyos-acpi/fixtures/ovmf-pure-efi/SOURCE +++ b/toyos-acpi/fixtures/ovmf-pure-efi/SOURCE @@ -1,8 +1,7 @@ The ACPI resource descriptors `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL::Configuration` answered with on the one root bridge of a QEMU 11.1.0 q35 guest, under the OVMF -this repository boots every guest with — `ovmf/OVMF_CODE-pure-efi.fd`, sha256 -9de33971d47958f42af86584b502f83256120b2482e4f7ed14db32fd68e92922, which `NOTICE` -also names. +build sha256 +9de33971d47958f42af86584b502f83256120b2482e4f7ed14db32fd68e92922. root-bridge-0.bin 186 bytes, off the `Root bridge: 0 (segment 0) 186 bytes:` line the bootloader writes to the 16550 before the kernel