diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ee59f..6ae201a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## M6B Virtio Block Transport candidate — 2026-07-26 + +- Architecture-independent 512-byte sector block-device contract. +- Validated I/O, 32-bit, and 64-bit PCI BAR decoding. +- Modern virtio PCI common, notify, device, and configuration-window parsing. +- `VIRTIO_F_VERSION_1` negotiation and fail-closed device initialization. +- Allocator-owned, direct-mapped split virtqueue with one outstanding request. +- Polling read, write, and device-ID requests with status/reset/timeout errors. +- Dedicated disk identity verification and known-sector read evidence. +- Disposable-sector write/readback followed by restoration of original bytes. +- Sector-boundary rejection and `block` shell diagnostics. +- Capability registry version 5 and full M5/FH1/FH2/FH3/M6A regressions. + +M6B does not mount a filesystem or expose general-purpose persistent writes. +Buffer caching and VFS contracts remain M6C; read-only FAT32 remains M6D. + ## M6A PCI and Storage Discovery candidate — 2026-07-26 - Architecture-independent PCI identity and storage classification. diff --git a/Cargo.lock b/Cargo.lock index c7d4403..a829ca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,11 @@ version = 4 [[package]] name = "sanju-boot" -version = "0.0.9" +version = "0.0.10" dependencies = [ "sanju-kernel", ] [[package]] name = "sanju-kernel" -version = "0.0.9" +version = "0.0.10" diff --git a/PROJECT.md b/PROJECT.md index 64227ed..667203c 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -6,9 +6,9 @@ - Architecture: x86-64 - Firmware: UEFI 2.x - Primary language: Rust 2024 -- Accepted checkpoint: Foundation Hardening Phase 3 -- Immutable release: `v0.0.8-fh3` -- Current development checkpoint: M6A PCI and storage discovery +- Accepted checkpoint: M6A PCI and Storage Discovery +- Immutable release: `v0.0.9-m6a` +- Current development checkpoint: M6B virtio block transport - Deployment policy: QEMU only until physical-install safety gates pass ## Mission @@ -21,11 +21,12 @@ Development is grouped into major milestone batches. Small formatting or CI corr ## Current objective -Enumerate the QEMU PCI topology through hardware configuration transactions, -retain a bounded device inventory, and match a dedicated virtio block target -without issuing disk I/O. +Activate the dedicated QEMU virtio block target behind an +architecture-independent sector contract. Validate modern PCI capabilities, +feature negotiation, DMA queue ownership, a known-sector read, and a confined +write/readback/restore transaction. ## Next major objective -Add a polling virtio-blk transport and architecture-independent block-device -API after M6A passes QEMU. +Add a fixed-capacity block cache and VFS contracts without enabling persistent +filesystem writes. diff --git a/README.md b/README.md index e23fb38..8011e81 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SanjuOS is an independent, Rust-first desktop operating-system project. It is not a Linux distribution. Development proceeds through emulator-verified kernel milestones before any physical-disk work. -## Current checkpoint: M6A PCI and Storage Discovery +## Current checkpoint: M6B Virtio Block Transport The accepted baseline entering this phase was the Foundation Hardening Phase 2 candidate. M0 through FH2 proved UEFI ownership transfer, protected kernel @@ -14,7 +14,7 @@ execution, interrupts, Ring 3 entry, `SYSCALL`/`SYSRET`, ELF64 loading, recoverable user faults, physical ownership, and a fresh SanjuOS page-table root. -The accepted `v0.0.8-fh3` release turns the process runtime into an active +The accepted `v0.0.8-fh3` release turned the process runtime into an active hardware boundary: - each M5 process owns a deep-cloned four-level page-table root; @@ -31,19 +31,30 @@ hardware boundary: The authoritative maturity status is generated at [`docs/CAPABILITY_MATRIX.md`](docs/CAPABILITY_MATRIX.md). -M6A now begins the storage stack without issuing disk I/O: +The accepted `v0.0.9-m6a` release begins the storage stack without issuing +disk I/O: - x86 PCI configuration mechanism #1 is probed directly; - bus/device/function discovery handles multifunction devices and bridges; - an allocation-free kernel inventory classifies storage controllers; - QEMU attaches a disposable second disk through `virtio-blk-pci`; - boot and shell evidence must identify that exact block target; -- sector I/O remains the separate M6B acceptance gate. +- sector I/O remains a separate acceptance gate. + +M6B implements that gate on the dedicated QEMU test disk: + +- an architecture-independent, sector-based block-device contract; +- validated PCI BARs and modern virtio PCI capabilities; +- `VIRTIO_F_VERSION_1` negotiation with unsupported read-only media rejected; +- one allocator-owned, direct-mapped DMA page containing a split virtqueue; +- bounded polling with reset, status, and timeout failures returned safely; +- a seeded read test plus a disposable write/readback/restore transaction; +- a `block` shell diagnostic and exact QEMU acceptance evidence. ## Shell commands ```text -help version userspace uptime memory irq tasks ls cat write echo clear +help version userspace uptime memory irq tasks pci block ls cat write echo clear ``` ## Build and verify @@ -73,11 +84,12 @@ docs/ Requirements, architecture, ADRs, testing, security, process ## Current boundary -M6A and FH3 remain single-core and PIT-driven. The combined EFI-stub kernel still +M6B and FH3 remain single-core and PIT-driven. Block completion is synchronous +and polling, one request is outstanding at a time, and only the explicitly +identified disposable QEMU disk is used. The combined EFI-stub kernel still retains a bounded identity mapping while a separate high-half kernel image is -designed. Per-process floating-point/SIMD state, SMP, local APIC timers, PCID, -copy-on-write, and demand paging remain future hardening work. PCI discovery, -the virtio block transport, a persistent VFS, and graphics are later gates. +designed. A block cache, VFS, read-only FAT32, persistent writes, physical-disk +installation, graphics, SMP, and local APIC timers remain later gates. ## Safety diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 5387f9f..74cc2ee 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -4,19 +4,20 @@ ae0cb6c3b2dcaed5941d193848a2d05028f12f0692e95aacf2808ed7b037dd8f .gitattributes 9f35a88b86d7601a4b40fe13e7987baac18a1310911aec01111ee123f8bbc3a4 .github/workflows/ci.yml 4337eb9c42b5bfdf2840306b3944a65e475e21af781307f998ef299b56f685d7 .gitignore 17fe0e368f6e7327b413a3113c56cce5f169a1ccb54f0b65375e4abcacc35145 assets/branding/sanjuos-logo.png -9fa60d55cd2b806393077b7abaaa62bb414a56fb216e1fc6d14917110dc13bb8 boot/uefi/Cargo.toml +41989b585af5a688ea5309a7246851be5a458c5707fa393c6e341e65743c9e8b boot/uefi/Cargo.toml d80a890c3bfcb13a3a9fc850b6e9b9a2eea333c83b1e78c5db60abd0272bc9b8 boot/uefi/src/arch/mod.rs -9eac8368fa9313ef48f096ba913f30666a4227bee28554d2b7e7bcc426bc3ed1 boot/uefi/src/arch/x86_64/mod.rs +dd4aea80aed8b6589dea0e9a0a98a206ff0822a4a729a910a6f2edc94b9a102b boot/uefi/src/arch/x86_64/mod.rs 3899462a2005889f9e86c3ef775364441b46f0b7da079cad52e792c02fb6b997 boot/uefi/src/arch/x86_64/paging.rs -59da4fe3599bcad80f53bceea8a6d1780edd03997c668e998d8ec493e95c1279 boot/uefi/src/arch/x86_64/pci.rs +dd017528cd4bd9206416bc0df717263aca92719b80ffd83009902577b519ce32 boot/uefi/src/arch/x86_64/pci.rs 74116134615fd003f8592bec4557a326920ffa0a70f1b2afde755c44844c4b9f boot/uefi/src/arch/x86_64/qemu.rs 7d11876ab13082c796a5a2af100ece7b713251c408e3c67b313cc51ed3a3d410 boot/uefi/src/arch/x86_64/serial.rs -da344eb5fbd6d0e5a7c01e70b081fe953653213791b3dea8533ace915f214d89 boot/uefi/src/main.rs -38736de28b1960cc3d033d665c2bc9ee871ed73229eaa1744545ab9acf9bea97 capabilities/capabilities.toml -a48eb659ae8d4f167f01a60a8fcf2ee60c287a867ae5a340f3e302d93bbdc21b capabilities/smoke-expectations.txt -14c0e3a2df7e38bdfb8f2a24c14706707af9ed4c9ab7fa494d1672f0b85a0031 Cargo.lock +7143759544dc7000e33aa1f2e7d554997596256ac9e1d3f4a4b71c7101082d3b boot/uefi/src/arch/x86_64/virtio_block.rs +aaeeb7a569404479f0d98055809b63639bf0646177d9f34d0755dd91fff73416 boot/uefi/src/main.rs +b7542374ecc32918dbe6554b78ade101d5f8c8ea99340dc702d01f3fccf01659 capabilities/capabilities.toml +b7888323ee3835d583b68de316e5b59a17f5038c144df6a9e74591d61d9f36bf capabilities/smoke-expectations.txt +d893a6f2a1e7b32c9377c8f5d04cbe570979422820a8ca0c2113db5f6efd0fbb Cargo.lock 2c03da856ec66c6d6407cfe6f007989ac5e48acdf5ac923bf1d1bc7d3d448675 Cargo.toml -bcb0b1a98459bb998f419f9455d049066f7f154272be305f7f07b8d400c92ef2 CHANGELOG.md +4db556995b861e272ab460a3bcd209f51f8a2918cd2c0dfb672ca2994e25fb10 CHANGELOG.md fa978f37685c47c2209dc2c11e4bb7d278ab47a813b051d1b25ad66e3eafc770 CONTRIBUTING.md 4b490a94642825c10c5876ab94d6e1390360ba754e9615735c649b64a5fea334 docs/adr/0001-rust-first.md 2408920abe6ae8d6c3861f0bcfbe5c3cb362a444fc5104eb5d245fee9334a22d docs/adr/0002-uefi-x86-64-first.md @@ -29,13 +30,13 @@ e57c9bfed03a3a425d8592033d8b5fc36146f56b0388ffb8af9c8ec8c75413b3 docs/adr/0008- 6748b021ba78f4bddd20e7f160fbed9d43d6d51e5efff943541ec01168fe4c6c docs/adr/0009-foundation-freeze-memory-ownership.md 756405de996d29dda52dc17c1c3cadc60df9168ae295f9da911f80587d5d5907 docs/adr/0010-page-table-sovereignty.md 012833274e4c031ef4b8b3fe59ae552ac2ad90b03cd3824abc940a91c0ee3b69 docs/adr/0011-private-address-spaces-preemptive-contexts.md -79b25205c83ec732cc2979dca8aba4d10d69d59a4e7fdbbacb6688fd6b26bc4f docs/adr/0012-pci-storage-foundation.md -c34623a530eb46173a5ea13fe59a36607bad539201e0c1e6133e683726348462 docs/architecture/SYSTEM_OVERVIEW.md -9dcd1899544652d376c847cb3fc2c9ac41f9aacad0aa3073edbfc764009dd86e docs/CAPABILITY_MATRIX.md -0eeed4e42af7290bbbf65937e6022b96276130d5070000636e9fc5c250d3a94a docs/process/BACKLOG.md +ce3f0b402a55436dd3039a4a238a50b02d32cf82cfe72cf784a9965f3d7de439 docs/adr/0012-pci-storage-foundation.md +15ec9d23c6f7bca03491c16622606f917a25f6bb9adb9e6bb4dc0e8982fa8e30 docs/architecture/SYSTEM_OVERVIEW.md +ed3365fbec4de8867e55e5295ad86c9b92ff89e05809e2fbfdcaa8bb0329a522 docs/CAPABILITY_MATRIX.md +c6aeeb6ac4ce860b7dfc313cf5590eafc56c37adca2e08a81148d48bda045e55 docs/process/BACKLOG.md fee8050e8dcc105d812af867282e410f229c2422e535add412d987b4ab70c890 docs/process/DEFINITION_OF_DONE.md 620312efa0d2c090a909a109f0303cdbfb10f6b756be238a41b182f80cf4c76c docs/process/ENVIRONMENT_NOTE.md -ad44783f6526d5ee36291b2800eeae040688ee1f2848fc11d2ddf75c1e4b7524 docs/process/RISK_REGISTER.md +6b5ced505728195064ed19e7518350b2e7a4344d30319805cbdcb9bb2faeffe4 docs/process/RISK_REGISTER.md 146760411de49ec93c4f4a6504f183ecda17e1f0b087816cad37c366de1f6232 docs/process/SDLC.md 53005001ed5a64131b96e384f02a038f7dc3f8af0b5bcfcfbe726544c196ca2e docs/process/SPRINT_0.md 39021ee209e0fe5c981c3a51a5bcbcc9b56551dd8d9e09e87a2498d029d6ce3a docs/process/SPRINT_1.md @@ -45,9 +46,9 @@ f1775d368dea5e3970dc3449e2b2431dc4b46c8d3517461e82f9b343575c73ca docs/process/S 5ef377c5d302832ac135ca68a5d2299a1cfefa84fd0cc1df3f2a759160c62734 docs/process/SPRINT_FOUNDATION_1.md e2e35b9e47bcaa4b909f6d52bd64c56fc02d03ff45e440d4d2f2ae5a207a7b6d docs/process/SPRINT_FOUNDATION_2.md 6f15cc971ee785b6211dfa469d345c13dc7ef6b65ab484adb9be0b76bf5ebec9 docs/process/SPRINT_FOUNDATION_3.md -981704eafdbf4b39bb8b980498a18d2e8c57f32a6131445b4e951212ba6366a4 docs/process/SPRINT_M6_STORAGE_FOUNDATION.md +d9033107b5c62800cec539c6fb47852b07a8c49cfc17e9b28671307504642fbf docs/process/SPRINT_M6_STORAGE_FOUNDATION.md 695d5c8f71075cc6f377a12c222d2b95e010192a7c6a428d6f3e830dcab8c7c2 docs/requirements/PRODUCT_REQUIREMENTS.md -b23b8df9b60a0a710ee485bffbdb63e7eed15e1c64e6ae1d022cc1f9e587f878 docs/security/THREAT_MODEL.md +5202f65d8d18c168efa2695cbdeb24a0e89d08b0a842b22825891b23e001de38 docs/security/THREAT_MODEL.md 80e655063c71d86e90895b289c57a61a127e2ea2b0ee8dfb26bbe78e032f027f docs/testing/FOUNDATION_HARDENING_1_VALIDATION.md a7706bae9b2288ef67a77263af7ef0672eeec34c0d9331671cda04cf0858c39f docs/testing/FOUNDATION_HARDENING_2_VALIDATION.md d9c7d5e718be2f21f50408551733c5860c04745bd4dd55c78b3876827cb1ea21 docs/testing/FOUNDATION_HARDENING_3_VALIDATION.md @@ -55,29 +56,31 @@ d9c7d5e718be2f21f50408551733c5860c04745bd4dd55c78b3876827cb1ea21 docs/testing/F 2e5a291acec6351b594166f9cb8a6107dc47fd4c421557fbafbfaf4eef3d9ff5 docs/testing/M4_VALIDATION.md 23c88c5b3bc0452637af6d8632234bd0be74ed6e356268cf02e24b4bd7d3e125 docs/testing/M5_VALIDATION.md ca41b8558018f1e32ffdf570f061c60dc9914f781ae17f755d2c6402607eb67e docs/testing/M6A_PCI_DISCOVERY_VALIDATION.md -c44ac254c3cbf6a934a65a241103ed7c03db1036ed73444e10ab11b662052be5 docs/testing/TEST_STRATEGY.md -82ad6b17dc1b2041f2052479019c270980251e35b5519b6ec98bcd0e3b5e0d08 kernel/Cargo.toml +c4556e45804712a1f0382fdbd2d4e4fecdf467c95a867a3d16ef0a8aaa515fc7 docs/testing/M6B_BLOCK_TRANSPORT_VALIDATION.md +2bcd201649ceaade95bc0621f1a1824625285dfe43a8f125b5e3866bb5d87bb8 docs/testing/TEST_STRATEGY.md +e16e6e8ee0ab798bd3c226fb83c81080cdce5069fe15b3d23180eaf7dec58a94 kernel/Cargo.toml +528774183d2c2c8a809b796a1dad91a336462bfd589255007da6d0447ffff04c kernel/src/block.rs 0f8884112d25f779cff472251105301b160b4368ee6aeca421eff75242a4404e kernel/src/boot_info.rs 069853672f94c05025c9342485c3f00453d76c95a228302e01b9ebff47a072a6 kernel/src/capabilities.rs 8010bfaa835197b850c4a45e007f8b74b72c1de78673ebf3f0bedddd5912cf5d kernel/src/elf.rs d2b8bfdd1472b3e75dd46a5f7d9752bcb70482b9b6c590e4579660b8ffc52b49 kernel/src/fs.rs -7534cd58803b08368b411c3e13b772c1d21567e76574fa67bdfbcdd7f1d21962 kernel/src/generated/capabilities.rs +93cac7beda0b9046a540c113f09439cd81dbb91a60a0a581d99ab5403a6ef8cc kernel/src/generated/capabilities.rs ae5160fc6d70367b7758afac6876faeb3c9e7766411b4dc1fc3ac10fa28dafa8 kernel/src/generated/mod.rs e6d19b13631029aefb179636a8b2cf20dad347bf2bc89365133912271c758008 kernel/src/heap.rs 9b88a5d4c56554cb1c4bd646925b28bc97a3feb4d3d6c576b5dc33dbe8496d02 kernel/src/input.rs -6fd92163f262578c23d9da284e34698853827e7ca0daf4efe986c1fb1339137b kernel/src/lib.rs +96df539fab698d08533d90ae8ce5df92e57982b6d121fe92e9003b32886c3300 kernel/src/lib.rs 472d2bb193b4c3449807927e2059db0ed94ea7218c3238f7f082aeda65582683 kernel/src/memory.rs 6188979546aeab5b63216056d9d148184fe89ede5226726b9c8c29471c0717a0 kernel/src/ownership.rs 52dabe4f896d71aa0dd67716cd2660ce1b96550a32ead52836185138384f4ba6 kernel/src/paging.rs -8f006b9915d50fa565b63d6aa0fa24fafa3ef1f73b109a2b6ee17b4605ef2adf kernel/src/pci.rs +f0ad8e59490e98186eb44b4dae04a42be46aae6b669e34b6548efa6646dbae82 kernel/src/pci.rs b36eff431fd41dada1848ccc363ac9a66f1c1ff38f99cb2e16a8655820ebbe06 kernel/src/process.rs 18426c0070887453f9de8a816927fe2c2b3cec57e40f90ef152d67546a462ca3 kernel/src/scheduler.rs -0d2ac72180b1e28d13c027f9994869929d3f5da475c111f2d528c7ddc601fdce kernel/src/shell.rs +f8518120e136c8073262134ed4cd08a22643d933e1287d17ad9f04324f06a102 kernel/src/shell.rs 453d92ba90decb230785feeb9d7b56cc0ba583c52237c550ce6fb2889304f9df kernel/src/startup.rs d47014298e0006dc63b6c346a5e823d1208891d91ccf9987fc8c46fe351e26bd kernel/src/syscall.rs b818e7d3c53abc2d759daca8bb901c43fb62aa7ab3804f9f6269e42247260898 Makefile -ba43bce2aaf5c212f33f3bda0c22f6c142c5d263f71348836e837fe50c82b3e4 PROJECT.md -cc13aa490db14684f53c349c89876e89199dad6c75c883bdb39680a315e4ae1e README.md +1905ee7c842ee951d0d41c465f0cf51755d756fcd4d28bec6dcdf217d9512256 PROJECT.md +acc46103cc5755aa4937f5beb584c8b2a377127b2e9ed82cd8e8e12d4e5ea81f README.md 5aa479510db257bb2f5c82eb11fbbd3c30d1cc5ee70864dd9af5b2387e709c43 rust-toolchain.toml cd7c4d2ac3a81f614a8134661c0353e238ea19d4cb000bf98143a8e5f4a9a521 scripts/build-llvm-probe.sh c26e150f860ee56bc77d7181d7ff3514b32615e17a21494c33c6a159108cb528 scripts/build-smoke.sh @@ -87,10 +90,10 @@ f5e3a26817aecf454c61b0bb0ff5f86fd03d7a3fb0ece74c67f1913a4dd94a74 scripts/find-o fc3cabdfc9f29ed493ae266ac73d19183224031d15f53d1e770b15ebaabe00ce scripts/generate-capabilities.py b6b36fc8d1c06cb824cae32821646d6e1f70946a761ca492a77e4f1dbbbcb749 scripts/generate-source-manifest.py b424e16e3444699ec89bb3d84e4e44c2dfaf4631431d26c88cc6a0e9e957cf42 scripts/image.sh -267ec27aecc9a0d8018dea651e411ba73447bb5456995219ae0ce3f30984ec42 scripts/run-qemu.sh +5ae79499617b1d5d44b0ff7f7d797606d1b8eb3a47081af0b36ac006269bd64e scripts/run-qemu.sh 52b6d67fe1d6c035a5bcee7b777b431d2d95645f9b0f26ac88f3130823b11e86 scripts/setup.sh -eb4590c3cd98ab169bcc023b130468c732dbbc7d1e05ed75cc8cc3b7eda76fca scripts/smoke-test.sh -877f1adcb675cc970c29c8a09f6841dc2826e8372ed7cd5997e6a4b503dfcad5 scripts/source-check.py +49f0f15e67fdf0dc91c12721ae201b9dfdc1413a6523c2a67914578a956abcae scripts/smoke-test.sh +d1be8a69d910175edec5c1a86d3bfaa9d3a221eb7dcb7ffbaae53afdd3e03095 scripts/source-check.py 4cd8845ab8db1a1702165dcd7b777917804a285a06a5270226aff85e8a53280a scripts/verify-llvm-probe.sh 1ab4c9f09e95c0779b58e3e792ead2eeaa53291cc1804e405f7ec6635223383d SECURITY.md 88cf4ef34b86ae3e33b04d4f1b7e67a15778d88e1728be97982c679a52361a82 user/programs/bin/fault-test.elf diff --git a/boot/uefi/Cargo.toml b/boot/uefi/Cargo.toml index 73662ca..d790cd0 100644 --- a/boot/uefi/Cargo.toml +++ b/boot/uefi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sanju-boot" -version = "0.0.9" +version = "0.0.10" edition.workspace = true rust-version.workspace = true publish.workspace = true diff --git a/boot/uefi/src/arch/x86_64/mod.rs b/boot/uefi/src/arch/x86_64/mod.rs index 5763f27..fc91511 100644 --- a/boot/uefi/src/arch/x86_64/mod.rs +++ b/boot/uefi/src/arch/x86_64/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod paging; mod pci; mod serial; +mod virtio_block; #[cfg(feature = "qemu-test")] pub mod qemu; @@ -14,6 +15,7 @@ pub(crate) use paging::{ }; pub(crate) use pci::discover_pci; pub use serial::SerialConsole; +pub(crate) use virtio_block::initialize_and_probe as initialize_virtio_block; use core::arch::{asm, global_asm}; use core::mem::size_of; diff --git a/boot/uefi/src/arch/x86_64/pci.rs b/boot/uefi/src/arch/x86_64/pci.rs index 6770047..9d56e1c 100644 --- a/boot/uefi/src/arch/x86_64/pci.rs +++ b/boot/uefi/src/arch/x86_64/pci.rs @@ -10,6 +10,89 @@ const CONFIG_ENABLE: u32 = 1 << 31; const MAX_QUEUED_BUSES: usize = 32; const INTERRUPT_FLAG: u64 = 1 << 9; +/// Serialized access to PCI configuration mechanism #1. +pub(super) struct PciConfigSession { + interrupts_were_enabled: bool, +} + +impl PciConfigSession { + /// Acquires the bootstrap CPU's PCI configuration-port ownership. + /// + /// # Safety + /// + /// The caller must ensure no other CPU accesses `0xCF8`/`0xCFC`. + pub(super) unsafe fn acquire() -> Self { + Self { + // SAFETY: The caller establishes single-CPU configuration access. + interrupts_were_enabled: unsafe { disable_interrupts() }, + } + } + + #[must_use] + pub(super) unsafe fn read_u8(&self, address: PciAddress, offset: u8) -> u8 { + let selector = config_selector(address, offset); + // SAFETY: This session owns the selector/data port pair. + unsafe { + outl(CONFIG_ADDRESS_PORT, selector); + inb(CONFIG_DATA_PORT + u16::from(offset & 0x03)) + } + } + + #[must_use] + pub(super) unsafe fn read_u16(&self, address: PciAddress, offset: u8) -> u16 { + debug_assert!(offset.is_multiple_of(2)); + let selector = config_selector(address, offset); + // SAFETY: This session owns the selector/data port pair. + unsafe { + outl(CONFIG_ADDRESS_PORT, selector); + inw(CONFIG_DATA_PORT + u16::from(offset & 0x02)) + } + } + + #[must_use] + pub(super) unsafe fn read_u32(&self, address: PciAddress, offset: u8) -> u32 { + // SAFETY: This session owns the selector/data port pair. + unsafe { read_config_u32(address, offset) } + } + + pub(super) unsafe fn write_u8(&self, address: PciAddress, offset: u8, value: u8) { + let selector = config_selector(address, offset); + // SAFETY: This session owns the selector/data port pair. + unsafe { + outl(CONFIG_ADDRESS_PORT, selector); + outb(CONFIG_DATA_PORT + u16::from(offset & 0x03), value); + } + } + + pub(super) unsafe fn write_u16(&self, address: PciAddress, offset: u8, value: u16) { + debug_assert!(offset.is_multiple_of(2)); + let selector = config_selector(address, offset); + // SAFETY: This session owns the selector/data port pair. + unsafe { + outl(CONFIG_ADDRESS_PORT, selector); + outw(CONFIG_DATA_PORT + u16::from(offset & 0x02), value); + } + } + + pub(super) unsafe fn write_u32(&self, address: PciAddress, offset: u8, value: u32) { + let selector = config_selector(address, offset); + // SAFETY: This session owns the selector/data port pair. + unsafe { + outl(CONFIG_ADDRESS_PORT, selector); + outl(CONFIG_DATA_PORT, value); + } + } +} + +impl Drop for PciConfigSession { + fn drop(&mut self) { + // SAFETY: Restores exactly the interrupt state captured on acquisition. + unsafe { + restore_interrupts(self.interrupts_were_enabled); + } + } +} + /// Hardware evidence returned by the allocation-free PCI scanner. #[derive(Clone, Copy)] pub struct PciDiscoveryReport { @@ -192,11 +275,7 @@ unsafe fn configuration_mechanism_one_available() -> bool { } unsafe fn read_config_u32(address: PciAddress, offset: u8) -> u32 { - let selector = CONFIG_ENABLE - | (u32::from(address.bus) << 16) - | (u32::from(address.device) << 11) - | (u32::from(address.function) << 8) - | u32::from(offset & 0xfc); + let selector = config_selector(address, offset); // SAFETY: The caller serializes the selector/data port pair. unsafe { outl(CONFIG_ADDRESS_PORT, selector); @@ -204,6 +283,38 @@ unsafe fn read_config_u32(address: PciAddress, offset: u8) -> u32 { } } +fn config_selector(address: PciAddress, offset: u8) -> u32 { + CONFIG_ENABLE + | (u32::from(address.bus) << 16) + | (u32::from(address.device) << 11) + | (u32::from(address.function) << 8) + | u32::from(offset & 0xfc) +} + +unsafe fn outb(port: u16, value: u8) { + // SAFETY: The caller owns the selected x86 I/O port. + unsafe { + asm!( + "out dx, al", + in("dx") port, + in("al") value, + options(nomem, nostack, preserves_flags) + ); + } +} + +unsafe fn outw(port: u16, value: u16) { + // SAFETY: The caller owns the selected x86 I/O port. + unsafe { + asm!( + "out dx, ax", + in("dx") port, + in("ax") value, + options(nomem, nostack, preserves_flags) + ); + } +} + unsafe fn disable_interrupts() -> bool { let flags: u64; // SAFETY: Reading RFLAGS and clearing IF is permitted in Ring 0. @@ -252,3 +363,31 @@ unsafe fn inl(port: u16) -> u32 { } value } + +unsafe fn inb(port: u16) -> u8 { + let value: u8; + // SAFETY: The caller owns the selected x86 I/O port. + unsafe { + asm!( + "in al, dx", + in("dx") port, + out("al") value, + options(nomem, nostack, preserves_flags) + ); + } + value +} + +unsafe fn inw(port: u16) -> u16 { + let value: u16; + // SAFETY: The caller owns the selected x86 I/O port. + unsafe { + asm!( + "in ax, dx", + in("dx") port, + out("ax") value, + options(nomem, nostack, preserves_flags) + ); + } + value +} diff --git a/boot/uefi/src/arch/x86_64/virtio_block.rs b/boot/uefi/src/arch/x86_64/virtio_block.rs new file mode 100644 index 0000000..5a0f7aa --- /dev/null +++ b/boot/uefi/src/arch/x86_64/virtio_block.rs @@ -0,0 +1,1142 @@ +//! Polling virtio-blk transport over modern PCI capabilities. + +use core::cmp; +use core::hint::spin_loop; +use core::ptr; +use core::sync::atomic::{Ordering, fence}; + +use sanju_kernel::block::{ + BlockDevice, BlockError, BlockGeometry, SECTOR_SIZE, validate_sector_range, +}; +use sanju_kernel::memory::{FrameAllocator, PAGE_SIZE, PhysicalFrame}; +use sanju_kernel::paging::VirtualMemoryLayout; +use sanju_kernel::pci::{PciAddress, PciBar, PciInventory, StorageControllerKind, decode_bar}; + +use super::pci::PciConfigSession; + +const PCI_COMMAND_OFFSET: u8 = 0x04; +const PCI_STATUS_OFFSET: u8 = 0x06; +const PCI_BAR_ZERO_OFFSET: u8 = 0x10; +const PCI_CAPABILITY_POINTER_OFFSET: u8 = 0x34; +const PCI_COMMAND_MEMORY_SPACE: u16 = 1 << 1; +const PCI_COMMAND_BUS_MASTER: u16 = 1 << 2; +const PCI_STATUS_CAPABILITY_LIST: u16 = 1 << 4; +const PCI_CAPABILITY_VENDOR_SPECIFIC: u8 = 0x09; +const MAX_CAPABILITIES: usize = 48; + +const VIRTIO_PCI_CAP_COMMON_CFG: u8 = 1; +const VIRTIO_PCI_CAP_NOTIFY_CFG: u8 = 2; +const VIRTIO_PCI_CAP_DEVICE_CFG: u8 = 4; +const VIRTIO_PCI_CAP_PCI_CFG: u8 = 5; + +const COMMON_DEVICE_FEATURE_SELECT: u32 = 0; +const COMMON_DEVICE_FEATURE: u32 = 4; +const COMMON_DRIVER_FEATURE_SELECT: u32 = 8; +const COMMON_DRIVER_FEATURE: u32 = 12; +const COMMON_DEVICE_STATUS: u32 = 20; +const COMMON_CONFIG_GENERATION: u32 = 21; +const COMMON_QUEUE_SELECT: u32 = 22; +const COMMON_QUEUE_SIZE: u32 = 24; +const COMMON_QUEUE_ENABLE: u32 = 28; +const COMMON_QUEUE_NOTIFY_OFFSET: u32 = 30; +const COMMON_QUEUE_DESC_LOW: u32 = 32; +const COMMON_QUEUE_DESC_HIGH: u32 = 36; +const COMMON_QUEUE_DRIVER_LOW: u32 = 40; +const COMMON_QUEUE_DRIVER_HIGH: u32 = 44; +const COMMON_QUEUE_DEVICE_LOW: u32 = 48; +const COMMON_QUEUE_DEVICE_HIGH: u32 = 52; +const MIN_COMMON_CONFIG_BYTES: u32 = 56; + +const DEVICE_STATUS_ACKNOWLEDGE: u8 = 1; +const DEVICE_STATUS_DRIVER: u8 = 2; +const DEVICE_STATUS_DRIVER_OK: u8 = 4; +const DEVICE_STATUS_FEATURES_OK: u8 = 8; +const DEVICE_STATUS_DEVICE_NEEDS_RESET: u8 = 64; +const DEVICE_STATUS_FAILED: u8 = 128; + +const VIRTIO_BLK_F_RO: u32 = 1 << 5; +const VIRTIO_F_VERSION_1_HIGH: u32 = 1; + +const VIRTIO_BLK_T_IN: u32 = 0; +const VIRTIO_BLK_T_OUT: u32 = 1; +const VIRTIO_BLK_T_GET_ID: u32 = 8; +const VIRTIO_BLK_S_OK: u8 = 0; +const VIRTIO_BLK_S_IOERR: u8 = 1; +const VIRTIO_BLK_S_UNSUPP: u8 = 2; + +const VIRTQ_DESC_F_NEXT: u16 = 1; +const VIRTQ_DESC_F_WRITE: u16 = 2; +const VIRTQ_AVAIL_F_NO_INTERRUPT: u16 = 1; +const DRIVER_QUEUE_SIZE: u16 = 8; +const MIN_REQUEST_DESCRIPTORS: u16 = 3; +const POLL_LIMIT: usize = 10_000_000; + +const DESCRIPTOR_AREA_OFFSET: usize = 0x000; +const AVAILABLE_AREA_OFFSET: usize = 0x080; +const USED_AREA_OFFSET: usize = 0x098; +const REQUEST_HEADER_OFFSET: usize = 0x100; +const DATA_BUFFER_OFFSET: usize = 0x200; +const STATUS_BYTE_OFFSET: usize = 0x400; + +const KNOWN_READ_SECTOR: u64 = 8; +const DISPOSABLE_WRITE_SECTOR: u64 = 16; +const EXPECTED_DEVICE_ID: &[u8] = b"SANJU-M6B"; +const EXPECTED_READ_PATTERN: &[u8] = b"SANJUOS-M6B-READ-PATTERN"; + +/// Hardware evidence returned by the M6B transport and request probes. +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VirtioBlockProbeReport { + pub modern_pci_capabilities_active: bool, + pub pci_bars_parsed: usize, + pub pci_bus_master_active: bool, + pub feature_negotiation_active: bool, + pub dma_queue_active: bool, + pub queue_size: u16, + pub capacity_sectors: u64, + pub dedicated_device_identity_verified: bool, + pub known_sector_read_passed: bool, + pub disposable_sector_write_readback_passed: bool, + pub disposable_sector_restored: bool, + pub bounds_check_passed: bool, + pub timeout_protection_active: bool, +} + +/// Failures that prevent the M6B virtio block device from becoming live. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VirtioBlockError { + TargetUnavailable, + AmbiguousTarget, + CapabilityListUnavailable, + MalformedCapabilityList, + MissingCommonConfiguration, + MissingNotificationConfiguration, + MissingDeviceConfiguration, + MissingPciConfigurationWindow, + InvalidCapability, + InvalidBar, + PciCommandRejected, + ResetTimeout, + UnsupportedFeatures, + FeatureNegotiationRejected, + QueueUnavailable, + QueueTooSmall, + DmaUnavailable, + DirectMapUnavailable, + QueueEnableRejected, + DriverRejected, + InvalidCapacity, + Block(BlockError), +} + +impl From for VirtioBlockError { + fn from(value: BlockError) -> Self { + Self::Block(value) + } +} + +#[derive(Clone, Copy)] +struct VirtioRegion { + bar: u8, + offset: u32, + length: u32, +} + +#[derive(Clone, Copy)] +struct VirtioPciTransport { + address: PciAddress, + common: VirtioRegion, + notify: VirtioRegion, + device: VirtioRegion, + pci_config_capability: u8, + notify_multiplier: u32, + parsed_bars: usize, +} + +#[derive(Clone, Copy)] +struct PciConfigWindow { + address: PciAddress, + capability: u8, +} + +impl PciConfigWindow { + unsafe fn read_u8( + self, + region: VirtioRegion, + register_offset: u32, + ) -> Result { + let absolute = validate_region_access(region, register_offset, 1)?; + // SAFETY: The bootstrap CPU is the only PCI configuration client. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: The parsed PCI configuration capability is bounded to the + // standard 256-byte configuration space. + unsafe { + self.select(&session, region.bar, absolute, 1)?; + Ok(session.read_u8(self.address, self.data_offset()?)) + } + } + + unsafe fn read_u16( + self, + region: VirtioRegion, + register_offset: u32, + ) -> Result { + let absolute = validate_region_access(region, register_offset, 2)?; + // SAFETY: The bootstrap CPU is the only PCI configuration client. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: Region validation enforces the required width and alignment. + unsafe { + self.select(&session, region.bar, absolute, 2)?; + Ok(session.read_u16(self.address, self.data_offset()?)) + } + } + + unsafe fn read_u32( + self, + region: VirtioRegion, + register_offset: u32, + ) -> Result { + let absolute = validate_region_access(region, register_offset, 4)?; + // SAFETY: The bootstrap CPU is the only PCI configuration client. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: Region validation enforces the required width and alignment. + unsafe { + self.select(&session, region.bar, absolute, 4)?; + Ok(session.read_u32(self.address, self.data_offset()?)) + } + } + + unsafe fn write_u8( + self, + region: VirtioRegion, + register_offset: u32, + value: u8, + ) -> Result<(), VirtioBlockError> { + let absolute = validate_region_access(region, register_offset, 1)?; + // SAFETY: The bootstrap CPU is the only PCI configuration client. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: Region validation bounds the selected device register. + unsafe { + self.select(&session, region.bar, absolute, 1)?; + session.write_u8(self.address, self.data_offset()?, value); + } + Ok(()) + } + + unsafe fn write_u16( + self, + region: VirtioRegion, + register_offset: u32, + value: u16, + ) -> Result<(), VirtioBlockError> { + let absolute = validate_region_access(region, register_offset, 2)?; + // SAFETY: The bootstrap CPU is the only PCI configuration client. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: Region validation enforces the required width and alignment. + unsafe { + self.select(&session, region.bar, absolute, 2)?; + session.write_u16(self.address, self.data_offset()?, value); + } + Ok(()) + } + + unsafe fn write_u32( + self, + region: VirtioRegion, + register_offset: u32, + value: u32, + ) -> Result<(), VirtioBlockError> { + let absolute = validate_region_access(region, register_offset, 4)?; + // SAFETY: The bootstrap CPU is the only PCI configuration client. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: Region validation enforces the required width and alignment. + unsafe { + self.select(&session, region.bar, absolute, 4)?; + session.write_u32(self.address, self.data_offset()?, value); + } + Ok(()) + } + + unsafe fn select( + self, + session: &PciConfigSession, + bar: u8, + absolute_offset: u32, + width: u32, + ) -> Result<(), VirtioBlockError> { + let bar_offset = self + .capability + .checked_add(4) + .ok_or(VirtioBlockError::InvalidCapability)?; + let region_offset = self + .capability + .checked_add(8) + .ok_or(VirtioBlockError::InvalidCapability)?; + let length_offset = self + .capability + .checked_add(12) + .ok_or(VirtioBlockError::InvalidCapability)?; + // SAFETY: The caller owns this configuration session and each field is + // writable for VIRTIO_PCI_CAP_PCI_CFG. + unsafe { + session.write_u8(self.address, bar_offset, bar); + session.write_u32(self.address, region_offset, absolute_offset); + session.write_u32(self.address, length_offset, width); + } + Ok(()) + } + + fn data_offset(self) -> Result { + self.capability + .checked_add(16) + .ok_or(VirtioBlockError::InvalidCapability) + } +} + +struct DmaQueue { + physical_base: u64, + virtual_base: *mut u8, + queue_size: u16, + available_index: u16, + last_used_index: u16, +} + +impl DmaQueue { + unsafe fn new(frame: PhysicalFrame, queue_size: u16) -> Result { + let virtual_address = VirtualMemoryLayout::sanjuos() + .direct_map_address(frame.start_address()) + .ok_or(VirtioBlockError::DirectMapUnavailable)?; + let virtual_address = + usize::try_from(virtual_address).map_err(|_| VirtioBlockError::DirectMapUnavailable)?; + let virtual_base = virtual_address as *mut u8; + // SAFETY: The allocator exclusively assigned this complete frame to + // the driver and the direct map aliases the same physical bytes. + unsafe { + ptr::write_bytes(virtual_base, 0, usize::try_from(PAGE_SIZE).unwrap_or(4096)); + write_u16( + virtual_base, + AVAILABLE_AREA_OFFSET, + VIRTQ_AVAIL_F_NO_INTERRUPT, + ); + } + Ok(Self { + physical_base: frame.start_address(), + virtual_base, + queue_size, + available_index: 0, + last_used_index: 0, + }) + } + + fn descriptor_physical(&self) -> u64 { + self.physical_base + u64::try_from(DESCRIPTOR_AREA_OFFSET).unwrap_or(0) + } + + fn available_physical(&self) -> u64 { + self.physical_base + u64::try_from(AVAILABLE_AREA_OFFSET).unwrap_or(0) + } + + fn used_physical(&self) -> u64 { + self.physical_base + u64::try_from(USED_AREA_OFFSET).unwrap_or(0) + } + + unsafe fn prepare_request( + &mut self, + request_type: u32, + sector: u64, + data: &[u8; SECTOR_SIZE], + data_length: usize, + device_writes_data: bool, + ) -> Result { + if data_length == 0 || data_length > SECTOR_SIZE { + return Err(BlockError::InvalidBuffer); + } + // SAFETY: All fixed offsets were selected to remain within the + // allocator-owned DMA frame and meet split-ring alignment. + unsafe { + write_u32( + self.virtual_base, + REQUEST_HEADER_OFFSET, + request_type.to_le(), + ); + write_u32(self.virtual_base, REQUEST_HEADER_OFFSET + 4, 0); + write_u64(self.virtual_base, REQUEST_HEADER_OFFSET + 8, sector.to_le()); + for (index, byte) in data.iter().take(data_length).enumerate() { + let value = if device_writes_data { 0 } else { *byte }; + self.virtual_base + .add(DATA_BUFFER_OFFSET + index) + .write_volatile(value); + } + self.virtual_base + .add(STATUS_BYTE_OFFSET) + .write_volatile(u8::MAX); + + self.write_descriptor( + 0, + self.physical_base + u64::try_from(REQUEST_HEADER_OFFSET).unwrap_or(0), + 16, + VIRTQ_DESC_F_NEXT, + 1, + ); + self.write_descriptor( + 1, + self.physical_base + u64::try_from(DATA_BUFFER_OFFSET).unwrap_or(0), + u32::try_from(data_length).map_err(|_| BlockError::InvalidBuffer)?, + VIRTQ_DESC_F_NEXT + | if device_writes_data { + VIRTQ_DESC_F_WRITE + } else { + 0 + }, + 2, + ); + self.write_descriptor( + 2, + self.physical_base + u64::try_from(STATUS_BYTE_OFFSET).unwrap_or(0), + 1, + VIRTQ_DESC_F_WRITE, + 0, + ); + + let slot = self.available_index % self.queue_size; + write_u16( + self.virtual_base, + AVAILABLE_AREA_OFFSET + 4 + usize::from(slot) * 2, + 0, + ); + fence(Ordering::SeqCst); + self.available_index = self.available_index.wrapping_add(1); + write_u16( + self.virtual_base, + AVAILABLE_AREA_OFFSET + 2, + self.available_index, + ); + fence(Ordering::SeqCst); + } + Ok(self.last_used_index.wrapping_add(1)) + } + + unsafe fn poll_completion( + &mut self, + expected_used_index: u16, + status_reader: impl Fn() -> Result, + ) -> Result<(), BlockError> { + for iteration in 0..POLL_LIMIT { + // SAFETY: The used index is device-owned coherent DMA memory. + let used_index = unsafe { read_u16(self.virtual_base, USED_AREA_OFFSET + 2) }; + if used_index == expected_used_index { + fence(Ordering::SeqCst); + let slot = self.last_used_index % self.queue_size; + // SAFETY: The device completed this used-ring entry before + // advancing the index observed above. + let descriptor_id = unsafe { + read_u32( + self.virtual_base, + USED_AREA_OFFSET + 4 + usize::from(slot) * 8, + ) + }; + if descriptor_id != 0 { + return Err(BlockError::Transport); + } + self.last_used_index = used_index; + return Ok(()); + } + if used_index != self.last_used_index { + return Err(BlockError::Transport); + } + if iteration.is_multiple_of(4096) + && status_reader()? & DEVICE_STATUS_DEVICE_NEEDS_RESET != 0 + { + return Err(BlockError::DeviceNeedsReset); + } + spin_loop(); + } + Err(BlockError::Timeout) + } + + unsafe fn read_status(&self) -> u8 { + // SAFETY: The status byte is inside the allocator-owned DMA frame. + unsafe { self.virtual_base.add(STATUS_BYTE_OFFSET).read_volatile() } + } + + unsafe fn copy_device_data(&self, destination: &mut [u8], data_length: usize) { + for (index, byte) in destination.iter_mut().take(data_length).enumerate() { + // SAFETY: `data_length` was bounded to the fixed DMA data buffer. + *byte = unsafe { + self.virtual_base + .add(DATA_BUFFER_OFFSET + index) + .read_volatile() + }; + } + } + + unsafe fn write_descriptor( + &self, + index: usize, + address: u64, + length: u32, + flags: u16, + next: u16, + ) { + let descriptor = DESCRIPTOR_AREA_OFFSET + index * 16; + // SAFETY: The queue size is at most the eight descriptor slots + // reserved in this DMA frame. + unsafe { + write_u64(self.virtual_base, descriptor, address.to_le()); + write_u32(self.virtual_base, descriptor + 8, length.to_le()); + write_u16(self.virtual_base, descriptor + 12, flags.to_le()); + write_u16(self.virtual_base, descriptor + 14, next.to_le()); + } + } +} + +/// One live, synchronous virtio block device. +pub struct VirtioBlockDevice { + window: PciConfigWindow, + common: VirtioRegion, + notify: VirtioRegion, + geometry: BlockGeometry, + queue: DmaQueue, +} + +impl VirtioBlockDevice { + fn submit( + &mut self, + request_type: u32, + sector: u64, + buffer: &mut [u8; SECTOR_SIZE], + data_length: usize, + device_writes_data: bool, + ) -> Result<(), BlockError> { + // SAFETY: This device permits exactly one outstanding request and the + // DMA queue remains allocated for its entire lifetime. + let expected = unsafe { + self.queue.prepare_request( + request_type, + sector, + buffer, + data_length, + device_writes_data, + )? + }; + // SAFETY: The notify region and queue index were validated during + // initialization, and DRIVER_OK is set before any notification. + unsafe { + self.window + .write_u16(self.notify, 0, 0) + .map_err(|_| BlockError::Transport)?; + self.queue.poll_completion(expected, || { + self.window + .read_u8(self.common, COMMON_DEVICE_STATUS) + .map_err(|_| BlockError::Transport) + })?; + } + fence(Ordering::SeqCst); + // SAFETY: Completion transfers ownership of device-writable buffers + // back to the driver. + let status = unsafe { self.queue.read_status() }; + match status { + VIRTIO_BLK_S_OK => {} + VIRTIO_BLK_S_IOERR => return Err(BlockError::Io), + VIRTIO_BLK_S_UNSUPP => return Err(BlockError::UnsupportedRequest), + _ => return Err(BlockError::Transport), + } + if device_writes_data { + // SAFETY: The status byte completed after the data descriptors. + unsafe { + self.queue + .copy_device_data(&mut buffer[..data_length], data_length); + } + } + Ok(()) + } + + fn read_device_id(&mut self) -> Result<[u8; 20], BlockError> { + let mut sector_buffer = [0_u8; SECTOR_SIZE]; + self.submit(VIRTIO_BLK_T_GET_ID, 0, &mut sector_buffer, 20, true)?; + let mut device_id = [0_u8; 20]; + device_id.copy_from_slice(§or_buffer[..20]); + Ok(device_id) + } + + /// Executes the bounded M6B hardware acceptance probes. + /// + /// # Errors + /// + /// Returns a transport or block error if device identity, known-sector + /// read, disposable write/readback, or restoration cannot be completed. + fn run_acceptance_probe( + &mut self, + transport: VirtioPciTransport, + ) -> Result { + let device_id = self.read_device_id()?; + let identity_verified = device_id.starts_with(EXPECTED_DEVICE_ID); + if !identity_verified { + return Err(VirtioBlockError::Block(BlockError::NotReady)); + } + + let mut known_sector = [0_u8; SECTOR_SIZE]; + self.read_sector(KNOWN_READ_SECTOR, &mut known_sector)?; + let known_sector_read_passed = known_sector.starts_with(EXPECTED_READ_PATTERN); + if !known_sector_read_passed { + return Err(VirtioBlockError::Block(BlockError::Io)); + } + + let mut original = [0_u8; SECTOR_SIZE]; + self.read_sector(DISPOSABLE_WRITE_SECTOR, &mut original)?; + let mut probe = [0_u8; SECTOR_SIZE]; + for (index, byte) in probe.iter_mut().enumerate() { + *byte = u8::try_from(index & 0xff).unwrap_or(0).wrapping_mul(37) ^ 0xa5; + } + probe[..EXPECTED_DEVICE_ID.len()].copy_from_slice(EXPECTED_DEVICE_ID); + + self.write_sector(DISPOSABLE_WRITE_SECTOR, &probe)?; + let mut readback = [0_u8; SECTOR_SIZE]; + let readback_result = self.read_sector(DISPOSABLE_WRITE_SECTOR, &mut readback); + let write_readback_passed = readback_result.is_ok() && readback == probe; + + let restore_result = self.write_sector(DISPOSABLE_WRITE_SECTOR, &original); + let mut restored = [0_u8; SECTOR_SIZE]; + let restored_result = self.read_sector(DISPOSABLE_WRITE_SECTOR, &mut restored); + let disposable_sector_restored = + restore_result.is_ok() && restored_result.is_ok() && restored == original; + if !write_readback_passed || !disposable_sector_restored { + return Err(VirtioBlockError::Block(BlockError::Io)); + } + + let mut bounds_probe = [0_u8; SECTOR_SIZE]; + let bounds_check_passed = self.read_sector(self.geometry.sectors, &mut bounds_probe) + == Err(BlockError::OutOfBounds); + if !bounds_check_passed { + return Err(VirtioBlockError::Block(BlockError::OutOfBounds)); + } + + Ok(VirtioBlockProbeReport { + modern_pci_capabilities_active: true, + pci_bars_parsed: transport.parsed_bars, + pci_bus_master_active: true, + feature_negotiation_active: true, + dma_queue_active: true, + queue_size: self.queue.queue_size, + capacity_sectors: self.geometry.sectors, + dedicated_device_identity_verified: identity_verified, + known_sector_read_passed, + disposable_sector_write_readback_passed: write_readback_passed, + disposable_sector_restored, + bounds_check_passed, + timeout_protection_active: POLL_LIMIT > 0, + }) + } +} + +impl BlockDevice for VirtioBlockDevice { + fn geometry(&self) -> BlockGeometry { + self.geometry + } + + fn read_sector( + &mut self, + sector: u64, + destination: &mut [u8; SECTOR_SIZE], + ) -> Result<(), BlockError> { + validate_sector_range(self.geometry, sector, 1)?; + self.submit(VIRTIO_BLK_T_IN, sector, destination, SECTOR_SIZE, true) + } + + fn write_sector(&mut self, sector: u64, source: &[u8; SECTOR_SIZE]) -> Result<(), BlockError> { + if self.geometry.read_only { + return Err(BlockError::ReadOnly); + } + validate_sector_range(self.geometry, sector, 1)?; + let mut request_buffer = *source; + self.submit( + VIRTIO_BLK_T_OUT, + sector, + &mut request_buffer, + SECTOR_SIZE, + false, + ) + } +} + +/// Initializes the first and only discovered virtio block function. +/// +/// # Safety +/// +/// The caller must own PCI configuration mechanism #1, the active physical +/// direct map, and the supplied frame allocator. No IOMMU translation may be +/// active between the device and guest physical memory. +unsafe fn initialize_virtio_block( + inventory: &PciInventory, + frame_allocator: &mut FrameAllocator<'_>, +) -> Result<(VirtioBlockDevice, VirtioPciTransport), VirtioBlockError> { + // SAFETY: The caller owns PCI configuration access for this bootstrap CPU. + let transport = unsafe { parse_transport(inventory)? }; + // SAFETY: The parsed function is the dedicated virtio block target. + unsafe { + enable_bus_master(transport.address)?; + } + let window = PciConfigWindow { + address: transport.address, + capability: transport.pci_config_capability, + }; + + // SAFETY: All common-register accesses use the validated PCI config window. + unsafe { + window.write_u8(transport.common, COMMON_DEVICE_STATUS, 0)?; + } + let mut reset_complete = false; + for _ in 0..POLL_LIMIT { + // SAFETY: The common status field is one aligned byte. + if unsafe { window.read_u8(transport.common, COMMON_DEVICE_STATUS)? } == 0 { + reset_complete = true; + break; + } + spin_loop(); + } + if !reset_complete { + return Err(VirtioBlockError::ResetTimeout); + } + + // SAFETY: Status bits are added in the order required by the specification. + unsafe { + window.write_u8( + transport.common, + COMMON_DEVICE_STATUS, + DEVICE_STATUS_ACKNOWLEDGE | DEVICE_STATUS_DRIVER, + )?; + window.write_u32(transport.common, COMMON_DEVICE_FEATURE_SELECT, 0)?; + } + // SAFETY: Feature selection above chooses bits 0 through 31. + let device_features_low = unsafe { window.read_u32(transport.common, COMMON_DEVICE_FEATURE)? }; + // SAFETY: Select and read feature bits 32 through 63. + unsafe { + window.write_u32(transport.common, COMMON_DEVICE_FEATURE_SELECT, 1)?; + } + // SAFETY: Feature selection above chooses bits 32 through 63. + let device_features_high = unsafe { window.read_u32(transport.common, COMMON_DEVICE_FEATURE)? }; + if device_features_high & VIRTIO_F_VERSION_1_HIGH == 0 + || device_features_low & VIRTIO_BLK_F_RO != 0 + { + // SAFETY: Mark this recognized device failed before giving up. + let _ = unsafe { + window.write_u8( + transport.common, + COMMON_DEVICE_STATUS, + DEVICE_STATUS_ACKNOWLEDGE | DEVICE_STATUS_DRIVER | DEVICE_STATUS_FAILED, + ) + }; + return Err(VirtioBlockError::UnsupportedFeatures); + } + + // Negotiate only VIRTIO_F_VERSION_1. Split rings, polling, one queue, and + // 512-byte sectors require no optional feature bits. + // SAFETY: Driver feature selectors and values are aligned 32-bit fields. + unsafe { + window.write_u32(transport.common, COMMON_DRIVER_FEATURE_SELECT, 0)?; + window.write_u32(transport.common, COMMON_DRIVER_FEATURE, 0)?; + window.write_u32(transport.common, COMMON_DRIVER_FEATURE_SELECT, 1)?; + window.write_u32( + transport.common, + COMMON_DRIVER_FEATURE, + VIRTIO_F_VERSION_1_HIGH, + )?; + window.write_u8( + transport.common, + COMMON_DEVICE_STATUS, + DEVICE_STATUS_ACKNOWLEDGE | DEVICE_STATUS_DRIVER | DEVICE_STATUS_FEATURES_OK, + )?; + } + // SAFETY: Re-read status to verify the device accepted the feature subset. + let features_status = unsafe { window.read_u8(transport.common, COMMON_DEVICE_STATUS)? }; + if features_status & DEVICE_STATUS_FEATURES_OK == 0 { + return Err(VirtioBlockError::FeatureNegotiationRejected); + } + + // SAFETY: The validated device configuration contains the mandatory + // capacity field, and the common configuration provides its generation. + let capacity = unsafe { read_capacity(window, transport)? }; + if capacity <= DISPOSABLE_WRITE_SECTOR { + return Err(VirtioBlockError::InvalidCapacity); + } + + // SAFETY: Queue zero is the virtio-blk request queue. + unsafe { + window.write_u16(transport.common, COMMON_QUEUE_SELECT, 0)?; + } + // SAFETY: Queue selection above chooses request queue zero. + let maximum_queue_size = unsafe { window.read_u16(transport.common, COMMON_QUEUE_SIZE)? }; + if maximum_queue_size == 0 { + return Err(VirtioBlockError::QueueUnavailable); + } + let queue_size = cmp::min(maximum_queue_size, DRIVER_QUEUE_SIZE); + if queue_size < MIN_REQUEST_DESCRIPTORS || !queue_size.is_power_of_two() { + return Err(VirtioBlockError::QueueTooSmall); + } + // SAFETY: Queue-enable is an aligned field in the validated common region. + let queue_enable = unsafe { window.read_u16(transport.common, COMMON_QUEUE_ENABLE)? }; + if queue_enable != 0 { + return Err(VirtioBlockError::QueueEnableRejected); + } + // SAFETY: Queue-notify offset is an aligned field in the validated common + // region for the selected queue. + let queue_notify_offset = + unsafe { window.read_u16(transport.common, COMMON_QUEUE_NOTIFY_OFFSET)? }; + let notify_delta = u32::from(queue_notify_offset) + .checked_mul(transport.notify_multiplier) + .ok_or(VirtioBlockError::InvalidCapability)?; + validate_region_access(transport.notify, notify_delta, 2)?; + let notify = VirtioRegion { + bar: transport.notify.bar, + offset: transport + .notify + .offset + .checked_add(notify_delta) + .ok_or(VirtioBlockError::InvalidCapability)?, + length: 2, + }; + + let dma_frame = frame_allocator + .allocate_contiguous(1, 1) + .ok_or(VirtioBlockError::DmaUnavailable)? + .start; + // SAFETY: The allocated frame is exclusively owned and direct-mapped. + let queue = unsafe { DmaQueue::new(dma_frame, queue_size)? }; + + // SAFETY: Queue fields are programmed before queue_enable as required. + unsafe { + window.write_u16(transport.common, COMMON_QUEUE_SIZE, queue_size)?; + write_common_u64( + window, + transport.common, + COMMON_QUEUE_DESC_LOW, + COMMON_QUEUE_DESC_HIGH, + queue.descriptor_physical(), + )?; + write_common_u64( + window, + transport.common, + COMMON_QUEUE_DRIVER_LOW, + COMMON_QUEUE_DRIVER_HIGH, + queue.available_physical(), + )?; + write_common_u64( + window, + transport.common, + COMMON_QUEUE_DEVICE_LOW, + COMMON_QUEUE_DEVICE_HIGH, + queue.used_physical(), + )?; + window.write_u16(transport.common, COMMON_QUEUE_ENABLE, 1)?; + } + // SAFETY: Read back the aligned queue-enable field just programmed. + if unsafe { window.read_u16(transport.common, COMMON_QUEUE_ENABLE)? } != 1 { + return Err(VirtioBlockError::QueueEnableRejected); + } + + let live_status = DEVICE_STATUS_ACKNOWLEDGE + | DEVICE_STATUS_DRIVER + | DEVICE_STATUS_FEATURES_OK + | DEVICE_STATUS_DRIVER_OK; + // SAFETY: Queue configuration is complete before DRIVER_OK. + unsafe { + window.write_u8(transport.common, COMMON_DEVICE_STATUS, live_status)?; + } + // SAFETY: Read back the device-status byte just programmed. + if unsafe { window.read_u8(transport.common, COMMON_DEVICE_STATUS)? } & live_status + != live_status + { + return Err(VirtioBlockError::DriverRejected); + } + + Ok(( + VirtioBlockDevice { + window, + common: transport.common, + notify, + geometry: BlockGeometry::new(capacity, false), + queue, + }, + transport, + )) +} + +/// Initializes the dedicated block target and executes the complete M6B gate. +/// +/// # Safety +/// +/// The caller must satisfy [`initialize_virtio_block`]'s PCI, direct-map, +/// allocator, and no-IOMMU ownership requirements. +pub unsafe fn initialize_and_probe( + inventory: &PciInventory, + frame_allocator: &mut FrameAllocator<'_>, +) -> Result<(VirtioBlockDevice, VirtioBlockProbeReport), VirtioBlockError> { + // SAFETY: The caller forwards the complete hardware ownership contract. + let (mut device, transport) = unsafe { initialize_virtio_block(inventory, frame_allocator)? }; + let report = device.run_acceptance_probe(transport)?; + Ok((device, report)) +} + +unsafe fn parse_transport( + inventory: &PciInventory, +) -> Result { + let mut targets = inventory + .devices() + .iter() + .filter(|device| device.storage_kind() == Some(StorageControllerKind::VirtioBlock)); + let target = *targets.next().ok_or(VirtioBlockError::TargetUnavailable)?; + if targets.next().is_some() { + return Err(VirtioBlockError::AmbiguousTarget); + } + + // SAFETY: The caller owns PCI configuration mechanism #1. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: Standard type-zero configuration header access. + let status = unsafe { session.read_u16(target.address, PCI_STATUS_OFFSET) }; + if status & PCI_STATUS_CAPABILITY_LIST == 0 { + return Err(VirtioBlockError::CapabilityListUnavailable); + } + + // Parse every assigned BAR once without destructive sizing writes. + let mut bars: [Option; 6] = [None; 6]; + let mut bar_index = 0_u8; + let mut parsed_bars = 0_usize; + while bar_index < 6 { + let offset = PCI_BAR_ZERO_OFFSET + bar_index * 4; + // SAFETY: BAR offsets are aligned standard-header dwords. + let low = unsafe { session.read_u32(target.address, offset) }; + if low == 0 { + bar_index += 1; + continue; + } + let is_memory_64 = low & 1 == 0 && ((low >> 1) & 0x03) == 2; + let upper = if is_memory_64 && bar_index < 5 { + // SAFETY: The upper half immediately follows a 64-bit BAR. + Some(unsafe { session.read_u32(target.address, offset + 4) }) + } else { + None + }; + let (bar, consumes_upper) = + decode_bar(bar_index, low, upper).map_err(|_| VirtioBlockError::InvalidBar)?; + bars[usize::from(bar_index)] = Some(bar); + parsed_bars = parsed_bars.saturating_add(1); + bar_index += if consumes_upper { 2 } else { 1 }; + } + + // SAFETY: Capability pointer is a standard one-byte header field. + let mut capability = + unsafe { session.read_u8(target.address, PCI_CAPABILITY_POINTER_OFFSET) } & 0xfc; + let mut visited = [false; 256]; + let mut common = None; + let mut notify = None; + let mut device = None; + let mut pci_config_capability = None; + let mut notify_multiplier = 0_u32; + let mut capability_count = 0_usize; + + while capability != 0 { + if !(0x40..=0xec).contains(&capability) + || !capability.is_multiple_of(4) + || visited[usize::from(capability)] + || capability_count == MAX_CAPABILITIES + { + return Err(VirtioBlockError::MalformedCapabilityList); + } + visited[usize::from(capability)] = true; + capability_count += 1; + // SAFETY: Capability traversal bounds this header within 256 bytes. + let capability_id = unsafe { session.read_u8(target.address, capability) }; + // SAFETY: The next pointer immediately follows the capability ID. + let next = unsafe { session.read_u8(target.address, capability + 1) } & 0xfc; + + if capability_id == PCI_CAPABILITY_VENDOR_SPECIFIC { + // SAFETY: Vendor capability prefix bytes are inside the bounded + // standard configuration-space capability. + let capability_length = unsafe { session.read_u8(target.address, capability + 2) }; + // SAFETY: Configuration type immediately follows the bounded + // capability length byte. + let configuration_type = unsafe { session.read_u8(target.address, capability + 3) }; + if capability_length < 16 { + return Err(VirtioBlockError::InvalidCapability); + } + if configuration_type == VIRTIO_PCI_CAP_PCI_CFG { + if capability_length < 20 || pci_config_capability.is_some() { + return Err(VirtioBlockError::InvalidCapability); + } + pci_config_capability = Some(capability); + } else if matches!( + configuration_type, + VIRTIO_PCI_CAP_COMMON_CFG | VIRTIO_PCI_CAP_NOTIFY_CFG | VIRTIO_PCI_CAP_DEVICE_CFG + ) { + // SAFETY: BAR index is within the validated 16-byte prefix. + let bar = unsafe { session.read_u8(target.address, capability + 4) }; + if bar >= 6 || bars[usize::from(bar)].is_none() { + return Err(VirtioBlockError::InvalidBar); + } + // SAFETY: Region offset is the aligned field at bytes 8..12. + let offset = unsafe { session.read_u32(target.address, capability + 8) }; + // SAFETY: Region length is the aligned field at bytes 12..16. + let length = unsafe { session.read_u32(target.address, capability + 12) }; + if length == 0 || offset.checked_add(length).is_none() { + return Err(VirtioBlockError::InvalidCapability); + } + let region = VirtioRegion { + bar, + offset, + length, + }; + match configuration_type { + VIRTIO_PCI_CAP_COMMON_CFG => { + if common.replace(region).is_some() { + return Err(VirtioBlockError::InvalidCapability); + } + } + VIRTIO_PCI_CAP_NOTIFY_CFG => { + if notify.is_some() { + return Err(VirtioBlockError::InvalidCapability); + } + if capability_length < 20 { + return Err(VirtioBlockError::InvalidCapability); + } + // SAFETY: A 20-byte notify capability contains the + // aligned multiplier at bytes 16..20. + notify_multiplier = + unsafe { session.read_u32(target.address, capability + 16) }; + notify = Some(region); + } + VIRTIO_PCI_CAP_DEVICE_CFG if device.replace(region).is_some() => { + return Err(VirtioBlockError::InvalidCapability); + } + _ => {} + } + } + } + capability = next; + } + + let common = common.ok_or(VirtioBlockError::MissingCommonConfiguration)?; + let notify = notify.ok_or(VirtioBlockError::MissingNotificationConfiguration)?; + let device = device.ok_or(VirtioBlockError::MissingDeviceConfiguration)?; + if common.length < MIN_COMMON_CONFIG_BYTES || notify.length < 2 || device.length < 8 { + return Err(VirtioBlockError::InvalidCapability); + } + Ok(VirtioPciTransport { + address: target.address, + common, + notify, + device, + pci_config_capability: pci_config_capability + .ok_or(VirtioBlockError::MissingPciConfigurationWindow)?, + notify_multiplier, + parsed_bars, + }) +} + +unsafe fn enable_bus_master(address: PciAddress) -> Result<(), VirtioBlockError> { + // SAFETY: The caller owns PCI configuration mechanism #1. + let session = unsafe { PciConfigSession::acquire() }; + // SAFETY: PCI command is an aligned standard-header word. + let command = unsafe { session.read_u16(address, PCI_COMMAND_OFFSET) }; + let required = PCI_COMMAND_MEMORY_SPACE | PCI_COMMAND_BUS_MASTER; + // SAFETY: Preserve firmware-selected command bits and enable only memory + // decoding plus bus mastering. + unsafe { + session.write_u16(address, PCI_COMMAND_OFFSET, command | required); + } + // SAFETY: Read back the same command word to prove acceptance. + if unsafe { session.read_u16(address, PCI_COMMAND_OFFSET) } & required != required { + return Err(VirtioBlockError::PciCommandRejected); + } + Ok(()) +} + +unsafe fn read_capacity( + window: PciConfigWindow, + transport: VirtioPciTransport, +) -> Result { + for _ in 0..256 { + // SAFETY: Generation is a one-byte common configuration field. + let before = unsafe { window.read_u8(transport.common, COMMON_CONFIG_GENERATION)? }; + // SAFETY: Capacity is always present as two aligned 32-bit halves. + let low = unsafe { window.read_u32(transport.device, 0)? }; + // SAFETY: This is the aligned upper half of the mandatory capacity. + let high = unsafe { window.read_u32(transport.device, 4)? }; + // SAFETY: Re-read generation to verify an atomic configuration view. + let after = unsafe { window.read_u8(transport.common, COMMON_CONFIG_GENERATION)? }; + if before == after { + return Ok((u64::from(high) << 32) | u64::from(low)); + } + } + Err(VirtioBlockError::InvalidCapacity) +} + +unsafe fn write_common_u64( + window: PciConfigWindow, + common: VirtioRegion, + low_offset: u32, + high_offset: u32, + value: u64, +) -> Result<(), VirtioBlockError> { + // SAFETY: Virtio PCI permits independent aligned access to both 32-bit + // halves of a 64-bit common-configuration field. + unsafe { + window.write_u32(common, low_offset, value as u32)?; + window.write_u32(common, high_offset, (value >> 32) as u32)?; + } + Ok(()) +} + +fn validate_region_access( + region: VirtioRegion, + register_offset: u32, + width: u32, +) -> Result { + if !matches!(width, 1 | 2 | 4) || !register_offset.is_multiple_of(width) { + return Err(VirtioBlockError::InvalidCapability); + } + let end = register_offset + .checked_add(width) + .ok_or(VirtioBlockError::InvalidCapability)?; + if end > region.length { + return Err(VirtioBlockError::InvalidCapability); + } + let absolute = region + .offset + .checked_add(register_offset) + .ok_or(VirtioBlockError::InvalidCapability)?; + if !absolute.is_multiple_of(width) { + return Err(VirtioBlockError::InvalidCapability); + } + Ok(absolute) +} + +unsafe fn write_u16(base: *mut u8, offset: usize, value: u16) { + // SAFETY: Callers provide an aligned, in-frame offset. + unsafe { + base.add(offset).cast::().write_volatile(value); + } +} + +unsafe fn write_u32(base: *mut u8, offset: usize, value: u32) { + // SAFETY: Callers provide an aligned, in-frame offset. + unsafe { + base.add(offset).cast::().write_volatile(value); + } +} + +unsafe fn write_u64(base: *mut u8, offset: usize, value: u64) { + // SAFETY: Callers provide an aligned, in-frame offset. + unsafe { + base.add(offset).cast::().write_volatile(value); + } +} + +unsafe fn read_u16(base: *mut u8, offset: usize) -> u16 { + // SAFETY: Callers provide an aligned, in-frame offset. + unsafe { base.add(offset).cast::().read_volatile() } +} + +unsafe fn read_u32(base: *mut u8, offset: usize) -> u32 { + // SAFETY: Callers provide an aligned, in-frame offset. + unsafe { base.add(offset).cast::().read_volatile() } +} diff --git a/boot/uefi/src/main.rs b/boot/uefi/src/main.rs index 074af45..176dbac 100644 --- a/boot/uefi/src/main.rs +++ b/boot/uefi/src/main.rs @@ -31,9 +31,9 @@ use sanju_kernel::shell::{Shell, ShellEnvironment}; use sanju_kernel::startup::{self, StartupStage}; use sanju_kernel::{ BootInfo, Console, FoundationHardeningPhase2Report, FoundationHardeningPhase3Report, - FoundationHardeningReport, M5Report, M6aReport, MemoryMapInfo, + FoundationHardeningReport, M5Report, M6aReport, M6bReport, MemoryMapInfo, kernel_main_foundation_hardening, kernel_main_foundation_hardening_phase2, - kernel_main_foundation_hardening_phase3, kernel_main_m5, kernel_main_m6a, + kernel_main_foundation_hardening_phase3, kernel_main_m5, kernel_main_m6a, kernel_main_m6b, }; type EfiHandle = *mut c_void; @@ -1285,6 +1285,10 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { pci_functions, storage_controllers, virtio_block_targets, + block_capacity_sectors: 0, + block_queue_size: 0, + block_read_test_passed: false, + block_write_test_passed: false, }; for byte in b"version\nuserspace\n" { shell.feed_byte(*byte, &mut null_console, &mut ramfs, &self_test_environment); @@ -1351,7 +1355,7 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { let foundation_report = FoundationHardeningReport { toolchain_pinned: true, capability_registry_synchronized: sanju_kernel::generated::capabilities::REGISTRY_VERSION - == 4, + == 5, architecture_separation_verified: true, boot_info_version: boot_info.version, ownership_map_active: !ownership_map.is_empty(), @@ -1462,6 +1466,46 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { ); } + // SAFETY: M6A identified exactly one dedicated virtio-blk PCI target. + // SanjuOS owns PCI configuration mechanism #1, the physical direct map, + // and the frame allocator; QEMU exposes no guest IOMMU for this machine. + let (_block_device, block_probe) = match unsafe { + cpu::initialize_virtio_block(&pci_discovery.inventory, &mut frame_allocator) + } { + Ok(result) => result, + Err(_) => boot_failure( + &mut console, + "M6B-BLK-001", + "virtio block initialization or acceptance probe failed", + ), + }; + let m6b_report = M6bReport { + block_device_api_active: true, + modern_pci_capabilities_active: block_probe.modern_pci_capabilities_active, + pci_bars_parsed: block_probe.pci_bars_parsed, + pci_bus_master_active: block_probe.pci_bus_master_active, + feature_negotiation_active: block_probe.feature_negotiation_active, + dma_queue_active: block_probe.dma_queue_active, + queue_size: block_probe.queue_size, + capacity_sectors: block_probe.capacity_sectors, + dedicated_device_identity_verified: block_probe.dedicated_device_identity_verified, + known_sector_read_passed: block_probe.known_sector_read_passed, + disposable_sector_write_readback_passed: block_probe + .disposable_sector_write_readback_passed, + disposable_sector_restored: block_probe.disposable_sector_restored, + bounds_check_passed: block_probe.bounds_check_passed, + timeout_protection_active: block_probe.timeout_protection_active, + m6a_regression_passed: m6a_report.gate_passed(), + }; + kernel_main_m6b(&mut console, m6b_report); + if !m6b_report.gate_passed() { + boot_failure( + &mut console, + "M6B-GATE-001", + "virtio block transport acceptance gate failed", + ); + } + startup::print_stage(&mut console, StartupStage::Shell, true); Shell::start(&mut console); @@ -1480,8 +1524,12 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { pci_functions, storage_controllers, virtio_block_targets, + block_capacity_sectors: m6b_report.capacity_sectors, + block_queue_size: usize::from(m6b_report.queue_size), + block_read_test_passed: m6b_report.known_sector_read_passed, + block_write_test_passed: m6b_report.disposable_sector_write_readback_passed, }; - let smoke_commands = b"help\nuserspace\npci\nls\ncat welcome.txt\ntasks\nuptime\n"; + let smoke_commands = b"help\nuserspace\npci\nblock\nls\ncat welcome.txt\ntasks\nuptime\n"; for byte in smoke_commands { shell.feed_byte(*byte, &mut console, &mut ramfs, &environment); } @@ -1515,6 +1563,10 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { pci_functions, storage_controllers, virtio_block_targets, + block_capacity_sectors: m6b_report.capacity_sectors, + block_queue_size: usize::from(m6b_report.queue_size), + block_read_test_passed: m6b_report.known_sector_read_passed, + block_write_test_passed: m6b_report.disposable_sector_write_readback_passed, }; shell.feed_byte(byte, &mut console, &mut ramfs, &environment); } diff --git a/capabilities/capabilities.toml b/capabilities/capabilities.toml index 99a9baf..58ce172 100644 --- a/capabilities/capabilities.toml +++ b/capabilities/capabilities.toml @@ -1,4 +1,4 @@ -registry_version = 4 +registry_version = 5 [[capability]] id = "SYS-TC-001" @@ -214,10 +214,20 @@ required_boot_line = "Virtio block PCI target: active" [[capability]] id = "STOR-BLK-001" name = "Architecture-independent block-device API" -status = "planned" +status = "hardware_active" milestone = "m6b" -evidence = ["docs/process/SPRINT_M6_STORAGE_FOUNDATION.md"] +evidence = ["kernel/src/block.rs", "boot/uefi/src/arch/x86_64/virtio_block.rs", "scripts/smoke-test.sh"] boot_label = "Block-device API" +required_boot_line = "Architecture-independent block-device API: active" + +[[capability]] +id = "STOR-VIRTIO-001" +name = "Polling modern virtio PCI block transport" +status = "hardware_active" +milestone = "m6b" +evidence = ["boot/uefi/src/arch/x86_64/virtio_block.rs", "scripts/smoke-test.sh"] +boot_label = "Virtio block transport" +required_boot_line = "M6B block transport gate: passed" [[capability]] id = "VFS-CORE-001" diff --git a/capabilities/smoke-expectations.txt b/capabilities/smoke-expectations.txt index 02a4430..a3e0464 100644 --- a/capabilities/smoke-expectations.txt +++ b/capabilities/smoke-expectations.txt @@ -18,3 +18,5 @@ User and Ring 0 guard holes: active Process page-table reclamation: passed PCI configuration mechanism #1: active Virtio block PCI target: active +Architecture-independent block-device API: active +M6B block transport gate: passed diff --git a/docs/CAPABILITY_MATRIX.md b/docs/CAPABILITY_MATRIX.md index e044b91..bbfe224 100644 --- a/docs/CAPABILITY_MATRIX.md +++ b/docs/CAPABILITY_MATRIX.md @@ -1,6 +1,6 @@ # SanjuOS Capability Matrix -Registry version: **4** +Registry version: **5** This file is generated from `capabilities/capabilities.toml`. Do not edit it manually. @@ -30,6 +30,7 @@ This file is generated from `capabilities/capabilities.toml`. Do not edit it man | `GFX-BOOT-001` | Graphical framebuffer startup splash | `planned` | `m6` | `assets/branding/sanjuos-logo.png` | | `PCI-ENUM-001` | PCI configuration mechanism #1 enumeration | `hardware_active` | `m6a` | `kernel/src/pci.rs`
`boot/uefi/src/arch/x86_64/pci.rs`
`scripts/smoke-test.sh` | | `STOR-DISC-001` | Virtio block PCI target discovery | `hardware_active` | `m6a` | `kernel/src/pci.rs`
`boot/uefi/src/arch/x86_64/pci.rs`
`scripts/smoke-test.sh` | -| `STOR-BLK-001` | Architecture-independent block-device API | `planned` | `m6b` | `docs/process/SPRINT_M6_STORAGE_FOUNDATION.md` | +| `STOR-BLK-001` | Architecture-independent block-device API | `hardware_active` | `m6b` | `kernel/src/block.rs`
`boot/uefi/src/arch/x86_64/virtio_block.rs`
`scripts/smoke-test.sh` | +| `STOR-VIRTIO-001` | Polling modern virtio PCI block transport | `hardware_active` | `m6b` | `boot/uefi/src/arch/x86_64/virtio_block.rs`
`scripts/smoke-test.sh` | | `VFS-CORE-001` | VFS inode, mount, path, and handle contracts | `planned` | `m6c` | `docs/process/SPRINT_M6_STORAGE_FOUNDATION.md` | | `FS-FAT32-001` | Read-only FAT32 persistent filesystem | `planned` | `m6d` | `docs/process/SPRINT_M6_STORAGE_FOUNDATION.md` | diff --git a/docs/adr/0012-pci-storage-foundation.md b/docs/adr/0012-pci-storage-foundation.md index 259dd82..000fec8 100644 --- a/docs/adr/0012-pci-storage-foundation.md +++ b/docs/adr/0012-pci-storage-foundation.md @@ -1,6 +1,6 @@ # ADR 0012: PCI and Storage Foundation -- Status: Accepted for M6 implementation +- Status: Accepted; M6B transport decision recorded - Date: 2026-07-26 ## Context @@ -25,17 +25,32 @@ MSI/MSI-X, DMA allocator, block layer, or persistent filesystem yet. raw port I/O remains inside the x86-64 adapter. 5. QEMU exposes a dedicated second `virtio-blk-pci` disk. M6A only proves that the exact controller is discovered and matched; it does not claim sector I/O. -6. M6B will introduce a polling virtio-blk transport behind a block-device API. - Interrupt-driven completion is deferred until the polling contract passes. -7. M6C will add bounded buffer-cache and VFS contracts without disk writes. -8. M6D will mount a read-only FAT32 volume on the dedicated second disk. The +6. M6B uses the modern virtio PCI capability model and negotiates only + `VIRTIO_F_VERSION_1`. Until the VM mapper supports explicit uncached device + mappings, the standardized virtio PCI configuration-access capability is + used for common, notify, and device-register access. +7. M6B exposes an architecture-independent 512-byte sector contract. Queue + zero uses one allocator-owned direct-mapped DMA page and a split ring, with + one synchronous outstanding request and bounded polling. +8. M6B identifies the dedicated disk through `VIRTIO_BLK_T_GET_ID`, reads a + seeded sector, writes and reads back one disposable sector, then restores + the original sector before the gate can pass. +9. Interrupt-driven completion, indirect descriptors, multiple outstanding + requests, MSI-X, and explicit uncached MMIO mappings remain later transport + work. +10. M6C will add bounded buffer-cache and VFS contracts without disk writes. +11. M6D will mount a read-only FAT32 volume on the dedicated second disk. The EFI system partition is never used for filesystem experiments. -9. Persistent writes require a later gate with device identity, bounds checks, +12. Persistent writes require a later gate with device identity, bounds checks, checksums, reboot verification, corruption tests, and recovery evidence. ## Consequences - PCI inventory evidence is independently testable before DMA begins. +- M6B DMA memory has one owner, a fixed layout, bounded request sizes, and no + lifetime shorter than the live device. +- A failed M6B request cannot be silently treated as successful, and the + acceptance write is confined to the disposable test image. - The first transport is optimized for reproducible QEMU validation, while AHCI and NVMe remain discoverable future physical-hardware targets. - Early filesystem work can use standard FAT32 images and host tooling. diff --git a/docs/architecture/SYSTEM_OVERVIEW.md b/docs/architecture/SYSTEM_OVERVIEW.md index 9f13b33..352ba6f 100644 --- a/docs/architecture/SYSTEM_OVERVIEW.md +++ b/docs/architecture/SYSTEM_OVERVIEW.md @@ -129,3 +129,11 @@ Raw configuration and future device-register access stay in the x86-64 adapter. PCI identity, block contracts, cache policy, VFS types, and filesystem validation remain architecture-independent. The EFI system partition is not a storage-development target. + +M6B selects the single discovered virtio-blk function, validates its BAR and +vendor-capability topology, and uses the standardized PCI configuration-access +window for modern common, notify, and device registers. One direct-mapped +allocator frame contains the descriptor table, available ring, used ring, +request header, sector buffer, and status byte. The transport permits one +bounded synchronous request at a time; M6C may consume only the block-device +contract, not the x86 PCI adapter. diff --git a/docs/process/BACKLOG.md b/docs/process/BACKLOG.md index f848934..4de7b72 100644 --- a/docs/process/BACKLOG.md +++ b/docs/process/BACKLOG.md @@ -42,16 +42,20 @@ - Timer-driven process preemption and CR3/TSS switching. - Blocking, wakeup, and deterministic page-table resource reclamation. -## M6A — PCI and Storage Discovery — in development +## M6A — PCI and Storage Discovery — accepted (`v0.0.9-m6a`) - PCI enumeration and driver matching. - Bounded multifunction and bridge-aware inventory. - QEMU virtio-blk target discovery. - Shell PCI diagnostics. -## M6B through M6D — Storage and VFS Foundation +## M6B — Virtio Block Transport — in development - Block-device abstraction and polling virtio-blk transport. +- Dedicated disk identity, known-sector read, and confined write/restore gate. + +## M6C through M6D — Storage and VFS Foundation + - Buffer cache and asynchronous I/O contracts. - VFS inode, mount, path, and file-handle model. - Read-only FAT32 persistent filesystem prototype. diff --git a/docs/process/RISK_REGISTER.md b/docs/process/RISK_REGISTER.md index 1a9f9f3..6fd7a22 100644 --- a/docs/process/RISK_REGISTER.md +++ b/docs/process/RISK_REGISTER.md @@ -16,4 +16,5 @@ | R-012 | Legacy PIT/PIC results do not generalize to SMP | High | High | explicit single-core boundary; APIC/SMP remains a separate milestone | | R-013 | PCI enumeration races configuration access | Low | High | single-CPU ownership and interrupt serialization during M6A | | R-014 | Malformed PCI topology overflows fixed discovery state | Medium | High | bounded inventory/bridge queue and fail-closed completeness gate | -| R-015 | DMA or block writes corrupt memory or persistent media | Medium | Critical | separate M6B gate, disposable second disk, bounds/timeouts, no M6A I/O | +| R-015 | DMA or block writes corrupt memory or persistent media | Medium | Critical | owned DMA page, fixed chains, identity/bounds/status/timeouts, disposable sector restoration | +| R-016 | Untrusted virtio PCI capabilities redirect register access | Medium | Critical | bounded capability traversal, validated BAR references and region offsets, exact-width access | diff --git a/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md b/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md index 245883a..95eb707 100644 --- a/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md +++ b/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md @@ -8,6 +8,8 @@ writes. ## M6A — PCI discovery +Status: accepted as `v0.0.9-m6a`. + - PCI configuration-mechanism #1 presence probe. - Bounded bus/device/function inventory. - Multifunction and PCI bridge traversal. @@ -25,12 +27,16 @@ Exit criteria: ## M6B — block transport -- block-device trait and sector geometry contract; -- PCI BAR and virtio capability parsing; -- dedicated DMA-safe request, descriptor, available, and used rings; -- polling virtio-blk initialization; -- bounded single-sector read and write against a disposable test disk; -- timeout, unsupported-feature, bounds, and status error handling. +Status: implementation candidate; QEMU acceptance required. + +- [x] block-device trait and sector geometry contract; +- [x] PCI BAR and modern virtio capability parsing; +- [x] dedicated DMA-safe request, descriptor, available, and used rings; +- [x] polling virtio-blk initialization; +- [x] bounded single-sector read and write against a disposable test disk; +- [x] restore the original disposable sector after the write probe; +- [x] timeout, reset, unsupported-feature, bounds, and status error handling; +- [ ] pass the pinned-toolchain headless QEMU smoke gate. Exit criteria: diff --git a/docs/security/THREAT_MODEL.md b/docs/security/THREAT_MODEL.md index 8a3e86a..836e82e 100644 --- a/docs/security/THREAT_MODEL.md +++ b/docs/security/THREAT_MODEL.md @@ -74,7 +74,11 @@ - bounded PCI device and bus inventories fail closed on overflow; - x86 configuration transactions are serialized on the bootstrap CPU; - M6A performs discovery only and cannot issue block requests; -- M6B uses a disposable, explicitly identified second QEMU disk; +- M6B requires the dedicated QEMU disk identity before its acceptance I/O; +- M6B uses allocator-owned DMA memory, fixed descriptor chains, sector bounds, + device status checks, and a finite polling limit; +- the sole M6B write target is a disposable sector whose original bytes are + restored before the acceptance gate passes; - filesystem work begins read-only with geometry and bounds validation; - persistent writes remain blocked until reboot, corruption, and recovery gates are implemented. diff --git a/docs/testing/M6B_BLOCK_TRANSPORT_VALIDATION.md b/docs/testing/M6B_BLOCK_TRANSPORT_VALIDATION.md new file mode 100644 index 0000000..025f6b1 --- /dev/null +++ b/docs/testing/M6B_BLOCK_TRANSPORT_VALIDATION.md @@ -0,0 +1,63 @@ +# M6B Virtio Block Transport Validation + +## Purpose + +Prove that SanjuOS can initialize the one dedicated QEMU virtio block target +and complete bounded sector requests without selecting the EFI system +partition or leaving acceptance-test data on the disk. + +## Host checks + +- Block geometry reports 512-byte sectors and checked byte capacity. +- Zero-length, end-of-device, and overflowing ranges are rejected. +- Read-only media rejects writes through the block contract. +- I/O, 32-bit memory, and 64-bit memory BARs decode without truncation. +- The M6B report cannot pass without every transport and M6A regression fact. + +## QEMU fixture + +The smoke test creates a temporary 8 MiB raw second disk, writes +`SANJUOS-M6B-READ-PATTERN` at sector 8, and attaches it with +`serial=SANJU-M6B`. The firmware ESP is a separate drive. + +Sector 16 is the sole acceptance write target. SanjuOS reads and retains its +original contents, writes a deterministic pattern, reads the pattern back, and +restores the original contents. + +## Required boot evidence + +```text +SanjuOS M6B Virtio Block Transport +Architecture-independent block-device API: active +Modern virtio PCI capabilities: active +PCI bus mastering: active +Virtio feature negotiation: active +DMA-safe split virtqueue: active +Dedicated storage identity: verified +Known sector read test: passed +Disposable sector write/readback test: passed +Disposable sector restoration: passed +Block bounds rejection test: passed +Block request timeout protection: active +M6A regression under M6B: passed +M6B block transport gate: passed +``` + +The scripted shell must also run `block` and report the capacity, queue size, +read result, and write/readback result. + +## Fail-closed cases + +- Missing, duplicate, cyclic, truncated, or out-of-range capabilities stop M6B. +- An unassigned or malformed BAR stops M6B. +- Failure to negotiate `VIRTIO_F_VERSION_1` stops M6B. +- Read-only media, zero capacity, an unavailable queue, or a wrong device ID + stops M6B. +- Device-reset requests, non-success status bytes, malformed used entries, and + polling timeouts return errors instead of advancing the gate. + +## Boundary + +M6B has no partition parser, block cache, VFS, or filesystem. Its write probe +does not authorize persistent filesystem writes. Physical disks remain +unsupported. diff --git a/docs/testing/TEST_STRATEGY.md b/docs/testing/TEST_STRATEGY.md index ffa9d19..077d188 100644 --- a/docs/testing/TEST_STRATEGY.md +++ b/docs/testing/TEST_STRATEGY.md @@ -18,6 +18,8 @@ - expected milestone strings observed through debug I/O; - explicit QEMU success exit code. - exact attached-device topology for hardware-driver milestones; +- seeded read and reversible disposable-sector write for M6B; +- block out-of-bounds, status, reset, and timeout failure paths; - every previously accepted milestone rerun as a regression gate. ## Future gates diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 81b50ac..ae1f7e4 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sanju-kernel" -version = "0.0.9" +version = "0.0.10" edition.workspace = true rust-version.workspace = true publish.workspace = true diff --git a/kernel/src/block.rs b/kernel/src/block.rs new file mode 100644 index 0000000..418dfda --- /dev/null +++ b/kernel/src/block.rs @@ -0,0 +1,193 @@ +//! Architecture-independent block-device contracts. + +pub const SECTOR_SIZE: usize = 512; +pub const SECTOR_SIZE_U64: u64 = SECTOR_SIZE as u64; + +/// Immutable geometry reported by one block device. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BlockGeometry { + pub sectors: u64, + pub sector_size: u32, + pub read_only: bool, +} + +impl BlockGeometry { + #[must_use] + pub const fn new(sectors: u64, read_only: bool) -> Self { + Self { + sectors, + sector_size: SECTOR_SIZE as u32, + read_only, + } + } + + #[must_use] + pub const fn byte_capacity(self) -> Option { + self.sectors.checked_mul(SECTOR_SIZE_U64) + } + + #[must_use] + pub const fn contains_sector(self, sector: u64) -> bool { + sector < self.sectors + } +} + +/// Stable errors returned through the architecture-independent block API. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BlockError { + NotReady, + InvalidBuffer, + OutOfBounds, + ReadOnly, + UnsupportedFeatures, + UnsupportedRequest, + Timeout, + DeviceNeedsReset, + Transport, + Io, +} + +/// One sector-addressed block device. +/// +/// M6B intentionally allows one synchronous request at a time. Asynchronous +/// completion and scatter/gather requests remain later transport extensions. +pub trait BlockDevice { + fn geometry(&self) -> BlockGeometry; + + /// Reads exactly one 512-byte sector. + /// + /// # Errors + /// + /// Returns [`BlockError::OutOfBounds`] when `sector` is outside the + /// reported geometry, plus transport-specific errors for failed requests. + fn read_sector( + &mut self, + sector: u64, + destination: &mut [u8; SECTOR_SIZE], + ) -> Result<(), BlockError>; + + /// Writes exactly one 512-byte sector. + /// + /// # Errors + /// + /// Returns [`BlockError::ReadOnly`] for read-only devices, + /// [`BlockError::OutOfBounds`] for an invalid sector, plus + /// transport-specific errors for failed requests. + fn write_sector(&mut self, sector: u64, source: &[u8; SECTOR_SIZE]) -> Result<(), BlockError>; +} + +/// Validates a sector range without wrapping its end. +/// +/// # Errors +/// +/// Returns [`BlockError::InvalidBuffer`] for a zero-sector request and +/// [`BlockError::OutOfBounds`] if the range exceeds the device. +pub const fn validate_sector_range( + geometry: BlockGeometry, + first_sector: u64, + sector_count: u64, +) -> Result<(), BlockError> { + if sector_count == 0 { + return Err(BlockError::InvalidBuffer); + } + let Some(end) = first_sector.checked_add(sector_count) else { + return Err(BlockError::OutOfBounds); + }; + if end > geometry.sectors { + return Err(BlockError::OutOfBounds); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{BlockDevice, BlockError, BlockGeometry, SECTOR_SIZE, validate_sector_range}; + + struct MemoryBlockDevice { + geometry: BlockGeometry, + sector: [u8; SECTOR_SIZE], + } + + impl BlockDevice for MemoryBlockDevice { + fn geometry(&self) -> BlockGeometry { + self.geometry + } + + fn read_sector( + &mut self, + sector: u64, + destination: &mut [u8; SECTOR_SIZE], + ) -> Result<(), BlockError> { + validate_sector_range(self.geometry, sector, 1)?; + destination.copy_from_slice(&self.sector); + Ok(()) + } + + fn write_sector( + &mut self, + sector: u64, + source: &[u8; SECTOR_SIZE], + ) -> Result<(), BlockError> { + if self.geometry.read_only { + return Err(BlockError::ReadOnly); + } + validate_sector_range(self.geometry, sector, 1)?; + self.sector.copy_from_slice(source); + Ok(()) + } + } + + #[test] + fn geometry_capacity_is_sector_based_and_checked() { + let geometry = BlockGeometry::new(16_384, false); + assert_eq!(geometry.byte_capacity(), Some(8 * 1024 * 1024)); + assert_eq!(BlockGeometry::new(u64::MAX, false).byte_capacity(), None); + } + + #[test] + fn range_validation_rejects_zero_overflow_and_end_boundary() { + let geometry = BlockGeometry::new(8, false); + assert_eq!( + validate_sector_range(geometry, 0, 0), + Err(BlockError::InvalidBuffer) + ); + assert_eq!(validate_sector_range(geometry, 7, 1), Ok(())); + assert_eq!( + validate_sector_range(geometry, 8, 1), + Err(BlockError::OutOfBounds) + ); + assert_eq!( + validate_sector_range(geometry, u64::MAX, 2), + Err(BlockError::OutOfBounds) + ); + } + + #[test] + fn block_contract_round_trips_one_sector_and_enforces_bounds() { + let mut device = MemoryBlockDevice { + geometry: BlockGeometry::new(1, false), + sector: [0; SECTOR_SIZE], + }; + let source = [0x5a; SECTOR_SIZE]; + let mut destination = [0; SECTOR_SIZE]; + device.write_sector(0, &source).unwrap(); + device.read_sector(0, &mut destination).unwrap(); + assert_eq!(destination, source); + assert_eq!( + device.read_sector(1, &mut destination), + Err(BlockError::OutOfBounds) + ); + } + + #[test] + fn block_contract_rejects_writes_to_read_only_media() { + let mut device = MemoryBlockDevice { + geometry: BlockGeometry::new(1, true), + sector: [0; SECTOR_SIZE], + }; + assert_eq!( + device.write_sector(0, &[0x5a; SECTOR_SIZE]), + Err(BlockError::ReadOnly) + ); + } +} diff --git a/kernel/src/generated/capabilities.rs b/kernel/src/generated/capabilities.rs index f0ac0f9..a562801 100644 --- a/kernel/src/generated/capabilities.rs +++ b/kernel/src/generated/capabilities.rs @@ -1,7 +1,7 @@ // @generated by scripts/generate-capabilities.py; do not edit. use crate::capabilities::{Capability, CapabilityStatus}; -pub const REGISTRY_VERSION: u32 = 4; +pub const REGISTRY_VERSION: u32 = 5; pub const CAPABILITIES: &[Capability] = &[ Capability { id: "SYS-TC-001", @@ -174,10 +174,17 @@ pub const CAPABILITIES: &[Capability] = &[ Capability { id: "STOR-BLK-001", name: "Architecture-independent block-device API", - status: CapabilityStatus::Planned, + status: CapabilityStatus::HardwareActive, milestone: "m6b", boot_label: "Block-device API", }, + Capability { + id: "STOR-VIRTIO-001", + name: "Polling modern virtio PCI block transport", + status: CapabilityStatus::HardwareActive, + milestone: "m6b", + boot_label: "Virtio block transport", + }, Capability { id: "VFS-CORE-001", name: "VFS inode, mount, path, and handle contracts", diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 8996438..b7d3497 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(test), no_std)] #![allow(clippy::pedantic)] +pub mod block; pub mod boot_info; pub mod capabilities; pub mod elf; @@ -864,6 +865,124 @@ pub fn kernel_main_m6a(console: &mut dyn Console, report: M6aReport) { } } +/// Runtime evidence for the M6B virtio block-transport gate. +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct M6bReport { + pub block_device_api_active: bool, + pub modern_pci_capabilities_active: bool, + pub pci_bars_parsed: usize, + pub pci_bus_master_active: bool, + pub feature_negotiation_active: bool, + pub dma_queue_active: bool, + pub queue_size: u16, + pub capacity_sectors: u64, + pub dedicated_device_identity_verified: bool, + pub known_sector_read_passed: bool, + pub disposable_sector_write_readback_passed: bool, + pub disposable_sector_restored: bool, + pub bounds_check_passed: bool, + pub timeout_protection_active: bool, + pub m6a_regression_passed: bool, +} + +impl M6bReport { + #[must_use] + pub const fn gate_passed(self) -> bool { + self.block_device_api_active + && self.modern_pci_capabilities_active + && self.pci_bars_parsed > 0 + && self.pci_bus_master_active + && self.feature_negotiation_active + && self.dma_queue_active + && self.queue_size >= 3 + && self.capacity_sectors > 16 + && self.dedicated_device_identity_verified + && self.known_sector_read_passed + && self.disposable_sector_write_readback_passed + && self.disposable_sector_restored + && self.bounds_check_passed + && self.timeout_protection_active + && self.m6a_regression_passed + } +} + +/// Prints the M6B hardware block-I/O acceptance report. +pub fn kernel_main_m6b(console: &mut dyn Console, report: M6bReport) { + console.write_line(""); + console.write_line("SanjuOS M6B Virtio Block Transport"); + write_state( + console, + "Architecture-independent block-device API", + report.block_device_api_active, + ); + write_state( + console, + "Modern virtio PCI capabilities", + report.modern_pci_capabilities_active, + ); + console.write_str("PCI BARs parsed: "); + console.write_usize(report.pci_bars_parsed); + console.write_line(""); + write_state(console, "PCI bus mastering", report.pci_bus_master_active); + write_state( + console, + "Virtio feature negotiation", + report.feature_negotiation_active, + ); + write_state(console, "DMA-safe split virtqueue", report.dma_queue_active); + console.write_str("Virtio request queue size: "); + console.write_u64(u64::from(report.queue_size)); + console.write_line(""); + console.write_str("Block capacity sectors: "); + console.write_u64(report.capacity_sectors); + console.write_line(""); + console.write_str("Block capacity bytes: "); + console.write_u64(report.capacity_sectors.saturating_mul(512)); + console.write_line(""); + console.write_line(if report.dedicated_device_identity_verified { + "Dedicated storage identity: verified" + } else { + "Dedicated storage identity: failed" + }); + console.write_line(if report.known_sector_read_passed { + "Known sector read test: passed" + } else { + "Known sector read test: failed" + }); + console.write_line(if report.disposable_sector_write_readback_passed { + "Disposable sector write/readback test: passed" + } else { + "Disposable sector write/readback test: failed" + }); + console.write_line(if report.disposable_sector_restored { + "Disposable sector restoration: passed" + } else { + "Disposable sector restoration: failed" + }); + console.write_line(if report.bounds_check_passed { + "Block bounds rejection test: passed" + } else { + "Block bounds rejection test: failed" + }); + write_state( + console, + "Block request timeout protection", + report.timeout_protection_active, + ); + console.write_line(if report.m6a_regression_passed { + "M6A regression under M6B: passed" + } else { + "M6A regression under M6B: failed" + }); + if report.gate_passed() { + console.write_line("M6B block transport gate: passed"); + console.write_line("Next gate: bounded buffer cache and virtual filesystem contracts"); + } else { + console.write_line("M6B block transport gate: failed"); + } +} + fn write_hex_u64(console: &mut dyn Console, value: u64) { for shift in (0..16).rev() { let nibble = u8::try_from((value >> (shift * 4)) & 0x0f).unwrap_or(0); @@ -888,9 +1007,9 @@ fn write_state(console: &mut dyn Console, label: &str, active: bool) { mod tests { use super::{ BootInfo, Console, FoundationHardeningPhase2Report, FoundationHardeningPhase3Report, - FoundationHardeningReport, M4Report, M5Report, M6aReport, MemoryMapInfo, kernel_main, - kernel_main_foundation_hardening, kernel_main_foundation_hardening_phase2, - kernel_main_foundation_hardening_phase3, kernel_main_m5, kernel_main_m6a, + FoundationHardeningReport, M4Report, M5Report, M6aReport, M6bReport, MemoryMapInfo, + kernel_main, kernel_main_foundation_hardening, kernel_main_foundation_hardening_phase2, + kernel_main_foundation_hardening_phase3, kernel_main_m5, kernel_main_m6a, kernel_main_m6b, }; use std::string::String; @@ -1131,6 +1250,52 @@ mod tests { ); } + #[test] + fn m6b_banner_requires_safe_sector_io_and_m6a_regression() { + let mut console = RecordingConsole::default(); + let report = M6bReport { + block_device_api_active: true, + modern_pci_capabilities_active: true, + pci_bars_parsed: 2, + pci_bus_master_active: true, + feature_negotiation_active: true, + dma_queue_active: true, + queue_size: 8, + capacity_sectors: 16_384, + dedicated_device_identity_verified: true, + known_sector_read_passed: true, + disposable_sector_write_readback_passed: true, + disposable_sector_restored: true, + bounds_check_passed: true, + timeout_protection_active: true, + m6a_regression_passed: true, + }; + kernel_main_m6b(&mut console, report); + assert!(report.gate_passed()); + assert!( + !M6bReport { + disposable_sector_restored: false, + ..report + } + .gate_passed() + ); + assert!( + console + .output + .contains("Architecture-independent block-device API: active\r\n") + ); + assert!( + console + .output + .contains("Disposable sector write/readback test: passed\r\n") + ); + assert!( + console + .output + .contains("M6B block transport gate: passed\r\n") + ); + } + #[test] fn m5_banner_confirms_protected_userspace_gate() { let mut console = RecordingConsole::default(); diff --git a/kernel/src/pci.rs b/kernel/src/pci.rs index bf08a71..7a6ccbc 100644 --- a/kernel/src/pci.rs +++ b/kernel/src/pci.rs @@ -2,6 +2,101 @@ pub const MAX_PCI_DEVICES: usize = 64; +/// Address-space type encoded by one PCI base-address register. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PciBarKind { + Io, + Memory32, + Memory64, +} + +/// Decoded, firmware-assigned PCI base-address register. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PciBar { + pub index: u8, + pub kind: PciBarKind, + pub base_address: u64, + pub prefetchable: bool, +} + +/// Invalid or unsupported BAR encoding. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PciBarError { + InvalidIndex, + MissingUpperHalf, + ReservedMemoryType, + Unassigned, +} + +/// Decodes one firmware-assigned BAR without modifying PCI configuration. +/// +/// The returned boolean is true when a 64-bit BAR consumes `index + 1`. +/// +/// # Errors +/// +/// Returns [`PciBarError`] for reserved, truncated, or unassigned encodings. +pub fn decode_bar(index: u8, low: u32, upper: Option) -> Result<(PciBar, bool), PciBarError> { + if index >= 6 { + return Err(PciBarError::InvalidIndex); + } + if low & 1 != 0 { + let base_address = u64::from(low & !0x03); + if base_address == 0 { + return Err(PciBarError::Unassigned); + } + return Ok(( + PciBar { + index, + kind: PciBarKind::Io, + base_address, + prefetchable: false, + }, + false, + )); + } + + let prefetchable = low & 0x08 != 0; + match (low >> 1) & 0x03 { + 0 => { + let base_address = u64::from(low & !0x0f); + if base_address == 0 { + return Err(PciBarError::Unassigned); + } + Ok(( + PciBar { + index, + kind: PciBarKind::Memory32, + base_address, + prefetchable, + }, + false, + )) + } + 2 => { + if index == 5 { + return Err(PciBarError::MissingUpperHalf); + } + let Some(upper) = upper else { + return Err(PciBarError::MissingUpperHalf); + }; + let base_address = (u64::from(upper) << 32) | u64::from(low & !0x0f); + if base_address == 0 { + return Err(PciBarError::Unassigned); + } + Ok(( + PciBar { + index, + kind: PciBarKind::Memory64, + base_address, + prefetchable, + }, + true, + )) + } + _ => Err(PciBarError::ReservedMemoryType), + } +} + /// One PCI bus/device/function address. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PciAddress { @@ -179,8 +274,8 @@ impl Default for PciInventory { #[cfg(test)] mod tests { use super::{ - MAX_PCI_DEVICES, PciAddress, PciDevice, PciInventory, PciInventoryError, - StorageControllerKind, + MAX_PCI_DEVICES, PciAddress, PciBar, PciBarError, PciBarKind, PciDevice, PciInventory, + PciInventoryError, StorageControllerKind, decode_bar, }; fn device( @@ -223,6 +318,39 @@ mod tests { assert!(PciAddress::new(0, 0, 8).is_none()); } + #[test] + fn decodes_memory_and_io_bars_without_configuration_writes() { + assert_eq!( + decode_bar(0, 0x0000_c001, None), + Ok(( + PciBar { + index: 0, + kind: PciBarKind::Io, + base_address: 0xc000, + prefetchable: false, + }, + false, + )) + ); + assert_eq!( + decode_bar(2, 0x9000_000c, Some(1)), + Ok(( + PciBar { + index: 2, + kind: PciBarKind::Memory64, + base_address: 0x0000_0001_9000_0000, + prefetchable: true, + }, + true, + )) + ); + assert_eq!( + decode_bar(5, 0x0000_0004, None), + Err(PciBarError::MissingUpperHalf) + ); + assert_eq!(decode_bar(0, 0, None), Err(PciBarError::Unassigned)); + } + #[test] fn inventory_rejects_duplicates_and_overflow() { let mut inventory = PciInventory::new(); diff --git a/kernel/src/shell.rs b/kernel/src/shell.rs index c540375..1fbca75 100644 --- a/kernel/src/shell.rs +++ b/kernel/src/shell.rs @@ -23,6 +23,10 @@ pub struct ShellEnvironment { pub pci_functions: usize, pub storage_controllers: usize, pub virtio_block_targets: usize, + pub block_capacity_sectors: u64, + pub block_queue_size: usize, + pub block_read_test_passed: bool, + pub block_write_test_passed: bool, } /// Interactive line editor and command dispatcher. @@ -126,11 +130,11 @@ fn execute_line( match command { "help" => { console.write_line(concat!( - "Commands: help version uptime memory irq tasks pci ls cat write echo ", + "Commands: help version uptime memory irq tasks pci block ls cat write echo ", "clear userspace", )); } - "version" => console.write_line("SanjuOS 0.0.9-prealpha (M6A)"), + "version" => console.write_line("SanjuOS 0.0.10-prealpha (M6B)"), "uptime" => { console.write_str("Timer ticks: "); console.write_u64(environment.timer_ticks); @@ -168,6 +172,24 @@ fn execute_line( console.write_usize(environment.virtio_block_targets); console.write_line(""); } + "block" => { + console.write_str("Virtio block: "); + console.write_u64(environment.block_capacity_sectors); + console.write_str(" sectors, queue "); + console.write_usize(environment.block_queue_size); + console.write_str(", read "); + console.write_str(if environment.block_read_test_passed { + "passed" + } else { + "failed" + }); + console.write_str(", write/readback "); + console.write_line(if environment.block_write_test_passed { + "passed" + } else { + "failed" + }); + } "ls" => { if fs.file_count() == 0 { console.write_line(""); @@ -263,9 +285,13 @@ mod tests { pci_functions: 7, storage_controllers: 2, virtio_block_targets: 1, + block_capacity_sectors: 16_384, + block_queue_size: 8, + block_read_test_passed: true, + block_write_test_passed: true, ..ShellEnvironment::default() }; - for byte in b"write note.txt hello\ncat note.txt\npci\n" { + for byte in b"write note.txt hello\ncat note.txt\npci\nblock\n" { shell.feed_byte(*byte, &mut console, &mut fs, &environment); } @@ -276,6 +302,9 @@ mod tests { .output .contains("PCI functions: 7, storage controllers: 2, virtio-blk targets: 1\r\n") ); - assert_eq!(shell.commands_executed(), 3); + assert!(console.output.contains( + "Virtio block: 16384 sectors, queue 8, read passed, write/readback passed\r\n" + )); + assert_eq!(shell.commands_executed(), 4); } } diff --git a/scripts/run-qemu.sh b/scripts/run-qemu.sh index 6e79f58..a7d0870 100755 --- a/scripts/run-qemu.sh +++ b/scripts/run-qemu.sh @@ -25,6 +25,8 @@ mkdir -p build if [[ ! -f build/sanju-storage.img ]]; then truncate -s 64M build/sanju-storage.img fi +printf '%s' 'SANJUOS-M6B-READ-PATTERN' | + dd of=build/sanju-storage.img bs=1 seek=$((8 * 512)) conv=notrunc status=none qemu-system-x86_64 \ -machine q35,accel=tcg \ @@ -34,7 +36,7 @@ qemu-system-x86_64 \ -drive if=pflash,format=raw,file="$OVMF_VARS_COPY" \ -drive format=raw,file=fat:rw:build/esp \ -drive if=none,id=sanju-storage,format=raw,file=build/sanju-storage.img \ - -device virtio-blk-pci,drive=sanju-storage,serial=SANJU-M6A \ + -device virtio-blk-pci,drive=sanju-storage,serial=SANJU-M6B \ -serial stdio \ -no-reboot \ -no-shutdown diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 47f3113..9818789 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -23,6 +23,8 @@ STORAGE_IMAGE="$(mktemp /tmp/sanjuos-storage.XXXXXX.img)" trap 'rm -f "$OVMF_VARS_COPY" "$STORAGE_IMAGE"' EXIT cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS_COPY" truncate -s 8M "$STORAGE_IMAGE" +printf '%s' 'SANJUOS-M6B-READ-PATTERN' | + dd of="$STORAGE_IMAGE" bs=1 seek=$((8 * 512)) conv=notrunc status=none mkdir -p build rm -f build/qemu-debug.log @@ -35,7 +37,7 @@ timeout 20s qemu-system-x86_64 \ -drive if=pflash,format=raw,file="$OVMF_VARS_COPY" \ -drive format=raw,file=fat:rw:build/smoke-esp \ -drive if=none,id=sanju-storage,format=raw,file="$STORAGE_IMAGE" \ - -device virtio-blk-pci,drive=sanju-storage,serial=SANJU-M6A \ + -device virtio-blk-pci,drive=sanju-storage,serial=SANJU-M6B \ -display none \ -serial none \ -monitor none \ @@ -97,9 +99,24 @@ grep -Fq "Virtio block PCI target: active" build/qemu-debug.log grep -Fq "Storage driver target: virtio-blk-pci" build/qemu-debug.log grep -Fq "FH3 regression under M6A: passed" build/qemu-debug.log grep -Fq "M6A PCI discovery gate: passed" build/qemu-debug.log +grep -Fq "SanjuOS M6B Virtio Block Transport" build/qemu-debug.log +grep -Fq "Architecture-independent block-device API: active" build/qemu-debug.log +grep -Fq "Modern virtio PCI capabilities: active" build/qemu-debug.log +grep -Fq "PCI bus mastering: active" build/qemu-debug.log +grep -Fq "Virtio feature negotiation: active" build/qemu-debug.log +grep -Fq "DMA-safe split virtqueue: active" build/qemu-debug.log +grep -Fq "Dedicated storage identity: verified" build/qemu-debug.log +grep -Fq "Known sector read test: passed" build/qemu-debug.log +grep -Fq "Disposable sector write/readback test: passed" build/qemu-debug.log +grep -Fq "Disposable sector restoration: passed" build/qemu-debug.log +grep -Fq "Block bounds rejection test: passed" build/qemu-debug.log +grep -Fq "Block request timeout protection: active" build/qemu-debug.log +grep -Fq "M6A regression under M6B: passed" build/qemu-debug.log +grep -Fq "M6B block transport gate: passed" build/qemu-debug.log grep -Fq "SanjuOS kernel shell ready." build/qemu-debug.log grep -Fq "M5 protected userspace, syscalls, and ELF loader are active." build/qemu-debug.log grep -Fq "virtio-blk targets: 1" build/qemu-debug.log +grep -Fq "write/readback passed" build/qemu-debug.log echo "QEMU smoke test passed." cat build/qemu-debug.log diff --git a/scripts/source-check.py b/scripts/source-check.py index 76fd090..c7aee04 100755 --- a/scripts/source-check.py +++ b/scripts/source-check.py @@ -15,6 +15,7 @@ CPU = ROOT / "boot/uefi/src/arch/x86_64/mod.rs" HARDWARE_PAGING = ROOT / "boot/uefi/src/arch/x86_64/paging.rs" HARDWARE_PCI = ROOT / "boot/uefi/src/arch/x86_64/pci.rs" +HARDWARE_VIRTIO_BLOCK = ROOT / "boot/uefi/src/arch/x86_64/virtio_block.rs" KERNEL = ROOT / "kernel/src/lib.rs" BOOT_INFO = ROOT / "kernel/src/boot_info.rs" OWNERSHIP = ROOT / "kernel/src/ownership.rs" @@ -30,6 +31,7 @@ HEAP = ROOT / "kernel/src/heap.rs" PROCESS = ROOT / "kernel/src/process.rs" PCI = ROOT / "kernel/src/pci.rs" +BLOCK = ROOT / "kernel/src/block.rs" SYSCALL = ROOT / "kernel/src/syscall.rs" ELF = ROOT / "kernel/src/elf.rs" STARTUP = ROOT / "kernel/src/startup.rs" @@ -333,6 +335,7 @@ def main() -> int: cpu = CPU.read_text(encoding="utf-8") hardware_paging = HARDWARE_PAGING.read_text(encoding="utf-8") hardware_pci = HARDWARE_PCI.read_text(encoding="utf-8") + hardware_virtio_block = HARDWARE_VIRTIO_BLOCK.read_text(encoding="utf-8") kernel = KERNEL.read_text(encoding="utf-8") boot_info = BOOT_INFO.read_text(encoding="utf-8") ownership = OWNERSHIP.read_text(encoding="utf-8") @@ -348,6 +351,7 @@ def main() -> int: heap = HEAP.read_text(encoding="utf-8") process = PROCESS.read_text(encoding="utf-8") pci = PCI.read_text(encoding="utf-8") + block = BLOCK.read_text(encoding="utf-8") syscall = SYSCALL.read_text(encoding="utf-8") elf = ELF.read_text(encoding="utf-8") startup = STARTUP.read_text(encoding="utf-8") @@ -375,6 +379,9 @@ def main() -> int: require(boot, "cpu::discover_pci", BOOT) require(boot, "M6aReport", BOOT) require(boot, "kernel_main_m6a", BOOT) + require(boot, "M6bReport", BOOT) + require(boot, "kernel_main_m6b", BOOT) + require(boot, "cpu::initialize_virtio_block", BOOT) if "core::arch" in boot or "asm!(" in boot or "global_asm!(" in boot: raise AssertionError(f"{BOOT}: architecture-specific assembly leaked into main.rs") require(cpu, 'asm!("int3"', CPU) @@ -421,6 +428,15 @@ def main() -> int: require(hardware_pci, "CONFIG_ADDRESS_PORT", HARDWARE_PCI) require(hardware_pci, "configuration_mechanism_one_available", HARDWARE_PCI) require(hardware_pci, "read_config_u32", HARDWARE_PCI) + require(hardware_pci, "pub(super) struct PciConfigSession", HARDWARE_PCI) + require(hardware_virtio_block, "VIRTIO_F_VERSION_1_HIGH", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "VIRTIO_PCI_CAP_PCI_CFG", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "struct DmaQueue", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "VIRTIO_BLK_T_GET_ID", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "EXPECTED_READ_PATTERN", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "DISPOSABLE_WRITE_SECTOR", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "validate_sector_range", HARDWARE_VIRTIO_BLOCK) + require(hardware_virtio_block, "BlockError::Timeout", HARDWARE_VIRTIO_BLOCK) require(memory, "pub struct FrameAllocator", MEMORY) require(memory, "pub struct FrameBitmap", MEMORY) require(memory, "pub fn free_frame", MEMORY) @@ -452,8 +468,12 @@ def main() -> int: require(pci, "pub struct PciInventory", PCI) require(pci, "pub struct PciDevice", PCI) require(pci, "StorageControllerKind::VirtioBlock", PCI) + require(pci, "pub fn decode_bar", PCI) if "asm!(" in pci or "0x0cf8" in pci or "0x0cfc" in pci: raise AssertionError(f"{PCI}: architecture-specific PCI access leaked into kernel") + require(block, "pub trait BlockDevice", BLOCK) + require(block, "pub struct BlockGeometry", BLOCK) + require(block, "pub const fn validate_sector_range", BLOCK) require(syscall, "pub struct SyscallDispatcher", SYSCALL) for syscall_name in ("Write", "Read", "Exit", "Yield", "GetPid", "Open", "Close", "Spawn"): require(syscall, syscall_name, SYSCALL) @@ -475,9 +495,11 @@ def main() -> int: ): require(ownership, f"fn {test_name}", OWNERSHIP) require(capability_registry, "SYS-TC-001", CAPABILITY_REGISTRY) - require(capability_registry, 'registry_version = 4', CAPABILITY_REGISTRY) + require(capability_registry, 'registry_version = 5', CAPABILITY_REGISTRY) require(capability_registry, "PCI-ENUM-001", CAPABILITY_REGISTRY) require(capability_registry, "STOR-DISC-001", CAPABILITY_REGISTRY) + require(capability_registry, "STOR-BLK-001", CAPABILITY_REGISTRY) + require(capability_registry, "STOR-VIRTIO-001", CAPABILITY_REGISTRY) require(capability_registry, "software_model", CAPABILITY_REGISTRY) require(toolchain, 'channel = "1.97.0"', TOOLCHAIN) require(toolchain, 'components = ["clippy", "rustfmt"]', TOOLCHAIN) @@ -487,9 +509,11 @@ def main() -> int: require(kernel, "pub struct FoundationHardeningReport", KERNEL) require(kernel, "pub struct FoundationHardeningPhase3Report", KERNEL) require(kernel, "pub struct M6aReport", KERNEL) + require(kernel, "pub struct M6bReport", KERNEL) require(kernel, "Foundation hardening phase 1: passed", KERNEL) require(kernel, "Foundation hardening phase 3: passed", KERNEL) require(kernel, "M6A PCI discovery gate: passed", KERNEL) + require(kernel, "M6B block transport gate: passed", KERNEL) require(kernel, "Ring 3 execution", KERNEL) require(kernel, "M5 protected user-space gate: passed", KERNEL) require( @@ -502,6 +526,10 @@ def main() -> int: require(setup, "rustup override set 1.97.0", SETUP) require(smoke, "virtio-blk-pci", SMOKE) require(smoke, "M6A PCI discovery gate: passed", SMOKE) + require(smoke, "SANJUOS-M6B-READ-PATTERN", SMOKE) + require(smoke, "serial=SANJU-M6B", SMOKE) + require(smoke, "M6B block transport gate: passed", SMOKE) + require(smoke, "Disposable sector restoration: passed", SMOKE) for executable in (INIT_ELF, HELLO_ELF, FAULT_ELF): validate_elf64(executable) if LOGO.read_bytes()[:8] != b"\x89PNG\r\n\x1a\n": @@ -555,7 +583,7 @@ def main() -> int: ) validate_source_manifest() - print("SanjuOS M6A PCI and storage-discovery source checks passed.") + print("SanjuOS M6B virtio-block transport source checks passed.") print("UEFI memory descriptor base size: 40 bytes") print("UEFI GOP mode-information size: 36 bytes") print("UEFI GOP mode size: 40 bytes")