A custom x86-64 operating system kernel built from scratch.
Exploidus is a personal operating system project developed in Bangladesh for operating system research and education. The project is built from scratch and continues to evolve with new kernel subsystems, userspace applications, networking, and graphics support. To the best of the author's knowledge, Exploidus is among the earliest publicly documented from-scratch operating system projects developed in Bangladesh. If you are aware of an earlier publicly documented project, please open an issue with supporting evidence.
Features:
- Multiboot2 boot via GRUB2
- 4-level x86-64 paging with NX enforcement
- Colored zone physical memory manager (GREEN / YELLOW / RED)
- BLAKE3 capability token system with RDRAND seeding
- Intent-based preemptive scheduler (5 priority classes)
- Blocking waitpid (no busy-spin)
- Crash isolation — a fault in a userspace process kills only that process; the kernel keeps running (kernel-mode faults still halt)
- Kernel synchronization primitives: IRQ-safe spinlocks, blocking mutexes/semaphores (kernel/sync/)
- Additive driver registry (kernel/drivers/driver.c) tracking every hardware driver initialized at boot
- VFS + ExFS filesystem with provenance records
- Generic block-device abstraction (kernel/drivers/blockdev.c) — ATA
and USB mass-storage both register as named block devices (
ata0,usb0); filesystem code goes through this interface instead of calling a specific driver directly - USB stack: UHCI host controller driver (kernel/usb/uhci.c) — PCI
detection, control/interrupt/bulk transfers, full device
enumeration, HID reports, and USB Mass Storage (Bulk-Only
Transport + SCSI READ/WRITE) with a
mountshell command to attach a USB stick's ExFS volume into the VFS - TCP/IP network stack (e1000, ARP, IP fragment reassembly, TCP, UDP, ICMP)
- 82 syscalls fully implemented (open/close/mmap/munmap/ps/audit/net/fb_blit/sigaction/chmod/rmdir/rtc/futex/tls/mount)
- exploish interactive shell with real ps and audit commands
- Userspace compositor (
aliencommand) with double-buffered rendering, dirty-region-aware redraws, single-syscall window blitting (SYS_FB_BLIT), and real-time-paced frame pacing
- Kali Linux (WSL2 on Windows, or native)
- x86_64-elf cross-compiler — built automatically by setup.sh
- NASM assembler
- QEMU x86_64 emulator
- xorriso + GRUB (for ISO builds)
- ~4 GB free disk space
- ~45 minutes first-time build (cross-compiler takes ~30 min)
# Step 1: Install system packages
sudo apt update
sudo apt install -y build-essential nasm qemu-system-x86 xorriso \
grub-pc-bin grub-common mtools curl
# Step 2: Build cross-compiler (once, takes ~30 min)
bash setup.sh
# Step 3: Reload PATH
source ~/.bashrc
# Step 4: Build kernel
make
# Step 5: Run (disk + network attached, so /bin/rahu works)
make qemu-disk
You will see the kernel boot and the exploish shell appear. Plain
make qemu also works and boots faster, but it has no disk attached —
/bin only contains what's embedded in the kernel image, so rahu and
hello won't be there. Use qemu-disk/qemu-gui/qemu-run whenever you
need a working /bin or networking.
qemu, qemu-vga and qemu-iso boot the kernel with no disk attached —
fine for testing the kernel/shell core, but /bin/rahu and /bin/hello
won't exist. The -disk/-gui/-run targets mount build/disk.img and
enable the e1000 NIC, so the full system (including rahu) is available.
make qemu
All output goes to your terminal. Type commands here.
make qemu-vga
Opens a QEMU window with VGA text display.
make qemu-disk
Mounts build/disk.img and enables networking, so /bin/rahu, /bin/hello
etc. are present and rahu install can reach the registry at
10.0.2.2:9090. Serial output goes to your terminal.
make qemu-gui
Same as qemu-disk, but also opens a VGA window alongside the serial output.
make qemu-run
Same as qemu-gui, but serial output is written to /tmp/serial.log instead
of your terminal — use tail -f /tmp/serial.log to follow it.
make iso
qemu-system-x86_64 -cdrom build/exploidus.iso -m 256M -serial stdio -display none
Terminal 1: make debug
Terminal 2: gdb build/exploidus.elf (gdb) target remote :1234 (gdb) break kernel_main (gdb) continue
help List all commands
ps Show running processes (real kernel data)
audit Show audit log entries
pid Show current PID
uname Kernel version
echo <text> Print text
clear Clear screen
cap Show capability info
rahu install Install package (downloads from registry)
rahu remove Remove package (stub — not yet implemented)
rahu list List installed packages (stub — use 'ls /bin')
rahu search Search local package index
rahu update Refresh local package index
mount <dev> <mountpoint> Mount a block device (e.g. mount usb0 /media/usb0)
exit [code] Exit
exploidus/
kernel/arch/x86_64/ GDT, IDT, IRQ, ISR stubs, crash-isolating fault handler
kernel/boot/ Multiboot2 entry, long mode
kernel/mm/ PMM, VMM, kmalloc (spinlock-protected heap)
kernel/cap/ BLAKE3 capability tokens
kernel/audit/ Ring-buffer audit log
kernel/proc/ Process table, scheduler, fork/exec
kernel/sync/ Spinlock, mutex, semaphore primitives
kernel/syscall/ 82 syscalls
kernel/drivers/ VGA, serial, keyboard, mouse, ATA, framebuffer,
driver.c (hardware driver registry),
blockdev.c (generic block-device abstraction)
kernel/usb/ UHCI host controller driver — control/interrupt/
bulk transfers, enumeration, HID, mass storage
kernel/fs/vfs/ Virtual filesystem
kernel/fs/exfs/ ExFS + provenance
kernel/elf/ ELF64 loader
kernel/net/ TCP/IP stack
userspace/libc/ syscall wrappers, crt0
userspace/shell/ exploish shell
userspace/compositor/ Windowing compositor (`alien` GUI mode)
linker.ld Kernel linker script
Makefile Build system
setup.sh Cross-compiler installer
make Build kernel + shell
make clean Remove build artifacts
make iso Build bootable ISO
make qemu Run (serial, no window, no disk)
make qemu-vga Run with VGA window (no disk)
make qemu-disk Run with disk + network attached (rahu works)
make qemu-usb-storage-test Run with a USB mass-storage device attached
instead of usb-tablet, to exercise the UHCI mass-storage path
make qemu-gui Same as qemu-disk, with a VGA window
make qemu-run Same as qemu-gui, serial output logged to a file
make debug Run with GDB on :1234
x86_64-elf-gcc not found: source ~/.bashrc # or: export PATH="$HOME/opt/cross/bin:$PATH"
QEMU blank screen: Use make qemu (serial mode), not make qemu-vga unless WSLg is available.
Build fails: make clean && make
WSL2 no GUI window: Use make qemu — no window needed for serial output.
GUI (alien)/mouse feels slow or stuttery:
All qemu-disk/qemu-gui targets now pass -accel kvm:tcg, which uses
KVM hardware acceleration when available and falls back to plain
software emulation (TCG) otherwise. Check whether KVM is actually
available on your host:
ls -la /dev/kvm
If that file doesn't exist, QEMU is running the CPU in pure
software emulation, which can be dramatically (10-50x) slower than
real hardware — enough on its own to make interrupt-heavy code
(mouse polling, the compositor) feel laggy no matter how optimized
the OS code is. If you're inside a VM (VMware/VirtualBox/cloud),
enable nested virtualization for the guest (e.g. VMware: VM
Settings -> Processor -> "Virtualize Intel VT-x/EPT or AMD-V/RVI";
VirtualBox: Settings -> System -> Processor -> "Enable Nested
VT-x/AMD-V") and make sure the kvm kernel module is loaded
(sudo modprobe kvm_intel or kvm_amd).
A round of fixes to the GUI/compositor path and kernel core:
- Mouse cursor race: the kernel's PS/2 IRQ handler no longer draws
its own cursor directly on the framebuffer while the compositor owns
the screen (
fb_console_enabled()gates it) — it was racing with the compositor's double-buffered draw and flip. - fb_blend_pixel double-buffer bug: shadows/blends were being written straight to the front buffer, bypassing double buffering, which caused visible flicker on every composite.
- SYS_FB_BLIT: a new syscall blits a whole window's ARGB buffer in one call instead of one syscall per pixel (previously ~120,000 syscalls per window per frame for a typical window size).
- composite_frame_light(): plain cursor movement no longer triggers a full wallpaper gradient repaint; only the affected windows/dock are redrawn.
- Software VSync: the compositor paces repaints against real
elapsed time (
uptime()), not a loop-iteration counter, so the frame rate stays consistent regardless of other work happening in the event loop. - Crash isolation: a fault (page fault, GPF, divide-by-zero, ...)
in userspace code now kills only that process, the same way
sys_exit()does, instead of halting the entire kernel. Faults in kernel-mode code still halt, since kernel state may be corrupted. - kmalloc/kfree race: the heap free-list is now protected by an IRQ-safe spinlock — previously an interrupt firing mid-allocation could corrupt it.
- kernel/sync/: new spinlock/mutex/semaphore primitives for future use protecting shared kernel/driver state.
- Driver registry:
kernel/drivers/driver.ctracks every hardware driver initialized at boot (additive — boot order is unchanged). - Removed hot-path debug logging:
unblock_sleepers()(runs from the timer IRQ at 100Hz) andipc_send()(runs on every message) used toserial_print()on every event. Serial writes busy-wait on real UART timing, so this was stalling keyboard/mouse/scheduling system-wide on every process wake-up or IPC message — likely the single biggest contributor to perceived input lag. - QEMU acceleration:
qemu-disk/qemu-guinow pass-accel kvm:tcg, using KVM hardware acceleration when the host supports it (see Troubleshooting if/dev/kvmisn't available).
Exploidus now has a real, working Lua 5.5.0 port (userspace/lua/,
run with lua from the shell) — the first third-party language
runtime running on the OS. Built entirely on native Exploidus
syscalls, not a Linux compatibility layer (see the syscall ABI
discussion in project history for why). Getting there required a
real libc foundation that mostly didn't exist before:
setjmp/longjmp(x86-64 asm), a real<math.h>(bit-trick + Taylor-series based, no hardware libm dependency),<errno.h>,<ctype.h>,<assert.h>,<locale.h>, real<time.h>(backed by a new CMOS RTC driver,kernel/drivers/rtc.c— Exploidus had no real calendar-time source before, only ticks-since-boot)- FPU/SSE support: userspace couldn't return a
doubleat all before (-mno-ssewas set everywhere) —kernel/arch/x86_64/fpu.cenables SSE and gives every process its own FXSAVE/FXRSTOR state, saved/restored on every context switch - Native
SYS_SET_TLSandSYS_FUTEX_WAIT/WAKE(intra- process scope) for anything needing thread-local storage or a wait/wake primitive to build synchronization on top of - A real PMM bug this surfaced:
parse_memory_map()used a hardcoded 2MB "safe to allocate" cutoff that didn't account for the kernel's own.bss(including its 4MB static heap array) extending well past that — small test binaries never needed enough pages to hit it, but Lua's ~60-page binary did, corrupting live kernel memory. Fixed via a real_kernel_endlinker symbol. - A second, general bug the Lua port surfaced: any function pointer in an ASLR-relocated binary holds its link-time address, not the real runtime one (the loader shifts binaries without processing relocations) — see the dedicated section below.
- Real signal delivery:
signal()now actually works — a hardware fault (SIGSEGV/SIGFPE/SIGILL) in a process with a registered handler redirects execution there instead of always killing the process. One-shot, and the handler must callexit()itself (no sigreturn/resume support yet). - Terminal echo + backspace-editable input:
kernel_read()had no cooked-mode line editing at all — invisible while typing, and a backspace became a literal embedded byte instead of deleting a character. Fine for exploish (which does its own full editor) but broke any ordinary program reading stdin normally (Lua's REPL,yolish). NewSYS_TTY_SET_RAWlets exploish opt out; everyone else gets real echo/backspace by default now.
- Permission enforcement: file mode bits were stored at creation
but never checked anywhere — any process could read/write any file
regardless of permissions.
vfs_open()now actually enforces owner read/write bits, and a newchmod()syscall lets you set them. rmdir(): was entirely missing (onlymkdirexisted). Refuses non-empty directories.- Write-through block cache: ExFS did a full 8-sector PIO ATA read
for every single block access, even ones read moments earlier
(directory traversal, inode lookups). New 256 KiB write-through
cache in
exfs_volume_t. - Fixed
fopen(path, "w")never actually creating a new file — it never passedO_CREAT, so it silently failed on any path that didn't already exist. Not specific to any one program; anything writing a new file viafopen()hit this.
- Fixed busy-spin polling (no yield, up to 500 back-to-back retries)
and a silent stack-buffer-overflow risk in
http_get()/http_download(). - UDP sockets could never receive any data, ever: the
net_socket(SOCK_UDP)API's receive ring buffer existed but nothing in the whole codebase ever wrote to it — went unnoticed because DNS resolution bypasses the socket API via its own direct callback. Bridged the two (socket_udp_recv_cbinkernel/net/socket/socket.c). - No loopback interface existed:
ip4_output()had no special case for127.0.0.1(or the interface's own IP) — such packets fell through to real ARP+Ethernet+NIC transmission, which can never succeed, so they were silently dropped. This broke all localhost communication even though the receiving side already had loopback-accept logic. Now delivers straight toip4_input()instead of transmitting, like a real OS'slointerface. net_connect()/net_recv()'s TCP "timeout" was a loop-iteration count, not real elapsed time (so it didn't reliably mean what it claimed to). Converted to a real wall-clock deadline.
- Per-process CPU time accounting (
ticks_used) was declared and exposed viaps's TICKS column but never actually incremented anywhere —psalways showed 0. Now wired up insched_tick(), plus a CPU% column. Honest caveat: a process idly halted waiting for keyboard input (kernel_read()'s wait loop) still counts as "current" during that halt, so CPU% for an interactive shell reads higher than its actual work would suggest — this is a real scheduler-level idle-vs-running distinction Exploidus doesn't make yet, not just a display bug.
The loader used to give ET_EXEC binaries ASLR (random load base) by
shifting the whole image without processing ELF relocations — RIP-
relative code self-corrected under a pure shift, but an explicit
function pointer value (a signal handler passed to signal(), a
static callback table, anything stored as data rather than computed
at the call site) kept its link-time (base-0) address instead of
the real runtime one, jumping to the wrong place if called. The old
workaround was linking anything that took a function pointer against
a fixed, non-ASLR'd base (fixed.ld) instead.
This is now fixed properly:
kernel/elf/elf.c'sapply_relocations()correctly processesR_X86_64_RELATIVEentries at load time.- Every userspace binary migrated from ET_EXEC to static-PIE
(
pie.ld+PIE_LDFLAGSin the Makefile) —shell,rahu,lua,compositor, and everything else now actually exercises the relocation path instead of opting out viafixed.ld. The old fixed-base.ldscripts are left in-tree for reference but are no longer used. - ASLR entropy widened (7→8 bits of real 2MB-granularity randomness)
and the stack is now randomized too (previously fixed at
USER_STACK_TOP) — both within the existing safe pd0 user address range, no page-table changes needed. - A real bug this surfaced and fixed:
syscall1()/syscall2()(userspace/libc/syscall.h) leftrsi/rdxunset for syscalls that only logically need one argument (e.g.spawn(path)). Under the old fixed-address build this happened to be harmless; under PIE (different code addresses → different leftover register values)spawn("/bin/lua")'s stalerdxsometimes looked like a valid pointer, andsys_spawn()readsrdxunconditionally to detect an optional args string — so garbage leaked into the child'sargv[1]and corrupted Lua'scollectargs()/handle_script(), crashing on a badstrcmp()pointer. Fixed by explicitly zeroing the unused registers in the syscall wrappers.
Remaining honest limitation: 8 bits of ASLR entropy is still weak
compared to a desktop OS (Linux gives 28+) — going further needs
make_isolated_pml4() to clear more than one PD table's worth of
user address space, a bigger architectural change not done yet.
kernel/usb/uhci.c — PCI class-code detection (not vendor-specific,
finds any UHCI controller), controller reset, and a real transfer
layer built from Transfer Descriptors / Queue Heads:
- Control transfers: full device enumeration — partial (8-byte)
device descriptor,
SET_ADDRESS, full 18-byte descriptor via multi-packet chaining, configuration descriptor parsing (interface class/endpoints),SET_CONFIGURATION. - Interrupt transfers: HID report reads (verified against a QEMU
usb-tablet), correctly distinguishing "NAK, nothing new to report" from a real transfer error. - Bulk transfers + USB Mass Storage (Bulk-Only Transport): CBW/CSW
command wrapping, SCSI
INQUIRY,READ CAPACITY (10),READ (10),WRITE (10)— verified end-to-end against QEMU'susb-storage(vendor/product strings decoded correctly, capacity matched the test image's real size exactly, and aWRITE(10)+READ(10)round-trip byte-matched). - Registers a detected mass-storage device as block device
usb0(see the block-device section above) —mount usb0 /media/usb0from the shell mounts its ExFS volume into the VFS.
Honest limitations: single-device only (hardcoded to USB address 1 —
a second simultaneously-connected device would collide), no
interrupt-endpoint periodic background polling (HID reads are
one-shot, not scheduled), no multi-packet reads/writes beyond what
fits in one TD chain (~512 bytes at typical bulk packet sizes), and
no WRITE-based filesystem operations tested yet (only raw sector
read/write, and the mount path so far).
To test the mass-storage path in QEMU: make qemu-usb-storage-test
(separate from qemu-disk, which attaches a usb-tablet for
mouse/GUI testing instead).
kmalloc()integer-overflow guard: a request size nearUINT64_MAXused to wrap(size + 15) & ~15around to a tiny value, silently handing back a 16-byte buffer while the caller believed it got the huge size it asked for — a heap-overflow primitive. Now rejected before any arithmetic on the size happens. Verified with a host-compiled standalone stress-test harness (tests/test_kmalloc.c) covering OOM exhaustion + recovery, double-free safety, coalescing, and a documented (not yet fixed) fragmentation limitation: first-fit + adjacent-only coalescing can fail a request even when total free memory is sufficient, if free blocks are scattered between still-used ones.- IRQ-driven ATA driver:
ata_wait_not_busy()/ata_wait_drq()now usehltinstead of busy-spinning when interrupts are enabled, falling back to the original busy-poll automatically when they aren't (early boot, beforesti—exfs_mount()for the root filesystem runs in exactly that window, so an IRQ-only wait would hang boot).