Skip to content

Wire /bin/sh into init and add shell build#894

Merged
dburkart merged 5 commits intomainfrom
wire-sh-boot
May 5, 2026
Merged

Wire /bin/sh into init and add shell build#894
dburkart merged 5 commits intomainfrom
wire-sh-boot

Conversation

@dburkart
Copy link
Copy Markdown
Owner

@dburkart dburkart commented May 5, 2026

Summary

  • Remove dead-code gate from build_userspace_sh() and fix the pre-existing Atomic::new(0) ambiguity (E0034) in the std fork's thread/vibix.rs
  • Add C-ABI syscall shims (base/sh/src/syscalls.rs) using raw inline asm, providing POSIX symbols (fork, pipe, dup2, execve, kill, wait4, sigaction, etc.) without double-linking vibix_abi
  • Extend ext2_image::build() with a new build_with_extras() variant that installs extra binaries (e.g. /bin/sh) alongside /init
  • Update init to fork+exec /bin/sh after the existing hello fork+exec+wait cycle
  • Add init: launching /bin/sh as a required smoke marker (43 markers total)
  • Add cargo xtask sh integration test subcommand
  • Expand vibix_libc with missing POSIX symbols for future use

Closes #883

Test plan

  • cargo xtask build succeeds
  • cargo xtask smoke passes with all 43 markers (including new init: launching /bin/sh)
  • Host unit tests pass (cargo test -p xtask -- 71 tests)
  • Shell host tests pass (cargo test in base/sh/ -- 399 tests)
  • CI integration tests

- Remove dead_code gate from build_userspace_sh() in xtask
- Fix Atomic::<u32>::new(0) ambiguity in std thread/vibix.rs (E0034)
- Add C-ABI syscall shims to sh (fork, pipe, dup2, execve, etc.)
  using raw inline asm to avoid double-linking vibix_abi
- Extend ext2_image::build() with build_with_extras() to install
  extra binaries (e.g. /bin/sh) alongside /init
- Update init to fork+exec /bin/sh after the hello cycle
- Add "init: launching /bin/sh" smoke marker
- Add `cargo xtask sh` integration test subcommand
- Expand vibix_libc with missing POSIX symbols (fork, wait4, kill,
  dup, dup2, pipe, access, setpgid, fcntl, sigaction)

Closes #883

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: baaade4a-90e7-4473-9bec-4b09c05366ee

📥 Commits

Reviewing files that changed from the base of the PR and between 8d27405 and f6177cc.

📒 Files selected for processing (1)
  • library/std/src/sys/args/vibix.rs
✅ Files skipped from review due to trivial changes (1)
  • library/std/src/sys/args/vibix.rs

📝 Walkthrough

Walkthrough

This PR implements userspace shell binary launch and integration. It adds C-ABI syscall shims to enable the shell binary to call POSIX functions, extends the vibix libc with process/signal operations, modifies init to fork and exec /bin/sh after boot, wires argc/argv through the runtime startup sequence, and updates xtask to build the shell binary, install it into the rootfs, and test it via QEMU.

Changes

Shell Launch Implementation

Layer / File(s) Summary
Syscall/libc Foundation
base/sh/src/syscalls.rs, base/vibix_libc/src/signal.rs, base/vibix_libc/src/fcntl.rs, base/vibix_libc/src/unistd.rs, base/vibix_libc/src/lib.rs, library/std/src/sys/thread/vibix.rs
Define C-ABI POSIX syscall wrappers (fork, wait4, kill, setpgid, sigaction, pipe, dup, dup2, fcntl, access, open, close, read, write, execve, getcwd, chdir, getpid) using inline syscall assembly, errno conversion, and SYS_* constants for x86_64 Linux. Minor thread type annotation fix for Atomic::<u32>::new(0).
Shell Binary
base/sh/src/main.rs
Add conditional syscalls module (non-test builds), skip standard environment import on vibix target (preserve $PATH only), force interactive mode (is_tty = true) for consistent prompt and job-control behavior.
Runtime/Init Integration
library/std/src/sys/args/mod.rs, library/std/src/sys/args/vibix.rs, library/std/src/sys/pal/vibix/mod.rs, base/init/src/main.rs
Extract argc/argv from initial stack frame via global_asm! stub in _start, pass to _start_rust(argc, argv), forward to args::init() during runtime init. Init adds shell constants and launches /bin/sh via fork/exec/wait sequence after hello process completes.
Build & Test
xtask/src/ext2_image.rs, xtask/src/main.rs
Add build_with_extras() API to install host binaries into rootfs. Add sh_test subcommand and wire shell installation into smoke and run --root=ext2 paths; require init: launching /bin/sh marker in smoke tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • dburkart/vibix#870: Modifies the vibix userspace entry point (library/std/src/sys/pal/vibix/mod.rs) in parallel with argc/argv handling changes.
  • dburkart/vibix#892: Implements shell executor and PATH-based exec, operating on the same base/sh/* subsystem.
  • dburkart/vibix#891: Implements shell builtins that call the C-ABI syscalls (execve, chdir, getcwd) now provided by this PR's shims.

Poem

🐰 A shell emerges from the void so deep,
With syscall whispers and a fork to keep,
Init springs to life, then spawns with care,
Argc and argv dance through the air,
Now /bin/sh awaits at the golden gate,
Where prompts and commands synchronize—fate! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Wire /bin/sh into init and add shell build' accurately captures the primary objective: integrating /bin/sh into the init sequence and adding shell build infrastructure.
Description check ✅ Passed The description is directly related to the changeset, detailing specific implementations like syscall shims, ext2_image updates, init modifications, and test markers.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #883: adds build_sh() equivalent via xtask updates, installs /bin/sh in rootfs via build_with_extras(), modifies init to fork/exec /bin/sh, and adds smoke markers.
Out of Scope Changes check ✅ Passed All changes are scoped to the objectives: fixing Atomic::new(0) ambiguity is prerequisite for compilation; syscall shims and vibix_libc expansions are necessary for shell execution; xtask and init updates directly support /bin/sh integration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wire-sh-boot

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@base/sh/src/syscalls.rs`:
- Around line 186-188: The syscall wrapper sigaction currently calls raw3 with
SYS_SIGACTION but must call the 4-argument rt_sigaction; change sigaction to
call raw4 (or the 4-arg syscall helper) and pass the correct fourth argument:
the sigset size (the size of sigset_t on x86_64, e.g. sizeof(sigset_t)/8 bytes
as used elsewhere). Update the call site referenced by sigaction (replace
raw3(SYS_SIGACTION, signum as u64, act as u64, oldact as u64) with the 4-arg
variant including the sigsetsize) so job::install_interactive_signals() can
succeed.

In `@base/vibix_libc/src/signal.rs`:
- Around line 7-17: The rt_sigaction syscall (SYS_SIGACTION) requires a fourth
sigsetsize argument; update the sigaction function to pass the sigset size as
the fourth parameter (e.g., compute sigsetsize =
core::mem::size_of::<sigset_t>() or the equivalent byte size for the platform,
cast to the syscall integer type) when calling syscall!(SYS_SIGACTION, signum,
act, oldact, sigsetsize), keeping the rest of the function (including
syscall_ret) unchanged.

In `@xtask/src/main.rs`:
- Around line 2865-2997: The test currently treats seeing SUCCESS_MARKER ("init:
launching /bin/sh") as enough; instead, after detecting SUCCESS_MARKER you must
drive the spawned QEMU's stdin to verify a real shell is running: when building
the Command in spawn, set .stdin(Stdio::piped()) and capture child.stdin (e.g.
let mut stdin = child.stdin.take().ok_or("no stdin pipe")?); then in the main
recv loop (where you match rx.recv_timeout) do not set success = true
immediately on SUCCESS_MARKER — write a short probe like "echo hello\n" to stdin
and wait (within HARD_CAP or a shorter per-probe timeout) for the probe output
"hello" on rx before setting success; update failure cases to include probe
timeouts and keep using TICK, HARD_CAP, SUCCESS_MARKER, PANIC_MARKER, rx, tx,
child, reader_handle to locate where to change behavior.
- Around line 1743-1752: The current code always builds and attaches a
deterministic ext2 image (via build_userspace_init, build_userspace_sh and
ext2_image::build_with_extras) even when no --root is provided; change the flow
so the ext2 image is only constructed and passed to run when the user explicitly
requested the ext2/root path (i.e., when the --root option or equivalent flag is
present). Locate the block that calls build_userspace_init(),
build_userspace_sh() and ext2_image::build_with_extras() and guard it with the
condition used to detect the ext2/root mode so the non-ext2 "plain cargo xtask
run" path continues to run without attaching the ext2 image.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93e6e145-d427-452e-9d2b-4a9400efe666

📥 Commits

Reviewing files that changed from the base of the PR and between 6c08d8a and 46bf406.

📒 Files selected for processing (11)
  • base/init/src/main.rs
  • base/sh/src/main.rs
  • base/sh/src/syscalls.rs
  • base/vibix_libc/src/fcntl.rs
  • base/vibix_libc/src/lib.rs
  • base/vibix_libc/src/signal.rs
  • base/vibix_libc/src/unistd.rs
  • library/std/src/sys/thread/vibix.rs
  • tests/fixtures/ext2_image.sha256
  • xtask/src/ext2_image.rs
  • xtask/src/main.rs

Comment thread base/sh/src/syscalls.rs
Comment on lines +186 to +188
#[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 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

On Linux x86_64, what syscall does number 13 map to, what arguments does rt_sigaction take, and what sigsetsize value is expected?

💡 Result:

On Linux x86_64, syscall number 13 maps to rt_sigaction. [1] Arguments taken by rt_sigaction (in order): 1) int signum 2) const struct sigaction *act 3) struct sigaction *oldact 4) size_t sigsetsize (These correspond to the documented parameter list for the system call.) [2][3] Expected sigsetsize value: the argument is currently required to have the value sizeof(sigset_t); otherwise the kernel returns EINVAL. [2]

Citations:


🏁 Script executed:

# Check SYS_SIGACTION value and verify raw4 exists
cd base/sh/src && head -50 syscalls.rs | grep -E "SYS_SIGACTION|raw"

Repository: dburkart/vibix

Length of output: 264


🏁 Script executed:

# Get the full syscall number definitions and raw helpers
cd base/sh/src && sed -n '1,150p' syscalls.rs | grep -E "SYS_SIGACTION|fn raw[0-9]"

Repository: dburkart/vibix

Length of output: 344


🏁 Script executed:

# Check the sigaction function implementation
cd base/sh/src && sed -n '180,195p' syscalls.rs

Repository: dburkart/vibix

Length of output: 533


🏁 Script executed:

# Verify job::install_interactive_signals() location in main.rs
cd base/sh/src && grep -n "install_interactive_signals" main.rs

Repository: dburkart/vibix

Length of output: 105


Pass the required fourth argument to rt_sigaction.

Line 187 is issuing syscall 13 via raw3, but on x86_64 that entry point is rt_sigaction, which takes a fourth sigsetsize argument. Without it, job::install_interactive_signals() in base/sh/src/main.rs Line 69 will fail with EINVAL, preventing the shell from installing its SIGINT/SIGTSTP handlers.

Suggested fix
 #[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 }
+    const KERNEL_SIGSET_SIZE: u64 = core::mem::size_of::<u64>() as u64;
+    unsafe {
+        cvt(raw4(
+            SYS_SIGACTION,
+            signum as u64,
+            act as u64,
+            oldact as u64,
+            KERNEL_SIGSET_SIZE,
+        )) as i32
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[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 sigaction(signum: i32, act: *const u8, oldact: *mut u8) -> i32 {
const KERNEL_SIGSET_SIZE: u64 = core::mem::size_of::<u64>() as u64;
unsafe {
cvt(raw4(
SYS_SIGACTION,
signum as u64,
act as u64,
oldact as u64,
KERNEL_SIGSET_SIZE,
)) as i32
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/sh/src/syscalls.rs` around lines 186 - 188, The syscall wrapper
sigaction currently calls raw3 with SYS_SIGACTION but must call the 4-argument
rt_sigaction; change sigaction to call raw4 (or the 4-arg syscall helper) and
pass the correct fourth argument: the sigset size (the size of sigset_t on
x86_64, e.g. sizeof(sigset_t)/8 bytes as used elsewhere). Update the call site
referenced by sigaction (replace raw3(SYS_SIGACTION, signum as u64, act as u64,
oldact as u64) with the 4-arg variant including the sigsetsize) so
job::install_interactive_signals() can succeed.

Comment on lines +7 to +17
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Linux x86_64 syscall 13 rt_sigaction

💡 Result:

On Linux x86_64, syscall number 13 is rt_sigaction, which examines and/or changes the action taken by a process when it receives a specific signal [1][2][3][4]. This replaced the older sigaction syscall to support larger sigset_t for real-time signals added in Linux 2.2 [1][5].

Citations:


🏁 Script executed:

# Check if there's any syscall table or documentation in the repo
fd -i syscall . -type f | head -20

Repository: dburkart/vibix

Length of output: 229


🌐 Web query:

rt_sigaction syscall signature x86_64 sigsetsize argument

💡 Result:

rt_sigaction (x86_64 Linux) system call signature: - syscall number: 13 [1] - C/kernel signature (argument order): long sys_rt_sigaction(int sig, const struct sigaction *act, struct sigaction *oact, size_t sigsetsize) [2] What the sigsetsize argument is (and what value it must be): - sigsetsize is the size in bytes of the signal sets in act.sa_mask and oldact.sa_mask [2][3][4]. - For correctness, the man page states this argument is currently required to be sizeof(sigset_t); otherwise the kernel returns EINVAL [2][3][4]. Practical note for x86_64 callers: - “sigsetsize” should be sizeof(sigset_t) from your C library headers/ABI, because rt_sigaction expects the kernel to receive that exact size in bytes [2][3][4].

Citations:


Add the required sigsetsize argument to the rt_sigaction syscall.

Syscall 13 on Linux x86_64 is rt_sigaction, which requires four arguments: signum, act, oldact, and sigsetsize. The current implementation passes only three arguments, causing the kernel to reject the call with EINVAL. This will cause signal handling to fail.

-const SYS_SIGACTION: u64 = 13;
+const SYS_RT_SIGACTION: u64 = 13;
+const SIGSET_SIZE: usize = 8;

 /// 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);
+    let ret = syscall!(SYS_RT_SIGACTION, signum, act, oldact, SIGSET_SIZE);
     syscall_ret(ret) as i32
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/vibix_libc/src/signal.rs` around lines 7 - 17, The rt_sigaction syscall
(SYS_SIGACTION) requires a fourth sigsetsize argument; update the sigaction
function to pass the sigset size as the fourth parameter (e.g., compute
sigsetsize = core::mem::size_of::<sigset_t>() or the equivalent byte size for
the platform, cast to the syscall integer type) when calling
syscall!(SYS_SIGACTION, signum, act, oldact, sigsetsize), keeping the rest of
the function (including syscall_ret) unchanged.

Comment thread xtask/src/main.rs
Comment on lines 1743 to 1752
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())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep plain cargo xtask run on the non-ext2 path.

Lines 1743-1752 now attach the deterministic ext2 image even when --root is absent. That silently changes the default run behavior away from the documented scratch-disk / auto-root path and can hide regressions that only reproduce outside the ext2-root flow.

Suggested fix
         None => {
-            let init_bin = build_userspace_init()?;
-            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())
+            (ensure_test_disk()?, Vec::new())
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@xtask/src/main.rs` around lines 1743 - 1752, The current code always builds
and attaches a deterministic ext2 image (via build_userspace_init,
build_userspace_sh and ext2_image::build_with_extras) even when no --root is
provided; change the flow so the ext2 image is only constructed and passed to
run when the user explicitly requested the ext2/root path (i.e., when the --root
option or equivalent flag is present). Locate the block that calls
build_userspace_init(), build_userspace_sh() and ext2_image::build_with_extras()
and guard it with the condition used to detect the ext2/root mode so the
non-ext2 "plain cargo xtask run" path continues to run without attaching the
ext2 image.

Comment thread xtask/src/main.rs
Comment on lines +2865 to +2997
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::<String>();
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<String> = None;
let mut tail: VecDeque<String> = 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()),
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Drive the shell over serial before declaring success.

SUCCESS_MARKER is init: launching /bin/sh, but init prints that before the shell fork/execve happens. This test will still pass if /bin/sh is missing, execve fails immediately, the prompt never appears, or serial stdin is broken. The linked objective was to wait for the prompt and round-trip a command such as echo hello.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@xtask/src/main.rs` around lines 2865 - 2997, The test currently treats seeing
SUCCESS_MARKER ("init: launching /bin/sh") as enough; instead, after detecting
SUCCESS_MARKER you must drive the spawned QEMU's stdin to verify a real shell is
running: when building the Command in spawn, set .stdin(Stdio::piped()) and
capture child.stdin (e.g. let mut stdin = child.stdin.take().ok_or("no stdin
pipe")?); then in the main recv loop (where you match rx.recv_timeout) do not
set success = true immediately on SUCCESS_MARKER — write a short probe like
"echo hello\n" to stdin and wait (within HARD_CAP or a shorter per-probe
timeout) for the probe output "hello" on rx before setting success; update
failure cases to include probe timeouts and keep using TICK, HARD_CAP,
SUCCESS_MARKER, PANIC_MARKER, rx, tx, child, reader_handle to locate where to
change behavior.

claude added 2 commits May 5, 2026 20:15
… target

The vibix PAL's _start entry point was passing argc=0, argv=null to main(),
which meant std::env::args() always returned empty on vibix. This prevented
the shell from seeing its -c flag when exec'd by init.

- Replace the hardcoded _start function with a global_asm stub that reads
  the real argc/argv from the SysV AMD64 initial stack layout
- Add a vibix-specific args module (sys/args/vibix.rs) that stores and
  retrieves argc/argv via atomics, matching the pattern used by unix targets
- Register the vibix args module in sys/args/mod.rs (before the unix branch)
- Forward argc/argv from the PAL init() to the args module
- Guard std::env::vars() with #[cfg(not(target_os = "vibix"))] in the shell
  (the unsupported env module panics on vibix)
- Guard mod syscalls with #[cfg(not(test))] so host tests can mock symbols
- Set is_tty = true unconditionally (vibix serial is not a POSIX tty)
- Update ext2 image hash for the rebuilt rootfs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ext2-image CI check builds the base image without extra binaries.
The previous commit incorrectly updated the hash to match a build that
included /bin/sh. Restore the hash to match the base image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@base/sh/src/main.rs`:
- Around line 7-8: The runtime shims module is currently exposed to all non-test
builds via #[cfg(not(test))] and should be limited to vibix target builds;
update the cfg on the mod syscalls declaration (the mod syscalls in
base/sh/src/main.rs) to only compile on the vibix target (for example use a cfg
like all(target_vendor = "vibix", not(test)) — or target_os = "vibix" if your
vibix target is set as the OS) so the raw syscall shims are not pulled into
non-vibix binaries.
- Around line 63-67: The code currently sets let is_tty = true; which forces
interactive mode for all hosts; change it so only vibix targets force
interactive mode by computing is_tty conditionally (e.g. replace the line in
main.rs with: let is_tty = if running_on_vibix() { true } else {
atty::is(atty::Stream::Stdin) };), add a small helper running_on_vibix() that
detects vibix either via a compile-time cfg/feature (cfg!(feature = "vibix") or
cfg!(target_os = "vibix")) or a clear runtime marker like an environment
variable (e.g. VIBIX_SERIAL), and keep references to is_tty and the new
running_on_vibix() helper so only vibix forces interactive behavior while all
other hosts use proper isatty detection.

In `@library/std/src/sys/args/vibix.rs`:
- Line 37: The call to CStr::from_ptr is using ptr which is a *const u8; change
the argument to CStr::from_ptr(ptr.cast()) so the pointer is explicitly cast to
*const c_char (matching wasip1.rs at its use), i.e., locate the statement with
CStr::from_ptr and replace the raw ptr with ptr.cast() to satisfy the expected
*const c_char/*const i8 type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f14152d-7d97-4d44-93d9-c544ecd091c7

📥 Commits

Reviewing files that changed from the base of the PR and between fb7e084 and 8d27405.

📒 Files selected for processing (5)
  • base/sh/src/main.rs
  • library/std/src/sys/args/mod.rs
  • library/std/src/sys/args/vibix.rs
  • library/std/src/sys/pal/vibix/mod.rs
  • tests/fixtures/ext2_image.sha256

Comment thread base/sh/src/main.rs
Comment on lines +7 to +8
#[cfg(not(test))]
mod syscalls;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope syscall shims to vibix-only builds.

#[cfg(not(test))] pulls raw syscall POSIX shims into all non-test binaries. That can unintentionally override host libc symbols and introduce host-specific breakage. Restrict this module to vibix target builds.

Suggested fix
-#[cfg(not(test))]
+#[cfg(all(target_os = "vibix", not(test)))]
 mod syscalls;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[cfg(not(test))]
mod syscalls;
#[cfg(all(target_os = "vibix", not(test)))]
mod syscalls;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/sh/src/main.rs` around lines 7 - 8, The runtime shims module is
currently exposed to all non-test builds via #[cfg(not(test))] and should be
limited to vibix target builds; update the cfg on the mod syscalls declaration
(the mod syscalls in base/sh/src/main.rs) to only compile on the vibix target
(for example use a cfg like all(target_vendor = "vibix", not(test)) — or
target_os = "vibix" if your vibix target is set as the OS) so the raw syscall
shims are not pulled into non-vibix binaries.

Comment thread base/sh/src/main.rs
Comment on lines +63 to +67
// 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t force interactive mode on all targets.

let is_tty = true; makes non-interactive host stdin sessions behave as interactive (prompt output + interactive signal behavior), which can break pipelines/scripts. Keep forced interactive behavior only for vibix.

Suggested fix
-    let is_tty = true;
+    #[cfg(target_os = "vibix")]
+    let is_tty = true;
+    #[cfg(not(target_os = "vibix"))]
+    let is_tty = std::env::var_os("TERM").is_some();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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;
// 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.
#[cfg(target_os = "vibix")]
let is_tty = true;
#[cfg(not(target_os = "vibix"))]
let is_tty = std::env::var_os("TERM").is_some();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/sh/src/main.rs` around lines 63 - 67, The code currently sets let is_tty
= true; which forces interactive mode for all hosts; change it so only vibix
targets force interactive mode by computing is_tty conditionally (e.g. replace
the line in main.rs with: let is_tty = if running_on_vibix() { true } else {
atty::is(atty::Stream::Stdin) };), add a small helper running_on_vibix() that
detects vibix either via a compile-time cfg/feature (cfg!(feature = "vibix") or
cfg!(target_os = "vibix")) or a clear runtime marker like an environment
variable (e.g. VIBIX_SERIAL), and keep references to is_tty and the new
running_on_vibix() helper so only vibix forces interactive behavior while all
other hosts use proper isatty detection.

Comment thread library/std/src/sys/args/vibix.rs Outdated
Explicitly cast *const u8 to *const c_char for type safety, matching
the pattern used by wasip1.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dburkart dburkart merged commit 46c38d0 into main May 5, 2026
15 checks passed
@dburkart dburkart deleted the wire-sh-boot branch May 5, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire /bin/sh into init and add shell integration test

2 participants