diff --git a/base/init/src/main.rs b/base/init/src/main.rs index 857ec36a..e281a673 100644 --- a/base/init/src/main.rs +++ b/base/init/src/main.rs @@ -104,6 +104,13 @@ const WAIT4_RETURN_MSG: &[u8] = b"init: wait4-return\n"; /// then falls back to Limine boot modules (basename match). const HELLO_PATH: &[u8] = b"/boot/userspace_hello.elf\0"; +/// Path to the POSIX shell. Resolved via VFS from the ext2 rootfs; +/// only available when booting with `root=/dev/vda`. +const SH_PATH: &[u8] = b"/bin/sh\0"; + +/// Smoke-test marker — emitted before exec-ing /bin/sh. +const SH_LAUNCH_MSG: &[u8] = b"init: launching /bin/sh\n"; + #[no_mangle] pub extern "C" fn _start() -> ! { // Pre-write diagnostic marker — see #478. Emitted on fd=2 so it @@ -216,6 +223,89 @@ pub extern "C" fn _start() -> ! { } } + // Launch /bin/sh — the POSIX shell. This is only meaningful when + // the ext2 rootfs is mounted (root=/dev/vda), which places the sh + // binary at /bin/sh. When booting from the ISO-only path (no ext2), + // execve will fail and the child exits with status 1; init continues + // to its idle loop regardless. + write(1, SH_LAUNCH_MSG); + + let sh_fork_ret: i64; + unsafe { + core::arch::asm!( + "syscall", + inlateout("rax") 57u64 => sh_fork_ret, + lateout("rcx") _, + lateout("rdx") _, + lateout("rdi") _, + lateout("rsi") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + lateout("r11") _, + options(nostack, preserves_flags), + ); + } + + if sh_fork_ret == 0 { + // Child: exec /bin/sh. + unsafe { + core::arch::asm!( + "syscall", + inlateout("rax") 59u64 => _, // execve + inlateout("rdi") SH_PATH.as_ptr() as u64 => _, // path + inlateout("rsi") 0u64 => _, // argv (NULL = empty) + inlateout("rdx") 0u64 => _, // envp (NULL = empty) + lateout("rcx") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + lateout("r11") _, + options(nostack, preserves_flags), + ); + } + // execve only returns on failure — exit with status 1. + unsafe { + core::arch::asm!( + "syscall", + inlateout("rax") 60u64 => _, // exit + inlateout("rdi") 1u64 => _, // status 1 (exec failed) + lateout("rcx") _, + lateout("rdx") _, + lateout("rsi") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + lateout("r11") _, + options(nostack, preserves_flags), + ); + } + loop { + core::hint::spin_loop(); + } + } + + // Parent: wait for the shell to exit, then idle. + if sh_fork_ret > 0 { + let sh_pid = sh_fork_ret as u64; + let mut sh_wstatus: i32 = 0; + unsafe { + core::arch::asm!( + "syscall", + inlateout("rax") 61u64 => _, // wait4 + inlateout("rdi") sh_pid => _, // pid + inlateout("rsi") &mut sh_wstatus as *mut i32 as u64 => _, // *wstatus + inlateout("rdx") 0u64 => _, // options + inlateout("r10") 0u64 => _, // rusage + lateout("rcx") _, + lateout("r8") _, + lateout("r9") _, + lateout("r11") _, + options(nostack), + ); + } + } + // Loop forever. loop { core::hint::spin_loop(); diff --git a/base/sh/src/main.rs b/base/sh/src/main.rs index e08ea250..bb9a5827 100644 --- a/base/sh/src/main.rs +++ b/base/sh/src/main.rs @@ -1,5 +1,12 @@ #![feature(restricted_std)] +// Provide #[no_mangle] C-ABI shims for POSIX functions that the shell's +// `extern "C"` declarations link against. On vibix, these delegate to +// raw syscalls via `vibix_abi`. The module is excluded from host tests +// which mock these symbols. +#[cfg(not(test))] +mod syscalls; + mod builtins; mod exec; mod expand; @@ -26,6 +33,9 @@ fn main() { env.arg0 = "sh".to_string(); // Import environment variables from the process environment. + // On vibix, std::env::vars() is not yet supported (panics), + // so we skip import on that target. $PATH is set above. + #[cfg(not(target_os = "vibix"))] for (key, value) in std::env::vars() { env.set(&key, &value, Some(true)); } @@ -50,10 +60,11 @@ fn main() { use std::io::BufRead; let stdin = std::io::stdin(); - // Detect if stdin is a terminal. On vibix, isatty may not be - // available via std, so we check the TERM variable or fall back - // to assuming interactive when stdin is not redirected. - let is_tty = std::env::var("TERM").is_ok(); + // On vibix, the serial console is not a POSIX tty, so `isatty()` + // would return false even for the primary interactive session. + // Always treat stdin-mode as interactive to ensure the prompt + // appears over the serial console. + let is_tty = true; // Create the job table for interactive mode. let mut jobs = JobTable::new(); diff --git a/base/sh/src/syscalls.rs b/base/sh/src/syscalls.rs new file mode 100644 index 00000000..00ae8b4a --- /dev/null +++ b/base/sh/src/syscalls.rs @@ -0,0 +1,272 @@ +//! C-ABI syscall shims for the vibix shell. +//! +//! The shell modules (`exec.rs`, `redirect.rs`, `job.rs`) use `extern "C"` +//! blocks to call standard POSIX functions. On vibix these symbols are not +//! provided by a system libc; we implement them here using raw `syscall` +//! instructions, matching the Linux x86_64 ABI that the vibix kernel uses. +//! +//! Each function follows POSIX return conventions: -1 on error with errno +//! set. +//! +//! The errno TLS cell lives in the std fork's vibix_abi (already linked into +//! this binary via `-Z build-std`). We access it through the C-ABI +//! `__errno_location` symbol that vibix_abi exports. + +use core::arch::asm; + +// Syscall numbers (Linux x86_64 ABI, matching the vibix kernel). +const SYS_READ: u64 = 0; +const SYS_WRITE: u64 = 1; +const SYS_OPEN: u64 = 2; +const SYS_CLOSE: u64 = 3; +const SYS_SIGACTION: u64 = 13; +const SYS_ACCESS: u64 = 21; +const SYS_PIPE: u64 = 22; +const SYS_DUP: u64 = 32; +const SYS_DUP2: u64 = 33; +const SYS_GETPID: u64 = 39; +const SYS_FORK: u64 = 57; +const SYS_EXECVE: u64 = 59; +const SYS_WAIT4: u64 = 61; +const SYS_KILL: u64 = 62; +const SYS_FCNTL: u64 = 72; +const SYS_GETCWD: u64 = 79; +const SYS_CHDIR: u64 = 80; +const SYS_SETPGID: u64 = 109; + +extern "C" { + fn __errno_location() -> *mut i32; +} + +/// Raw syscall with 0-3 args. Returns the raw kernel return value. +#[inline(always)] +unsafe fn raw0(nr: u64) -> i64 { + let ret: i64; + unsafe { + asm!( + "syscall", + inlateout("rax") nr => ret, + lateout("rcx") _, + lateout("r11") _, + lateout("rdx") _, + lateout("rdi") _, + lateout("rsi") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + } + ret +} + +#[inline(always)] +unsafe fn raw1(nr: u64, a0: u64) -> i64 { + let ret: i64; + unsafe { + asm!( + "syscall", + inlateout("rax") nr => ret, + inlateout("rdi") a0 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("rdx") _, + lateout("rsi") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + } + ret +} + +#[inline(always)] +unsafe fn raw2(nr: u64, a0: u64, a1: u64) -> i64 { + let ret: i64; + unsafe { + asm!( + "syscall", + inlateout("rax") nr => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("rdx") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + } + ret +} + +#[inline(always)] +unsafe fn raw3(nr: u64, a0: u64, a1: u64, a2: u64) -> i64 { + let ret: i64; + unsafe { + asm!( + "syscall", + inlateout("rax") nr => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + inlateout("rdx") a2 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("r8") _, + lateout("r9") _, + lateout("r10") _, + options(nostack, preserves_flags), + ); + } + ret +} + +#[inline(always)] +unsafe fn raw4(nr: u64, a0: u64, a1: u64, a2: u64, a3: u64) -> i64 { + let ret: i64; + unsafe { + asm!( + "syscall", + inlateout("rax") nr => ret, + inlateout("rdi") a0 => _, + inlateout("rsi") a1 => _, + inlateout("rdx") a2 => _, + inlateout("r10") a3 => _, + lateout("rcx") _, + lateout("r11") _, + lateout("r8") _, + lateout("r9") _, + options(nostack, preserves_flags), + ); + } + ret +} + +/// Convert raw syscall return to C convention: on error set errno, return -1. +#[inline] +unsafe fn cvt(r: i64) -> i64 { + if r < 0 { + unsafe { *__errno_location() = (-r) as i32 }; + -1 + } else { + r + } +} + +#[no_mangle] +pub unsafe extern "C" fn fork() -> i32 { + unsafe { cvt(raw0(SYS_FORK)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn wait4(pid: i32, wstatus: *mut i32, options: i32, rusage: *mut u8) -> i32 { + unsafe { + cvt(raw4( + SYS_WAIT4, + pid as u64, + wstatus as u64, + options as u64, + rusage as u64, + )) as i32 + } +} + +#[no_mangle] +pub unsafe extern "C" fn kill(pid: i32, sig: i32) -> i32 { + unsafe { cvt(raw2(SYS_KILL, pid as u64, sig as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn setpgid(pid: i32, pgid: i32) -> i32 { + unsafe { cvt(raw2(SYS_SETPGID, pid as u64, pgid as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn sigaction(signum: i32, act: *const u8, oldact: *mut u8) -> i32 { + unsafe { cvt(raw3(SYS_SIGACTION, signum as u64, act as u64, oldact as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn pipe(pipefd: *mut i32) -> i32 { + unsafe { cvt(raw1(SYS_PIPE, pipefd as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn dup(oldfd: i32) -> i32 { + unsafe { cvt(raw1(SYS_DUP, oldfd as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn dup2(oldfd: i32, newfd: i32) -> i32 { + unsafe { cvt(raw2(SYS_DUP2, oldfd as u64, newfd as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn fcntl(fd: i32, cmd: i32, arg: u64) -> i32 { + unsafe { cvt(raw3(SYS_FCNTL, fd as u64, cmd as u64, arg)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn access(pathname: *const u8, mode: i32) -> i32 { + unsafe { cvt(raw2(SYS_ACCESS, pathname as u64, mode as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn open(pathname: *const u8, flags: i32, mode: u32) -> i32 { + unsafe { cvt(raw3(SYS_OPEN, pathname as u64, flags as u64, mode as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn close(fd: i32) -> i32 { + unsafe { cvt(raw1(SYS_CLOSE, fd as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn write(fd: i32, buf: *const u8, count: usize) -> isize { + unsafe { cvt(raw3(SYS_WRITE, fd as u64, buf as u64, count as u64)) as isize } +} + +#[no_mangle] +pub unsafe extern "C" fn read(fd: i32, buf: *mut u8, count: usize) -> isize { + unsafe { cvt(raw3(SYS_READ, fd as u64, buf as u64, count as u64)) as isize } +} + +#[no_mangle] +pub unsafe extern "C" fn execve( + pathname: *const u8, + argv: *const *const u8, + envp: *const *const u8, +) -> i32 { + unsafe { + cvt(raw3( + SYS_EXECVE, + pathname as u64, + argv as u64, + envp as u64, + )) as i32 + } +} + +#[no_mangle] +pub unsafe extern "C" fn getcwd(buf: *mut u8, size: usize) -> *mut u8 { + let r = unsafe { raw2(SYS_GETCWD, buf as u64, size as u64) }; + if r < 0 { + unsafe { *__errno_location() = (-r) as i32 }; + core::ptr::null_mut() + } else { + buf + } +} + +#[no_mangle] +pub unsafe extern "C" fn chdir(path: *const u8) -> i32 { + unsafe { cvt(raw1(SYS_CHDIR, path as u64)) as i32 } +} + +#[no_mangle] +pub unsafe extern "C" fn getpid() -> i32 { + // getpid never fails, so no errno handling needed. + unsafe { raw0(SYS_GETPID) as i32 } +} diff --git a/base/vibix_libc/src/fcntl.rs b/base/vibix_libc/src/fcntl.rs index 69e523fe..8df9d9f4 100644 --- a/base/vibix_libc/src/fcntl.rs +++ b/base/vibix_libc/src/fcntl.rs @@ -1,10 +1,11 @@ -//! File control operations: open, openat. +//! File control operations: open, openat, fcntl. use crate::helpers::syscall_ret; use vibix_abi::syscall; // Syscall numbers (Linux x86_64) const SYS_OPEN: u64 = 2; +const SYS_FCNTL: u64 = 72; const SYS_OPENAT: u64 = 257; /// Open a file by path. @@ -26,3 +27,13 @@ pub unsafe extern "C" fn openat(dirfd: i32, pathname: *const u8, flags: i32, mod let ret = syscall!(SYS_OPENAT, dirfd, pathname, flags, mode); syscall_ret(ret) as i32 } + +/// File control operations. +/// +/// Variadic in POSIX; here the third argument is passed as a `u64` covering +/// both integer and pointer cases. +#[no_mangle] +pub unsafe extern "C" fn fcntl(fd: i32, cmd: i32, arg: u64) -> i32 { + let ret = syscall!(SYS_FCNTL, fd, cmd, arg); + syscall_ret(ret) as i32 +} diff --git a/base/vibix_libc/src/lib.rs b/base/vibix_libc/src/lib.rs index 2a1cde3b..e8d47fea 100644 --- a/base/vibix_libc/src/lib.rs +++ b/base/vibix_libc/src/lib.rs @@ -15,6 +15,7 @@ mod helpers; pub mod errno; pub mod fcntl; +pub mod signal; pub mod stat; pub mod unistd; diff --git a/base/vibix_libc/src/signal.rs b/base/vibix_libc/src/signal.rs new file mode 100644 index 00000000..a4df020e --- /dev/null +++ b/base/vibix_libc/src/signal.rs @@ -0,0 +1,25 @@ +//! Signal operations: sigaction, kill. + +use crate::helpers::syscall_ret; +use vibix_abi::syscall; + +// Syscall numbers (Linux x86_64) +const SYS_SIGACTION: u64 = 13; +const SYS_KILL: u64 = 62; + +/// Install or query a signal handler. +/// +/// # Safety +/// `act` and `oldact` must be valid pointers to `sigaction` structs (or null). +#[no_mangle] +pub unsafe extern "C" fn sigaction(signum: i32, act: *const u8, oldact: *mut u8) -> i32 { + let ret = syscall!(SYS_SIGACTION, signum, act, oldact); + syscall_ret(ret) as i32 +} + +/// Send a signal to a process. +#[no_mangle] +pub unsafe extern "C" fn kill(pid: i32, sig: i32) -> i32 { + let ret = syscall!(SYS_KILL, pid, sig); + syscall_ret(ret) as i32 +} diff --git a/base/vibix_libc/src/unistd.rs b/base/vibix_libc/src/unistd.rs index 2316f13f..03e8a126 100644 --- a/base/vibix_libc/src/unistd.rs +++ b/base/vibix_libc/src/unistd.rs @@ -1,5 +1,6 @@ //! Unix-style operations: read, write, close, link, unlink, symlink, readlink, -//! mkdir, rmdir, rename, getcwd, chdir. +//! mkdir, rmdir, rename, getcwd, chdir, fork, dup, dup2, pipe, access, +//! setpgid, wait4. use crate::helpers::syscall_ret; use vibix_abi::syscall; @@ -8,6 +9,12 @@ use vibix_abi::syscall; const SYS_READ: u64 = 0; const SYS_WRITE: u64 = 1; const SYS_CLOSE: u64 = 3; +const SYS_ACCESS: u64 = 21; +const SYS_PIPE: u64 = 22; +const SYS_DUP: u64 = 32; +const SYS_DUP2: u64 = 33; +const SYS_FORK: u64 = 57; +const SYS_WAIT4: u64 = 61; const SYS_GETCWD: u64 = 79; const SYS_CHDIR: u64 = 80; const SYS_RENAME: u64 = 82; @@ -17,6 +24,7 @@ const SYS_LINK: u64 = 86; const SYS_UNLINK: u64 = 87; const SYS_SYMLINK: u64 = 88; const SYS_READLINK: u64 = 89; +const SYS_SETPGID: u64 = 109; /// Read from a file descriptor. /// @@ -140,3 +148,64 @@ pub unsafe extern "C" fn chdir(path: *const u8) -> i32 { let ret = syscall!(SYS_CHDIR, path); syscall_ret(ret) as i32 } + +/// Fork the current process. +/// +/// Returns 0 to the child, child PID to the parent, or -1 on error. +#[no_mangle] +pub unsafe extern "C" fn fork() -> i32 { + let ret = syscall!(SYS_FORK); + syscall_ret(ret) as i32 +} + +/// Duplicate a file descriptor. +#[no_mangle] +pub unsafe extern "C" fn dup(oldfd: i32) -> i32 { + let ret = syscall!(SYS_DUP, oldfd); + syscall_ret(ret) as i32 +} + +/// Duplicate a file descriptor to a specific number. +#[no_mangle] +pub unsafe extern "C" fn dup2(oldfd: i32, newfd: i32) -> i32 { + let ret = syscall!(SYS_DUP2, oldfd, newfd); + syscall_ret(ret) as i32 +} + +/// Create a pipe. +/// +/// # Safety +/// `pipefd` must point to an array of two `i32` values. +#[no_mangle] +pub unsafe extern "C" fn pipe(pipefd: *mut i32) -> i32 { + let ret = syscall!(SYS_PIPE, pipefd); + syscall_ret(ret) as i32 +} + +/// Check file accessibility. +/// +/// # Safety +/// `pathname` must be a valid null-terminated C string. +#[no_mangle] +pub unsafe extern "C" fn access(pathname: *const u8, mode: i32) -> i32 { + let ret = syscall!(SYS_ACCESS, pathname, mode); + syscall_ret(ret) as i32 +} + +/// Set process group ID. +#[no_mangle] +pub unsafe extern "C" fn setpgid(pid: i32, pgid: i32) -> i32 { + let ret = syscall!(SYS_SETPGID, pid, pgid); + syscall_ret(ret) as i32 +} + +/// Wait for a child process. +/// +/// # Safety +/// `wstatus` must be a valid pointer to an `i32` (or null). +/// `rusage` must be a valid pointer or null. +#[no_mangle] +pub unsafe extern "C" fn wait4(pid: i32, wstatus: *mut i32, options: i32, rusage: *mut u8) -> i32 { + let ret = syscall!(SYS_WAIT4, pid, wstatus, options, rusage); + syscall_ret(ret) as i32 +} diff --git a/library/std/src/sys/args/mod.rs b/library/std/src/sys/args/mod.rs index 5424d40a..98d793ab 100644 --- a/library/std/src/sys/args/mod.rs +++ b/library/std/src/sys/args/mod.rs @@ -8,12 +8,17 @@ target_os = "hermit", target_os = "motor", target_os = "uefi", + target_os = "vibix", target_os = "wasi", target_os = "xous", ))] mod common; cfg_select! { + target_os = "vibix" => { + mod vibix; + pub use vibix::*; + } any( all(target_family = "unix", not(any(target_os = "espidf", target_os = "vita"))), target_os = "hermit", diff --git a/library/std/src/sys/args/vibix.rs b/library/std/src/sys/args/vibix.rs new file mode 100644 index 00000000..8c065bdd --- /dev/null +++ b/library/std/src/sys/args/vibix.rs @@ -0,0 +1,41 @@ +//! Command line argument retrieval for vibix. +//! +//! The vibix kernel writes the standard SysV AMD64 initial stack layout +//! (argc, argv[], NULL, envp[], NULL, auxv[]) so we can read argc/argv +//! from the pointer the PAL passes through `init`. + +#![allow(dead_code)] + +pub use super::common::Args; +use crate::ffi::CStr; +use crate::os::unix::ffi::OsStringExt; +use crate::ptr; +use crate::sync::atomic::{AtomicIsize, AtomicPtr, Ordering}; + +/// Stored argc. +static ARGC: AtomicIsize = AtomicIsize::new(0); +/// Stored argv pointer. +static ARGV: AtomicPtr<*const u8> = AtomicPtr::new(ptr::null_mut()); + +/// One-time global initialization called from the vibix PAL. +pub unsafe fn init(argc: isize, argv: *const *const u8) { + ARGC.store(argc, Ordering::Relaxed); + ARGV.store(argv as *mut _, Ordering::Relaxed); +} + +/// Returns the command line arguments. +pub fn args() -> Args { + let argv = ARGV.load(Ordering::Relaxed); + let argc = if argv.is_null() { 0 } else { ARGC.load(Ordering::Relaxed) }; + + let mut vec = Vec::with_capacity(argc as usize); + for i in 0..argc { + let ptr = unsafe { argv.offset(i).read() }; + if ptr.is_null() { + break; + } + let cstr = unsafe { CStr::from_ptr(ptr.cast()) }; + vec.push(OsStringExt::from_vec(cstr.to_bytes().to_vec())); + } + Args::new(vec) +} diff --git a/library/std/src/sys/pal/vibix/mod.rs b/library/std/src/sys/pal/vibix/mod.rs index e835441a..11f9f5e5 100644 --- a/library/std/src/sys/pal/vibix/mod.rs +++ b/library/std/src/sys/pal/vibix/mod.rs @@ -30,7 +30,8 @@ pub fn abort_internal() -> ! { } // SAFETY: must be called only once during runtime initialization. -pub unsafe fn init(_argc: isize, _argv: *const *const u8, _sigpipe: u8) { +pub unsafe fn init(argc: isize, argv: *const *const u8, _sigpipe: u8) { + unsafe { crate::sys::args::init(argc, argv) }; } // SAFETY: must be called only once during runtime cleanup. @@ -41,15 +42,36 @@ pub unsafe fn cleanup() {} /// The kernel loads ELF binaries with entry point set to `_start`. /// Since vibix has no CRT, std provides the entry point directly. /// `main` is the symbol rustc generates that calls `lang_start`. +/// +/// The kernel writes the standard SysV AMD64 initial stack layout: +/// [rsp] = argc +/// [rsp + 8] = argv[0] +/// ... +/// [rsp + 8*argc] = argv[argc-1] +/// [rsp + 8*(argc+1)] = NULL (argv terminator) +/// followed by envp and auxv. +/// +/// We use `global_asm!` to emit a raw entry stub that reads argc/argv +/// from the stack before any Rust prologue can disturb rsp, then calls +/// `_start_rust(argc, argv)`. +#[cfg(not(test))] +core::arch::global_asm!( + ".global _start", + "_start:", + " mov rdi, [rsp]", // argc + " lea rsi, [rsp + 8]", // argv + " call _start_rust", + " ud2", // unreachable +); + #[cfg(not(test))] #[unsafe(no_mangle)] -pub unsafe extern "C" fn _start() -> ! { +unsafe extern "C" fn _start_rust(argc: isize, argv: *const *const u8) -> ! { unsafe extern "C" { fn main(argc: isize, argv: *const *const u8) -> isize; } - // No args/env on vibix yet; pass zeros. - let ret = unsafe { main(0, crate::ptr::null()) }; + let ret = unsafe { main(argc, argv) }; // exit_group(ret) unsafe { diff --git a/library/std/src/sys/thread/vibix.rs b/library/std/src/sys/thread/vibix.rs index 8b636dae..21e5480b 100644 --- a/library/std/src/sys/thread/vibix.rs +++ b/library/std/src/sys/thread/vibix.rs @@ -108,7 +108,7 @@ impl Thread { // Allocate ThreadData on the heap. let data = Box::into_raw(Box::new(ThreadData { - child_tid: Atomic::new(0), + child_tid: Atomic::::new(0), stack_base, stack_size, init: Some(init), diff --git a/xtask/src/ext2_image.rs b/xtask/src/ext2_image.rs index 97feb33a..0a8f87bc 100644 --- a/xtask/src/ext2_image.rs +++ b/xtask/src/ext2_image.rs @@ -128,6 +128,22 @@ pub fn expected_hash_path(workspace_root: &Path) -> PathBuf { /// yet (e.g. CI's first bootstrap run before the kernel-side mount /// plumbing lands in #577). pub fn build(workspace_root: &Path, init_src: Option<&Path>, update_hash: bool) -> R { + build_with_extras(workspace_root, init_src, &[], update_hash) +} + +/// Like [`build`], but also installs extra binaries into the image. +/// +/// Each entry in `extra_bins` is `(host_path, image_path)` — e.g. +/// `(&sh_bin, "/bin/sh")`. The host path must be whitespace-free (a +/// debugfs limitation). The image path's parent directory must already +/// exist in the fixture tree (currently: `/bin`, `/lib`, `/tmp`, `/dev`, +/// `/etc`, `/etc/init`). +pub fn build_with_extras( + workspace_root: &Path, + init_src: Option<&Path>, + extra_bins: &[(&Path, &str)], + update_hash: bool, +) -> R { let out = image_path(workspace_root); if let Some(parent) = out.parent() { fs::create_dir_all(parent)?; @@ -147,7 +163,7 @@ pub fn build(workspace_root: &Path, init_src: Option<&Path>, update_hash: bool) // `write` tokenises on whitespace, so paths with embedded spaces // would break) and makes the command transcript reproducible. let init_bin = stage_init(workspace_root, init_src)?; - run_debugfs_populate(&out, &init_bin)?; + run_debugfs_populate(&out, &init_bin, extra_bins)?; // Step 4: pin s_hash_seed in the superblock. pin_hash_seed(&out)?; @@ -265,7 +281,7 @@ fn run_mkfs(image: &Path) -> R<()> { /// /// The `sif` (set inode field) command takes the field name verbatim; /// the field list is documented in `debugfs(8)`. -fn run_debugfs_populate(image: &Path, init_bin: &Path) -> R<()> { +fn run_debugfs_populate(image: &Path, init_bin: &Path, extra_bins: &[(&Path, &str)]) -> R<()> { let init_str = init_bin.to_str().ok_or("init path is not UTF-8")?; if init_str.chars().any(|c| c.is_whitespace()) { return Err(format!("init path contains whitespace: {init_str}").into()); @@ -292,6 +308,23 @@ fn run_debugfs_populate(image: &Path, init_bin: &Path) -> R<()> { script.push_str("sif /init gid 0\n"); stamp(&mut script, "/init"); + // Install extra binaries (e.g. /bin/sh). + for (host_path, image_path) in extra_bins { + let host_str = host_path + .to_str() + .ok_or_else(|| format!("extra bin path is not UTF-8: {}", host_path.display()))?; + if host_str.chars().any(|c| c.is_whitespace()) { + return Err(format!("extra bin path contains whitespace: {host_str}").into()); + } + // debugfs `write` takes (host_path, image_path_without_leading_slash). + let img_name = image_path.strip_prefix('/').unwrap_or(image_path); + script.push_str(&format!("write {host_str} {img_name}\n")); + script.push_str(&format!("sif {image_path} mode 0100755\n")); + script.push_str(&format!("sif {image_path} uid 0\n")); + script.push_str(&format!("sif {image_path} gid 0\n")); + stamp(&mut script, image_path); + } + let mut child = Command::new("debugfs") .env("E2FSPROGS_FAKE_TIME", FIXED_EPOCH.to_string()) .args(["-w", "-f", "-"]) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 4af0b997..6125b229 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -183,6 +183,12 @@ const SMOKE_MARKERS: &[&str] = &[ // returned (this fires) vs. parent never woke (this missing). "init: wait4-return", "init: fork+exec+wait ok", + // #883: init launches /bin/sh after the hello fork+exec+wait cycle. + // The marker fires before the fork+exec, proving init reached the + // shell-launch code path. Whether execve succeeds depends on the + // ext2 rootfs having /bin/sh installed (guaranteed by the ext2 + // image builder when build_userspace_sh() is wired in). + "init: launching /bin/sh", ]; // Note: an earlier draft of #647 added a delta floor parsed out of @@ -260,6 +266,7 @@ fn main() -> R<()> { } "shell-pipeline" => shell_pipeline(&opts)?, "std-hello" => std_hello(&opts)?, + "sh" => sh_test(&opts)?, "lint" => lint()?, "isr-audit" => isr_audit::run(&workspace_root())?, "nm-check" => { @@ -441,7 +448,7 @@ fn main() -> R<()> { other => { eprintln!("unknown subcommand: {other}"); eprintln!( - "usage: cargo xtask [build|initrd|ext2-image|iso|run|test|test-unit|test-integration|smoke|pjdfstest|repro-fork|repro-fork-build|shell-pipeline|std-hello|lint|isr-audit|nm-check|validate-target|bench|fuzz|clean] [--release] [--fault-test] [--panic-test] [--bench] [--fork-trace] [--shard=I/N (test-integration only)]" + "usage: cargo xtask [build|initrd|ext2-image|iso|run|test|test-unit|test-integration|smoke|pjdfstest|repro-fork|repro-fork-build|shell-pipeline|std-hello|sh|lint|isr-audit|nm-check|validate-target|bench|fuzz|clean] [--release] [--fault-test] [--panic-test] [--bench] [--fork-trace] [--shard=I/N (test-integration only)]" ); std::process::exit(2); } @@ -797,12 +804,6 @@ fn build_userspace_std_hello() -> R { /// /// Uses the same out-of-tree `-Z build-std` approach as `std_hello`. /// The crate lives in `base/sh/` (base system program, not a test). -/// -/// Not yet wired into `cargo xtask build` because the in-repo std fork -/// has a pre-existing compile error on the vibix target (E0034 in -/// `sys/thread/vibix.rs`). The function is ready to be called once that -/// is resolved. -#[allow(dead_code)] fn build_userspace_sh() -> R { let ws = workspace_root(); let target_spec = ws.join(VIBIX_USERSPACE_TARGET); @@ -1720,7 +1721,10 @@ fn run_with_root(opts: &BuildOpts, root_flag: Option<&str>, cmdline_extras: &[&s let (disk, mut extra_cmdline): (PathBuf, Vec) = match root_flag { Some("ext2") => { let init_bin = build_userspace_init()?; - let img = ext2_image::build(&workspace_root(), Some(&init_bin), true)?; + let sh_bin = build_userspace_sh()?; + let extras: Vec<(&Path, &str)> = vec![(&sh_bin, "/bin/sh")]; + let img = + ext2_image::build_with_extras(&workspace_root(), Some(&init_bin), &extras, true)?; println!("→ root=ext2: booting {}", img.display()); (img, vec!["root=/dev/vda".to_string()]) } @@ -1733,7 +1737,10 @@ fn run_with_root(opts: &BuildOpts, root_flag: Option<&str>, cmdline_extras: &[&s } None => { let init_bin = build_userspace_init()?; - let img = ext2_image::build(&workspace_root(), Some(&init_bin), true)?; + let sh_bin = build_userspace_sh()?; + let extras: Vec<(&Path, &str)> = vec![(&sh_bin, "/bin/sh")]; + let img = + ext2_image::build_with_extras(&workspace_root(), Some(&init_bin), &extras, true)?; (img, Vec::new()) } }; @@ -2071,7 +2078,10 @@ fn smoke(opts: &BuildOpts) -> R<()> { // binary to keep the smoke lane in sync with code changes. let kernel = build(opts)?; let userspace_init = build_userspace_init()?; - let disk = ext2_image::build(&workspace_root(), Some(&userspace_init), true)?; + let sh_bin = build_userspace_sh()?; + let extras: Vec<(&Path, &str)> = vec![(&sh_bin, "/bin/sh")]; + let disk = + ext2_image::build_with_extras(&workspace_root(), Some(&userspace_init), &extras, true)?; let iso = workspace_root().join("target").join("vibix.iso"); make_iso_with_cmdline(&kernel, &iso, "iso_root", "root=/dev/vda")?; @@ -2828,6 +2838,147 @@ fn std_hello(opts: &BuildOpts) -> R<()> { } } +/// Boot vibix with `/bin/sh` installed in the ext2 rootfs and verify +/// that init launches the shell (issue #883). +/// +/// The test builds the regular init (which fork+exec's `/bin/sh` after +/// the hello cycle), builds the sh binary, installs both in the ext2 +/// image, and asserts that the `init: launching /bin/sh` marker +/// appears on the serial console. +fn sh_test(opts: &BuildOpts) -> R<()> { + use std::collections::VecDeque; + use std::io::BufRead as _; + use std::time::Instant; + + const HARD_CAP: Duration = Duration::from_secs(120); + const SUCCESS_MARKER: &str = "init: launching /bin/sh"; + const PANIC_MARKER: &str = "KERNEL PANIC:"; + + let kernel = build(opts)?; + let init_bin = build_userspace_init()?; + let sh_bin = build_userspace_sh()?; + + // Install init as /init and sh as /bin/sh in the ext2 rootfs image. + let extras: Vec<(&Path, &str)> = vec![(&sh_bin, "/bin/sh")]; + let disk = ext2_image::build_with_extras(&workspace_root(), Some(&init_bin), &extras, true)?; + let iso = workspace_root().join("target").join("vibix-sh.iso"); + make_iso_with_cmdline(&kernel, &iso, "iso_sh", "root=/dev/vda")?; + + let mut child = Command::new("qemu-system-x86_64") + .args([ + "-M", + "q35", + "-cpu", + "max", + "-m", + "256M", + "-serial", + "stdio", + "-display", + "none", + "-no-reboot", + "-no-shutdown", + "-device", + "isa-debug-exit,iobase=0xf4,iosize=0x04", + ]) + .args(virtio_blk_args(&disk)) + .arg("-cdrom") + .arg(&iso) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?; + + let pid = child.id(); + let stdout = child.stdout.take().ok_or("no stdout pipe")?; + + let (cancel_tx, cancel_rx) = std::sync::mpsc::channel::<()>(); + let hard_pid = pid; + let hard_watchdog = std::thread::spawn(move || { + if let Err(std::sync::mpsc::RecvTimeoutError::Timeout) = cancel_rx.recv_timeout(HARD_CAP) { + let _ = Command::new("kill").arg(hard_pid.to_string()).status(); + } + }); + + let (tx, rx) = std::sync::mpsc::channel::(); + let reader_handle = std::thread::spawn(move || { + let mut reader = std::io::BufReader::new(stdout); + let mut line = String::new(); + loop { + line.clear(); + match reader.read_line(&mut line) { + Ok(0) => break, + Ok(_) => { + if tx.send(line.clone()).is_err() { + break; + } + } + Err(_) => break, + } + } + }); + + let start = Instant::now(); + let mut success = false; + let mut failure: Option = None; + let mut tail: VecDeque = VecDeque::with_capacity(64); + const TICK: Duration = Duration::from_millis(200); + + loop { + match rx.recv_timeout(TICK) { + Ok(line) => { + print!("{line}"); + if tail.len() == 64 { + tail.pop_front(); + } + tail.push_back(line.clone()); + + if line.contains(PANIC_MARKER) { + failure = Some(format!("kernel panic: {}", line.trim_end())); + break; + } + if line.contains(SUCCESS_MARKER) { + success = true; + break; + } + } + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + if start.elapsed() > HARD_CAP { + failure = Some(format!("hard cap exceeded ({HARD_CAP:?}) without success")); + break; + } + } + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + if !success && failure.is_none() { + failure = Some(format!("QEMU exited before `{SUCCESS_MARKER}` marker")); + } + break; + } + } + } + + let _ = Command::new("kill").arg(pid.to_string()).status(); + drop(cancel_tx); + let _ = hard_watchdog.join(); + let _ = reader_handle.join(); + let _ = child.wait(); + + match (success, failure) { + (true, _) => { + println!("→ sh: `{SUCCESS_MARKER}` in {:?} ✓", start.elapsed()); + Ok(()) + } + (false, Some(msg)) => { + eprintln!("--- captured serial (tail) ---"); + for line in &tail { + eprint!("{line}"); + } + eprintln!("------------------------------"); + Err(format!("sh: {msg}").into()) + } + (false, None) => Err("sh: terminated with no success and no failure marker".into()), + } +} + /// Parsed view of the repro-fork harness start banner. /// /// The harness emits a dedicated `repro: CYCLES=N\n` line before the