m³OS docs GitHub

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).

Complete

Learning goal

See how a shell built for another custom OS can be ported by targeting the Linux ABI.

Category

productivity

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-musl Confirm the binary is fully static (ldd ion reports “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 at xtask/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 the execve call from /bin/sh to /bin/ion. Add a fallback: if /bin/ion execve returns ENOENT, 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 $PS1 resolves to), run basic commands:
  • echo hello world
  • ls | cat (two-stage pipeline)
  • for i in 1 2 3 { echo $i } (ion loop syntax)
  • let x = 5; echo $x (variable assignment)
  • cd /bin && pwd
  • Ctrl-C should kill the foreground child, not ion itself
  • Validate signal behavior: confirm SIGINT (from Ctrl-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_INTERP must be absent, and why x86_64-unknown-linux-musl produces a fully self-contained ELF that the m3OS ELF loader can handle without a dynamic linker.
  • Run cargo xtask check to 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

kernel/src/arch/x86_64/syscall.rs L3926-L3949

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

xtask/src/main.rs L235-L258

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

kernel/src/fs/ramdisk.rs See file

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