Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,35 @@ jobs:
run: cargo test --workspace

qemu:
name: Build boot image
name: Build and boot smoke test
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: llvm-tools-preview
components: llvm-tools-preview, rust-src
- name: Install QEMU
run: sudo apt-get update && sudo apt-get install -y qemu-system-x86
- name: Build boot image
run: cargo build --verbose
- name: Boot BIOS image and assert serial success marker
shell: bash
run: |
set +e
timeout --signal=TERM --kill-after=5s 20s cargo run -- bios > qemu.log 2>&1
status=$?
set -e
cat qemu.log

if ! grep -Fq "[BOOT_OK] Rustalis booted successfully." qemu.log; then
echo "Kernel did not emit the expected boot-success marker."
exit 1
fi

# A healthy kernel intentionally remains in its idle loop; timeout
# is therefore expected after the marker has been observed.
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
echo "QEMU runner exited unexpectedly with status $status."
exit "$status"
fi
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ On Windows PowerShell:
cargo run -- uefi
```

For common toolchain, QEMU, firmware, and boot troubleshooting, see [docs/troubleshooting.md](docs/troubleshooting.md).

## Contributing

Rustalis is intended to be a genuine open-source collaboration project. Start with the issue tracker before implementing a large subsystem.
Expand Down
57 changes: 57 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Troubleshooting Rustalis

This guide separates host setup failures from kernel boot failures. Rustalis is experimental; a successful image build does not prove that the kernel boots correctly.

## Prerequisites

- Use the toolchain specified by `rust-toolchain.toml` (Rust nightly).
- Install Cargo and Git.
- Install QEMU with the `qemu-system-x86_64` executable available on `PATH`.
- For UEFI runs, the runner pins OVMF to EDK2 stable 202508-r1 through `Source::EDK2_STABLE202508_R1`. The `ovmf-prebuilt` crate validates the downloaded archive against its embedded SHA-256 and caches firmware under `target/ovmf`.

Check the toolchain and QEMU executable:

```sh
rustc --version
cargo --version
qemu-system-x86_64 --version
```

## Build or test failures

Run the checks individually to identify the failing stage:

```sh
cargo fmt --all -- --check
cargo check --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo build
```

- **Toolchain/target errors:** confirm Rustup selected the repository's toolchain and that the target named in the error is installed.
- **Dependency errors:** check network access and Cargo's registry cache, then retry the failing command.
- **Formatting errors:** run `cargo fmt --all` and inspect the resulting diff before committing.

## Host runner failures

Run either supported mode:

```sh
cargo run -- uefi
cargo run -- bios
```

- **QEMU cannot start:** install the QEMU x86_64 system emulator and ensure `qemu-system-x86_64` is on `PATH`.
- **UEFI firmware acquisition fails:** check network access and write permissions for the repository's `target/ovmf` cache. BIOS mode does not require OVMF.
- **Invalid command:** use exactly one mode argument, `uefi` or `bios`; use `--help` for usage.

## Kernel boot failures

The QEMU runner configures serial output and disables the graphical display. Read the serial log first:

- If no kernel log appears, investigate image creation, boot firmware, and QEMU configuration.
- If initialization logs appear but the final success message is absent, investigate the last subsystem log before the failure.
- A successful `cargo build` only verifies image construction. Treat runtime boot as verified only after the kernel actually runs and its expected serial output is observed.

When reporting a failure, include the command, host OS, Rust toolchain version, QEMU version, and complete relevant serial/error output. Do not include secrets or private environment values.
4 changes: 3 additions & 1 deletion kernel/src/arch/x86_64/interrupts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ pub struct InterruptStackFrame {
pub fn init() {
// SAFETY: maskable interrupts stay disabled until an IDT and handlers
// have been installed. This prevents delivery to an incomplete table.
unsafe { asm!("cli", options(nomem, nostack, preserves_flags)); }
unsafe {
asm!("cli", options(nomem, nostack, preserves_flags));
}
}
87 changes: 78 additions & 9 deletions kernel/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
//! This is a RAM-backed prototype, not persistent storage. It deliberately avoids
//! allocation so it can be used before the kernel heap and block-device layer exist.

const MAX_PATH_BYTES: usize = 64;
const MAX_FILE_BYTES: usize = 256;
/// Maximum path length in bytes, including the leading slash.
pub const MAX_PATH_BYTES: usize = 64;
/// Maximum file content size in bytes.
pub const MAX_FILE_BYTES: usize = 256;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FsError {
Expand All @@ -17,6 +19,9 @@ pub enum FsError {
}

/// A small, allocation-free filesystem suitable for kernel bring-up.
///
/// Each slot stores a fixed-size path and content buffer. On x86_64, an entry
/// currently occupies approximately 344 bytes; exact layout is compiler-dependent.
pub struct RamFs<const FILES: usize> {
entries: [Entry; FILES],
}
Expand Down Expand Up @@ -59,7 +64,10 @@ impl<const FILES: usize> RamFs<FILES> {
if self.find(path).is_some() {
return Err(FsError::AlreadyExists);
}
let slot = self.entries.iter().position(|entry| !entry.used)
let slot = self
.entries
.iter()
.position(|entry| !entry.used)
.ok_or(FsError::NoFreeSlots)?;
let entry = &mut self.entries[slot];
entry.path[..path.len()].copy_from_slice(path.as_bytes());
Expand Down Expand Up @@ -100,16 +108,21 @@ impl<const FILES: usize> RamFs<FILES> {

/// Return the path of the Nth occupied slot, for simple directory listings.
pub fn path_at(&self, occupied_index: usize) -> Option<&str> {
self.entries.iter().filter(|entry| entry.used)
.nth(occupied_index).map(Entry::path)
self.entries
.iter()
.filter(|entry| entry.used)
.nth(occupied_index)
.map(Entry::path)
}

pub fn file_count(&self) -> usize {
self.entries.iter().filter(|entry| entry.used).count()
}

fn find(&self, path: &str) -> Option<usize> {
self.entries.iter().position(|entry| entry.used && entry.path() == path)
self.entries
.iter()
.position(|entry| entry.used && entry.path() == path)
}
}

Expand All @@ -120,15 +133,21 @@ fn validate_path(path: &str) -> Result<&str, FsError> {
if path.len() > MAX_PATH_BYTES {
return Err(FsError::PathTooLong);
}
if path.split('/').any(|part| part.is_empty() || part == "." || part == "..") {
if path
.split('/')
.any(|part| part.is_empty() || part == "." || part == "..")
{
return Err(FsError::InvalidPath);
}
Ok(path)
}

#[cfg(test)]
mod tests {
use super::{FsError, RamFs};
extern crate std;

use super::{FsError, MAX_FILE_BYTES, MAX_PATH_BYTES, RamFs};
use std::format;

#[test]
fn create_write_read_and_remove() {
Expand Down Expand Up @@ -156,7 +175,57 @@ mod tests {
fs.create("/a").unwrap();
assert_eq!(fs.create("/b"), Err(FsError::NoFreeSlots));
fs.write("/a", b"ok").unwrap();
assert_eq!(fs.write("/a", &[0; 257]), Err(FsError::FileTooLarge));
assert_eq!(
fs.write("/a", &[0; MAX_FILE_BYTES + 1]),
Err(FsError::FileTooLarge)
);
assert_eq!(fs.read("/a").unwrap(), b"ok");
}

#[test]
fn handles_empty_files_and_content_replacement_boundaries() {
let mut fs = RamFs::<1>::new();
fs.create("/data").unwrap();
assert_eq!(fs.read("/data").unwrap(), b"");

let maximum = [b'x'; MAX_FILE_BYTES];
fs.write("/data", &maximum).unwrap();
assert_eq!(fs.read("/data").unwrap(), &maximum);

fs.write("/data", b"short").unwrap();
assert_eq!(fs.read("/data").unwrap(), b"short");

assert_eq!(
fs.write("/data", &[b'z'; MAX_FILE_BYTES + 1]),
Err(FsError::FileTooLarge)
);
assert_eq!(fs.read("/data").unwrap(), b"short");
}

#[test]
fn rejects_malformed_paths_without_consuming_slots() {
let mut fs = RamFs::<2>::new();
fs.create("/keep").unwrap();

for path in ["relative", "/", "/trailing/", "/a//b", "/./a", "/a/../b"] {
assert_eq!(fs.create(path), Err(FsError::InvalidPath), "path: {path}");
}

assert_eq!(fs.file_count(), 1);
assert_eq!(fs.read("/keep").unwrap(), b"");
assert_eq!(fs.create("/b"), Ok(()));
assert_eq!(fs.file_count(), 2);
}

#[test]
fn accepts_maximum_path_and_rejects_one_byte_over() {
let mut fs = RamFs::<2>::new();
let maximum = format!("/{}", "a".repeat(MAX_PATH_BYTES - 1));
let too_long = format!("/{}", "a".repeat(MAX_PATH_BYTES));

assert_eq!(maximum.len(), MAX_PATH_BYTES);
assert_eq!(fs.create(&maximum), Ok(()));
assert_eq!(fs.create(&too_long), Err(FsError::PathTooLong));
assert_eq!(fs.file_count(), 1);
}
}
11 changes: 8 additions & 3 deletions kernel/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![no_std]
#![no_main]

use bootloader_api::{entry_point, BootInfo};
use bootloader_api::{BootInfo, entry_point};
use core::panic::PanicInfo;

mod arch;
Expand Down Expand Up @@ -34,10 +34,15 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
logger::info("RAM filesystem initialized; /welcome.txt verified");
logger::info_u64("RAM filesystem files", ramfs.file_count() as u64);
} else {
logger::error("RAM filesystem smoke test failed");
logger::error("RAM filesystem smoke test failed; boot success withheld");
loop {
core::hint::spin_loop();
}
}

logger::info("Rustalis booted successfully.");
// This marker is a CI/debugging contract: emit it only after early init and
// the RAM filesystem smoke test have completed successfully.
logger::info("[BOOT_OK] Rustalis booted successfully.");
loop {
core::hint::spin_loop();
}
Expand Down
84 changes: 69 additions & 15 deletions kernel/src/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,82 @@ pub struct MemorySummary {
}

pub fn summarize(regions: &MemoryRegions) -> MemorySummary {
let mut usable_bytes = 0;
let mut reserved_bytes = 0;

for region in regions.iter() {
let size = region.end.saturating_sub(region.start);
match region.kind {
MemoryRegionKind::Usable => usable_bytes = usable_bytes.saturating_add(size),
_ => reserved_bytes = reserved_bytes.saturating_add(size),
summarize_regions(
regions
.iter()
.map(|region| (region.kind, region.start, region.end)),
)
}

fn summarize_regions(
regions: impl IntoIterator<Item = (MemoryRegionKind, u64, u64)>,
) -> MemorySummary {
let mut summary = MemorySummary {
usable_bytes: 0,
reserved_bytes: 0,
region_count: 0,
};

for (kind, start, end) in regions {
let size = end.saturating_sub(start);
match kind {
MemoryRegionKind::Usable => {
summary.usable_bytes = summary.usable_bytes.saturating_add(size);
}
_ => {
summary.reserved_bytes = summary.reserved_bytes.saturating_add(size);
}
}
summary.region_count += 1;
}

MemorySummary {
usable_bytes,
reserved_bytes,
region_count: regions.iter().count(),
}
summary
}

#[cfg(test)]
mod tests {
use super::{MemoryRegionKind, MemorySummary, summarize_regions};

#[test]
fn saturating_arithmetic_is_safe_for_memory_counters() {
assert_eq!(u64::MAX.saturating_add(1), u64::MAX);
fn summarizes_usable_reserved_and_region_count() {
let summary = summarize_regions([
(MemoryRegionKind::Usable, 0, 1024),
(MemoryRegionKind::Bootloader, 1024, 1536),
(MemoryRegionKind::Usable, 4096, 8192),
]);

assert_eq!(
summary,
MemorySummary {
usable_bytes: 5120,
reserved_bytes: 512,
region_count: 3,
}
);
}

#[test]
fn empty_memory_map_has_zero_totals() {
assert_eq!(
summarize_regions(core::iter::empty::<(MemoryRegionKind, u64, u64)>()),
MemorySummary {
usable_bytes: 0,
reserved_bytes: 0,
region_count: 0,
}
);
}

#[test]
fn invalid_ranges_and_overflow_are_handled_safely() {
let summary = summarize_regions([
(MemoryRegionKind::Usable, 10, 5),
(MemoryRegionKind::Usable, 0, u64::MAX),
(MemoryRegionKind::Usable, 0, 1),
]);

assert_eq!(summary.usable_bytes, u64::MAX);
assert_eq!(summary.reserved_bytes, 0);
assert_eq!(summary.region_count, 3);
}
}
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[toolchain]
channel = "nightly"
components = ["llvm-tools-preview", "rustfmt", "clippy"]
components = ["llvm-tools-preview", "rustfmt", "clippy", "rust-src"]
targets = ["x86_64-unknown-none", "x86_64-unknown-uefi"]
profile = "minimal"
Loading
Loading