███████
███▒▒▒▒▒███
███ ▒▒███ ████████ █████ ████ █████ █████
▒███ ▒███▒▒███▒▒███ ▒▒███ ▒███ ▒▒███ ▒▒███
▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒▒█████▒
▒▒███ ███ ▒███ ▒███ ▒███ ▒███ ███▒▒▒███
▒▒▒███████▒ ████ █████ ▒▒███████ █████ █████
▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒███ ▒▒▒▒▒ ▒▒▒▒▒
███ ▒███
▒▒██████
▒▒▒▒▒▒
█████████ █████ ████ ████
███▒▒▒▒▒███▒▒███ ▒▒███ ▒▒███
▒███ ▒▒▒ ▒███████ ██████ ▒███ ▒███
▒▒█████████ ▒███▒▒███ ███▒▒███ ▒███ ▒███
▒▒▒▒▒▒▒▒███ ▒███ ▒███ ▒███████ ▒███ ▒███
███ ▒███ ▒███ ▒███ ▒███▒▒▒ ▒███ ▒███
▒▒█████████ ████ █████▒▒██████ █████ █████
▒▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒
A user-space shell for OnyxOS with built-in file operations, navigation, and system commands
OnyxShell (/bin/osh) is the default command-line shell for
OnyxOS. It is a freestanding
RISC-V 64-bit binary written in 90% Rust (no_std, no_main) that compiles
to the OnyxExec v2 format and runs in ring 1 (root space) when launched by
/bin/login.
The shell provides built-in implementations of the most common Unix commands —
ls, cat, rm, cd, cp, mv, mkdir — plus touch, stat, pwd,
echo, whoami, uname, date, clear, help, exit, exec, run, and
ver. No external binaries are required for basic file management.
Part of the OnyxOS ecosystem.
- 90% Rust —
no_std,no_main, compiled withriscv64gc-unknown-none-elf - OnyxExec v2 format — compressed with
elf2onx --ring=1 --compress - 20 built-in commands —
ls,cat,cp,mv,rm,mkdir,touch,stat,cd,pwd,echo,whoami,uname,date,clear,help,exit,exec,run,ver - Path resolution — relative paths (
foo,./bar,../baz) resolved against the kernel's cwd viagetcwd;.and..normalized lexically - Long-format
ls -l— shows file type, permissions, size, and name - Detailed
stat— displays inode, mode, size, blocks, timestamps, and more from the kernel's Linux-compatiblestruct stat - External program execution —
execreplaces the shell;runspawns a child process and waits (root-only, viaSYS_spawn+SYS_wait) - Error reporting — all file-mutation commands translate kernel errno codes to human-readable messages
- Line editing — the kernel's UART driver provides backspace and echo;
the shell just reads complete lines via
SYS_read
| Command | Description | Root-only? |
|---|---|---|
ls [path] [-l] |
List directory contents (use -l for long format) |
|
cat <file>... |
Print file contents to stdout | |
cp <src> <dst> |
Copy a file | ✓ |
mv <src> <dst> |
Move or rename a file (uses rename, falls back to copy+remove) |
✓ |
rm <file>... |
Remove (unlink) a file | ✓ |
mkdir <dir>... |
Create a directory | ✓ |
touch <file>... |
Create an empty file (no error if it exists) | ✓ |
stat <file> |
Show file metadata (inode, size, mode, timestamps) | |
cd [path] |
Change working directory (default: /) |
|
pwd |
Print working directory | |
echo [text] |
Print text followed by a newline | |
whoami |
Print current user (uid) and privilege ring | |
uname |
Print system information (sysname, nodename, release, version, machine) | |
date |
Print current epoch time (seconds + nanoseconds) | |
clear |
Clear the terminal screen (ANSI escape) | |
help |
List all available commands | |
exec <path> [args] |
Replace the shell process with a binary | |
run <path> [args] |
Spawn a binary as a child and wait for it | ✓ |
exit |
Exit the shell (calls SYS_exit(0)) |
|
ver |
Print shell version and copyright |
Root-only commands require ring 1 (root space). The default first-boot
auto-login is root, so all commands work out of the box. Regular users (ring 2)
will get Permission denied from file-mutation commands.
osh/
├── Cargo.toml — package definition (no_std, no_main)
├── .cargo/config.toml — RISC-V target + linker flags
├── linker.ld — linker script (page-aligned .bss, entry at 0x10000)
├── build.rs — passes linker script to rustc
├── src/
│ ├── main.rs — _start entry point + main REPL loop
│ ├── syscalls.rs — ecall wrappers for the OnyxKernel syscall ABI
│ ├── io.rs — write_str / write_u64 / write_hex / read_line
│ ├── path.rs — relative-to-absolute path resolution + normalization
│ └── commands.rs — all 20 command implementations
├── build.sh — builds the shell + converts ELF → osh.onx
├── test_qemu.sh — full-stack QEMU test (builds kernel, boot, disk)
└── README.md — this file
| Tool | Version | Purpose |
|---|---|---|
| Rust nightly | ≥ 1.85 | Shell compilation |
riscv64gc-unknown-none-elf target |
— | Cross-compilation |
elf2onx |
— | ELF → OnyxExec v2 conversion (from OnyxKernel/tools) |
qemu-system-riscv64 |
— | Testing (optional) |
$ rustup target add riscv64gc-unknown-none-elf$ ./build.shThis produces build/osh.onx — a compressed OnyxExec v2 binary tagged as
ring 1 (root space). Place this file at /bin/osh in your OnyxFS disk image.
$ cargo build --release
$ elf2onx --ring=1 --compress target/riscv64gc-unknown-none-elf/release/onyx-osh build/osh.onxWhy
--ring=1?OnyxKernel's
execsyscall sets the new process's ring based on the binary's RING1 flag. Without--ring=1,/bin/login(ring 1) execs/bin/oshand the shell is dropped to ring 2 (user space). In ring 2, file-mutation syscalls (unlink,mkdir,create,rename) returnEPERM, sorm,mkdir,cp,mv, andtouchwould fail.
The test_qemu.sh script builds the entire OnyxOS stack (OnyxBoot +
OnyxKernel + OnyxShell) and launches QEMU:
$ ./test_qemu.sh # interactive mode — type commands at osh$ prompt
$ ./test_qemu.sh -s # scripted mode — runs a test suite and exits- OnyxBoot, OnyxKernel, and OnyxCompiller repos cloned as siblings of
osh/ riscv64-elf-gcc(orriscv64-unknown-elf-gcc) for OnyxBootqemu-system-riscv64,parted,mkfs.fat,mcopy
OnyxBoot v0.4 [riscv-virtio,qemu]
...
[kernel] OnyxKernel v0.4 — RISC-V 64-bit
...
[init] OnyxOS init v0.4 (service manager)
[init] launching /bin/login
[login] no users found - auto-login as root
[login] launching /bin/osh (root, ring 1)
OnyxShell v0.2.0 (built-in commands)
osh$ _
osh$ help
osh$ ls /
osh$ ls -l /bin
osh$ cat /etc/passwd
osh$ mkdir /tmp
osh$ touch /tmp/test.txt
osh$ cp /etc/passwd /tmp/copy.txt
osh$ cat /tmp/copy.txt
osh$ mv /tmp/copy.txt /tmp/moved.txt
osh$ ls /tmp
osh$ rm /tmp/test.txt
osh$ rm /tmp/moved.txt
osh$ stat /bin/osh
osh$ whoami
osh$ uname
osh$ cd /tmp
osh$ pwd
osh$ exit
- OnyxBoot (M-mode) loads
kernel.elffrom the VirtIO disk and jumps tokmain. - OnyxKernel initializes hardware, mounts OnyxFS, and spawns
/bin/init(PID 1, ring 1). /bin/init(OnyxInit service manager) scans/service/, then spawns/bin/login./bin/loginauto-logs in as root on first boot (no users in/etc/passwd), then execs/bin/osh./bin/osh(this shell) enters its read-eval-print loop.
The shell communicates with the kernel via RISC-V ecall instructions. Each
syscall wrapper in src/syscalls.rs is a thin inline-assembly block that
loads the syscall number into a7, arguments into a0–a2, executes
ecall, and reads the return value from a0.
The shell uses these syscalls:
| Syscall | Number | Purpose |
|---|---|---|
write |
1 | Write to stdout (fd 1) |
read |
2 | Read from stdin (fd 0) — kernel handles line editing |
exit |
3 | Terminate the shell |
open |
8 | Open a file (O_RDONLY, O_CREAT, O_WRONLY, O_TRUNC) |
close |
9 | Close a file descriptor |
stat |
11 | Get file metadata (128-byte struct stat) |
readdir |
16 | Read next directory entry (stateful, path-based) |
getring |
17 | Get current privilege ring (0/1/2) |
write_fd |
24 | Write to a file descriptor (fd ≥ 3) |
create |
25 | Create a new regular file (root-only) |
mkdir |
26 | Create a directory (root-only) |
unlink |
37 | Remove a file (root-only) |
rename |
38 | Rename/move a file (root-only) |
chdir |
39 | Change working directory |
getcwd |
40 | Get current working directory |
getuid |
45 | Get current user ID |
uname |
48 | Get system information |
spawn |
14 | Spawn a child process (root-only) |
wait |
15 | Wait for a child to exit (root-only) |
exec |
12 | Replace process with a new binary |
clock_gettime |
64 | Get current time |
OnyxKernel's VFS requires all paths passed to open, stat, unlink,
mkdir, rename, etc. to be absolute (starting with /). The shell
accepts relative paths from the user and resolves them in src/path.rs:
- If the path starts with
/, it is already absolute — normalize.and..components lexically. - Otherwise, fetch the cwd via
getcwd, joincwd + "/" + path, then normalize. .components are skipped;..pops the last component.
The shell is a freestanding binary with no heap allocation. All buffers are stack-allocated with fixed sizes:
- Input line: 256 bytes
- Path buffer: 256 bytes
- Stat buffer: 256 bytes
- Read/write buffer: 512 bytes
- Token array: 16 tokens × (offset, length)
The linker script places .text at 0x10000 (USER_BASE), .rodata after
it, and page-aligns .bss so it does not share a page with .rodata (which
would cause a page fault on the first .bss write).
To integrate the shell into your OnyxKernel build:
-
Build the shell:
$ cd osh && ./build.sh -
Copy
build/osh.onxto your OnyxKernel build directory. -
Update
scripts/run_qemu.shto use the newosh.onx:elf2onx --ring=1 --compress $OSH_DIR/target/.../onyx-osh $BUILD/osh.onx
-
Run QEMU:
$ ./scripts/run_qemu.sh
Alternatively, use the included test_qemu.sh which automates the entire
build-and-test cycle.
This shell was tested with the following patches applied to OnyxKernel
(from onyx-init-patches.zip):
01-init-service-manager.patch— rewrites init as a service manager02-login-auto-root.patch— auto-login as root on first boot03-auth-plaintext.patch— plaintext password storage04-linker-bss-page-align.patch— page-align.bssin init's linker.ld05-run-qemu-manifest.patch— conditional manifest entries in run_qemu.sh
Two bugs in OnyxKernel were fixed to make the shell work correctly. These are minimal, surgical fixes that do not change the kernel's architecture:
-
vfs::createfd table mismatch (kernel/src/fs/vfs/create.rs) —createcalledalloc_fd(which usesG_KERNEL_FDSwhenis_kernel_boot()is true) but then initialized the fd incurrent().fdsdirectly. This causedEBADFon subsequentwrite_fdcalls becausefd_checkread from a different table than the onecreatewrote to. Fixed by usingfd_set/fd_get(which respectis_kernel_boot()) instead of accessingcurrent().fdsdirectly. -
sys_unameuser-pointer dereference (kernel/src/syscall/fs_sys3/info.rs) —sys_unamewrote to the user buffer viabuf as *mut u8without translating the VA to a PA, causing a kernel page fault. Fixed by callingvmm::translatefirst. -
current_pidstate check (kernel/src/proc/process/current.rs) —current_pidreturned 0 when the current process was not inRunningstate, causingis_kernel_boot()to return true during syscalls (if a timer tick had changed the state). This madealloc_fduseG_KERNEL_FDSinstead of the process's own fd table. Fixed by returning(*p).pidregardless of state. -
OnyxBoot
stdbool.h(OnyxBoot/include/types.h) — added#include <stdbool.h>soboolis defined forext4.candfat.c. Required for GCC 14+ which enforces C99 type correctness.
- Tab completion for file paths
- Command history (up/down arrows)
- Pipe (
|) and redirect (>,<) operators - Wildcard globbing (
*,?) - Environment variables (
$HOME,$PATH) - Background processes (
&) - Shell scripts (batch file execution)
| Project | Description |
|---|---|
| OnyxKernel | RISC-V 64-bit operating system kernel |
| OnyxBoot | Minimalist RISC-V 64-bit bootloader |
| OnyxCompiller | C → RV64 compiler (runs on OnyxOS) |
GPL-3.0-or-later — same as OnyxKernel.