From f49f1d14ac14b9c394635771c508fd61015bfee0 Mon Sep 17 00:00:00 2001 From: chavalasantosh Date: Tue, 28 Jul 2026 00:20:36 +0530 Subject: [PATCH] Implement M6C bounded cache and VFS --- CHANGELOG.md | 21 + Cargo.lock | 4 +- Makefile | 2 +- PROJECT.md | 20 +- README.md | 45 +- SECURITY.md | 2 +- SOURCE_MANIFEST.sha256 | 76 +- boot/uefi/Cargo.toml | 2 +- boot/uefi/src/arch/x86_64/mod.rs | 2 +- boot/uefi/src/main.rs | 159 +++- capabilities/capabilities.toml | 33 +- capabilities/smoke-expectations.txt | 5 +- docs/CAPABILITY_MATRIX.md | 10 +- docs/adr/0012-pci-storage-foundation.md | 5 +- docs/adr/0013-bounded-cache-vfs-contracts.md | 47 ++ docs/architecture/SYSTEM_OVERVIEW.md | 15 +- docs/process/BACKLOG.md | 17 +- docs/process/RISK_REGISTER.md | 2 + docs/process/SPRINT_M6_STORAGE_FOUNDATION.md | 33 +- docs/security/THREAT_MODEL.md | 9 + docs/testing/M6C_CACHE_VFS_VALIDATION.md | 54 ++ docs/testing/TEST_STRATEGY.md | 3 + kernel/Cargo.toml | 2 +- kernel/src/cache.rs | 384 ++++++++++ kernel/src/fs.rs | 107 ++- kernel/src/generated/capabilities.rs | 24 +- kernel/src/lib.rs | 221 +++++- kernel/src/shell.rs | 153 +++- kernel/src/startup.rs | 21 +- kernel/src/syscall.rs | 2 +- kernel/src/vfs.rs | 742 +++++++++++++++++++ scripts/build-user-programs.sh | 2 +- scripts/generate-capabilities.py | 6 +- scripts/generate-source-manifest.py | 4 +- scripts/setup.sh | 2 +- scripts/smoke-test.sh | 39 +- scripts/source-check.py | 33 +- user/programs/bin/hello.elf | Bin 1968 -> 1968 bytes user/programs/bin/init.elf | Bin 1992 -> 1992 bytes user/programs/src/hello.S | 2 +- user/programs/src/init.S | 2 +- 41 files changed, 2120 insertions(+), 192 deletions(-) create mode 100644 docs/adr/0013-bounded-cache-vfs-contracts.md create mode 100644 docs/testing/M6C_CACHE_VFS_VALIDATION.md create mode 100644 kernel/src/cache.rs create mode 100644 kernel/src/vfs.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae201a..4fb3300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## M6C Bounded Cache and VFS candidate — 2026-07-27 + +- Adopted Soma OS as the temporary user-facing working identity while retaining + historical tags and internal crate identifiers. +- Added a 16-sector, allocation-free, read-through LRU block cache. +- Made the M6C dirty-state policy explicit: every persistent write is rejected + before transport and the cache must retain zero dirty entries. +- Added live first-miss/repeat-hit evidence against the accepted virtio device. +- Added fixed inode, superblock, mount, canonical path, directory, and backend + contracts. +- Added bounded absolute-path normalization with component and depth limits. +- Added a four-entry mount table with component-boundary longest-prefix lookup. +- Added a 32-entry generation-protected user file-handle table. +- Adapted RAMFS and shell file operations behind the VFS contract. +- Added `cache` and `mounts` diagnostics. +- Added capability registry version 6 and mandatory M6B regression evidence. + +M6C does not mount a persistent filesystem and cannot leave persistent dirty +data. Read-only FAT32 remains the M6D gate; persistent writes remain deferred +until corruption, recovery, and power-loss behavior are designed and tested. + ## M6B Virtio Block Transport candidate — 2026-07-26 - Architecture-independent 512-byte sector block-device contract. diff --git a/Cargo.lock b/Cargo.lock index a829ca4..84181e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,11 @@ version = 4 [[package]] name = "sanju-boot" -version = "0.0.10" +version = "0.0.11" dependencies = [ "sanju-kernel", ] [[package]] name = "sanju-kernel" -version = "0.0.10" +version = "0.0.11" diff --git a/Makefile b/Makefile index 9be55b9..64436a5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SHELL := /usr/bin/env bash help: @printf '%s\n' \ - 'SanjuOS developer commands:' \ + 'Soma OS developer commands:' \ ' make setup Install the Rust target used by the UEFI loader' \ ' make source-check Validate critical source and UEFI ABI invariants' \ ' make user-programs Rebuild the embedded Ring 3 ELF programs' \ diff --git a/PROJECT.md b/PROJECT.md index 667203c..fcec4ac 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -1,14 +1,14 @@ # Project Charter -- Working name: SanjuOS +- Working name: Soma OS - Start date: 2026-07-21 - Product owner: Sanju - Architecture: x86-64 - Firmware: UEFI 2.x - Primary language: Rust 2024 -- Accepted checkpoint: M6A PCI and Storage Discovery -- Immutable release: `v0.0.9-m6a` -- Current development checkpoint: M6B virtio block transport +- Accepted checkpoint: M6B Virtio Block Transport +- Immutable release: `v0.0.10-m6b` +- Current development checkpoint: M6C bounded cache and VFS - Deployment policy: QEMU only until physical-install safety gates pass ## Mission @@ -21,12 +21,12 @@ Development is grouped into major milestone batches. Small formatting or CI corr ## Current objective -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. +Add an allocation-free, fixed-capacity read-through cache over the accepted +virtio block transport. Establish bounded inode, superblock, mount, canonical +path, directory, and generation-protected user file-handle contracts. Adapt +RAMFS behind that VFS boundary without enabling persistent writes. ## Next major objective -Add a fixed-capacity block cache and VFS contracts without enabling persistent -filesystem writes. +Validate and mount a dedicated FAT32 image read-only, then expose bounded +persistent directory listing and file reads through the accepted VFS. diff --git a/README.md b/README.md index 8011e81..93431d6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# SanjuOS +# Soma OS -![SanjuOS logo](assets/branding/sanjuos-logo.png) - -SanjuOS is an independent, Rust-first desktop operating-system project. It is +Soma OS is the current working identity of an independent, Rust-first desktop +operating-system project. Historical release tags and internal `sanju-*` crate +identifiers remain unchanged until the public product identity is frozen. It is not a Linux distribution. Development proceeds through emulator-verified kernel milestones before any physical-disk work. -## Current checkpoint: M6B Virtio Block Transport +## Current checkpoint: M6C Bounded Cache and VFS -The accepted baseline entering this phase was the Foundation Hardening Phase 2 -candidate. M0 through FH2 proved UEFI ownership transfer, protected kernel +The accepted baseline entering this phase is `v0.0.10-m6b`. M0 through FH2 +proved UEFI ownership transfer, protected kernel execution, interrupts, Ring 3 entry, `SYSCALL`/`SYSRET`, ELF64 loading, -recoverable user faults, physical ownership, and a fresh SanjuOS page-table +recoverable user faults, physical ownership, and a fresh Soma OS page-table root. The accepted `v0.0.8-fh3` release turned the process runtime into an active @@ -51,10 +51,21 @@ M6B implements that gate on the dedicated QEMU test disk: - a seeded read test plus a disposable write/readback/restore transaction; - a `block` shell diagnostic and exact QEMU acceptance evidence. +M6C builds the filesystem boundary without enabling persistent writes: + +- a 16-sector, allocation-free, read-through LRU cache; +- one live first-miss/repeat-hit probe proving the second read avoids transport; +- a hard read-only cache policy with zero dirty entries; +- fixed inode, superblock, mount, canonical path, and directory contracts; +- component-boundary mount resolution and bounded `.`/`..` normalization; +- generation-protected user file handles that reject stale identifiers; +- RAMFS accessed through the same VFS interface reserved for FAT32; +- `cache` and `mounts` shell diagnostics plus exact smoke evidence. + ## Shell commands ```text -help version userspace uptime memory irq tasks pci block ls cat write echo clear +help version userspace uptime memory irq tasks pci block cache mounts ls cat write echo clear ``` ## Build and verify @@ -77,20 +88,22 @@ boot/uefi/ UEFI orchestration and x86-64 platform implementation kernel/ Boot contracts, memory ownership, allocators, kernel models capabilities/ Canonical capability registry and generated smoke evidence user/programs/ Position-independent Ring 3 assembly programs -assets/branding/ Approved SanjuOS graphical logo +assets/branding/ Historical and future product identity assets scripts/ Build, generation, ABI, QEMU, and validation automation docs/ Requirements, architecture, ADRs, testing, security, process ``` ## Current boundary -M6B and FH3 remain single-core and PIT-driven. Block completion is synchronous +M6C 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. A block cache, VFS, read-only FAT32, persistent writes, physical-disk -installation, graphics, SMP, and local APIC timers remain later gates. +identified disposable QEMU disk is used. The cache cannot issue writes and +cannot contain dirty entries. RAMFS remains the only writable filesystem. The +combined EFI-stub kernel still retains a bounded identity mapping while a +separate high-half kernel image is designed. Read-only FAT32, persistent +writes, physical-disk installation, graphics, SMP, and local APIC timers remain +later gates. ## Safety -SanjuOS remains emulator-only. Do not install it on a physical disk. +Soma OS remains emulator-only. Do not install it on a physical disk. diff --git a/SECURITY.md b/SECURITY.md index 763e922..4fc52be 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -SanjuOS is pre-alpha research software and must not be used to protect sensitive data or replace a production operating system. +Soma OS is pre-alpha research software and must not be used to protect sensitive data or replace a production operating system. ## Reporting diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 74cc2ee..2b11207 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -4,20 +4,20 @@ ae0cb6c3b2dcaed5941d193848a2d05028f12f0692e95aacf2808ed7b037dd8f .gitattributes 9f35a88b86d7601a4b40fe13e7987baac18a1310911aec01111ee123f8bbc3a4 .github/workflows/ci.yml 4337eb9c42b5bfdf2840306b3944a65e475e21af781307f998ef299b56f685d7 .gitignore 17fe0e368f6e7327b413a3113c56cce5f169a1ccb54f0b65375e4abcacc35145 assets/branding/sanjuos-logo.png -41989b585af5a688ea5309a7246851be5a458c5707fa393c6e341e65743c9e8b boot/uefi/Cargo.toml +7ba85a6000841f1851b06532a09ea534d1cf8f30e821cf700a8d7c8500128358 boot/uefi/Cargo.toml d80a890c3bfcb13a3a9fc850b6e9b9a2eea333c83b1e78c5db60abd0272bc9b8 boot/uefi/src/arch/mod.rs -dd4aea80aed8b6589dea0e9a0a98a206ff0822a4a729a910a6f2edc94b9a102b boot/uefi/src/arch/x86_64/mod.rs +0421c9e8f0f8d9831db2e45f0755baaac2d6685325b90f42454318db3e1f1351 boot/uefi/src/arch/x86_64/mod.rs 3899462a2005889f9e86c3ef775364441b46f0b7da079cad52e792c02fb6b997 boot/uefi/src/arch/x86_64/paging.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 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 +47d9c858ae79958d4c09a61f1bdd9bc64d3f5a852f7aade95463f6decac0ad3a boot/uefi/src/main.rs +395268c2b70e15139514429ccce4e04628560ac42ccf94ced6448eb6602f0001 capabilities/capabilities.toml +3f1809f5a5df415a9e2c514a53590f5e8faad1d0c58bc5e41f023999472b57ca capabilities/smoke-expectations.txt +9e9b7c91e0240a56ca882439b3f84e14994c76e4ba1408af7f2cc55deffd6123 Cargo.lock 2c03da856ec66c6d6407cfe6f007989ac5e48acdf5ac923bf1d1bc7d3d448675 Cargo.toml -4db556995b861e272ab460a3bcd209f51f8a2918cd2c0dfb672ca2994e25fb10 CHANGELOG.md +fcb21c956affe13e6da974cb3086cebefa090da4f5f6b834a8021a019e26d942 CHANGELOG.md fa978f37685c47c2209dc2c11e4bb7d278ab47a813b051d1b25ad66e3eafc770 CONTRIBUTING.md 4b490a94642825c10c5876ab94d6e1390360ba754e9615735c649b64a5fea334 docs/adr/0001-rust-first.md 2408920abe6ae8d6c3861f0bcfbe5c3cb362a444fc5104eb5d245fee9334a22d docs/adr/0002-uefi-x86-64-first.md @@ -30,13 +30,14 @@ 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 -ce3f0b402a55436dd3039a4a238a50b02d32cf82cfe72cf784a9965f3d7de439 docs/adr/0012-pci-storage-foundation.md -15ec9d23c6f7bca03491c16622606f917a25f6bb9adb9e6bb4dc0e8982fa8e30 docs/architecture/SYSTEM_OVERVIEW.md -ed3365fbec4de8867e55e5295ad86c9b92ff89e05809e2fbfdcaa8bb0329a522 docs/CAPABILITY_MATRIX.md -c6aeeb6ac4ce860b7dfc313cf5590eafc56c37adca2e08a81148d48bda045e55 docs/process/BACKLOG.md +7a0311f75934b7c354720bc5b6df7537521d8dd3ac347893b58dd5a4eb69ae09 docs/adr/0012-pci-storage-foundation.md +0f73d36c6c092c8fcd9da7fdce333dca1730c82a0ad1f6e51bf7f6538a9c3996 docs/adr/0013-bounded-cache-vfs-contracts.md +8218e319a472f46e49fb77d7211d47b0bb9ef439a02ce37cf2b691d1efc633ea docs/architecture/SYSTEM_OVERVIEW.md +ab1a238796efa11410026c202e590f78a23e460f128f44a34ec39c2f94f487ff docs/CAPABILITY_MATRIX.md +9ff539a3bc489a25ca113fd28d05ecb157af365d22dbc8a1472b71b6d11dbc3d docs/process/BACKLOG.md fee8050e8dcc105d812af867282e410f229c2422e535add412d987b4ab70c890 docs/process/DEFINITION_OF_DONE.md 620312efa0d2c090a909a109f0303cdbfb10f6b756be238a41b182f80cf4c76c docs/process/ENVIRONMENT_NOTE.md -6b5ced505728195064ed19e7518350b2e7a4344d30319805cbdcb9bb2faeffe4 docs/process/RISK_REGISTER.md +e88116bc1322b9bb9bd971f45c27ae9a104231f7e7175e2dabaa3bdcc874aef5 docs/process/RISK_REGISTER.md 146760411de49ec93c4f4a6504f183ecda17e1f0b087816cad37c366de1f6232 docs/process/SDLC.md 53005001ed5a64131b96e384f02a038f7dc3f8af0b5bcfcfbe726544c196ca2e docs/process/SPRINT_0.md 39021ee209e0fe5c981c3a51a5bcbcc9b56551dd8d9e09e87a2498d029d6ce3a docs/process/SPRINT_1.md @@ -46,9 +47,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 -d9033107b5c62800cec539c6fb47852b07a8c49cfc17e9b28671307504642fbf docs/process/SPRINT_M6_STORAGE_FOUNDATION.md +f725fbcc2dc35f4ff05a234e8b52369deaf2cae0203cc532f95852a95c21c388 docs/process/SPRINT_M6_STORAGE_FOUNDATION.md 695d5c8f71075cc6f377a12c222d2b95e010192a7c6a428d6f3e830dcab8c7c2 docs/requirements/PRODUCT_REQUIREMENTS.md -5202f65d8d18c168efa2695cbdeb24a0e89d08b0a842b22825891b23e001de38 docs/security/THREAT_MODEL.md +194ea4a76bc203f7888ac75ea9ad4d1e55c5eed9d3e1c643592e1b1e878759bc 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 @@ -57,50 +58,53 @@ d9c7d5e718be2f21f50408551733c5860c04745bd4dd55c78b3876827cb1ea21 docs/testing/F 23c88c5b3bc0452637af6d8632234bd0be74ed6e356268cf02e24b4bd7d3e125 docs/testing/M5_VALIDATION.md ca41b8558018f1e32ffdf570f061c60dc9914f781ae17f755d2c6402607eb67e docs/testing/M6A_PCI_DISCOVERY_VALIDATION.md c4556e45804712a1f0382fdbd2d4e4fecdf467c95a867a3d16ef0a8aaa515fc7 docs/testing/M6B_BLOCK_TRANSPORT_VALIDATION.md -2bcd201649ceaade95bc0621f1a1824625285dfe43a8f125b5e3866bb5d87bb8 docs/testing/TEST_STRATEGY.md -e16e6e8ee0ab798bd3c226fb83c81080cdce5069fe15b3d23180eaf7dec58a94 kernel/Cargo.toml +9f082036ffb705d275ad4d8ccdff940d71fac78b59c7d73c409baebb0a122d36 docs/testing/M6C_CACHE_VFS_VALIDATION.md +efa26f528172a01bec7dc627f9164d19808f590d05fffd3a7400711fef041017 docs/testing/TEST_STRATEGY.md +43f52e072f297d1ba7a3e7a4f92f9e8a3d9bfdc5c4476972be62dc411f0fc20a kernel/Cargo.toml 528774183d2c2c8a809b796a1dad91a336462bfd589255007da6d0447ffff04c kernel/src/block.rs 0f8884112d25f779cff472251105301b160b4368ee6aeca421eff75242a4404e kernel/src/boot_info.rs +00f58db133b7d506c6c71b921ba5b8f3a62590c11583201277bae152f3c02a08 kernel/src/cache.rs 069853672f94c05025c9342485c3f00453d76c95a228302e01b9ebff47a072a6 kernel/src/capabilities.rs 8010bfaa835197b850c4a45e007f8b74b72c1de78673ebf3f0bedddd5912cf5d kernel/src/elf.rs -d2b8bfdd1472b3e75dd46a5f7d9752bcb70482b9b6c590e4579660b8ffc52b49 kernel/src/fs.rs -93cac7beda0b9046a540c113f09439cd81dbb91a60a0a581d99ab5403a6ef8cc kernel/src/generated/capabilities.rs +165004c146db5716c8fc5f0f264dcdbc2920c70676caecf3f0e63635b271b8d9 kernel/src/fs.rs +e2624581521c46725a393ed2070c56f23b266738773a7aef1891dd5e16438459 kernel/src/generated/capabilities.rs ae5160fc6d70367b7758afac6876faeb3c9e7766411b4dc1fc3ac10fa28dafa8 kernel/src/generated/mod.rs e6d19b13631029aefb179636a8b2cf20dad347bf2bc89365133912271c758008 kernel/src/heap.rs 9b88a5d4c56554cb1c4bd646925b28bc97a3feb4d3d6c576b5dc33dbe8496d02 kernel/src/input.rs -96df539fab698d08533d90ae8ce5df92e57982b6d121fe92e9003b32886c3300 kernel/src/lib.rs +6180752d6eb7f79d2d174e9a300b186553a88a3c2ee1e7709dc55a5e85426e57 kernel/src/lib.rs 472d2bb193b4c3449807927e2059db0ed94ea7218c3238f7f082aeda65582683 kernel/src/memory.rs 6188979546aeab5b63216056d9d148184fe89ede5226726b9c8c29471c0717a0 kernel/src/ownership.rs 52dabe4f896d71aa0dd67716cd2660ce1b96550a32ead52836185138384f4ba6 kernel/src/paging.rs f0ad8e59490e98186eb44b4dae04a42be46aae6b669e34b6548efa6646dbae82 kernel/src/pci.rs b36eff431fd41dada1848ccc363ac9a66f1c1ff38f99cb2e16a8655820ebbe06 kernel/src/process.rs 18426c0070887453f9de8a816927fe2c2b3cec57e40f90ef152d67546a462ca3 kernel/src/scheduler.rs -f8518120e136c8073262134ed4cd08a22643d933e1287d17ad9f04324f06a102 kernel/src/shell.rs -453d92ba90decb230785feeb9d7b56cc0ba583c52237c550ce6fb2889304f9df kernel/src/startup.rs -d47014298e0006dc63b6c346a5e823d1208891d91ccf9987fc8c46fe351e26bd kernel/src/syscall.rs -b818e7d3c53abc2d759daca8bb901c43fb62aa7ab3804f9f6269e42247260898 Makefile -1905ee7c842ee951d0d41c465f0cf51755d756fcd4d28bec6dcdf217d9512256 PROJECT.md -acc46103cc5755aa4937f5beb584c8b2a377127b2e9ed82cd8e8e12d4e5ea81f README.md +293e9bb2c227fe95f8df14870434207f23aeb5be236225d0e44db12d2b898f50 kernel/src/shell.rs +48ade91d48b51bb4b4cb48d4c3c5e060b5d9cf6e04af139f80ccbe5aa0d64edd kernel/src/startup.rs +1f6223882f082385c2152ef1d6e2938768f995c73eb02e9aaa6845b2e097d739 kernel/src/syscall.rs +7616eda0e38ebf9c2a56f16bb82a2484846e1879dcdd2b6c55b5e689d8c0a3fe kernel/src/vfs.rs +718742da4c6d89cf96babe555371260bcaaadca3ce715e74342cb445e4d80245 Makefile +b2e953a9fae4f33d871d23b45ba726c6035a0c13f04bfaa493122333701c5055 PROJECT.md +de6c9c714fb60079d38d9f403583b2d2bb28ec813111b5c89d5766437501b3e7 README.md 5aa479510db257bb2f5c82eb11fbbd3c30d1cc5ee70864dd9af5b2387e709c43 rust-toolchain.toml cd7c4d2ac3a81f614a8134661c0353e238ea19d4cb000bf98143a8e5f4a9a521 scripts/build-llvm-probe.sh c26e150f860ee56bc77d7181d7ff3514b32615e17a21494c33c6a159108cb528 scripts/build-smoke.sh -d95c634d0c5112adc1ca324eb5731b4bb12e0ae0f3db8e0fd6485113242475e7 scripts/build-user-programs.sh +644efa62e87b7fd2b884d694f54d0a020a3c94040b9dcba8c3712ed6924bc64f scripts/build-user-programs.sh 2906bb8debe2870d78b51b72fe33dfeb725cf2f6bffe03af8f8454ad9715c100 scripts/build.sh f5e3a26817aecf454c61b0bb0ff5f86fd03d7a3fb0ece74c67f1913a4dd94a74 scripts/find-ovmf.sh -fc3cabdfc9f29ed493ae266ac73d19183224031d15f53d1e770b15ebaabe00ce scripts/generate-capabilities.py -b6b36fc8d1c06cb824cae32821646d6e1f70946a761ca492a77e4f1dbbbcb749 scripts/generate-source-manifest.py +dee85ec792e3681c8bd60d98122c3dee6efcee1ca185643ff2475674496eb456 scripts/generate-capabilities.py +ebb9021802198356ab3068e70f73519ee409919d5785e018447c4d880c1048e1 scripts/generate-source-manifest.py b424e16e3444699ec89bb3d84e4e44c2dfaf4631431d26c88cc6a0e9e957cf42 scripts/image.sh 5ae79499617b1d5d44b0ff7f7d797606d1b8eb3a47081af0b36ac006269bd64e scripts/run-qemu.sh -52b6d67fe1d6c035a5bcee7b777b431d2d95645f9b0f26ac88f3130823b11e86 scripts/setup.sh -49f0f15e67fdf0dc91c12721ae201b9dfdc1413a6523c2a67914578a956abcae scripts/smoke-test.sh -d1be8a69d910175edec5c1a86d3bfaa9d3a221eb7dcb7ffbaae53afdd3e03095 scripts/source-check.py +e16f345f1ba68ffe4960ffbc162126c6bd8e9a8e753f72e91c88e6f1f8317d22 scripts/setup.sh +7a5880e9ba6e6ead03ca59fd1433fe19b3de942ab99b5fc82e0aa09c7f8963e0 scripts/smoke-test.sh +598e74922b59a8cb4d264105c77310143234cc7cd32dcbe0299ee16bf300c0e5 scripts/source-check.py 4cd8845ab8db1a1702165dcd7b777917804a285a06a5270226aff85e8a53280a scripts/verify-llvm-probe.sh -1ab4c9f09e95c0779b58e3e792ead2eeaa53291cc1804e405f7ec6635223383d SECURITY.md +3ebceddec88cf60e09b74d7f17cbe6f67a5672a9653d7f5084edc3f43547756c SECURITY.md 88cf4ef34b86ae3e33b04d4f1b7e67a15778d88e1728be97982c679a52361a82 user/programs/bin/fault-test.elf -92360bebdb3397cd32486117f8ab97a779db2a444ff45b64897994d78898780e user/programs/bin/hello.elf -da2d3758fc62ade5bb40f422c9842c9f77cffae380afc3422ccd78d395907d3e user/programs/bin/init.elf +005acc8f60ef17a3db46b01f8c17257ed77a21168649fc1367013080387ba77c user/programs/bin/hello.elf +abc0d075a3f46d74a8bd26eafbc8320b0449835287ac5e05b8fa40aae34e88d6 user/programs/bin/init.elf b6bfae7312024c4f6a58168103374fdd8ef78d6fe1b13637c414f996161453c5 user/programs/src/fault-test.S -8f10baeff5792ee0d4e64750a4b675d2a7833630d9ed8b6852a2f2a74277b21a user/programs/src/hello.S -6ccdf178d9bd1af5899ba00d245059324ed399392c9abb04e846238d1ca2d148 user/programs/src/init.S +1dd645bc1eb5fda9d15685f359a564be1346fd08fefa384ea85da829abe3ad46 user/programs/src/hello.S +3dcc935b116e777b03821a59082b05a44e9a636ac39dcecf3915368ffaeda389 user/programs/src/init.S 613a7ccaa0b692edca8b0f823d60874df86459fdfe6310ef863bc310be1a9deb verification/uefi-probe/main.c 5ff24f8132b0ee21d0e54b7d1c91aefef56e3a49c55c25a3437e0f0a8b6b8d5f verification/uefi-probe/README.md diff --git a/boot/uefi/Cargo.toml b/boot/uefi/Cargo.toml index d790cd0..b7d2497 100644 --- a/boot/uefi/Cargo.toml +++ b/boot/uefi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sanju-boot" -version = "0.0.10" +version = "0.0.11" 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 fc91511..2bca3bf 100644 --- a/boot/uefi/src/arch/x86_64/mod.rs +++ b/boot/uefi/src/arch/x86_64/mod.rs @@ -1638,7 +1638,7 @@ extern "efiapi" fn sanju_user_fault_dispatch(vector: u64, error_code: u64, fault SANJU_USER_FAULTED = 1; SANJU_USER_EXIT_REQUESTED = 1; } - debug_write_line("SanjuOS: isolated user exception"); + debug_write_line("Soma OS: isolated user exception"); debug_write_label_hex("User PID: ", CURRENT_USER_PID.load(Ordering::Relaxed)); debug_write_label_hex("Vector: ", vector); debug_write_label_hex("Error code: ", error_code); diff --git a/boot/uefi/src/main.rs b/boot/uefi/src/main.rs index 176dbac..f878b4f 100644 --- a/boot/uefi/src/main.rs +++ b/boot/uefi/src/main.rs @@ -13,6 +13,7 @@ use core::ptr::{addr_of, addr_of_mut}; use sanju_kernel::boot_info::{ FramebufferInfo, OptionalPhysicalAddress, PhysicalRange, PixelFormat, }; +use sanju_kernel::cache::{BlockCache, CacheError, DEFAULT_CACHE_ENTRIES, DirtyStatePolicy}; use sanju_kernel::elf::load_position_independent; use sanju_kernel::fs::RamFs; use sanju_kernel::heap::KernelHeap; @@ -29,11 +30,15 @@ use sanju_kernel::process::{AddressSpace, ProcessTable}; use sanju_kernel::scheduler::{Scheduler, TaskKind}; use sanju_kernel::shell::{Shell, ShellEnvironment}; use sanju_kernel::startup::{self, StartupStage}; +use sanju_kernel::vfs::{ + HandleRights, MAX_PATH_COMPONENTS, NodeKind, NormalizedPath, PathError, Vfs, VfsError, +}; use sanju_kernel::{ BootInfo, Console, FoundationHardeningPhase2Report, FoundationHardeningPhase3Report, - FoundationHardeningReport, M5Report, M6aReport, M6bReport, MemoryMapInfo, + FoundationHardeningReport, M5Report, M6aReport, M6bReport, M6cReport, MemoryMapInfo, kernel_main_foundation_hardening, kernel_main_foundation_hardening_phase2, kernel_main_foundation_hardening_phase3, kernel_main_m5, kernel_main_m6a, kernel_main_m6b, + kernel_main_m6c, }; type EfiHandle = *mut c_void; @@ -449,7 +454,7 @@ extern "efiapi" fn efi_main( pre_exit.clear_screen(); startup::print_logo(&mut pre_exit); - pre_exit.write_line("SanjuOS M5 boot transition"); + pre_exit.write_line("Soma OS M5 boot transition"); startup::print_stage(&mut pre_exit, StartupStage::Firmware, true); pre_exit.write_line("Capturing UEFI memory map..."); @@ -1271,6 +1276,7 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { let mut ramfs = RamFs::with_defaults(); let _ = ramfs.write("init.elf", b"embedded protected user executable"); let _ = ramfs.write("hello.elf", b"embedded protected user executable"); + let mut vfs = Vfs::new(ramfs); let mut shell = Shell::new(); let mut null_console = NullConsole; let self_test_environment = ShellEnvironment { @@ -1289,9 +1295,18 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { block_queue_size: 0, block_read_test_passed: false, block_write_test_passed: false, + cache_capacity: 0, + cache_hits: 0, + cache_misses: 0, + cache_device_reads: 0, + cache_dirty_entries: 0, + cache_read_only_policy: false, + vfs_mounts: vfs.mounts().len(), + vfs_handle_capacity: vfs.handles().capacity(), + vfs_path_normalization_passed: false, }; for byte in b"version\nuserspace\n" { - shell.feed_byte(*byte, &mut null_console, &mut ramfs, &self_test_environment); + shell.feed_byte(*byte, &mut null_console, &mut vfs, &self_test_environment); } let roots_are_distinct = @@ -1355,7 +1370,7 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { let foundation_report = FoundationHardeningReport { toolchain_pinned: true, capability_registry_synchronized: sanju_kernel::generated::capabilities::REGISTRY_VERSION - == 5, + == 6, architecture_separation_verified: true, boot_info_version: boot_info.version, ownership_map_active: !ownership_map.is_empty(), @@ -1469,7 +1484,7 @@ 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 { + let (block_device, block_probe) = match unsafe { cpu::initialize_virtio_block(&pci_discovery.inventory, &mut frame_allocator) } { Ok(result) => result, @@ -1506,6 +1521,113 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { ); } + let mut block_cache = match BlockCache::<_, DEFAULT_CACHE_ENTRIES>::new( + block_device, + DirtyStatePolicy::RejectWrites, + ) { + Ok(cache) => cache, + Err(_) => boot_failure( + &mut console, + "M6C-CACHE-001", + "bounded block cache initialization failed", + ), + }; + let mut first_cache_read = [0_u8; sanju_kernel::block::SECTOR_SIZE]; + let mut repeat_cache_read = [0_u8; sanju_kernel::block::SECTOR_SIZE]; + if block_cache.read_sector(8, &mut first_cache_read).is_err() { + boot_failure( + &mut console, + "M6C-CACHE-002", + "first cache-backed sector read failed", + ); + } + let first_cache_stats = block_cache.stats(); + if block_cache.read_sector(8, &mut repeat_cache_read).is_err() { + boot_failure( + &mut console, + "M6C-CACHE-003", + "repeat cache-backed sector read failed", + ); + } + let cache_write_rejected = + block_cache.write_sector(8, &first_cache_read) == Err(CacheError::ReadOnlyPolicy); + let cache_stats = block_cache.stats(); + + let normalized_path = NormalizedPath::parse("/workspace/./../welcome.txt"); + let path_normalization_passed = normalized_path + .as_ref() + .is_ok_and(|path| path.as_str() == "/welcome.txt" && path.component_count() == 1); + let root_bounded_path = NormalizedPath::parse("/../../welcome.txt"); + let excessive_depth = NormalizedPath::parse("/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q"); + let traversal_bounds_passed = MAX_PATH_COMPONENTS == 16 + && root_bounded_path + .as_ref() + .is_ok_and(|path| path.as_str() == "/welcome.txt") + && excessive_depth == Err(PathError::TooManyComponents); + + let vfs_contracts_active = vfs + .resolve("/") + .is_ok_and(|inode| inode.kind == NodeKind::Directory) + && vfs + .resolve("/welcome.txt") + .is_ok_and(|inode| inode.kind == NodeKind::File && inode.size > 0); + let vfs_mounts = vfs.mounts().len(); + let vfs_handle_capacity = vfs.handles().capacity(); + let mut vfs_probe_data = [0_u8; 16]; + let (ramfs_adapter_active, stale_handle_rejection_passed) = + match vfs.open("/welcome.txt", HandleRights::ReadOnly) { + Ok(handle) => { + let read = vfs.read(handle, &mut vfs_probe_data); + let close = vfs.close(handle); + let stale_rejected = + vfs.read(handle, &mut vfs_probe_data) == Err(VfsError::StaleHandle); + ( + read.is_ok_and(|count| { + count >= 7 + && count <= vfs_probe_data.len() + && vfs_probe_data[..count].starts_with(b"Welcome") + }) && close.is_ok(), + stale_rejected, + ) + } + Err(_) => (false, false), + }; + let user_handle_table_active = + vfs_handle_capacity == sanju_kernel::vfs::MAX_USER_HANDLES && vfs.handles().is_empty(); + + let m6c_report = M6cReport { + block_cache_active: block_cache.capacity() == DEFAULT_CACHE_ENTRIES, + cache_capacity_entries: block_cache.capacity(), + first_read_miss_passed: first_cache_stats.misses == 1 + && first_cache_stats.hits == 0 + && first_cache_stats.device_reads == 1, + repeat_read_hit_passed: cache_stats.misses == 1 + && cache_stats.hits == 1 + && cache_stats.device_reads == 1, + cached_data_consistent: first_cache_read == repeat_cache_read, + read_only_dirty_policy_active: block_cache.policy() == DirtyStatePolicy::RejectWrites, + rejected_cache_writes: cache_stats.rejected_writes, + dirty_cache_entries: cache_stats.dirty_entries, + vfs_contracts_active, + mount_table_active: !vfs.mounts().is_empty(), + mounts: vfs_mounts, + ramfs_adapter_active, + path_normalization_passed, + traversal_bounds_passed, + user_handle_table_active, + stale_handle_rejection_passed, + persistent_writes_disabled: cache_write_rejected && cache_stats.dirty_entries == 0, + m6b_regression_passed: m6b_report.gate_passed(), + }; + kernel_main_m6c(&mut console, m6c_report); + if !m6c_report.gate_passed() { + boot_failure( + &mut console, + "M6C-GATE-001", + "bounded block cache and VFS acceptance gate failed", + ); + } + startup::print_stage(&mut console, StartupStage::Shell, true); Shell::start(&mut console); @@ -1528,10 +1650,20 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { 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, + cache_capacity: m6c_report.cache_capacity_entries, + cache_hits: cache_stats.hits, + cache_misses: cache_stats.misses, + cache_device_reads: cache_stats.device_reads, + cache_dirty_entries: cache_stats.dirty_entries, + cache_read_only_policy: m6c_report.read_only_dirty_policy_active, + vfs_mounts, + vfs_handle_capacity, + vfs_path_normalization_passed: path_normalization_passed, }; - let smoke_commands = b"help\nuserspace\npci\nblock\nls\ncat welcome.txt\ntasks\nuptime\n"; + let smoke_commands = + b"help\nuserspace\npci\nblock\ncache\nmounts\nls\ncat welcome.txt\ntasks\nuptime\n"; for byte in smoke_commands { - shell.feed_byte(*byte, &mut console, &mut ramfs, &environment); + shell.feed_byte(*byte, &mut console, &mut vfs, &environment); } cpu::qemu::exit_success(); } @@ -1567,8 +1699,17 @@ extern "efiapi" fn sanju_m5_kernel_entry() -> ! { 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, + cache_capacity: m6c_report.cache_capacity_entries, + cache_hits: cache_stats.hits, + cache_misses: cache_stats.misses, + cache_device_reads: cache_stats.device_reads, + cache_dirty_entries: cache_stats.dirty_entries, + cache_read_only_policy: m6c_report.read_only_dirty_policy_active, + vfs_mounts, + vfs_handle_capacity, + vfs_path_normalization_passed: path_normalization_passed, }; - shell.feed_byte(byte, &mut console, &mut ramfs, &environment); + shell.feed_byte(byte, &mut console, &mut vfs, &environment); } } @@ -1883,7 +2024,7 @@ fn capture_memory_map(get_memory_map: GetMemoryMap) -> Result) -> ! { let mut console = KernelConsole::initialize(); - console.write_line("FATAL: SanjuOS panic during early boot."); + console.write_line("FATAL: Soma OS panic during early boot."); #[cfg(feature = "qemu-test")] cpu::qemu::exit_failure(); diff --git a/capabilities/capabilities.toml b/capabilities/capabilities.toml index 58ce172..42b0ff6 100644 --- a/capabilities/capabilities.toml +++ b/capabilities/capabilities.toml @@ -1,4 +1,4 @@ -registry_version = 5 +registry_version = 6 [[capability]] id = "SYS-TC-001" @@ -74,12 +74,12 @@ required_boot_line = "Four-level hardware mapper: active" [[capability]] id = "MEM-CR3-001" -name = "Fresh SanjuOS-owned PML4 and CR3 transition" +name = "Fresh Soma OS-owned PML4 and CR3 transition" status = "hardware_active" milestone = "foundation-hardening-2" evidence = ["boot/uefi/src/arch/x86_64/paging.rs", "scripts/smoke-test.sh"] -boot_label = "Fresh SanjuOS PML4" -required_boot_line = "Fresh SanjuOS PML4: active" +boot_label = "Fresh Soma OS PML4" +required_boot_line = "Fresh Soma OS PML4: active" [[capability]] id = "MEM-DMAP-001" @@ -229,13 +229,32 @@ 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 = "STOR-CACHE-001" +name = "Bounded read-only block cache" +status = "verified" +milestone = "m6c" +evidence = ["kernel/src/cache.rs", "boot/uefi/src/main.rs", "scripts/smoke-test.sh"] +boot_label = "Fixed-capacity block cache" +required_boot_line = "Fixed-capacity block cache: active" + [[capability]] id = "VFS-CORE-001" name = "VFS inode, mount, path, and handle contracts" -status = "planned" +status = "verified" milestone = "m6c" -evidence = ["docs/process/SPRINT_M6_STORAGE_FOUNDATION.md"] -boot_label = "Virtual filesystem" +evidence = ["kernel/src/vfs.rs", "kernel/src/fs.rs", "boot/uefi/src/main.rs", "scripts/smoke-test.sh"] +boot_label = "Virtual filesystem contracts" +required_boot_line = "VFS contracts: active" + +[[capability]] +id = "VFS-HANDLE-001" +name = "Generation-protected bounded user file handles" +status = "verified" +milestone = "m6c" +evidence = ["kernel/src/vfs.rs", "boot/uefi/src/main.rs", "scripts/smoke-test.sh"] +boot_label = "User file handles" +required_boot_line = "Generation-protected user handle table: active" [[capability]] id = "FS-FAT32-001" diff --git a/capabilities/smoke-expectations.txt b/capabilities/smoke-expectations.txt index a3e0464..a6557ef 100644 --- a/capabilities/smoke-expectations.txt +++ b/capabilities/smoke-expectations.txt @@ -7,7 +7,7 @@ Physical ownership map: active Frame allocation/free test: passed Page-table bootstrap pool: active Four-level hardware mapper: active -Fresh SanjuOS PML4: active +Fresh Soma OS PML4: active Physical direct map: active Kernel W^X policy: enforced Hardware guard-hole probe: active @@ -20,3 +20,6 @@ PCI configuration mechanism #1: active Virtio block PCI target: active Architecture-independent block-device API: active M6B block transport gate: passed +Fixed-capacity block cache: active +VFS contracts: active +Generation-protected user handle table: active diff --git a/docs/CAPABILITY_MATRIX.md b/docs/CAPABILITY_MATRIX.md index bbfe224..8c78b8b 100644 --- a/docs/CAPABILITY_MATRIX.md +++ b/docs/CAPABILITY_MATRIX.md @@ -1,6 +1,6 @@ -# SanjuOS Capability Matrix +# Soma OS Capability Matrix -Registry version: **5** +Registry version: **6** This file is generated from `capabilities/capabilities.toml`. Do not edit it manually. @@ -14,7 +14,7 @@ This file is generated from `capabilities/capabilities.toml`. Do not edit it man | `MEM-PF-001` | Bitmap physical frame allocator with alloc and free | `hardware_active` | `foundation-hardening-1` | `kernel/src/memory.rs` | | `MEM-PTB-001` | Dedicated page-table bootstrap pool | `hardware_active` | `foundation-hardening-1` | `kernel/src/memory.rs` | | `MEM-VM-001` | Hardware four-level page-table manager | `acceptance_prototype` | `foundation-hardening-2` | `kernel/src/paging.rs`
`boot/uefi/src/arch/x86_64/paging.rs` | -| `MEM-CR3-001` | Fresh SanjuOS-owned PML4 and CR3 transition | `hardware_active` | `foundation-hardening-2` | `boot/uefi/src/arch/x86_64/paging.rs`
`scripts/smoke-test.sh` | +| `MEM-CR3-001` | Fresh Soma OS-owned PML4 and CR3 transition | `hardware_active` | `foundation-hardening-2` | `boot/uefi/src/arch/x86_64/paging.rs`
`scripts/smoke-test.sh` | | `MEM-DMAP-001` | Higher-half physical direct map | `acceptance_prototype` | `foundation-hardening-2` | `kernel/src/paging.rs`
`boot/uefi/src/arch/x86_64/paging.rs` | | `MEM-WX-001` | Page-granular kernel W^X enforcement | `verified` | `foundation-hardening-2` | `boot/uefi/src/arch/x86_64/paging.rs`
`scripts/smoke-test.sh` | | `MEM-GUARD-001` | Hardware-unmapped guard-hole probe | `acceptance_prototype` | `foundation-hardening-2` | `kernel/src/paging.rs`
`boot/uefi/src/arch/x86_64/paging.rs` | @@ -32,5 +32,7 @@ This file is generated from `capabilities/capabilities.toml`. Do not edit it man | `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 | `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` | +| `STOR-CACHE-001` | Bounded read-only block cache | `verified` | `m6c` | `kernel/src/cache.rs`
`boot/uefi/src/main.rs`
`scripts/smoke-test.sh` | +| `VFS-CORE-001` | VFS inode, mount, path, and handle contracts | `verified` | `m6c` | `kernel/src/vfs.rs`
`kernel/src/fs.rs`
`boot/uefi/src/main.rs`
`scripts/smoke-test.sh` | +| `VFS-HANDLE-001` | Generation-protected bounded user file handles | `verified` | `m6c` | `kernel/src/vfs.rs`
`boot/uefi/src/main.rs`
`scripts/smoke-test.sh` | | `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 000fec8..5b62670 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; M6B transport decision recorded +- Status: Accepted; M6B transport accepted and M6C delegated to ADR 0013 - Date: 2026-07-26 ## Context @@ -38,7 +38,8 @@ MSI/MSI-X, DMA allocator, block layer, or persistent filesystem yet. 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. +10. M6C adds bounded buffer-cache and VFS contracts without disk writes, as + specified by ADR 0013. 11. M6D will mount a read-only FAT32 volume on the dedicated second disk. The EFI system partition is never used for filesystem experiments. 12. Persistent writes require a later gate with device identity, bounds checks, diff --git a/docs/adr/0013-bounded-cache-vfs-contracts.md b/docs/adr/0013-bounded-cache-vfs-contracts.md new file mode 100644 index 0000000..c7c4d9f --- /dev/null +++ b/docs/adr/0013-bounded-cache-vfs-contracts.md @@ -0,0 +1,47 @@ +# ADR 0013: Bounded Cache and VFS Contracts + +- Status: Accepted for M6C implementation +- Date: 2026-07-27 + +## Context + +M6B established one synchronous 512-byte block device and proved a confined +write/readback/restore transaction on a dedicated QEMU disk. Filesystem parsing +must not depend directly on the virtio transport, and persistent writes cannot +be enabled before recovery and power-loss behavior exists. + +The kernel remains `no_std`, single-core, allocation-conscious, and subject to +deterministic memory limits. Its existing RAMFS predates mount, path, inode, and +user file-handle contracts. + +## Decision + +1. M6C adds a 16-entry, allocation-free, read-through sector cache. +2. Cache replacement is least-recently-used among clean entries. +3. A failed device read does not evict or alter an existing cache entry. +4. The only M6C dirty-state policy is `RejectWrites`. An in-range write returns + an error before reaching the block device and cannot create dirty data. +5. The live acceptance probe reads sector 8 twice. The first request must + produce one miss and one device read; the second must produce one hit and no + additional device read. +6. VFS paths are canonical, absolute, limited to 256 bytes and 16 components, + collapse repeated separators and `.`, and bound `..` at root. +7. The mount table has four fixed slots and resolves only on component + boundaries using the longest matching prefix. +8. User file handles have 32 fixed slots. Every identifier contains a slot and + generation so a closed identifier cannot access a reused slot. +9. RAMFS implements the common inode, superblock, lookup, read, create/replace, + and directory-visit contracts. RAMFS may remain volatile and writable. +10. M6D may add a read-only FAT32 backend behind these contracts. It must not + bypass the cache or expose transport-specific state through the VFS. + +## Consequences + +- Filesystem code no longer depends on PCI or virtio types. +- Cache memory and handle memory have compile-time upper bounds. +- A repeat read has hardware-backed cache evidence rather than only a unit + model. +- Persistent storage remains unable to receive general-purpose writes. +- RAMFS shell behavior is preserved while exercising the future FAT32 boundary. +- Multiple live filesystem backend types require a later mount-dispatch design; + M6C records mount contracts but activates only the RAMFS root backend. diff --git a/docs/architecture/SYSTEM_OVERVIEW.md b/docs/architecture/SYSTEM_OVERVIEW.md index 352ba6f..e8a00cc 100644 --- a/docs/architecture/SYSTEM_OVERVIEW.md +++ b/docs/architecture/SYSTEM_OVERVIEW.md @@ -6,7 +6,7 @@ UEFI firmware | v -SanjuOS UEFI boot layer +Soma OS UEFI boot layer - validates firmware tables - captures memory and platform information (M1) - loads kernel image (later) @@ -93,7 +93,7 @@ transition window is intentional rather than an accidental firmware dependency. ## Foundation Hardening Phase 3 process runtime ```text -SanjuOS kernel CR3 +Soma OS kernel CR3 -> sanitized private root per process -> explicit Ring 3 image/data/stack pages -> supervisor kernel/direct-map pages @@ -137,3 +137,14 @@ 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. + +M6C wraps that contract in a 16-entry read-through LRU cache. Its only +dirty-state policy rejects writes before transport, so no entry can become +dirty. A failed read is staged separately and cannot destroy an existing cache +entry. + +The VFS layer defines fixed inode, superblock, mount, path, directory, and +generation-tagged handle contracts. Paths are absolute and canonical with +bounded depth; mount selection observes component boundaries. RAMFS is the only +active backend and remains volatile. M6D will attach a read-only FAT32 backend +without importing PCI or virtio types into filesystem code. diff --git a/docs/process/BACKLOG.md b/docs/process/BACKLOG.md index 4de7b72..38486b7 100644 --- a/docs/process/BACKLOG.md +++ b/docs/process/BACKLOG.md @@ -17,7 +17,7 @@ - User pointer validation and eight-call syscall ABI. - Process/address-space/context models and timer-quantum evidence. - ELF64 PIE loader and three embedded user programs. -- Branded startup, error codes, SanjuOS ASCII output, and graphical logo asset. +- Branded startup, error codes, historical ASCII output, and graphical logo asset. ## Foundation Hardening Phase 1 — accepted (`v0.0.6-fh1`) @@ -29,7 +29,7 @@ - Frozen x86-64 virtual-memory layout. - Complete inherited page-table reservation. -- Fresh SanjuOS-owned PML4 and safe CR3 transition. +- Fresh OS-owned PML4 and safe CR3 transition. - Physical direct map and hardware map/translate/protect/unmap API. - PE-section W^X permissions and real kernel guard holes. - M5 and FH1 regression preservation. @@ -49,15 +49,20 @@ - QEMU virtio-blk target discovery. - Shell PCI diagnostics. -## M6B — Virtio Block Transport — in development +## M6B — Virtio Block Transport — accepted (`v0.0.10-m6b`) - 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 +## M6C — Bounded Cache and VFS — implementation ready for CI + +- Fixed-capacity read-through cache with a hard no-dirty-data policy. +- VFS inode, superblock, mount, canonical path, and file-handle contracts. +- RAMFS adapter and generation-protected bounded user handles. +- Live first-miss/repeat-hit hardware evidence and shell diagnostics. + +## M6D — Read-Only Persistent Filesystem -- Buffer cache and asynchronous I/O contracts. -- VFS inode, mount, path, and file-handle model. - Read-only FAT32 persistent filesystem prototype. - User VFS handles and executable spawning after the kernel VFS gate. diff --git a/docs/process/RISK_REGISTER.md b/docs/process/RISK_REGISTER.md index 6fd7a22..edbaf54 100644 --- a/docs/process/RISK_REGISTER.md +++ b/docs/process/RISK_REGISTER.md @@ -18,3 +18,5 @@ | 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 | 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 | +| R-017 | Cache eviction loses or silently exposes dirty persistent data | Medium | Critical | M6C rejects every cache write before transport and gates on zero dirty entries | +| R-018 | Path or stale-handle confusion crosses a filesystem boundary | Medium | High | canonical bounded paths, component-aware mounts, and generation-protected handles | diff --git a/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md b/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md index 95eb707..bf0a560 100644 --- a/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md +++ b/docs/process/SPRINT_M6_STORAGE_FOUNDATION.md @@ -27,7 +27,7 @@ Exit criteria: ## M6B — block transport -Status: implementation candidate; QEMU acceptance required. +Status: accepted as `v0.0.10-m6b`. - [x] block-device trait and sector geometry contract; - [x] PCI BAR and modern virtio capability parsing; @@ -36,7 +36,7 @@ Status: implementation candidate; QEMU acceptance required. - [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. +- [x] pass the pinned-toolchain headless QEMU smoke gate. Exit criteria: @@ -47,11 +47,30 @@ Exit criteria: ## M6C — buffer cache and VFS -- fixed-capacity block cache with explicit dirty-state policy; -- inode, superblock, mount, path, and file-handle types; -- absolute-path normalization and traversal bounds; -- RAMFS adapted behind the VFS contract; -- user handle table design without enabling persistent writes. +Status: implementation candidate; QEMU acceptance required. + +- [x] 16-sector, allocation-free, read-through LRU block cache; +- [x] hard read-only dirty-state policy that rejects writes before transport; +- [x] failed reads preserve existing cache contents; +- [x] inode, superblock, mount, path, and file-handle types; +- [x] canonical absolute-path normalization with bounded root traversal; +- [x] fixed mount table with component-boundary longest-prefix resolution; +- [x] generation-protected, fixed-capacity user handle table; +- [x] RAMFS adapted behind the VFS contract; +- [x] live virtio first-miss/repeat-hit cache probe; +- [x] `cache` and `mounts` shell diagnostics; +- [ ] pass the pinned-toolchain headless QEMU smoke gate. + +Exit criteria: + +- one live sector read produces exactly one miss and one device request; +- repeating the read produces one hit without another device request; +- cached bytes equal the hardware-read bytes; +- an attempted cache write is rejected and leaves zero dirty entries; +- path normalization cannot escape root or exceed traversal bounds; +- RAMFS files resolve and read through VFS inode and handle contracts; +- a closed generation-tagged handle is rejected as stale; +- M6B and every earlier regression gate remains passed. ## M6D — read-only FAT32 diff --git a/docs/security/THREAT_MODEL.md b/docs/security/THREAT_MODEL.md index 836e82e..fa174b4 100644 --- a/docs/security/THREAT_MODEL.md +++ b/docs/security/THREAT_MODEL.md @@ -67,6 +67,8 @@ - untrusted BAR and capability-list metadata; - device-controlled DMA addresses and lengths; - malformed sector geometry, partition tables, FAT metadata, and cluster chains; +- stale file handles, path traversal, and mount-prefix confusion; +- dirty cache eviction without recovery or flush guarantees; - accidental selection of the EFI system partition or a physical user disk. ## M6 controls @@ -79,6 +81,13 @@ 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; +- the M6C cache rejects all writes before transport and requires zero dirty + entries at its acceptance boundary; +- canonical absolute paths are byte- and depth-bounded, and `..` cannot escape + the namespace root; +- mount resolution checks component boundaries and uses the longest prefix; +- user file handles include generations so closed identifiers cannot alias a + reused slot; - 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/M6C_CACHE_VFS_VALIDATION.md b/docs/testing/M6C_CACHE_VFS_VALIDATION.md new file mode 100644 index 0000000..7b58709 --- /dev/null +++ b/docs/testing/M6C_CACHE_VFS_VALIDATION.md @@ -0,0 +1,54 @@ +# M6C Cache and VFS Validation + +## Acceptance target + +Prove that the accepted virtio block device can be read through a bounded cache +and that RAMFS operates through bounded VFS path, mount, inode, directory, and +user-handle contracts without enabling persistent writes. + +## Automated evidence + +The pinned CI and local validation sequence is: + +```bash +python3 scripts/generate-capabilities.py --check +python3 scripts/source-check.py +cargo fmt --all -- --check +cargo clippy -p sanju-kernel --all-targets -- -D warnings +cargo clippy -p sanju-boot --target x86_64-unknown-uefi -- -D warnings +cargo test -p sanju-kernel +bash scripts/smoke-test.sh +``` + +The headless QEMU gate must prove: + +- the first cache-backed sector read records one miss and one device request; +- the repeat read records one hit without another device request; +- both returned sector images are identical; +- one in-range cache write is rejected; +- zero dirty entries remain; +- the root RAMFS mount is active behind VFS; +- canonical path normalization and traversal limits pass; +- a RAMFS file resolves and reads through an inode and user handle; +- a closed handle is rejected as stale; +- the complete M6B and earlier boot regressions remain passed; +- the shell exposes `cache`, `mounts`, `ls`, and `cat` through the accepted + runtime. + +## Unit boundaries + +Kernel tests cover: + +- cache hit, miss, device-read, and LRU-eviction accounting; +- write rejection before the backing device; +- zero-capacity and out-of-range failures; +- absolute path normalization, root-bounded `..`, and depth rejection; +- component-boundary longest-prefix mount resolution; +- RAMFS lookup and read through VFS; +- fixed handle capacity and stale-generation rejection. + +## Safety boundary + +M6C does not mount FAT32 and does not expose persistent writes. The only +writeable backend is volatile RAMFS. The dedicated QEMU storage image remains +disposable, and M6B still restores its one acceptance sector before M6C starts. diff --git a/docs/testing/TEST_STRATEGY.md b/docs/testing/TEST_STRATEGY.md index 077d188..ab979b6 100644 --- a/docs/testing/TEST_STRATEGY.md +++ b/docs/testing/TEST_STRATEGY.md @@ -20,6 +20,9 @@ - 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; +- live first-miss/repeat-hit cache evidence with one underlying device read; +- explicit write rejection and zero-dirty-entry evidence for M6C; +- bounded path, mount-prefix, RAMFS adapter, handle-capacity, and stale-handle tests; - every previously accepted milestone rerun as a regression gate. ## Future gates diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index ae1f7e4..f8fc103 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sanju-kernel" -version = "0.0.10" +version = "0.0.11" edition.workspace = true rust-version.workspace = true publish.workspace = true diff --git a/kernel/src/cache.rs b/kernel/src/cache.rs new file mode 100644 index 0000000..188db81 --- /dev/null +++ b/kernel/src/cache.rs @@ -0,0 +1,384 @@ +#![allow(clippy::module_name_repetitions)] + +//! Fixed-capacity, allocation-free block cache. +//! +//! M6C deliberately uses a read-through cache with a hard read-only policy. +//! Persistent dirty data, flushing, and writeback are not enabled until a +//! later recovery and power-loss safety gate. + +use crate::block::{BlockDevice, BlockError, BlockGeometry, SECTOR_SIZE, validate_sector_range}; + +/// Number of sectors retained by the M6C cache. +pub const DEFAULT_CACHE_ENTRIES: usize = 16; + +/// Persistent-write policy enforced by the cache. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DirtyStatePolicy { + /// Reject every write before it can reach the device or create dirty data. + RejectWrites, +} + +/// Failures returned through the cache boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CacheError { + ZeroCapacity, + ReadOnlyPolicy, + NoEvictableEntry, + Block(BlockError), +} + +impl From for CacheError { + fn from(value: BlockError) -> Self { + Self::Block(value) + } +} + +/// Observable cache counters used by diagnostics and acceptance tests. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct CacheStats { + pub hits: u64, + pub misses: u64, + pub evictions: u64, + pub device_reads: u64, + pub rejected_writes: u64, + pub dirty_entries: usize, +} + +#[derive(Clone, Copy)] +struct CacheEntry { + valid: bool, + dirty: bool, + sector: u64, + last_used: u64, + data: [u8; SECTOR_SIZE], +} + +impl CacheEntry { + const fn empty() -> Self { + Self { + valid: false, + dirty: false, + sector: 0, + last_used: 0, + data: [0; SECTOR_SIZE], + } + } +} + +/// One fixed-capacity read-through cache over a sector-addressed device. +pub struct BlockCache { + device: D, + entries: [CacheEntry; ENTRIES], + policy: DirtyStatePolicy, + clock: u64, + stats: CacheStats, +} + +impl BlockCache { + /// Creates an empty cache. + /// + /// # Errors + /// + /// Returns [`CacheError::ZeroCapacity`] when `ENTRIES` is zero. + pub fn new(device: D, policy: DirtyStatePolicy) -> Result { + if ENTRIES == 0 { + return Err(CacheError::ZeroCapacity); + } + Ok(Self { + device, + entries: [CacheEntry::empty(); ENTRIES], + policy, + clock: 0, + stats: CacheStats::default(), + }) + } + + /// Returns the geometry of the underlying device. + #[must_use] + pub fn geometry(&self) -> BlockGeometry { + self.device.geometry() + } + + /// Returns the compile-time entry capacity. + #[must_use] + pub const fn capacity(&self) -> usize { + ENTRIES + } + + /// Returns the active dirty-state policy. + #[must_use] + pub const fn policy(&self) -> DirtyStatePolicy { + self.policy + } + + /// Reads exactly one sector, serving a cached copy when present. + /// + /// A failed device read leaves the existing cache contents unchanged. + /// + /// # Errors + /// + /// Returns a block error for an invalid or failed device read, or + /// [`CacheError::NoEvictableEntry`] if every entry were dirty. + pub fn read_sector( + &mut self, + sector: u64, + destination: &mut [u8; SECTOR_SIZE], + ) -> Result<(), CacheError> { + validate_sector_range(self.device.geometry(), sector, 1)?; + let timestamp = self.next_timestamp(); + + if let Some(index) = self + .entries + .iter() + .position(|entry| entry.valid && entry.sector == sector) + { + let entry = &mut self.entries[index]; + entry.last_used = timestamp; + destination.copy_from_slice(&entry.data); + self.stats.hits = self.stats.hits.saturating_add(1); + return Ok(()); + } + + self.stats.misses = self.stats.misses.saturating_add(1); + let replacement = self.replacement_index()?; + let mut fetched = [0_u8; SECTOR_SIZE]; + self.device.read_sector(sector, &mut fetched)?; + self.stats.device_reads = self.stats.device_reads.saturating_add(1); + + if self.entries[replacement].valid { + self.stats.evictions = self.stats.evictions.saturating_add(1); + } + self.entries[replacement] = CacheEntry { + valid: true, + dirty: false, + sector, + last_used: timestamp, + data: fetched, + }; + destination.copy_from_slice(&fetched); + Ok(()) + } + + /// Rejects a sector write under the M6C read-only policy. + /// + /// The request is range-checked first, but is never forwarded to the + /// device and never creates a dirty cache entry. + /// + /// # Errors + /// + /// Returns [`CacheError::ReadOnlyPolicy`] for an in-range request. + pub fn write_sector( + &mut self, + sector: u64, + _source: &[u8; SECTOR_SIZE], + ) -> Result<(), CacheError> { + validate_sector_range(self.device.geometry(), sector, 1)?; + self.stats.rejected_writes = self.stats.rejected_writes.saturating_add(1); + Err(CacheError::ReadOnlyPolicy) + } + + /// Invalidates all clean entries. + /// + /// # Errors + /// + /// Returns [`CacheError::NoEvictableEntry`] if a future policy leaves any + /// dirty entry behind. M6C cannot normally reach that state. + pub fn invalidate_all(&mut self) -> Result<(), CacheError> { + if self.dirty_entries() != 0 { + return Err(CacheError::NoEvictableEntry); + } + for entry in &mut self.entries { + *entry = CacheEntry::empty(); + } + Ok(()) + } + + /// Returns current diagnostic counters. + #[must_use] + pub fn stats(&self) -> CacheStats { + CacheStats { + dirty_entries: self.dirty_entries(), + ..self.stats + } + } + + /// Returns ownership of the wrapped block device. + #[must_use] + pub fn into_inner(self) -> D { + self.device + } + + fn next_timestamp(&mut self) -> u64 { + self.clock = self.clock.saturating_add(1); + self.clock + } + + fn replacement_index(&self) -> Result { + if let Some(index) = self.entries.iter().position(|entry| !entry.valid) { + return Ok(index); + } + + self.entries + .iter() + .enumerate() + .filter(|(_, entry)| !entry.dirty) + .min_by_key(|(_, entry)| entry.last_used) + .map(|(index, _)| index) + .ok_or(CacheError::NoEvictableEntry) + } + + fn dirty_entries(&self) -> usize { + self.entries.iter().filter(|entry| entry.dirty).count() + } +} + +#[cfg(test)] +mod tests { + use super::{BlockCache, CacheError, DirtyStatePolicy}; + use crate::block::{BlockDevice, BlockError, BlockGeometry, SECTOR_SIZE}; + + struct CountingDevice { + sectors: [[u8; SECTOR_SIZE]; 4], + reads: u64, + writes: u64, + failing_read: Option, + } + + impl CountingDevice { + fn new() -> Self { + let mut sectors = [[0_u8; SECTOR_SIZE]; 4]; + for (sector, data) in sectors.iter_mut().enumerate() { + data.fill(u8::try_from(sector).unwrap()); + } + Self { + sectors, + reads: 0, + writes: 0, + failing_read: None, + } + } + + fn with_failing_read(sector: u64) -> Self { + Self { + failing_read: Some(sector), + ..Self::new() + } + } + } + + impl BlockDevice for CountingDevice { + fn geometry(&self) -> BlockGeometry { + BlockGeometry::new(self.sectors.len() as u64, false) + } + + fn read_sector( + &mut self, + sector: u64, + destination: &mut [u8; SECTOR_SIZE], + ) -> Result<(), BlockError> { + self.reads += 1; + if self.failing_read == Some(sector) { + return Err(BlockError::Io); + } + let index = usize::try_from(sector).map_err(|_| BlockError::OutOfBounds)?; + let source = self.sectors.get(index).ok_or(BlockError::OutOfBounds)?; + destination.copy_from_slice(source); + Ok(()) + } + + fn write_sector( + &mut self, + sector: u64, + source: &[u8; SECTOR_SIZE], + ) -> Result<(), BlockError> { + let index = usize::try_from(sector).map_err(|_| BlockError::OutOfBounds)?; + let destination = self.sectors.get_mut(index).ok_or(BlockError::OutOfBounds)?; + destination.copy_from_slice(source); + self.writes += 1; + Ok(()) + } + } + + #[test] + fn repeat_read_hits_cache_without_second_device_request() { + let device = CountingDevice::new(); + let mut cache = BlockCache::<_, 2>::new(device, DirtyStatePolicy::RejectWrites).unwrap(); + let mut first = [0_u8; SECTOR_SIZE]; + let mut second = [0_u8; SECTOR_SIZE]; + + cache.read_sector(2, &mut first).unwrap(); + cache.read_sector(2, &mut second).unwrap(); + + assert_eq!(first, second); + assert_eq!(cache.stats().misses, 1); + assert_eq!(cache.stats().hits, 1); + assert_eq!(cache.stats().device_reads, 1); + assert_eq!(cache.into_inner().reads, 1); + } + + #[test] + fn least_recently_used_clean_entry_is_evicted() { + let device = CountingDevice::new(); + let mut cache = BlockCache::<_, 2>::new(device, DirtyStatePolicy::RejectWrites).unwrap(); + let mut buffer = [0_u8; SECTOR_SIZE]; + + cache.read_sector(0, &mut buffer).unwrap(); + cache.read_sector(1, &mut buffer).unwrap(); + cache.read_sector(0, &mut buffer).unwrap(); + cache.read_sector(2, &mut buffer).unwrap(); + cache.read_sector(1, &mut buffer).unwrap(); + + assert_eq!(cache.stats().evictions, 2); + assert_eq!(cache.stats().hits, 1); + assert_eq!(cache.stats().misses, 4); + } + + #[test] + fn read_only_policy_rejects_write_before_device() { + let device = CountingDevice::new(); + let mut cache = BlockCache::<_, 2>::new(device, DirtyStatePolicy::RejectWrites).unwrap(); + + assert_eq!( + cache.write_sector(0, &[0xa5; SECTOR_SIZE]), + Err(CacheError::ReadOnlyPolicy) + ); + assert_eq!(cache.stats().rejected_writes, 1); + assert_eq!(cache.stats().dirty_entries, 0); + assert_eq!(cache.into_inner().writes, 0); + } + + #[test] + fn failed_device_read_preserves_existing_cached_entry() { + let device = CountingDevice::with_failing_read(1); + let mut cache = BlockCache::<_, 1>::new(device, DirtyStatePolicy::RejectWrites).unwrap(); + let mut original = [0_u8; SECTOR_SIZE]; + let mut destination = [0_u8; SECTOR_SIZE]; + + cache.read_sector(0, &mut original).unwrap(); + assert_eq!( + cache.read_sector(1, &mut destination), + Err(CacheError::Block(BlockError::Io)) + ); + cache.read_sector(0, &mut destination).unwrap(); + + assert_eq!(destination, original); + assert_eq!(cache.stats().hits, 1); + assert_eq!(cache.stats().misses, 2); + assert_eq!(cache.into_inner().reads, 2); + } + + #[test] + fn zero_capacity_and_out_of_bounds_are_rejected() { + assert!(matches!( + BlockCache::<_, 0>::new(CountingDevice::new(), DirtyStatePolicy::RejectWrites), + Err(CacheError::ZeroCapacity) + )); + + let mut cache = + BlockCache::<_, 1>::new(CountingDevice::new(), DirtyStatePolicy::RejectWrites).unwrap(); + assert_eq!( + cache.read_sector(4, &mut [0_u8; SECTOR_SIZE]), + Err(CacheError::Block(BlockError::OutOfBounds)) + ); + } +} diff --git a/kernel/src/fs.rs b/kernel/src/fs.rs index 19df745..e8b2cbc 100644 --- a/kernel/src/fs.rs +++ b/kernel/src/fs.rs @@ -4,9 +4,12 @@ use core::str; +use crate::vfs::{FileSystem, Inode, InodeId, NodeKind, PathError, Superblock, VfsError}; + pub const MAX_FILES: usize = 8; -const MAX_NAME_LEN: usize = 24; -const MAX_FILE_BYTES: usize = 512; +pub const MAX_NAME_LEN: usize = 24; +pub const MAX_FILE_BYTES: usize = 512; +pub const RAMFS_ROOT_INODE: InodeId = InodeId(1); /// Filesystem operation failures. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -70,14 +73,14 @@ impl RamFs { let _ = fs.write( "welcome.txt", concat!( - "Welcome to SanjuOS. The kernel shell, scheduler, timer, ", - "keyboard pipeline, and RAM filesystem are active.\n" + "Welcome to Soma OS. Protected userspace, storage, the virtual ", + "filesystem, and the interactive shell are active.\n" ) .as_bytes(), ); let _ = fs.write( "system.txt", - b"SanjuOS M5: protected userspace, syscalls, ELF loading, and branded startup.\n", + b"Soma OS M6C: bounded block caching and VFS contracts are active.\n", ); fs } @@ -149,6 +152,100 @@ impl Default for RamFs { } } +impl FileSystem for RamFs { + fn superblock(&self) -> Superblock { + Superblock { + filesystem_name: "ramfs", + root_inode: RAMFS_ROOT_INODE, + block_size: 1, + read_only: false, + } + } + + fn lookup(&self, parent: InodeId, name: &str) -> Result { + if parent != RAMFS_ROOT_INODE { + return Err(VfsError::NotDirectory); + } + let index = self.find_index(name).ok_or(VfsError::NotFound)?; + Ok(self.inode_for_index(index)) + } + + fn read(&self, inode: InodeId, offset: u64, destination: &mut [u8]) -> Result { + if inode == RAMFS_ROOT_INODE { + return Err(VfsError::IsDirectory); + } + let index = inode_index(inode)?; + let entry = self + .files + .get(index) + .filter(|entry| entry.occupied) + .ok_or(VfsError::NotFound)?; + let offset = usize::try_from(offset).map_err(|_| VfsError::InvalidOffset)?; + if offset >= entry.data_len { + return Ok(0); + } + let count = destination.len().min(entry.data_len - offset); + destination[..count].copy_from_slice(&entry.data[offset..offset + count]); + Ok(count) + } + + fn create_or_replace( + &mut self, + parent: InodeId, + name: &str, + data: &[u8], + ) -> Result { + if parent != RAMFS_ROOT_INODE { + return Err(VfsError::NotDirectory); + } + self.write(name, data).map_err(vfs_error)?; + let index = self.find_index(name).ok_or(VfsError::Backend)?; + Ok(self.inode_for_index(index)) + } + + fn visit_directory( + &self, + inode: InodeId, + visitor: &mut dyn FnMut(&str, Inode), + ) -> Result<(), VfsError> { + if inode != RAMFS_ROOT_INODE { + return Err(VfsError::NotDirectory); + } + for (index, entry) in self.files.iter().enumerate() { + if entry.occupied { + visitor(entry.name(), self.inode_for_index(index)); + } + } + Ok(()) + } +} + +impl RamFs { + fn inode_for_index(&self, index: usize) -> Inode { + let entry = &self.files[index]; + Inode { + id: InodeId(u64::try_from(index).unwrap_or(u64::MAX).saturating_add(2)), + kind: NodeKind::File, + size: u64::try_from(entry.data_len).unwrap_or(u64::MAX), + } + } +} + +fn inode_index(inode: InodeId) -> Result { + let raw = inode.0.checked_sub(2).ok_or(VfsError::NotFound)?; + usize::try_from(raw).map_err(|_| VfsError::NotFound) +} + +const fn vfs_error(error: FsError) -> VfsError { + match error { + FsError::EmptyName => VfsError::Path(PathError::Empty), + FsError::NameTooLong => VfsError::Path(PathError::ComponentTooLong), + FsError::DataTooLarge => VfsError::FileTooLarge, + FsError::FileTableFull => VfsError::NoSpace, + FsError::NotFound => VfsError::NotFound, + } +} + fn validate(name: &str, data: &[u8]) -> Result<(), FsError> { if name.is_empty() { return Err(FsError::EmptyName); diff --git a/kernel/src/generated/capabilities.rs b/kernel/src/generated/capabilities.rs index a562801..b11fa0b 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 = 5; +pub const REGISTRY_VERSION: u32 = 6; pub const CAPABILITIES: &[Capability] = &[ Capability { id: "SYS-TC-001", @@ -61,10 +61,10 @@ pub const CAPABILITIES: &[Capability] = &[ }, Capability { id: "MEM-CR3-001", - name: "Fresh SanjuOS-owned PML4 and CR3 transition", + name: "Fresh Soma OS-owned PML4 and CR3 transition", status: CapabilityStatus::HardwareActive, milestone: "foundation-hardening-2", - boot_label: "Fresh SanjuOS PML4", + boot_label: "Fresh Soma OS PML4", }, Capability { id: "MEM-DMAP-001", @@ -185,12 +185,26 @@ pub const CAPABILITIES: &[Capability] = &[ milestone: "m6b", boot_label: "Virtio block transport", }, + Capability { + id: "STOR-CACHE-001", + name: "Bounded read-only block cache", + status: CapabilityStatus::Verified, + milestone: "m6c", + boot_label: "Fixed-capacity block cache", + }, Capability { id: "VFS-CORE-001", name: "VFS inode, mount, path, and handle contracts", - status: CapabilityStatus::Planned, + status: CapabilityStatus::Verified, + milestone: "m6c", + boot_label: "Virtual filesystem contracts", + }, + Capability { + id: "VFS-HANDLE-001", + name: "Generation-protected bounded user file handles", + status: CapabilityStatus::Verified, milestone: "m6c", - boot_label: "Virtual filesystem", + boot_label: "User file handles", }, Capability { id: "FS-FAT32-001", diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index b7d3497..a5081b6 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -3,6 +3,7 @@ pub mod block; pub mod boot_info; +pub mod cache; pub mod capabilities; pub mod elf; pub mod fs; @@ -18,6 +19,7 @@ pub mod scheduler; pub mod shell; pub mod startup; pub mod syscall; +pub mod vfs; /// Minimal output boundary used before the full device and logging stacks exist. pub trait Console { @@ -126,7 +128,7 @@ impl M4Report { #[allow(clippy::too_many_lines)] pub fn kernel_main(console: &mut dyn Console, boot_info: BootInfo, report: M4Report) { console.write_line(""); - console.write_line("SanjuOS"); + console.write_line("Soma OS"); console.write_line(boot_info.milestone()); console.write_str("Architecture: "); console.write_line(boot_info.architecture()); @@ -274,7 +276,7 @@ impl M5Report { #[allow(clippy::too_many_lines)] pub fn kernel_main_m5(console: &mut dyn Console, boot_info: BootInfo, report: M5Report) { startup::print_logo(console); - console.write_line("SanjuOS M5"); + console.write_line("Soma OS M5"); console.write_line(boot_info.milestone()); console.write_str("Architecture: "); console.write_line(boot_info.architecture()); @@ -282,7 +284,7 @@ pub fn kernel_main_m5(console: &mut dyn Console, boot_info: BootInfo, report: M5 console.write_line(boot_info.firmware()); write_state( console, - "SanjuOS page-table ownership", + "Soma OS page-table ownership", report.paging_ownership_active, ); console.write_str("Active page-table root: 0x"); @@ -375,7 +377,7 @@ pub fn kernel_main_m5(console: &mut dyn Console, boot_info: BootInfo, report: M5 "Branded startup experience", report.startup_experience_active, ); - write_state(console, "SanjuOS logo print", report.sanjuos_brand_printed); + write_state(console, "Soma OS logo print", report.sanjuos_brand_printed); if report.gate_passed() { console.write_line("M5 protected user-space gate: passed"); @@ -433,7 +435,7 @@ pub fn kernel_main_foundation_hardening( report: FoundationHardeningReport, ) { console.write_line(""); - console.write_line("SanjuOS Foundation Hardening"); + console.write_line("Soma OS Foundation Hardening"); console.write_line(if report.toolchain_pinned { "Pinned toolchain: verified" } else { @@ -563,7 +565,7 @@ pub fn kernel_main_foundation_hardening_phase2( report: FoundationHardeningPhase2Report, ) { console.write_line(""); - console.write_line("SanjuOS Foundation Hardening Phase 2"); + console.write_line("Soma OS Foundation Hardening Phase 2"); console.write_line(if report.virtual_memory_layout_frozen { "Virtual-memory layout: frozen" } else { @@ -577,7 +579,7 @@ pub fn kernel_main_foundation_hardening_phase2( console.write_str("Inherited page-table root: 0x"); write_hex_u64(console, report.old_page_table_root); console.write_line(""); - console.write_str("SanjuOS page-table root: 0x"); + console.write_str("Soma OS page-table root: 0x"); write_hex_u64(console, report.new_page_table_root); console.write_line(""); console.write_str("Inherited page-table frames reserved: "); @@ -586,10 +588,10 @@ pub fn kernel_main_foundation_hardening_phase2( console.write_str("Physical window mapped bytes: "); console.write_u64(report.mapped_physical_bytes); console.write_line(""); - console.write_str("SanjuOS page-table frames used: "); + console.write_str("Soma OS page-table frames used: "); console.write_usize(report.page_table_frames_used); console.write_line(""); - write_state(console, "Fresh SanjuOS PML4", report.fresh_pml4_active); + write_state(console, "Fresh Soma OS PML4", report.fresh_pml4_active); console.write_line(if report.inherited_root_retired { "Inherited firmware page tables: retired" } else { @@ -712,7 +714,7 @@ pub fn kernel_main_foundation_hardening_phase3( report: FoundationHardeningPhase3Report, ) { console.write_line(""); - console.write_line("SanjuOS Foundation Hardening Phase 3"); + console.write_line("Soma OS Foundation Hardening Phase 3"); write_state( console, "Private process CR3 roots", @@ -819,7 +821,7 @@ impl M6aReport { /// Prints the M6A hardware-discovery acceptance report. pub fn kernel_main_m6a(console: &mut dyn Console, report: M6aReport) { console.write_line(""); - console.write_line("SanjuOS M6A PCI and Storage Discovery"); + console.write_line("Soma OS M6A PCI and Storage Discovery"); write_state( console, "PCI configuration mechanism #1", @@ -910,7 +912,7 @@ impl M6bReport { /// 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"); + console.write_line("Soma OS M6B Virtio Block Transport"); write_state( console, "Architecture-independent block-device API", @@ -983,6 +985,136 @@ pub fn kernel_main_m6b(console: &mut dyn Console, report: M6bReport) { } } +/// Runtime evidence for the M6C bounded cache and VFS gate. +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct M6cReport { + pub block_cache_active: bool, + pub cache_capacity_entries: usize, + pub first_read_miss_passed: bool, + pub repeat_read_hit_passed: bool, + pub cached_data_consistent: bool, + pub read_only_dirty_policy_active: bool, + pub rejected_cache_writes: u64, + pub dirty_cache_entries: usize, + pub vfs_contracts_active: bool, + pub mount_table_active: bool, + pub mounts: usize, + pub ramfs_adapter_active: bool, + pub path_normalization_passed: bool, + pub traversal_bounds_passed: bool, + pub user_handle_table_active: bool, + pub stale_handle_rejection_passed: bool, + pub persistent_writes_disabled: bool, + pub m6b_regression_passed: bool, +} + +impl M6cReport { + #[must_use] + pub const fn gate_passed(self) -> bool { + self.block_cache_active + && self.cache_capacity_entries > 0 + && self.first_read_miss_passed + && self.repeat_read_hit_passed + && self.cached_data_consistent + && self.read_only_dirty_policy_active + && self.rejected_cache_writes > 0 + && self.dirty_cache_entries == 0 + && self.vfs_contracts_active + && self.mount_table_active + && self.mounts > 0 + && self.ramfs_adapter_active + && self.path_normalization_passed + && self.traversal_bounds_passed + && self.user_handle_table_active + && self.stale_handle_rejection_passed + && self.persistent_writes_disabled + && self.m6b_regression_passed + } +} + +/// Prints the M6C cache and virtual-filesystem acceptance report. +pub fn kernel_main_m6c(console: &mut dyn Console, report: M6cReport) { + console.write_line(""); + console.write_line("Soma OS M6C Cache and Virtual Filesystem"); + write_state( + console, + "Fixed-capacity block cache", + report.block_cache_active, + ); + console.write_str("Block cache capacity sectors: "); + console.write_usize(report.cache_capacity_entries); + console.write_line(""); + console.write_line(if report.first_read_miss_passed { + "Cache first-read miss test: passed" + } else { + "Cache first-read miss test: failed" + }); + console.write_line(if report.repeat_read_hit_passed { + "Cache repeat-read hit test: passed" + } else { + "Cache repeat-read hit test: failed" + }); + console.write_line(if report.cached_data_consistent { + "Cached data consistency test: passed" + } else { + "Cached data consistency test: failed" + }); + write_state( + console, + "Read-only dirty-state policy", + report.read_only_dirty_policy_active, + ); + console.write_str("Rejected cache writes: "); + console.write_u64(report.rejected_cache_writes); + console.write_line(""); + console.write_str("Dirty cache entries: "); + console.write_usize(report.dirty_cache_entries); + console.write_line(""); + write_state(console, "VFS contracts", report.vfs_contracts_active); + write_state(console, "Bounded mount table", report.mount_table_active); + console.write_str("Mounted filesystems: "); + console.write_usize(report.mounts); + console.write_line(""); + write_state(console, "RAMFS VFS adapter", report.ramfs_adapter_active); + console.write_line(if report.path_normalization_passed { + "Absolute-path normalization test: passed" + } else { + "Absolute-path normalization test: failed" + }); + console.write_line(if report.traversal_bounds_passed { + "Path traversal bounds test: passed" + } else { + "Path traversal bounds test: failed" + }); + write_state( + console, + "Generation-protected user handle table", + report.user_handle_table_active, + ); + console.write_line(if report.stale_handle_rejection_passed { + "Stale file-handle rejection test: passed" + } else { + "Stale file-handle rejection test: failed" + }); + console.write_line(if report.persistent_writes_disabled { + "Persistent storage writes: disabled" + } else { + "Persistent storage writes: unsafe" + }); + console.write_line(if report.m6b_regression_passed { + "M6B regression under M6C: passed" + } else { + "M6B regression under M6C: failed" + }); + if report.gate_passed() { + console.write_line("M6C cache and VFS gate: passed"); + console.write_line("Next gate: validated read-only FAT32 mounting and persistent reads"); + } else { + console.write_line("M6C cache and VFS 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); @@ -1007,9 +1139,10 @@ fn write_state(console: &mut dyn Console, label: &str, active: bool) { mod tests { use super::{ BootInfo, Console, FoundationHardeningPhase2Report, FoundationHardeningPhase3Report, - 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, + FoundationHardeningReport, M4Report, M5Report, M6aReport, M6bReport, M6cReport, + 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, kernel_main_m6c, }; use std::string::String; @@ -1296,6 +1429,60 @@ mod tests { ); } + #[test] + fn m6c_banner_requires_clean_read_only_cache_and_vfs_evidence() { + let mut console = RecordingConsole::default(); + let report = M6cReport { + block_cache_active: true, + cache_capacity_entries: 16, + first_read_miss_passed: true, + repeat_read_hit_passed: true, + cached_data_consistent: true, + read_only_dirty_policy_active: true, + rejected_cache_writes: 1, + dirty_cache_entries: 0, + vfs_contracts_active: true, + mount_table_active: true, + mounts: 1, + ramfs_adapter_active: true, + path_normalization_passed: true, + traversal_bounds_passed: true, + user_handle_table_active: true, + stale_handle_rejection_passed: true, + persistent_writes_disabled: true, + m6b_regression_passed: true, + }; + kernel_main_m6c(&mut console, report); + assert!(report.gate_passed()); + assert!( + !M6cReport { + dirty_cache_entries: 1, + ..report + } + .gate_passed() + ); + assert!( + console + .output + .contains("Soma OS M6C Cache and Virtual Filesystem\r\n") + ); + assert!( + console + .output + .contains("Generation-protected user handle table: active\r\n") + ); + assert!( + console + .output + .contains("Persistent storage writes: disabled\r\n") + ); + assert!( + console + .output + .contains("M6C cache and VFS gate: passed\r\n") + ); + } + #[test] fn m5_banner_confirms_protected_userspace_gate() { let mut console = RecordingConsole::default(); @@ -1307,11 +1494,11 @@ mod tests { ) .unwrap(); kernel_main_m5(&mut console, info, sample_m5_report()); - assert!(console.output.contains("SanjuOS M5\r\n")); + assert!(console.output.contains("Soma OS M5\r\n")); assert!( console .output - .contains("SanjuOS page-table ownership: active\r\n") + .contains("Soma OS page-table ownership: active\r\n") ); assert!(console.output.contains("Ring 3 execution: active\r\n")); assert!(console.output.contains("System-call interface: active\r\n")); diff --git a/kernel/src/shell.rs b/kernel/src/shell.rs index 1fbca75..90df788 100644 --- a/kernel/src/shell.rs +++ b/kernel/src/shell.rs @@ -5,7 +5,8 @@ use core::str; use crate::Console; -use crate::fs::{FsError, RamFs}; +use crate::fs::{MAX_FILE_BYTES, RamFs}; +use crate::vfs::{HandleRights, MAX_PATH_BYTES, Vfs, VfsError}; const COMMAND_BUFFER_BYTES: usize = 128; @@ -27,6 +28,15 @@ pub struct ShellEnvironment { pub block_queue_size: usize, pub block_read_test_passed: bool, pub block_write_test_passed: bool, + pub cache_capacity: usize, + pub cache_hits: u64, + pub cache_misses: u64, + pub cache_device_reads: u64, + pub cache_dirty_entries: usize, + pub cache_read_only_policy: bool, + pub vfs_mounts: usize, + pub vfs_handle_capacity: usize, + pub vfs_path_normalization_passed: bool, } /// Interactive line editor and command dispatcher. @@ -50,7 +60,7 @@ impl Shell { /// Prints the shell banner and first prompt. pub fn start(console: &mut dyn Console) { console.write_line(""); - console.write_line("SanjuOS kernel shell ready."); + console.write_line("Soma OS kernel shell ready."); console.write_line("Type 'help' for commands."); write_prompt(console); } @@ -60,7 +70,7 @@ impl Shell { &mut self, byte: u8, console: &mut dyn Console, - fs: &mut RamFs, + vfs: &mut Vfs, environment: &ShellEnvironment, ) { match byte { @@ -74,7 +84,7 @@ impl Shell { if let Ok(line) = str::from_utf8(&command_copy[..command_len]) && !line.trim().is_empty() { - execute_line(line.trim(), console, fs, environment); + execute_line(line.trim(), console, vfs, environment); self.commands_executed = self.commands_executed.saturating_add(1); } write_prompt(console); @@ -119,7 +129,7 @@ impl Default for Shell { fn execute_line( line: &str, console: &mut dyn Console, - fs: &mut RamFs, + vfs: &mut Vfs, environment: &ShellEnvironment, ) { let mut parts = line.split_whitespace(); @@ -130,11 +140,11 @@ fn execute_line( match command { "help" => { console.write_line(concat!( - "Commands: help version uptime memory irq tasks pci block ls cat write echo ", - "clear userspace", + "Commands: help version uptime memory irq tasks pci block cache mounts ls cat ", + "write echo clear userspace", )); } - "version" => console.write_line("SanjuOS 0.0.10-prealpha (M6B)"), + "version" => console.write_line("Soma OS 0.0.11-prealpha (M6C)"), "uptime" => { console.write_str("Timer ticks: "); console.write_u64(environment.timer_ticks); @@ -190,21 +200,81 @@ fn execute_line( "failed" }); } + "cache" => { + console.write_str("Block cache: "); + console.write_usize(environment.cache_capacity); + console.write_str(" sectors, hits "); + console.write_u64(environment.cache_hits); + console.write_str(", misses "); + console.write_u64(environment.cache_misses); + console.write_str(", device reads "); + console.write_u64(environment.cache_device_reads); + console.write_str(", dirty "); + console.write_usize(environment.cache_dirty_entries); + console.write_str(", policy "); + console.write_line(if environment.cache_read_only_policy { + "read-only" + } else { + "unverified" + }); + } + "mounts" => { + console.write_str("VFS mounts: "); + console.write_usize(environment.vfs_mounts); + console.write_str(", handle capacity: "); + console.write_usize(environment.vfs_handle_capacity); + console.write_str(", normalized paths: "); + console.write_line(if environment.vfs_path_normalization_passed { + "active" + } else { + "inactive" + }); + vfs.mounts().visit(|mount| { + console.write_str(mount.path.as_str()); + console.write_str(" "); + console.write_str(mount.superblock.filesystem_name); + console.write_str(" "); + console.write_line(if mount.superblock.read_only { + "read-only" + } else { + "read-write" + }); + }); + } "ls" => { - if fs.file_count() == 0 { + let mut found = false; + let result = vfs.visit_directory("/", &mut |name, _inode| { + found = true; + console.write_line(name); + }); + if result.is_err() { + console.write_line("filesystem error"); + } else if !found { console.write_line(""); - } else { - fs.visit_names(|name| console.write_line(name)); } } "cat" => { - let Some(name) = parts.next() else { + let Some(input_path) = parts.next() else { console.write_line("usage: cat "); return; }; - match fs.read(name) { - Ok(data) => write_bytes(console, data), - Err(FsError::NotFound) => console.write_line("file not found"), + let mut path_storage = [0_u8; MAX_PATH_BYTES]; + let Some(path) = shell_path(input_path, &mut path_storage) else { + console.write_line("invalid path"); + return; + }; + match vfs.open(path, HandleRights::ReadOnly) { + Ok(handle) => { + let mut data = [0_u8; MAX_FILE_BYTES]; + let read_result = vfs.read(handle, &mut data); + let close_result = vfs.close(handle); + match (read_result, close_result) { + (Ok(read), Ok(())) => write_bytes(console, &data[..read]), + _ => console.write_line("filesystem error"), + } + } + Err(VfsError::NotFound) => console.write_line("file not found"), + Err(VfsError::Path(_)) => console.write_line("invalid path"), Err(_) => console.write_line("filesystem error"), } } @@ -219,11 +289,17 @@ fn execute_line( console.write_line("usage: write "); return; }; - match fs.write(name, data.as_bytes()) { - Ok(()) => console.write_line("written"), - Err(FsError::NameTooLong) => console.write_line("filename too long"), - Err(FsError::DataTooLarge) => console.write_line("file data too large"), - Err(FsError::FileTableFull) => console.write_line("file table full"), + let mut path_storage = [0_u8; MAX_PATH_BYTES]; + let Some(path) = shell_path(name, &mut path_storage) else { + console.write_line("invalid path"); + return; + }; + match vfs.create_or_replace(path, data.as_bytes()) { + Ok(_) => console.write_line("written"), + Err(VfsError::Path(_)) => console.write_line("invalid path"), + Err(VfsError::FileTooLarge) => console.write_line("file data too large"), + Err(VfsError::NoSpace) => console.write_line("file table full"), + Err(VfsError::ReadOnly) => console.write_line("filesystem is read-only"), Err(_) => console.write_line("filesystem error"), } } @@ -240,7 +316,20 @@ fn execute_line( } fn write_prompt(console: &mut dyn Console) { - console.write_str("sanju> "); + console.write_str("soma> "); +} + +fn shell_path<'a>(input: &'a str, storage: &'a mut [u8; MAX_PATH_BYTES]) -> Option<&'a str> { + if input.starts_with('/') { + return Some(input); + } + let required = input.len().checked_add(1)?; + if required > storage.len() { + return None; + } + storage[0] = b'/'; + storage[1..required].copy_from_slice(input.as_bytes()); + str::from_utf8(&storage[..required]).ok() } fn write_bytes(console: &mut dyn Console, bytes: &[u8]) { @@ -261,6 +350,7 @@ mod tests { use super::{Shell, ShellEnvironment}; use crate::Console; use crate::fs::RamFs; + use crate::vfs::Vfs; use std::string::String; #[derive(Default)] @@ -278,7 +368,7 @@ mod tests { fn shell_executes_commands_and_writes_files() { let mut shell = Shell::new(); let mut console = RecordingConsole::default(); - let mut fs = RamFs::with_defaults(); + let mut vfs = Vfs::new(RamFs::with_defaults()); Shell::start(&mut console); let environment = ShellEnvironment { @@ -289,10 +379,19 @@ mod tests { block_queue_size: 8, block_read_test_passed: true, block_write_test_passed: true, + cache_capacity: 16, + cache_hits: 1, + cache_misses: 1, + cache_device_reads: 1, + cache_dirty_entries: 0, + cache_read_only_policy: true, + vfs_mounts: 1, + vfs_handle_capacity: 32, + vfs_path_normalization_passed: true, ..ShellEnvironment::default() }; - for byte in b"write note.txt hello\ncat note.txt\npci\nblock\n" { - shell.feed_byte(*byte, &mut console, &mut fs, &environment); + for byte in b"write note.txt hello\ncat note.txt\npci\nblock\ncache\nmounts\n" { + shell.feed_byte(*byte, &mut console, &mut vfs, &environment); } assert!(console.output.contains("written\r\n")); @@ -305,6 +404,10 @@ mod tests { assert!(console.output.contains( "Virtio block: 16384 sectors, queue 8, read passed, write/readback passed\r\n" )); - assert_eq!(shell.commands_executed(), 4); + assert!(console.output.contains( + "Block cache: 16 sectors, hits 1, misses 1, device reads 1, dirty 0, policy read-only\r\n" + )); + assert!(console.output.contains("/ ramfs read-write\r\n")); + assert_eq!(shell.commands_executed(), 6); } } diff --git a/kernel/src/startup.rs b/kernel/src/startup.rs index 5eb6956..c555048 100644 --- a/kernel/src/startup.rs +++ b/kernel/src/startup.rs @@ -1,14 +1,13 @@ -//! SanjuOS branded startup experience. +//! Soma OS branded startup experience. use crate::Console; pub const STARTUP_LOGO: &[&str] = &[ - " _____ _ ____ _____ ", - " / ____| (_) / __ \\ / ____|", - "| (___ __ _ _ __ _ _ _| | | | (___ ", - " \\___ \\ / _` | '_ \\| | | | | | | |\\___ \\ ", - " ____) | (_| | | | | | |_| | |__| |____) |", - "|_____/ \\__,_|_| |_|_|\\__,_|\\____/|_____/ ", + " ____ ___ ____ ", + " / ___| ___ _ __ ___ __ _ / _ \\/ ___| ", + " \\___ \\ / _ \\| '_ ` _ \\ / _` | | | \\___ \\ ", + " ___) | (_) | | | | | | (_| | |_| |___) |", + " |____/ \\___/|_| |_| |_|\\__,_|\\___/|____/ ", ]; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -44,7 +43,7 @@ pub fn print_logo(console: &mut dyn Console) { for line in STARTUP_LOGO { console.write_line(line); } - console.write_line("Secure. Fast. Yours."); + console.write_line("Human. Private. Yours."); console.write_line(""); } @@ -55,7 +54,7 @@ pub fn print_stage(console: &mut dyn Console, stage: StartupStage, active: bool) pub fn print_failure(console: &mut dyn Console, code: &str, message: &str) { console.write_line(""); - console.write_line("SANJUOS STARTUP FAILURE"); + console.write_line("SOMA OS STARTUP FAILURE"); console.write_str("Code: "); console.write_line(code); console.write_str("Reason: "); @@ -78,11 +77,11 @@ mod tests { } #[test] - fn startup_prints_sanjuos_brand() { + fn startup_prints_soma_os_brand() { let mut console = RecordingConsole::default(); print_logo(&mut console); print_stage(&mut console, StartupStage::Userspace, true); - assert!(console.0.contains("Secure. Fast. Yours.")); + assert!(console.0.contains("Human. Private. Yours.")); assert!(console.0.contains("[OK] Protected userspace")); } } diff --git a/kernel/src/syscall.rs b/kernel/src/syscall.rs index 92a17f7..b2c3946 100644 --- a/kernel/src/syscall.rs +++ b/kernel/src/syscall.rs @@ -418,7 +418,7 @@ mod tests { #[test] fn dispatcher_validates_user_pointers_and_file_descriptors() { - let message = b"SanjuOS"; + let message = b"Soma OS"; let name = b"welcome.txt"; let mut memory = UserMemory::new(); assert!(memory.add_region(message.as_ptr().addr(), message.len(), true, false)); diff --git a/kernel/src/vfs.rs b/kernel/src/vfs.rs new file mode 100644 index 0000000..d416355 --- /dev/null +++ b/kernel/src/vfs.rs @@ -0,0 +1,742 @@ +#![allow(clippy::module_name_repetitions)] + +//! Allocation-free virtual-filesystem contracts for M6C. +//! +//! The first backend is RAMFS. The same inode, mount, path, directory, and +//! handle contracts form the boundary for the read-only FAT32 backend in M6D. + +use core::str; + +pub const MAX_PATH_BYTES: usize = 256; +pub const MAX_PATH_COMPONENTS: usize = 16; +pub const MAX_COMPONENT_BYTES: usize = 64; +pub const MAX_MOUNTS: usize = 4; +pub const MAX_USER_HANDLES: usize = 32; + +/// Path parsing and normalization failures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PathError { + Empty, + NotAbsolute, + InvalidCharacter, + PathTooLong, + ComponentTooLong, + TooManyComponents, +} + +/// Canonical absolute path stored without allocation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct NormalizedPath { + bytes: [u8; MAX_PATH_BYTES], + len: usize, + components: usize, +} + +impl NormalizedPath { + /// Returns the canonical root path. + #[must_use] + pub const fn root() -> Self { + let mut bytes = [0_u8; MAX_PATH_BYTES]; + bytes[0] = b'/'; + Self { + bytes, + len: 1, + components: 0, + } + } + + /// Normalizes an absolute UTF-8 path. + /// + /// Repeated separators and `.` are removed. `..` is bounded at the root, + /// so normalization can never escape the mounted namespace. + /// + /// # Errors + /// + /// Returns a [`PathError`] for relative, oversized, malformed, or overly + /// deep paths. + pub fn parse(raw: &str) -> Result { + if raw.is_empty() { + return Err(PathError::Empty); + } + if !raw.starts_with('/') { + return Err(PathError::NotAbsolute); + } + if raw.bytes().any(|byte| byte == 0 || byte.is_ascii_control()) { + return Err(PathError::InvalidCharacter); + } + + let mut path = Self::root(); + for component in raw[1..].split('/') { + if component.is_empty() || component == "." { + continue; + } + if component == ".." { + path.pop_component(); + continue; + } + if component.len() > MAX_COMPONENT_BYTES { + return Err(PathError::ComponentTooLong); + } + if path.components == MAX_PATH_COMPONENTS { + return Err(PathError::TooManyComponents); + } + + let separator = usize::from(path.len > 1); + let Some(required) = path + .len + .checked_add(separator) + .and_then(|length| length.checked_add(component.len())) + else { + return Err(PathError::PathTooLong); + }; + if required > MAX_PATH_BYTES { + return Err(PathError::PathTooLong); + } + if separator != 0 { + path.bytes[path.len] = b'/'; + path.len += 1; + } + let end = path.len + component.len(); + path.bytes[path.len..end].copy_from_slice(component.as_bytes()); + path.len = end; + path.components += 1; + } + Ok(path) + } + + /// Returns the canonical path text. + #[must_use] + pub fn as_str(&self) -> &str { + str::from_utf8(&self.bytes[..self.len]).unwrap_or("/") + } + + /// Returns the number of retained components. + #[must_use] + pub const fn component_count(&self) -> usize { + self.components + } + + /// Iterates over canonical path components. + pub fn components(&self) -> impl Iterator { + self.as_str().split('/').filter(|part| !part.is_empty()) + } + + /// Returns the final component, or `None` for root. + #[must_use] + pub fn file_name(&self) -> Option<&str> { + self.components().last() + } + + /// Returns the canonical parent path. + #[must_use] + pub fn parent(&self) -> Self { + let mut parent = *self; + parent.pop_component(); + parent + } + + fn pop_component(&mut self) { + if self.components == 0 { + return; + } + while self.len > 1 && self.bytes[self.len - 1] != b'/' { + self.len -= 1; + } + if self.len > 1 { + self.len -= 1; + } + self.bytes[self.len..].fill(0); + self.components -= 1; + } + + fn is_prefix_of(&self, other: &Self) -> bool { + if self.len == 1 { + return true; + } + if other.len < self.len || other.bytes[..self.len] != self.bytes[..self.len] { + return false; + } + other.len == self.len || other.bytes[self.len] == b'/' + } +} + +/// Stable identifier for one filesystem inode. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct InodeId(pub u64); + +/// Filesystem object type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NodeKind { + File, + Directory, +} + +/// Backend-independent inode metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Inode { + pub id: InodeId, + pub kind: NodeKind, + pub size: u64, +} + +/// Immutable metadata for one mounted filesystem. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Superblock { + pub filesystem_name: &'static str, + pub root_inode: InodeId, + pub block_size: u32, + pub read_only: bool, +} + +/// Backend-independent filesystem failures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VfsError { + Path(PathError), + NotFound, + NotDirectory, + IsDirectory, + ReadOnly, + FileTooLarge, + NoSpace, + InvalidOffset, + MountTableFull, + DuplicateMount, + UnsupportedMount, + HandleTableFull, + InvalidHandle, + StaleHandle, + Backend, +} + +impl From for VfsError { + fn from(value: PathError) -> Self { + Self::Path(value) + } +} + +/// Contract implemented by every VFS backend. +pub trait FileSystem { + fn superblock(&self) -> Superblock; + + fn lookup(&self, parent: InodeId, name: &str) -> Result; + + fn read(&self, inode: InodeId, offset: u64, destination: &mut [u8]) -> Result; + + fn create_or_replace( + &mut self, + parent: InodeId, + name: &str, + data: &[u8], + ) -> Result; + + fn visit_directory( + &self, + inode: InodeId, + visitor: &mut dyn FnMut(&str, Inode), + ) -> Result<(), VfsError>; +} + +/// Stable identifier for one mount-table entry. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MountId(pub u16); + +/// One occupied mount-table entry. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Mount { + pub id: MountId, + pub path: NormalizedPath, + pub superblock: Superblock, +} + +#[derive(Clone, Copy)] +struct MountSlot { + occupied: bool, + mount: Mount, +} + +impl MountSlot { + const fn empty() -> Self { + Self { + occupied: false, + mount: Mount { + id: MountId(0), + path: NormalizedPath::root(), + superblock: Superblock { + filesystem_name: "", + root_inode: InodeId(0), + block_size: 0, + read_only: true, + }, + }, + } + } +} + +/// Fixed-capacity mount table with longest-prefix resolution. +pub struct MountTable { + slots: [MountSlot; MAX_MOUNTS], + mount_count: usize, +} + +impl MountTable { + /// Creates a table with one root mount. + #[must_use] + pub fn with_root(superblock: Superblock) -> Self { + let mut table = Self { + slots: [MountSlot::empty(); MAX_MOUNTS], + mount_count: 1, + }; + table.slots[0] = MountSlot { + occupied: true, + mount: Mount { + id: MountId(0), + path: NormalizedPath::root(), + superblock, + }, + }; + table + } + + /// Adds a mount contract. + /// + /// # Errors + /// + /// Returns an error when the path is invalid, already mounted, or the + /// table is full. + pub fn mount(&mut self, raw_path: &str, superblock: Superblock) -> Result { + let path = NormalizedPath::parse(raw_path)?; + if self + .slots + .iter() + .any(|slot| slot.occupied && slot.mount.path == path) + { + return Err(VfsError::DuplicateMount); + } + let Some(index) = self.slots.iter().position(|slot| !slot.occupied) else { + return Err(VfsError::MountTableFull); + }; + let id = MountId(u16::try_from(index).map_err(|_| VfsError::MountTableFull)?); + self.slots[index] = MountSlot { + occupied: true, + mount: Mount { + id, + path, + superblock, + }, + }; + self.mount_count += 1; + Ok(id) + } + + /// Resolves a path to its most-specific mount. + #[must_use] + pub fn resolve(&self, path: &NormalizedPath) -> Option { + self.slots + .iter() + .filter(|slot| slot.occupied && slot.mount.path.is_prefix_of(path)) + .max_by_key(|slot| slot.mount.path.len) + .map(|slot| slot.mount) + } + + /// Returns the number of occupied entries. + #[must_use] + pub const fn len(&self) -> usize { + self.mount_count + } + + /// Returns whether the table has no mounts. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.mount_count == 0 + } + + /// Visits every occupied mount. + pub fn visit(&self, mut visitor: impl FnMut(Mount)) { + for slot in self.slots.iter().filter(|slot| slot.occupied) { + visitor(slot.mount); + } + } +} + +/// Rights attached to one open handle. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum HandleRights { + ReadOnly, + ReadWrite, +} + +/// Generation-protected user-visible handle identifier. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FileHandleId { + pub slot: u16, + pub generation: u16, +} + +/// Open-file state retained by the kernel. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FileHandle { + pub inode: InodeId, + pub mount: MountId, + pub offset: u64, + pub rights: HandleRights, +} + +#[derive(Clone, Copy)] +struct HandleSlot { + generation: u16, + handle: Option, +} + +impl HandleSlot { + const fn empty() -> Self { + Self { + generation: 1, + handle: None, + } + } +} + +/// Fixed-capacity process-facing handle table. +pub struct UserHandleTable { + slots: [HandleSlot; MAX_USER_HANDLES], + active: usize, +} + +impl UserHandleTable { + #[must_use] + pub const fn new() -> Self { + Self { + slots: [HandleSlot::empty(); MAX_USER_HANDLES], + active: 0, + } + } + + /// Allocates one handle slot. + /// + /// # Errors + /// + /// Returns [`VfsError::HandleTableFull`] when no slot is available. + pub fn open(&mut self, handle: FileHandle) -> Result { + let Some(index) = self.slots.iter().position(|slot| slot.handle.is_none()) else { + return Err(VfsError::HandleTableFull); + }; + let slot = &mut self.slots[index]; + slot.handle = Some(handle); + self.active += 1; + Ok(FileHandleId { + slot: u16::try_from(index).map_err(|_| VfsError::HandleTableFull)?, + generation: slot.generation, + }) + } + + /// Returns an immutable open handle. + /// + /// # Errors + /// + /// Rejects out-of-range, closed, and stale identifiers. + pub fn get(&self, id: FileHandleId) -> Result<&FileHandle, VfsError> { + let slot = self + .slots + .get(usize::from(id.slot)) + .ok_or(VfsError::InvalidHandle)?; + if slot.generation != id.generation { + return Err(VfsError::StaleHandle); + } + slot.handle.as_ref().ok_or(VfsError::InvalidHandle) + } + + /// Returns a mutable open handle. + /// + /// # Errors + /// + /// Rejects out-of-range, closed, and stale identifiers. + pub fn get_mut(&mut self, id: FileHandleId) -> Result<&mut FileHandle, VfsError> { + let slot = self + .slots + .get_mut(usize::from(id.slot)) + .ok_or(VfsError::InvalidHandle)?; + if slot.generation != id.generation { + return Err(VfsError::StaleHandle); + } + slot.handle.as_mut().ok_or(VfsError::InvalidHandle) + } + + /// Closes a handle and advances its generation. + /// + /// # Errors + /// + /// Rejects out-of-range, already-closed, and stale identifiers. + pub fn close(&mut self, id: FileHandleId) -> Result<(), VfsError> { + let slot = self + .slots + .get_mut(usize::from(id.slot)) + .ok_or(VfsError::InvalidHandle)?; + if slot.generation != id.generation { + return Err(VfsError::StaleHandle); + } + if slot.handle.take().is_none() { + return Err(VfsError::InvalidHandle); + } + slot.generation = slot.generation.wrapping_add(1); + if slot.generation == 0 { + slot.generation = 1; + } + self.active -= 1; + Ok(()) + } + + #[must_use] + pub const fn len(&self) -> usize { + self.active + } + + #[must_use] + pub const fn is_empty(&self) -> bool { + self.active == 0 + } + + #[must_use] + pub const fn capacity(&self) -> usize { + MAX_USER_HANDLES + } +} + +impl Default for UserHandleTable { + fn default() -> Self { + Self::new() + } +} + +/// Root VFS instance for one concrete backend. +pub struct Vfs { + root: F, + mounts: MountTable, + handles: UserHandleTable, +} + +impl Vfs { + #[must_use] + pub fn new(root: F) -> Self { + let mounts = MountTable::with_root(root.superblock()); + Self { + root, + mounts, + handles: UserHandleTable::new(), + } + } + + /// Resolves one canonical path to inode metadata. + /// + /// # Errors + /// + /// Returns path, mount, lookup, or type errors from the VFS/backend. + pub fn resolve(&self, raw_path: &str) -> Result { + let path = NormalizedPath::parse(raw_path)?; + let mount = self + .mounts + .resolve(&path) + .ok_or(VfsError::UnsupportedMount)?; + if mount.id != MountId(0) { + return Err(VfsError::UnsupportedMount); + } + + let mut inode = Inode { + id: mount.superblock.root_inode, + kind: NodeKind::Directory, + size: 0, + }; + for component in path.components() { + if inode.kind != NodeKind::Directory { + return Err(VfsError::NotDirectory); + } + inode = self.root.lookup(inode.id, component)?; + } + Ok(inode) + } + + /// Opens one non-directory object. + /// + /// # Errors + /// + /// Returns resolution, permission, type, or handle-capacity errors. + pub fn open(&mut self, raw_path: &str, rights: HandleRights) -> Result { + let inode = self.resolve(raw_path)?; + if inode.kind == NodeKind::Directory { + return Err(VfsError::IsDirectory); + } + let superblock = self.root.superblock(); + if rights == HandleRights::ReadWrite && superblock.read_only { + return Err(VfsError::ReadOnly); + } + self.handles.open(FileHandle { + inode: inode.id, + mount: MountId(0), + offset: 0, + rights, + }) + } + + /// Reads from the current handle offset and advances it. + /// + /// # Errors + /// + /// Returns stale-handle, offset, or backend errors. + pub fn read(&mut self, id: FileHandleId, destination: &mut [u8]) -> Result { + let handle = *self.handles.get(id)?; + if handle.mount != MountId(0) { + return Err(VfsError::UnsupportedMount); + } + let read = self.root.read(handle.inode, handle.offset, destination)?; + let next_offset = handle + .offset + .checked_add(u64::try_from(read).map_err(|_| VfsError::InvalidOffset)?) + .ok_or(VfsError::InvalidOffset)?; + self.handles.get_mut(id)?.offset = next_offset; + Ok(read) + } + + /// Closes one open handle. + /// + /// # Errors + /// + /// Returns invalid or stale-handle errors. + pub fn close(&mut self, id: FileHandleId) -> Result<(), VfsError> { + self.handles.close(id) + } + + /// Creates or replaces one file through the mounted backend. + /// + /// # Errors + /// + /// Returns path, mount, permission, or backend errors. + pub fn create_or_replace(&mut self, raw_path: &str, data: &[u8]) -> Result { + let path = NormalizedPath::parse(raw_path)?; + let name = path.file_name().ok_or(VfsError::IsDirectory)?; + let parent = self.resolve(path.parent().as_str())?; + if parent.kind != NodeKind::Directory { + return Err(VfsError::NotDirectory); + } + if self.root.superblock().read_only { + return Err(VfsError::ReadOnly); + } + self.root.create_or_replace(parent.id, name, data) + } + + /// Visits one directory through its backend. + /// + /// # Errors + /// + /// Returns path, type, mount, or backend errors. + pub fn visit_directory( + &self, + raw_path: &str, + visitor: &mut dyn FnMut(&str, Inode), + ) -> Result<(), VfsError> { + let inode = self.resolve(raw_path)?; + if inode.kind != NodeKind::Directory { + return Err(VfsError::NotDirectory); + } + self.root.visit_directory(inode.id, visitor) + } + + #[must_use] + pub const fn mounts(&self) -> &MountTable { + &self.mounts + } + + #[must_use] + pub const fn handles(&self) -> &UserHandleTable { + &self.handles + } + + #[must_use] + pub const fn backend(&self) -> &F { + &self.root + } +} + +#[cfg(test)] +mod tests { + use super::{ + FileHandle, HandleRights, InodeId, MAX_PATH_COMPONENTS, MountId, MountTable, + NormalizedPath, PathError, Superblock, UserHandleTable, Vfs, VfsError, + }; + use crate::fs::RamFs; + + #[test] + fn path_normalization_is_absolute_bounded_and_canonical() { + let path = NormalizedPath::parse("/docs//./draft/../welcome.txt").unwrap(); + assert_eq!(path.as_str(), "/docs/welcome.txt"); + assert_eq!(path.component_count(), 2); + assert_eq!(NormalizedPath::parse("/../../").unwrap().as_str(), "/"); + assert_eq!( + NormalizedPath::parse("relative/path"), + Err(PathError::NotAbsolute) + ); + + let too_deep = "/x".repeat(MAX_PATH_COMPONENTS + 1); + assert_eq!( + NormalizedPath::parse(&too_deep), + Err(PathError::TooManyComponents) + ); + } + + #[test] + fn mount_resolution_uses_longest_component_boundary_prefix() { + let root = Superblock { + filesystem_name: "ramfs", + root_inode: InodeId(1), + block_size: 1, + read_only: false, + }; + let disk = Superblock { + filesystem_name: "fat32", + root_inode: InodeId(2), + block_size: 512, + read_only: true, + }; + let mut mounts = MountTable::with_root(root); + let disk_id = mounts.mount("/disk", disk).unwrap(); + + let resolved = mounts + .resolve(&NormalizedPath::parse("/disk/readme.txt").unwrap()) + .unwrap(); + assert_eq!(resolved.id, disk_id); + let root_resolved = mounts + .resolve(&NormalizedPath::parse("/diskette").unwrap()) + .unwrap(); + assert_eq!(root_resolved.id, MountId(0)); + assert_eq!(mounts.mount("/disk/", disk), Err(VfsError::DuplicateMount)); + } + + #[test] + fn ramfs_resolves_and_reads_through_generation_checked_handle() { + let mut vfs = Vfs::new(RamFs::with_defaults()); + let handle = vfs.open("/./welcome.txt", HandleRights::ReadOnly).unwrap(); + let mut data = [0_u8; 512]; + let read = vfs.read(handle, &mut data).unwrap(); + assert!(data[..read].starts_with(b"Welcome")); + vfs.close(handle).unwrap(); + assert_eq!(vfs.read(handle, &mut data), Err(VfsError::StaleHandle)); + } + + #[test] + fn handle_table_is_fixed_capacity_and_rejects_stale_ids() { + let mut handles = UserHandleTable::new(); + let handle = FileHandle { + inode: InodeId(7), + mount: MountId(0), + offset: 0, + rights: HandleRights::ReadOnly, + }; + let mut ids = [None; super::MAX_USER_HANDLES]; + for id in &mut ids { + *id = Some(handles.open(handle).unwrap()); + } + assert_eq!(handles.open(handle), Err(VfsError::HandleTableFull)); + let first = ids[0].unwrap(); + handles.close(first).unwrap(); + assert_eq!(handles.get(first), Err(VfsError::StaleHandle)); + assert!(handles.open(handle).is_ok()); + } +} diff --git a/scripts/build-user-programs.sh b/scripts/build-user-programs.sh index f87e0c4..fb1d9d2 100755 --- a/scripts/build-user-programs.sh +++ b/scripts/build-user-programs.sh @@ -36,4 +36,4 @@ for program in init hello fault-test; do -o "user/programs/bin/${program}.elf" done -echo "Built SanjuOS M5 user programs." +echo "Built Soma OS M5 user programs." diff --git a/scripts/generate-capabilities.py b/scripts/generate-capabilities.py index 1691388..b065a60 100644 --- a/scripts/generate-capabilities.py +++ b/scripts/generate-capabilities.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate SanjuOS capability data, documentation, and smoke expectations.""" +"""Generate Soma OS capability data, documentation, and smoke expectations.""" from __future__ import annotations @@ -70,7 +70,7 @@ def render_rust(version: int, entries: list[dict[str, object]]) -> str: def render_docs(version: int, entries: list[dict[str, object]]) -> str: lines = [ - "# SanjuOS Capability Matrix", + "# Soma OS Capability Matrix", "", f"Registry version: **{version}**", "", @@ -128,7 +128,7 @@ def main() -> int: valid = all(write_or_check(path, content, args.check) for path, content in outputs) if valid: action = "verified" if args.check else "generated" - print(f"SanjuOS capability registry {action}: {len(entries)} entries") + print(f"Soma OS capability registry {action}: {len(entries)} entries") return 0 return 1 diff --git a/scripts/generate-source-manifest.py b/scripts/generate-source-manifest.py index 7d1190a..2bd05d9 100644 --- a/scripts/generate-source-manifest.py +++ b/scripts/generate-source-manifest.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate or verify SanjuOS's canonical source-integrity manifest.""" +"""Generate or verify Soma OS's canonical source-integrity manifest.""" from __future__ import annotations @@ -68,7 +68,7 @@ def main() -> int: if args.check: if not MANIFEST.exists() or MANIFEST.read_text(encoding="utf-8") != rendered: raise SystemExit("SOURCE_MANIFEST.sha256 is out of date") - print("SanjuOS source manifest is current.") + print("Soma OS source manifest is current.") return 0 MANIFEST.write_text(rendered, encoding="utf-8", newline="\n") diff --git a/scripts/setup.sh b/scripts/setup.sh index 1ca6ade..5772692 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -14,4 +14,4 @@ rustup toolchain install 1.97.0 \ rustup override set 1.97.0 rustup show active-toolchain -printf '%s\n' "SanjuOS Rust 1.97.0 toolchain is ready." +printf '%s\n' "Soma OS Rust 1.97.0 toolchain is ready." diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 9818789..137573e 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -56,12 +56,12 @@ if [[ "$qemu_status" -ne 33 ]]; then exit 1 fi -grep -Fq "SanjuOS M5 boot transition" build/qemu-debug.log +grep -Fq "Soma OS M5 boot transition" build/qemu-debug.log grep -Fq "Milestone M5: protected user-space foundation and branded startup." build/qemu-debug.log -grep -Fq "init: SanjuOS protected userspace online" build/qemu-debug.log -grep -Fq "hello: running from SanjuOS Ring 3" build/qemu-debug.log -grep -Fq "SanjuOS: isolated user exception" build/qemu-debug.log -grep -Fq "SanjuOS page-table ownership: active" build/qemu-debug.log +grep -Fq "init: Soma OS protected userspace online" build/qemu-debug.log +grep -Fq "hello: running from Soma OS Ring 3" build/qemu-debug.log +grep -Fq "Soma OS: isolated user exception" build/qemu-debug.log +grep -Fq "Soma OS page-table ownership: active" build/qemu-debug.log grep -Fq "Kernel heap: active" build/qemu-debug.log grep -Fq "Ring 3 execution: active" build/qemu-debug.log grep -Fq "User address-space model: active" build/qemu-debug.log @@ -69,7 +69,7 @@ grep -Fq "System-call interface: active" build/qemu-debug.log grep -Fq "ELF64 loader: active" build/qemu-debug.log grep -Fq "User processes launched: 3" build/qemu-debug.log grep -Fq "User fault isolation: passed" build/qemu-debug.log -grep -Fq "SanjuOS logo print: active" build/qemu-debug.log +grep -Fq "Soma OS logo print: active" build/qemu-debug.log grep -Fq "M5 protected user-space gate: passed" build/qemu-debug.log while IFS= read -r expected_line; do [[ -z "$expected_line" || "$expected_line" == \#* ]] && continue @@ -92,14 +92,14 @@ grep -Fq "Ring 3 preemption processes: 2" build/qemu-debug.log grep -Fq "M5 regression under private CR3: passed" build/qemu-debug.log grep -Fq "FH2 paging regression under FH3: passed" build/qemu-debug.log grep -Fq "Foundation hardening phase 3: passed" build/qemu-debug.log -grep -Fq "SanjuOS M6A PCI and Storage Discovery" build/qemu-debug.log +grep -Fq "Soma OS M6A PCI and Storage Discovery" build/qemu-debug.log grep -Fq "PCI configuration mechanism #1: active" build/qemu-debug.log grep -Fq "PCI inventory completeness: active" build/qemu-debug.log 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 "Soma OS 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 @@ -113,10 +113,31 @@ 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 "Soma OS M6C Cache and Virtual Filesystem" build/qemu-debug.log +grep -Fq "Fixed-capacity block cache: active" build/qemu-debug.log +grep -Fq "Cache first-read miss test: passed" build/qemu-debug.log +grep -Fq "Cache repeat-read hit test: passed" build/qemu-debug.log +grep -Fq "Cached data consistency test: passed" build/qemu-debug.log +grep -Fq "Read-only dirty-state policy: active" build/qemu-debug.log +grep -Fq "Dirty cache entries: 0" build/qemu-debug.log +grep -Fq "VFS contracts: active" build/qemu-debug.log +grep -Fq "Bounded mount table: active" build/qemu-debug.log +grep -Fq "RAMFS VFS adapter: active" build/qemu-debug.log +grep -Fq "Absolute-path normalization test: passed" build/qemu-debug.log +grep -Fq "Path traversal bounds test: passed" build/qemu-debug.log +grep -Fq "Generation-protected user handle table: active" build/qemu-debug.log +grep -Fq "Stale file-handle rejection test: passed" build/qemu-debug.log +grep -Fq "Persistent storage writes: disabled" build/qemu-debug.log +grep -Fq "M6B regression under M6C: passed" build/qemu-debug.log +grep -Fq "M6C cache and VFS gate: passed" build/qemu-debug.log +grep -Fq "Soma OS 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 +grep -Fq "Block cache: 16 sectors, hits 1, misses 1, device reads 1, dirty 0, policy read-only" build/qemu-debug.log +grep -Fq "VFS mounts: 1, handle capacity: 32, normalized paths: active" build/qemu-debug.log +grep -Fq "/ ramfs read-write" build/qemu-debug.log +grep -Fq "Welcome to Soma OS." 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 c7aee04..b706b8c 100755 --- a/scripts/source-check.py +++ b/scripts/source-check.py @@ -32,6 +32,8 @@ PROCESS = ROOT / "kernel/src/process.rs" PCI = ROOT / "kernel/src/pci.rs" BLOCK = ROOT / "kernel/src/block.rs" +CACHE = ROOT / "kernel/src/cache.rs" +VFS = ROOT / "kernel/src/vfs.rs" SYSCALL = ROOT / "kernel/src/syscall.rs" ELF = ROOT / "kernel/src/elf.rs" STARTUP = ROOT / "kernel/src/startup.rs" @@ -352,6 +354,8 @@ def main() -> int: process = PROCESS.read_text(encoding="utf-8") pci = PCI.read_text(encoding="utf-8") block = BLOCK.read_text(encoding="utf-8") + cache = CACHE.read_text(encoding="utf-8") + vfs = VFS.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") @@ -381,7 +385,10 @@ def main() -> int: require(boot, "kernel_main_m6a", BOOT) require(boot, "M6bReport", BOOT) require(boot, "kernel_main_m6b", BOOT) + require(boot, "M6cReport", BOOT) + require(boot, "kernel_main_m6c", BOOT) require(boot, "cpu::initialize_virtio_block", BOOT) + require(boot, "BlockCache::<_, DEFAULT_CACHE_ENTRIES>::new", 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) @@ -474,12 +481,25 @@ def main() -> int: require(block, "pub trait BlockDevice", BLOCK) require(block, "pub struct BlockGeometry", BLOCK) require(block, "pub const fn validate_sector_range", BLOCK) + require(cache, "pub struct BlockCache", CACHE) + require(cache, "pub enum DirtyStatePolicy", CACHE) + require(cache, "RejectWrites", CACHE) + require(cache, "fn repeat_read_hits_cache_without_second_device_request", CACHE) + require(cache, "fn read_only_policy_rejects_write_before_device", CACHE) + require(cache, "fn failed_device_read_preserves_existing_cached_entry", CACHE) + require(vfs, "pub struct NormalizedPath", VFS) + require(vfs, "pub struct Superblock", VFS) + require(vfs, "pub struct MountTable", VFS) + require(vfs, "pub struct UserHandleTable", VFS) + require(vfs, "pub trait FileSystem", VFS) + require(vfs, "fn path_normalization_is_absolute_bounded_and_canonical", VFS) + require(vfs, "fn ramfs_resolves_and_reads_through_generation_checked_handle", VFS) require(syscall, "pub struct SyscallDispatcher", SYSCALL) for syscall_name in ("Write", "Read", "Exit", "Yield", "GetPid", "Open", "Close", "Spawn"): require(syscall, syscall_name, SYSCALL) require(elf, "load_position_independent", ELF) require(startup, "STARTUP_LOGO", STARTUP) - require(startup, "Secure. Fast. Yours.", STARTUP) + require(startup, "Human. Private. Yours.", STARTUP) require(boot_info, "pub struct BootInfoV1", BOOT_INFO) require(boot_info, "pub struct FramebufferInfo", BOOT_INFO) require(boot_info, "pub active_page_table_root: u64", BOOT_INFO) @@ -495,11 +515,14 @@ def main() -> int: ): require(ownership, f"fn {test_name}", OWNERSHIP) require(capability_registry, "SYS-TC-001", CAPABILITY_REGISTRY) - require(capability_registry, 'registry_version = 5', CAPABILITY_REGISTRY) + require(capability_registry, 'registry_version = 6', 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, "STOR-CACHE-001", CAPABILITY_REGISTRY) + require(capability_registry, "VFS-CORE-001", CAPABILITY_REGISTRY) + require(capability_registry, "VFS-HANDLE-001", CAPABILITY_REGISTRY) require(capability_registry, "software_model", CAPABILITY_REGISTRY) require(toolchain, 'channel = "1.97.0"', TOOLCHAIN) require(toolchain, 'components = ["clippy", "rustfmt"]', TOOLCHAIN) @@ -510,10 +533,12 @@ def main() -> int: require(kernel, "pub struct FoundationHardeningPhase3Report", KERNEL) require(kernel, "pub struct M6aReport", KERNEL) require(kernel, "pub struct M6bReport", KERNEL) + require(kernel, "pub struct M6cReport", 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, "M6C cache and VFS gate: passed", KERNEL) require(kernel, "Ring 3 execution", KERNEL) require(kernel, "M5 protected user-space gate: passed", KERNEL) require( @@ -530,6 +555,8 @@ def main() -> int: require(smoke, "serial=SANJU-M6B", SMOKE) require(smoke, "M6B block transport gate: passed", SMOKE) require(smoke, "Disposable sector restoration: passed", SMOKE) + require(smoke, "M6C cache and VFS gate: passed", SMOKE) + require(smoke, "Persistent storage writes: disabled", SMOKE) for executable in (INIT_ELF, HELLO_ELF, FAULT_ELF): validate_elf64(executable) if LOGO.read_bytes()[:8] != b"\x89PNG\r\n\x1a\n": @@ -583,7 +610,7 @@ def main() -> int: ) validate_source_manifest() - print("SanjuOS M6B virtio-block transport source checks passed.") + print("Soma OS M6C cache and VFS 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") diff --git a/user/programs/bin/hello.elf b/user/programs/bin/hello.elf index cb28f1c57600fdc527d57fd6291a1a8293e76e85..465bfe9e55b49431e6d2cf03bf9520c592e7740f 100755 GIT binary patch delta 17 YcmdnMzkz>)5)(^)Zlc0wHKs0B05XLI9smFU delta 17 YcmdnMzkz>)5)(^eURLR5HKs0B05nVmZvX%Q diff --git a/user/programs/bin/init.elf b/user/programs/bin/init.elf index de071eaf4a870ec5c02e49ac2f0e477f5cd79d99..4445dc9e357ed9493f0781bd11f3cbddcc4db961 100755 GIT binary patch delta 17 YcmX@Xe}aF52op@~ diff --git a/user/programs/src/hello.S b/user/programs/src/hello.S index 01ca38c..2576fa4 100644 --- a/user/programs/src/hello.S +++ b/user/programs/src/hello.S @@ -16,5 +16,5 @@ _start: ud2 .section .rodata message: - .ascii "hello: running from SanjuOS Ring 3\n" + .ascii "hello: running from Soma OS Ring 3\n" .set message_len, . - message diff --git a/user/programs/src/init.S b/user/programs/src/init.S index c4bd4d0..52f9468 100644 --- a/user/programs/src/init.S +++ b/user/programs/src/init.S @@ -21,5 +21,5 @@ _start: ud2 .section .rodata message: - .ascii "init: SanjuOS protected userspace online\n" + .ascii "init: Soma OS protected userspace online\n" .set message_len, . - message