Phase 21
Ion Shell
Replace the minimal `no_std` shell from Phase 20 with [ion](https://github.com/redox-os/ion) -- the shell built for Redox OS, another custom Rust OS. After this phase `/bin/ion` is the interactive shell that userspace init spawns. The kernel-level and custom-shell scaffolding from Phase 20 remains as a regression harness; the user-facing shell gains a real scripting language, job control, pipelines, and interactive line editing (the latter pending Phase 22 TTY).
Learning goal
See how a shell built for another custom OS can be ported by targeting the Linux ABI.
Category
productivity
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Replace the minimal no_std shell from Phase 20 with ion — the shell built for Redox OS, another custom Rust OS. After this phase /bin/ion is the interactive shell that userspace init spawns. The kernel-level and custom-shell scaffolding from Phase 20 remains as a regression harness; the user-facing shell gains a real scripting language, job control, pipelines, and interactive line editing (the latter pending Phase 22 TTY).
How it works in m3OS
The roadmap entry defines the learning scope for this phase; a deeper implementation doc has not been matched yet.
How it builds on earlier phases
This phase builds on Userspace Init Shell and turns that foundation into Ion Shell.
Implementation outline
- Verify ion compiles on the host against
x86_64-unknown-linux-musl:bash rustup target add x86_64-unknown-linux-musl cargo install --locked ion-shell --target x86_64-unknown-linux-musl # or: git clone + cargo build --release --target x86_64-unknown-linux-muslConfirm the binary is fully static (ldd ionreports “not a dynamic executable”). - Add ion to xtask build pipeline: in
xtask/src/main.rs, after building the kernel and userspace binaries, check for a vendored ion binary atxtask/vendor/ion; if absent, build it from source using the musl target. Copy the resulting ELF into the ramdisk at/bin/ion. - Update
userspace/init: change theexecvecall from/bin/shto/bin/ion. Add a fallback: if/bin/ionexecvereturnsENOENT, fall back to/bin/sh0. - Smoke test script mode inside QEMU: have init launch
ion -c 'echo ion is alive'before spawning the interactive shell; verify output appears on console. - Verify cooked interactive mode: boot into QEMU, observe the ion prompt (
>or whatever$PS1resolves to), run basic commands: echo hello worldls | cat(two-stage pipeline)for i in 1 2 3 { echo $i }(ion loop syntax)let x = 5; echo $x(variable assignment)cd /bin && pwdCtrl-Cshould kill the foreground child, not ion itself- Validate signal behavior: confirm
SIGINT(fromCtrl-C) is delivered to the foreground child process group and not to ion, consistent with Phase 19’s signal implementation. - Document the musl-static binary requirement in the xtask section: why
PT_INTERPmust be absent, and whyx86_64-unknown-linux-muslproduces a fully self-contained ELF that the m3OS ELF loader can handle without a dynamic linker. - Run
cargo xtask checkto confirm no new clippy warnings from the xtask changes.
Task progress snapshot
51 of 51 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- Ion’s interactive raw-mode line editor (requires
tcgetattr/tcsetattr) - History persistence (requires a writable filesystem path for
~/.local/share/ion/history) - Tab completion with reedline-style highlighting
SIGWINCH/ window size changes
Source materials
Important code
Code spotlight cards
Code spotlight
fcntl stub
fcntl stub. Why it matters: musl uses fcntl for F_DUPFD_CLOEXEC and F_SETFD; ion cannot start without it. Focus on `sys_fcntl` and `F_DUPFD_CLOEXEC` in `kernel/src/arch/x86_64/syscall.rs` while you trace the snippet.
/// Minimal fcntl: F_DUPFD, F_GETFD, F_SETFD, F_GETFL, F_SETFL.
fn sys_fcntl(fd: u64, cmd: u64, arg: u64) -> u64 {
const F_DUPFD: u64 = 0;
const F_GETFD: u64 = 1;
const F_SETFD: u64 = 2;
const F_GETFL: u64 = 3;
const F_SETFL: u64 = 4;
const F_DUPFD_CLOEXEC: u64 = 1030;
match cmd {
F_DUPFD | F_DUPFD_CLOEXEC => {
// Find the next free fd >= arg, duplicate oldfd into it.
let oldfd = fd as usize;
let min_fd = arg as usize;
if oldfd >= MAX_FDS {
return NEG_EBADF;
}
if min_fd >= MAX_FDS {
return NEG_EINVAL;
}
let entry = match current_fd_entry(oldfd) {
Some(e) => e,
None => return NEG_EBADF,
}; Code spotlight
build_ion() in xtask
build_ion() in xtask. Why it matters: Ion must be cross-compiled with musl for a statically linked, non-PIE binary that our ELF loader can handle. Focus on `build_ion` and `ion.elf` in `xtask/src/main.rs` while you trace the snippet.
/// Cross-compile ion shell for musl and place it in kernel/initrd/.
///
/// Strategy: clone ion from GitHub (or use cached clone in target/ion-src/),
/// build with `cargo build --release --target x86_64-unknown-linux-musl`,
/// strip, and copy to kernel/initrd/ion.elf.
///
/// If the ion.elf binary already exists and is newer than ion's Cargo.toml,
/// the build is skipped (cache hit).
fn build_ion() {
let root = workspace_root();
let initrd = root.join("kernel/initrd");
let ion_elf = initrd.join("ion.elf");
// If a pre-built ion.elf exists, skip the build.
if ion_elf.exists() && ion_elf.metadata().map(|m| m.len() > 0).unwrap_or(false) {
println!("ion: using cached {}", ion_elf.display());
return;
}
fs::create_dir_all(&initrd).unwrap();
let ion_src = root.join("target/ion-src");
if !ion_src.join("Cargo.toml").exists() {
println!("ion: cloning ion shell from GitHub..."); Code spotlight
Ramdisk entries for ion
Ramdisk entries for ion. Why it matters: Ion must be discoverable at /bin/ion for exec to find it. Focus on `kernel/src/fs/ramdisk.rs` while you trace the snippet.
Key files
- userspace/init — Update userspace/init to execve("/bin/ion", ...) instead of /bin/sh;
- xtask/vendor/ — pre-built binary is not found in xtask/vendor/
- xtask/src/main.rs — build_ion(): clone, cross-compile, strip, cache
- xtask/vendor/ion — xtask/vendor/ion; if absent, build it from source using the musl target. Copy
- kernel/initrd/ion.elf — Cached ion binary (delete to rebuild)
- userspace/init/src/main.rs — Shell spawn: sh0 primary, ion fallback
- userspace/shell/src/main.rs — sh0 (Phase 20 shell, renamed)
- kernel/src/arch/x86_64/syscall.rs — 18 new syscall stubs
- kernel/src/arch/x86_64/mod.rs — ForkEntryCtx with full register set
- kernel/src/arch/x86_64/interrupts.rs — Stack demand paging
How real OSes differ
- Production Linux distributions ship bash, dash, zsh, or fish as `/bin/sh` or the default interactive shell.
- These are either C programs linked against glibc or statically linked against musl (Alpine Linux uses busybox ash linked with musl).
- The key difference is that real distributions have package managers that download pre-built shell binaries; m3OS builds everything from source or vendors binaries directly into the ramdisk during the image build step.
- The educational insight here is that a shell is just a program -- as long as the kernel provides the right syscalls, any shell binary built for the same ABI will run unmodified.
Success criteria
- `cargo xtask image` produces a disk image containing `/bin/ion` without manual intervention.
- Booting in QEMU presents the ion prompt.
- `echo hello` prints `hello`.
- `let x = world; echo $x` prints `world`.
- `ls | cat` produces a directory listing via ion's pipeline execution.
- `for i in a b c { echo $i }` prints three lines.
- `cd /tmp && pwd` prints `/tmp`.
- `Ctrl-C` during `sleep 10` kills the sleep child; ion returns to its prompt.
- `/bin/sh0` still boots and works as a fallback.
- `file /bin/ion` (or equivalent ELF header inspection) confirms the binary is statically linked with no `PT_INTERP` segment.
- The Phase 20 acceptance criteria still pass when using `/bin/sh0`.
Related phases
Keep exploring the roadmap
Phase 20
CompleteUserspace Init Shell
Replace the kernel-resident `init_task` and `shell_task` ring-0 functions with real ring-3 userspace processes. After this phase PID 1 is a `no_std` Rust binary loaded from the ramdisk; the interactive shell is a userspace ELF that init spawns. The kernel is no longer responsible for parsing commands or managing the interactive session.
Phase 22
CompleteTTY PTY
Give userspace programs a real terminal abstraction. The kernel's raw stdin buffer and hardcoded `stdin_feeder_task` are replaced by a proper TTY layer with a configurable line discipline. Programs can switch between cooked and raw mode via `tcgetattr` / `tcsetattr`, query the window size via `TIOCGWINSZ`, and detect whether they are attached to a terminal via `isatty`. The phase also lays the infrastructure for a future PTY master/slave pair.