Add a proper VFS in a struct which now lives on the heap with no hacky

static lifetimes, move cursor.bmp to image assets and add wallpapers,
and a logo, add Github Action that auto-releases, add build_all script
which just builds without running
This commit is contained in:
csd4ni3l
2026-05-31 23:41:02 +02:00
parent f943cf5426
commit 43ec0e97df
15 changed files with 387 additions and 222 deletions
+86
View File
@@ -0,0 +1,86 @@
name: Build & Release XunilOS
on:
push:
branches:
- main
jobs:
build:
name: Build (${{ matrix.karch }})
runs-on: ubuntu-latest
strategy:
matrix:
karch: [x86_64, aarch64]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
gcc \
gcc-aarch64-linux-gnu \
binutils-aarch64-linux-gnu \
nasm \
xorriso \
grub-pc-bin \
grub-efi-amd64-bin \
grub-efi-arm64-bin \
mtools \
curl
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: |
x86_64-unknown-none
aarch64-unknown-none
- name: Run build script
run: bash build_all.sh
env:
KARCH: ${{ matrix.karch }}
- name: Upload ISO artifact
uses: actions/upload-artifact@v4
with:
name: XunilOS-${{ matrix.karch }}.iso
path: XunilOS-${{ matrix.karch }}.iso
if-no-files-found: error
release:
name: Publish Release
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download x86_64 ISO
uses: actions/download-artifact@v4
with:
name: XunilOS-x86_64.iso
- name: Download aarch64 ISO
uses: actions/download-artifact@v4
with:
name: XunilOS-aarch64.iso
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: commit-${{ github.sha }}
name: Commit ${{ github.sha }}
body: |
Automated release for commit [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
files: |
XunilOS-x86_64.iso
XunilOS-aarch64.iso
fail_on_unmatched_files: true
+37 -25
View File
@@ -1,34 +1,46 @@
# XunilOS
XunilOS is an OS made from scratch in Rust.
The repo is based on the limine-rust-template.
## How to use this?
It supports aarch64 inside QEMU, and x86_64 even on bare machines!
### Dependencies
# Features
Any `make` command depends on GNU make (`gmake`) and is expected to be run using it. This usually means using `make` on most GNU/Linux distros, or `gmake` on other non-GNU systems.
## Kernel
- x86_64 IDT, GDT, interrupts, kernel heap, PS2 mouse/keyboard, paging, syscalls and usermode.
- aarch64 kernel heap, paging, interrupts, Virtio mouse/keyboard, syscalls, usermode
- Scheduler which does round-robin switching as well as sleeping and waking processes
- ELF64 ET_EXEC and ET_DYN loading, verifying and running support
- A readonly VFS which currently includes the ELF files which can be ran
- IPC with granular permissions (read, write, manage)
- basic (and insecure) SHM support
- Limine bootloader
- Framebuffer and serial support
- Timing support based on IRQ
- Per-process address space, kernel & user stack
- Copy to and from userspace
All `make all*` targets depend on Rust.
## Apps
- doomgeneric: Doom ported to XunilOS. Isn't as easy, as i am using Rust and had to write my own libc stub.
- badapple: Grayscale Bad Apple by using numbers to represent shades of gray. Pretty easy, but this is the only place where I also used Python.
- shell: simple shell with elf running, echo and file read commands.
- helloworld: just prints helloworld to serial
Additionally, building an ISO with `make all` requires `xorriso`, and building a HDD/USB image with `make all-hdd` requires `sgdisk` (usually from `gdisk` or `gptfdisk` packages) and `mtools`.
## Init
- Desktop-like experience
- Window Management (close and minimize)
- Start menu to open applications
- BMP background
- Mouse support with a BMP image
- Dock where you can see currently open applications which can be minimized or unminimized
### Architectural targets
The `KARCH` make variable determines the target architecture to build the kernel and image for.
The default `KARCH` is `x86_64`. Other options include: `aarch64`, `riscv64`, and `loongarch64`.
Other architectures will need to be enabled in kernel/rust-toolchain.toml
### Makefile targets
Running `make all` will compile the kernel (from the `kernel/` directory) and then generate a bootable ISO image.
Running `make all-hdd` will compile the kernel and then generate a raw image suitable to be flashed onto a USB stick or hard drive/SSD.
Running `make run` will build the kernel and a bootable ISO (equivalent to make all) and then run it using `qemu` (if installed).
Running `make run-hdd` will build the kernel and a raw HDD image (equivalent to make all-hdd) and then run it using `qemu` (if installed).
The `run-uefi` and `run-hdd-uefi` targets are equivalent to their non `-uefi` counterparts except that they boot `qemu` using a UEFI-compatible firmware.
## Libxunil (libc stub)
- Basic functions of C (printf, strlen, etc)
- File I\O, IPC, time
- Input reading from Window Management
- SHM
- User Heap
- Window management
- IPC support
- Syscalls to call back to the kernel
- Primitives, Framebuffer and font rendering

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

+7
View File
@@ -0,0 +1,7 @@
bash build_rust_app.sh libxunil
bash build_rust_app.sh init
bash build_doomgeneric.sh
bash build_helloworld.sh
bash build_rust_app.sh badapple
bash build_rust_app.sh shell
make all
+2 -7
View File
@@ -1,8 +1,3 @@
export KARCH=x86_64
bash build_rust_app.sh libxunil
bash build_rust_app.sh init
bash build_doomgeneric.sh
bash build_helloworld.sh
bash build_rust_app.sh badapple
bash build_rust_app.sh shell
export KARCH=aarch64
bash build_all.sh
make run
+5 -1
View File
@@ -7,7 +7,10 @@ use crate::{
},
arch::KERNEL_MAPPER,
},
driver::{io::virtio::scan_virtio_devices, ipc::init_ipc},
driver::{
io::{fs::vfs::init_vfs, virtio::scan_virtio_devices},
ipc::init_ipc,
},
mm::shm::init_shm,
};
use limine::response::{ExecutableAddressResponse, HhdmResponse, MemoryMapResponse};
@@ -40,6 +43,7 @@ pub extern "C" fn init_aarch64(mapper: &mut AArchPageTable) {
init_interrupts();
init_ipc();
init_shm();
init_vfs();
}
pub fn preinit_aarch64<'a>(
+42 -1
View File
@@ -4,9 +4,11 @@ use core::sync::atomic::Ordering;
#[cfg(target_arch = "x86_64")]
use crate::arch::x86_64::paging::create_and_map_multiple_pages;
use crate::driver::io::fs::vfs::vfs_write;
use crate::driver::io::input::{InputEvent, process_input};
#[cfg(target_arch = "x86_64")]
use crate::driver::io::ps2::process_scancodes;
use crate::mm::usercopy::copy_from_user;
use alloc::vec;
use alloc::{string::String, vec::Vec};
#[cfg(target_arch = "x86_64")]
@@ -204,6 +206,39 @@ fn open(path: isize, mode: isize) -> isize {
.unwrap_or(-1)
}
fn write_fd(ptr: isize, size: isize, count: isize, fd: isize) -> isize {
let pid = current_pid().unwrap_or(0);
if pid == 0 {
return -1;
}
SCHEDULER
.with_process(pid, |process| {
let len = (size as usize).checked_mul(count as usize).ok_or(-1isize)?;
if len == 0 {
return Ok(0isize);
}
let address_space = process.address_space.as_mut().ok_or(-1isize)?;
let mut buf: alloc::vec::Vec<u8> = alloc::vec![0u8; len];
copy_from_user(
&mut address_space.mapper,
buf.as_mut_ptr(),
ptr as *const u8,
len,
)
.map_err(|_| -14isize)?;
Ok(
vfs_write(buf.as_ptr(), size as usize, count as usize, fd as i64).unwrap_or(0)
as isize,
)
})
.unwrap_or(Err(-1))
.unwrap_or(-1)
}
fn close(fd: isize) -> isize {
vfs_close(fd as i64) as isize
}
@@ -656,6 +691,7 @@ pub unsafe extern "C" fn syscall_dispatch(
BRK => unsafe { sbrk(arg0) },
READ => read(arg0, arg1, arg2, arg3) as isize,
WRITE => {
if arg0 == 1 {
let buf_ptr = arg1 as *const u8;
let len = arg2 as usize;
let bytes: &[u8] = unsafe { core::slice::from_raw_parts(buf_ptr, len) };
@@ -669,6 +705,9 @@ pub unsafe extern "C" fn syscall_dispatch(
print!("{}", *byte as char);
}
}
} else {
write_fd(arg0, arg1, arg2, arg3);
}
0
}
@@ -678,7 +717,9 @@ pub unsafe extern "C" fn syscall_dispatch(
EXIT => kill(current_pid().unwrap() as isize, arg0),
SLEEP => sleep(arg0),
EXECVE => exec(arg0),
CLOCK_GETTIME => ((TIMER.now().elapsed() as usize) * (TIMER_FREQUENCY_HZ / 1000)) as isize,
CLOCK_GETTIME => {
(TIMER.now().elapsed() + TIMER.get_date_at_boot() * TIMER_FREQUENCY_HZ as u64) as isize
}
MAP_FRAMEBUFFER => map_framebuffer(),
INPUT_READ => input_read(arg0 as *mut InputEvent, arg1),
GETPID => {
+2 -1
View File
@@ -8,7 +8,7 @@ use crate::{
syscall::init_syscalls,
},
config::TIMER_FREQUENCY_HZ,
driver::ipc::init_ipc,
driver::{io::fs::vfs::init_vfs, ipc::init_ipc},
mm::shm::init_shm,
};
@@ -93,6 +93,7 @@ pub fn init_x86_64<'a>(
init_ipc();
init_shm();
init_vfs();
return mapper;
}
+167 -148
View File
@@ -1,180 +1,110 @@
use alloc::{
collections::btree_map::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use lazy_static::lazy_static;
use crate::driver::io::fs::assets::*;
use core::ptr::{null, null_mut};
lazy_static! {
static ref FILE_CONTENT: BTreeMap<&'static str, &'static [u8]> = {
let mut map = BTreeMap::new();
map.insert("testfile", &b"Hello, World!"[..]);
map.insert("helloworld.elf", HELLOWORLD_ELF);
map.insert("badapple", BADAPPLE_ELF);
map.insert("doomgeneric", DOOM_ELF);
map.insert("shell", SHELL_ELF);
map.insert("doom1.wad", DOOM_WAD);
map.insert("doom.cfg", &b""[..]);
map.insert("default.cfg", &b""[..]);
map
};
}
#[repr(C)]
#[derive(Clone, Copy)]
#[derive(Clone, Debug)]
pub struct FILE {
pub data: *const u8, // pointer to the file's data
pub size: usize, // total size
pub cursor: usize, // current position
pub writable: bool, // is this a write buffer?
pub write_buf: *mut u8, // for writable fake files
pub write_cap: usize,
pub name: String,
pub size: usize,
pub data: Vec<u8>,
pub cursor: usize,
pub writable: bool,
pub fd: i64,
}
impl FILE {
pub const fn zeroed() -> FILE {
pub fn new(name: String, data: Vec<u8>, writable: bool) -> FILE {
FILE {
data: null(),
size: 0,
name,
data: data.clone(),
cursor: 0,
writable: false,
write_buf: null_mut(),
write_cap: 0,
writable,
fd: -1,
size: data.len(),
}
}
}
struct FakeFileEntry {
name: &'static str,
data: &'static [u8],
pub struct VFS {
files: Vec<FILE>,
next_fd: i64,
}
pub type Fd = i64;
const MAX_FD: usize = 16;
fn fd_ok(fd: Fd) -> bool {
fd >= 0 && (fd as usize) < MAX_FD
}
static FILES: &[FakeFileEntry] = &[
FakeFileEntry {
name: "testfile",
data: b"Hello, World!",
},
FakeFileEntry {
name: "helloworld.elf",
data: HELLOWORLD_ELF,
},
FakeFileEntry {
name: "badapple",
data: BADAPPLE_ELF,
},
FakeFileEntry {
name: "doomgeneric",
data: DOOM_ELF,
},
FakeFileEntry {
name: "shell",
data: SHELL_ELF,
},
FakeFileEntry {
name: "doom1.wad",
data: DOOM_WAD,
},
FakeFileEntry {
name: "default.cfg",
data: b"",
},
FakeFileEntry {
name: "doom.cfg",
data: b"",
},
];
static mut FILE_POOL: [FILE; 16] = [FILE::zeroed(); 16];
static mut FILE_POOL_USED: [bool; 16] = [false; 16];
pub unsafe fn get_file_pool_slot() -> (*mut FILE, i64) {
unsafe {
for i in 0..16 {
if !FILE_POOL_USED[i] {
FILE_POOL_USED[i] = true;
return (&mut FILE_POOL[i], i as i64);
impl VFS {
pub fn new() -> VFS {
VFS {
files: Vec::new(),
next_fd: 0,
}
}
(null_mut(), -1)
}
pub fn open(&mut self, name: &str, mode: &str) -> i64 {
let is_write = mode.contains("w") || mode.contains("a");
if let Some(file) = self
.files
.iter_mut()
.find(|file| file.name.as_str() == name)
{
file.cursor = 0;
file.writable = is_write;
return file.fd;
}
unsafe fn file_mut(fd: Fd) -> Option<&'static mut FILE> {
if !fd_ok(fd) {
return None;
}
let idx = fd as usize;
let fd = self.next_fd;
self.next_fd += 1;
if unsafe { !FILE_POOL_USED[idx] } {
return None;
let empty_data = &"".as_bytes();
let data = FILE_CONTENT.get(name).unwrap_or(empty_data);
let file = FILE {
name: name.to_string(),
data: data.to_vec(),
cursor: 0,
writable: is_write,
fd,
size: data.len(),
};
self.files.push(file);
fd
}
return unsafe { Some(&mut FILE_POOL[idx]) };
}
#[unsafe(no_mangle)]
pub fn vfs_open(name: &str, _mode: &str) -> Fd {
for entry in FILES {
if entry.name.contains(name) {
let (slot, fd) = unsafe { get_file_pool_slot() };
if slot.is_null() {
return -1;
}
unsafe {
(*slot).data = entry.data.as_ptr();
(*slot).size = entry.data.len();
(*slot).cursor = 0;
(*slot).writable = false;
(*slot).write_buf = null_mut();
(*slot).write_cap = 0;
(*slot).fd = fd;
}
return fd;
}
}
pub fn close(&mut self, fd: i64) -> i32 {
if let Some(file_pos) = self.files.iter().position(|file| file.fd == fd) {
self.files.remove(file_pos);
0
} else {
-1
}
#[unsafe(no_mangle)]
pub fn vfs_close(fd: Fd) -> i32 {
if !fd_ok(fd) {
return -1;
}
unsafe {
let idx = fd as usize;
if !FILE_POOL_USED[idx] {
return -1;
}
FILE_POOL_USED[idx] = false;
FILE_POOL[idx] = FILE::zeroed();
}
0
}
#[unsafe(no_mangle)]
#[allow(unused_variables)]
pub fn vfs_write(ptr: *mut u8, size: usize, count: usize, fp: *mut FILE) -> usize {
if ptr.is_null() || fp.is_null() || unsafe { (*fp).fd < 0 || (*fp).fd >= 16 } {
return 0;
}
count
}
#[unsafe(no_mangle)]
pub fn vfs_read(fd: Fd, len: usize) -> Option<(*const u8, usize)> {
unsafe {
let f = file_mut(fd)?;
if f.cursor > f.size {
return Some((f.data, 0));
}
let available = f.size - f.cursor;
let to_read = len.min(available);
let src = f.data.add(f.cursor);
f.cursor = f.cursor.saturating_add(to_read);
Some((src, to_read))
}
}
#[unsafe(no_mangle)]
pub extern "C" fn vfs_lseek(fd: Fd, offset: i64, whence: i32) -> i64 {
let f = match unsafe { file_mut(fd) } {
pub fn lseek(&mut self, fd: i64, offset: i64, whence: i32) -> i64 {
let f = match self.files.iter_mut().find(|file| file.fd == fd) {
Some(f) => f,
None => return -1,
};
@@ -212,3 +142,92 @@ pub extern "C" fn vfs_lseek(fd: Fd, offset: i64, whence: i32) -> i64 {
f.cursor = new_pos;
f.cursor as i64
}
pub fn read(&mut self, fd: i64, len: usize) -> Option<(*const u8, usize)> {
if let Some(f) = self.files.iter_mut().find(|file| file.fd == fd) {
if f.cursor > f.size {
return Some((f.data.as_ptr(), 0));
}
let available = f.size - f.cursor;
let to_read = len.min(available);
let src = unsafe { f.data.as_ptr().add(f.cursor) };
f.cursor = f.cursor.saturating_add(to_read);
Some((src, to_read))
} else {
None
}
}
pub fn write(&mut self, fd: i64, data: &[u8]) -> Option<usize> {
let file = self.files.iter_mut().find(|f| f.fd == fd)?;
if !file.writable {
return None;
}
file.data.extend_from_slice(data);
file.size = file.data.len();
Some(data.len())
}
}
pub static mut VFS_INSTANCE: Option<VFS> = None;
pub fn init_vfs() {
unsafe {
VFS_INSTANCE = Some(VFS {
files: Vec::new(),
next_fd: 3,
})
}
}
#[unsafe(no_mangle)]
pub fn vfs_open(name: &str, mode: &str) -> i64 {
#[allow(static_mut_refs)]
unsafe {
VFS_INSTANCE.as_mut().unwrap().open(name, mode)
}
}
#[unsafe(no_mangle)]
pub fn vfs_close(fd: i64) -> i32 {
#[allow(static_mut_refs)]
unsafe {
VFS_INSTANCE.as_mut().unwrap().close(fd)
}
}
#[unsafe(no_mangle)]
pub fn vfs_write(ptr: *const u8, size: usize, count: usize, fd: i64) -> Option<usize> {
if ptr.is_null() {
return None;
}
let len = size.checked_mul(count)?;
unsafe {
let slice = core::slice::from_raw_parts(ptr, len);
#[allow(static_mut_refs)]
VFS_INSTANCE.as_mut().unwrap().write(fd, slice)
}
}
#[unsafe(no_mangle)]
pub fn vfs_read(fd: i64, len: usize) -> Option<(*const u8, usize)> {
#[allow(static_mut_refs)]
unsafe {
VFS_INSTANCE.as_mut().unwrap().read(fd, len)
}
}
#[unsafe(no_mangle)]
pub extern "C" fn vfs_lseek(fd: i64, offset: i64, whence: i32) -> i64 {
#[allow(static_mut_refs)]
unsafe {
VFS_INSTANCE.as_mut().unwrap().lseek(fd, offset, whence)
}
}