Skip to content

Fix interactive /bin/sh boot: four root causes#920

Open
dburkart wants to merge 7 commits into
mainfrom
wire-sh-boot
Open

Fix interactive /bin/sh boot: four root causes#920
dburkart wants to merge 7 commits into
mainfrom
wire-sh-boot

Conversation

@dburkart
Copy link
Copy Markdown
Owner

@dburkart dburkart commented May 7, 2026

Summary

  • EFBIG from execve: shell binary (1.7 MB) exceeded 1 MB READ_ALL_MAX limit. Raised to 4 MB.
  • Compiler-generated _start: replaced global_asm! + _start_rust with a single #[naked] _start that reads SysV initial stack directly.
  • Initial RSP not 16-byte aligned: SysV AMD64 ABI requires %rsp % 16 == 0 at process entry. Fixed auxv stack builder parity logic.
  • Misaligned TLS TCB: compute_tls_layout placed TCB at tdata_start + total_size which was not 8-byte aligned when p_memsz isn't a multiple of 8.
  • execve missing growsdown stack: added VMA_GROWSDOWN flag and guard page to execve's stack setup (matching init_process).

After these fixes the shell fully initializes, prints $ , reads from stdin, and exits cleanly on EOF.

Test plan

  • cargo xtask smoke — all 43 markers pass
  • cargo xtask sh — shell launch marker detected
  • cargo test -p vibix --lib — 716 kernel unit tests pass (including auxv alignment + TLS layout tests)
  • cargo test (sh) — 399 shell host tests pass
  • Manual QEMU boot: shell prints $ prompt and exits cleanly on stdin EOF

🤖 Generated with Claude Code

claude added 6 commits May 5, 2026 20:01
- 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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… 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>
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>
…lignment, and ELF size limit

Four root causes prevented the shell from booting interactively:

1. EFBIG from execve — the shell binary (1.7 MB) exceeded the 1 MB
   READ_ALL_MAX limit in resolve_execve_binary. Raised to 4 MB.

2. Compiler-generated _start overriding PAL — the Rust compiler's
   default _start hardcoded argc=0/argv=null. Replaced global_asm! +
   _start_rust with a single #[naked] _start that reads directly from
   the SysV initial stack.

3. Initial RSP not 16-byte aligned — the SysV AMD64 ABI requires
   %rsp % 16 == 0 at process entry. The auxv stack builder only
   8-byte-aligned, causing #GP on movaps instructions. Fixed by
   computing the parity of pushed 8-byte words and adjusting the
   alignment base accordingly.

4. Misaligned TLS TCB — compute_tls_layout placed the TCB at
   tdata_start + total_size which could be misaligned when p_memsz
   is not a multiple of 8. Round TCB VA up to 8-byte alignment.

Additionally, execve now creates a VMA_GROWSDOWN stack with a guard
page (matching init_process), so the shell's Rust runtime has room
to grow beyond a single 4 KiB page.

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

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete /bin/sh userspace shell for vibix, including C-ABI POSIX syscall shim infrastructure, stdlib platform support, shell binary implementation, init integration to fork/exec the shell, and kernel-side stack/TLS alignment adjustments to support the new execution environment.

Changes

Shell and Platform Foundation

Layer / File(s) Summary
Stdlib Platform Support
library/std/src/sys/pal/vibix/mod.rs, library/std/src/sys/args/vibix.rs, library/std/src/sys/args/mod.rs, library/std/src/sys/thread/vibix.rs
Entry point _start now reads real argc/argv from stack and passes to main. Argument retrieval via args() loads stored argc/argv from atomics and constructs OS string vector. Platform initialization init() calls sys::args::init() with real values. Thread Atomic initialization uses turbofish syntax for clarity.
Vibix Libc Syscall Shims
base/vibix_libc/src/unistd.rs, base/vibix_libc/src/signal.rs, base/vibix_libc/src/fcntl.rs, base/vibix_libc/src/lib.rs
Exported C-ABI POSIX syscall wrappers: fork, wait4, dup, dup2, pipe, access, setpgid (unistd); sigaction, kill (signal); fcntl (fcntl). Each wraps Linux x86_64 syscall via syscall! macro and converts return via syscall_ret. Module exports organized for platform reusability.
Shell Syscall Infrastructure
base/sh/src/syscalls.rs
Standalone C-ABI syscall shim layer for shell: defines Linux x86_64 syscall constants, inline-asm helpers (raw0raw4), error-to-errno conversion via cvt, and #[no_mangle] extern "C" wrappers for fork, wait4, kill, setpgid, sigaction, pipe, dup, dup2, fcntl, access, open, close, read, write, execve, getcwd, chdir, getpid.
Shell Binary Implementation
base/sh/src/main.rs
Interactive shell with POSIX command parsing and execution; #[cfg(not(test))] gates syscalls module to exclude from host tests; skips env::vars() import on vibix (panics); unconditionally treats stdin as interactive; uses stdin.lock().lines() for line iteration with multi-line error handling.
Init Integration
base/init/src/main.rs
Extends init to fork/exec /bin/sh after hello smoke test: writes launch marker, forks child, child execs /bin/sh (exits status 1 on failure), parent waits via wait4, then loops forever.
Kernel Stack & TLS Alignment
kernel/src/arch/x86_64/syscall.rs, kernel/src/mem/auxv.rs, kernel/src/mem/elf.rs
Stack setup now uses explicit stack_top and grows-down VMA with separate guard-region VMA (sized by DEFAULT_STACK_RLIMIT). Initial stack enforces SysV AMD64 16-byte alignment by computing word count under initial RSP and conditionally adjusting offset. TLS TCB location rounded to 8-byte boundary. Tests assert stack/RSP alignment requirements.
Build & Test Plumbing
xtask/src/ext2_image.rs, xtask/src/main.rs, kernel/src/shell/vfs_helpers.rs, tests/fixtures/ext2_image.sha256
ext2_image::build_with_extras() installs optional extra userspace binaries (e.g., /bin/sh) into ext2 filesystem with deterministic timestamps. xtask adds new sh subcommand that boots kernel with shell, asserts init: launching /bin/sh on serial output. READ_ALL_MAX increased to 4 MiB to support larger binary reads. Fixture hash updated for new image layout.

Sequence Diagram

sequenceDiagram
  participant Init
  participant Kernel
  participant Shell
  Init->>Init: run hello test
  Init->>Kernel: fork()
  Kernel-->>Init: returns child_pid
  Init->>Kernel: child execve("/bin/sh")
  Kernel->>Kernel: load ELF, setup stack (16-byte aligned)
  Init->>Kernel: wait4(child_pid)
  Kernel-->>Init: child status
  Shell->>Kernel: read(stdin, buf)
  Kernel-->>Shell: bytes
  Shell->>Shell: parse command
  Shell->>Kernel: fork()
  Kernel-->>Shell: returns pid
  Shell->>Kernel: wait4(pid)
  Shell->>Kernel: execve(cmd_path, argv, envp)
  Kernel->>Kernel: load command ELF
  Shell->>Kernel: write(stdout, outbuf)
  Kernel-->>Shell: bytes written
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • dburkart/vibix#918: Modifies init /bin/sh launch behavior; overlaps with init shell-launch changes here.
  • dburkart/vibix#864: Implements vibix libc/syscall-wrapper functionality overlapping with the syscall shims and exported wrappers added here.
  • dburkart/vibix#885: Modifies base/sh userspace shell entrypoint and runtime, related to the shell implementation changes.

Poem

A rabbit hops where syscalls ring,
Init forks a tiny spring,
A shell awakes on aligned stack,
It reads, it writes, and then comes back —
🐇 "init: launching /bin/sh" — a jaunty ping!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix interactive /bin/sh boot: four root causes' clearly and specifically identifies the main objective of the PR—fixing shell boot issues by addressing four distinct root causes.
Description check ✅ Passed The description is directly related to the changeset, detailing each of the five issues fixed (EFBIG, compiler-generated _start, RSP alignment, TLS TCB alignment, execve stack) and providing test plan evidence.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

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/main.rs`:
- Around line 63-67: The code unconditionally forces stdin interactive by
setting let is_tty = true; which breaks non-vibix sessions; change this to only
force true when running on vibix and otherwise use normal tty detection (e.g.
replace the assignment with a conditional such as: if running_on_vibix { true }
else { atty::is(atty::Stream::Stdin) } ), where "running_on_vibix" can be
implemented by checking an environment variable (std::env::var("VIBIX") ==
Ok("1")) or by inspecting the host name (gethostname()/std::env::var("HOSTNAME")
contains "vibix"); keep the variable name is_tty and preserve the existing
comment about vibix.

In `@kernel/src/mem/auxv.rs`:
- Around line 183-195: Preflight the remaining stack budget before touching
offset parity: compute the required stack space based on argc, envp_count, and
auxv pair count (the existing total_words calculation) and assert/return an
overflow error if offset has fewer than that many bytes remaining before doing
offset &= !15 or applying the -8 parity adjustment; update the logic around the
variables total_words, argc, envp_count and offset in auxv.rs so the boundary
check happens prior to changing offset (and still considers the extra 8 bytes
when total_words is odd).

In `@xtask/src/ext2_image.rs`:
- Around line 134-146: build_with_extras() currently updates/validates the same
fixtures hash as the base image which causes hash collisions between normal and
extra-bin variants; change the hash fixture used when update_hash is true for
the extra-bins variant so it writes/validates a distinct file (e.g. derive a
different fixture name like "ext2_image_extras.sha256" or append a suffix) and
update the code paths in build_with_extras (and any callers that pass
update_hash) to use that distinct filename when computing/writing the checksum
so the base image's tests continue to reference the original "ext2_image.sha256"
while extra-bin builds use the separate fixture.

In `@xtask/src/main.rs`:
- Around line 2853-2855: The test is asserting init’s pre-exec log
(SUCCESS_MARKER) instead of a shell-owned, post-exec signal; change the marker
and reader logic so the test waits for a shell-emitted token after exec rather
than "init: launching /bin/sh". Replace uses of SUCCESS_MARKER with a unique
post-exec sentinel produced by the shell (for example have the shell print a
distinct marker after successful exec or assert the shell prompt string) and
update the line-based reader to match substrings or bytes (since the prompt may
lack a trailing newline) so it detects the marker without requiring newline;
update the corresponding assertions that reference SUCCESS_MARKER (also at the
other block around the 2902-2953 diff) and keep PANIC_MARKER and HARD_CAP
behavior unchanged.
🪄 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: 3b34cea0-9380-4889-88bd-87b96cc215ad

📥 Commits

Reviewing files that changed from the base of the PR and between 6c11c22 and a2a9671.

📒 Files selected for processing (18)
  • 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
  • kernel/src/arch/x86_64/syscall.rs
  • kernel/src/mem/auxv.rs
  • kernel/src/mem/elf.rs
  • kernel/src/shell/vfs_helpers.rs
  • library/std/src/sys/args/mod.rs
  • library/std/src/sys/args/vibix.rs
  • library/std/src/sys/pal/vibix/mod.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/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

Gate the forced interactive mode to vibix only.

This now makes every stdin-driven host session look interactive too, so pipes/files get prompts and interactive signal handling. Keep the forced true on vibix, but preserve normal tty detection everywhere else.

🤖 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 unconditionally forces
stdin interactive by setting let is_tty = true; which breaks non-vibix sessions;
change this to only force true when running on vibix and otherwise use normal
tty detection (e.g. replace the assignment with a conditional such as: if
running_on_vibix { true } else { atty::is(atty::Stream::Stdin) } ), where
"running_on_vibix" can be implemented by checking an environment variable
(std::env::var("VIBIX") == Ok("1")) or by inspecting the host name
(gethostname()/std::env::var("HOSTNAME") contains "vibix"); keep the variable
name is_tty and preserve the existing comment about vibix.

Comment thread kernel/src/mem/auxv.rs
Comment on lines +183 to +195
// SysV AMD64 ABI: rsp must be 16-byte aligned at process entry.
// We push a known number of 8-byte words below this point (auxv pairs,
// envp/argv pointer arrays + NULLs, and argc). If that total is odd
// the final rsp would land on 8-mod-16, so we start at 8-mod-16 here
// to compensate.
let argc = argv.len().min(MAX_STRINGS);
let envp_count = envp.len().min(MAX_STRINGS);
let total_words = 16 /* auxv pairs */ + 1 /* envp NULL */ + envp_count
+ 1 /* argv NULL */ + argc + 1 /* argc */;
offset &= !15; // 16-byte align
if total_words % 2 != 0 {
offset -= 8;
}
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

Preflight the remaining stack budget before the parity adjustment.

Line 194 can wrap offset before the final boundary assert runs. If argv/envp string data leaves less than 8 bytes here, the next raw writes walk past the start of the frame instead of failing with the documented overflow panic.

Suggested fix
     let argc = argv.len().min(MAX_STRINGS);
     let envp_count = envp.len().min(MAX_STRINGS);
     let total_words = 16 /* auxv pairs */ + 1 /* envp NULL */ + envp_count
         + 1 /* argv NULL */ + argc + 1 /* argc */;
     offset &= !15; // 16-byte align
-    if total_words % 2 != 0 {
-        offset -= 8;
-    }
+    let padding = if total_words % 2 != 0 { 8 } else { 0 };
+    let required = padding + total_words * 8;
+    assert!(
+        offset >= required,
+        "auxv: initial stack layout exceeds page boundary"
+    );
+    offset -= padding;
📝 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
// SysV AMD64 ABI: rsp must be 16-byte aligned at process entry.
// We push a known number of 8-byte words below this point (auxv pairs,
// envp/argv pointer arrays + NULLs, and argc). If that total is odd
// the final rsp would land on 8-mod-16, so we start at 8-mod-16 here
// to compensate.
let argc = argv.len().min(MAX_STRINGS);
let envp_count = envp.len().min(MAX_STRINGS);
let total_words = 16 /* auxv pairs */ + 1 /* envp NULL */ + envp_count
+ 1 /* argv NULL */ + argc + 1 /* argc */;
offset &= !15; // 16-byte align
if total_words % 2 != 0 {
offset -= 8;
}
// SysV AMD64 ABI: rsp must be 16-byte aligned at process entry.
// We push a known number of 8-byte words below this point (auxv pairs,
// envp/argv pointer arrays + NULLs, and argc). If that total is odd
// the final rsp would land on 8-mod-16, so we start at 8-mod-16 here
// to compensate.
let argc = argv.len().min(MAX_STRINGS);
let envp_count = envp.len().min(MAX_STRINGS);
let total_words = 16 /* auxv pairs */ + 1 /* envp NULL */ + envp_count
1 /* argv NULL */ + argc + 1 /* argc */;
offset &= !15; // 16-byte align
let padding = if total_words % 2 != 0 { 8 } else { 0 };
let required = padding + total_words * 8;
assert!(
offset >= required,
"auxv: initial stack layout exceeds page boundary"
);
offset -= padding;
🤖 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 `@kernel/src/mem/auxv.rs` around lines 183 - 195, Preflight the remaining stack
budget before touching offset parity: compute the required stack space based on
argc, envp_count, and auxv pair count (the existing total_words calculation) and
assert/return an overflow error if offset has fewer than that many bytes
remaining before doing offset &= !15 or applying the -8 parity adjustment;
update the logic around the variables total_words, argc, envp_count and offset
in auxv.rs so the boundary check happens prior to changing offset (and still
considers the extra 8 bytes when total_words is odd).

Comment thread xtask/src/ext2_image.rs
Comment on lines +134 to +146
/// 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<PathBuf> {
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

Use a distinct hash fixture for extra-bin ext2 variants.

build_with_extras() introduces a second image variant, but it still shares the base image’s expected-hash contract. That lets smoke/run/sh validate or rewrite tests/fixtures/ext2_image.sha256 against different bytes than xtask ext2-image, so the next plain image build can drift for the wrong reason.

🤖 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/ext2_image.rs` around lines 134 - 146, build_with_extras()
currently updates/validates the same fixtures hash as the base image which
causes hash collisions between normal and extra-bin variants; change the hash
fixture used when update_hash is true for the extra-bins variant so it
writes/validates a distinct file (e.g. derive a different fixture name like
"ext2_image_extras.sha256" or append a suffix) and update the code paths in
build_with_extras (and any callers that pass update_hash) to use that distinct
filename when computing/writing the checksum so the base image's tests continue
to reference the original "ext2_image.sha256" while extra-bin builds use the
separate fixture.

Comment thread xtask/src/main.rs
Comment on lines +2853 to +2855
const HARD_CAP: Duration = Duration::from_secs(120);
const SUCCESS_MARKER: &str = "init: launching /bin/sh";
const PANIC_MARKER: &str = "KERNEL PANIC:";
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

Assert a post-exec shell signal here, not init’s pre-exec marker.

SUCCESS_MARKER is printed by init before the /bin/sh fork/exec, so this test still passes when the shell binary is missing or execve fails immediately. Also, the line-based reader cannot observe the shell’s $ prompt because it has no trailing newline. This needs a shell-owned, post-exec signal instead.

Also applies to: 2902-2953

🤖 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 2853 - 2855, The test is asserting init’s
pre-exec log (SUCCESS_MARKER) instead of a shell-owned, post-exec signal; change
the marker and reader logic so the test waits for a shell-emitted token after
exec rather than "init: launching /bin/sh". Replace uses of SUCCESS_MARKER with
a unique post-exec sentinel produced by the shell (for example have the shell
print a distinct marker after successful exec or assert the shell prompt string)
and update the line-based reader to match substrings or bytes (since the prompt
may lack a trailing newline) so it detects the marker without requiring newline;
update the corresponding assertions that reference SUCCESS_MARKER (also at the
other block around the 2902-2953 diff) and keep PANIC_MARKER and HARD_CAP
behavior unchanged.

The shell's stdin was broken in two ways: std's Stdin::read() was a stub
returning Ok(0) (immediate EOF), and SerialBackend::read() only polled
the serial RX ring — PS/2 keyboard scancodes fed into PS2_TTY's NTtyLdisc
never reached the readv syscall path.

- vibix_abi: add read_stdin() using readv syscall (nr 19)
- std/stdio/vibix: wire Stdin::read() to vibix_abi::stdio::read_stdin()
- ntty: add NTty::try_read_byte() to pop committed bytes from raw ring
- ntty: add ECHO support — echo typed characters to both serial and
  framebuffer (using try_lock for soft-IRQ safety)
- ps2: extract PS2_LDISC static for direct NTtyLdisc access without
  trait-object downcasting; expose try_read_byte() and reader_len()
- fs/SerialBackend::read(): drain PS/2 N_TTY raw ring with proper
  wait-queue blocking (register_wait + block_current pattern)
- fs/SerialBackend::write(): mirror output to framebuffer console

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 `@kernel/src/fs/mod.rs`:
- Around line 656-668: The code can miss COM1 wakeups because it only registers
on ps2.read_wait after a poll; modify the loop so you register a serial wait
(e.g., crate::serial::read_wait.register_wait) before checking ps2::reader_len
and before blocking, or register a unified stdin wait that combines serial and
ps2; specifically, in the loop around crate::serial::try_read_byte() and
ps2.read_wait.register_wait(tid) add registration on the serial wait queue (or
the unified stdin wait) into a tok variable, check both reader_len() and
crate::serial::has_bytes() (or equivalent) after registering, cancel both tokens
if waking or continuing, and only call crate::task::block_current() when both
queues report empty to ensure COM1 bytes can wake the sleeper.
- Around line 635-668: The read method (fn read(&self, buf: &mut [u8])) must
return Ok(0) immediately for zero-length buffers to avoid indexing buf[0] or
blocking; add an early check like if buf.is_empty() { return Ok(0); } at the
start of read so the fallback path that calls crate::serial::try_read_byte() or
registers on ps2.read_wait never executes for empty slices, preserving current
behavior of try_read_byte(), ps2.read_wait.register_wait(), and
ps2.read_wait.cancel().

In `@kernel/src/tty/ntty.rs`:
- Around line 529-542: Replace the manual echo handling in the ECHO branch with
a call into the TTY's canonical echo pipeline: instead of replicating
ECHO/VERASE/OPOST/ECHOE/ECHONL logic here, call queue_echo(...) (so the byte is
enqueued and later transformed by process_output()) and ensure the actual
serial/framebuffer writes use the same bytes emitted by process_output()/output
queue (mirror those processed bytes to crate::serial::write_bytes and
fb_echo_str/fb_echo_byte) so visual erase and newline expansion follow the
existing termios rules.
- Around line 262-272: try_read_byte currently ignores RawRing::eof_pos so EOF
boundaries are lost; update try_read_byte to first check st.raw.eof_pos and if
it equals st.raw.head then consume that EOF (clear st.raw.eof_pos) and return a
dedicated EOF marker (e.g. Some(0) or whatever the codebase uses to represent
EOF), otherwise proceed to read a byte as before; specifically, in try_read_byte
check if let Some(ep) = st.raw.eof_pos { if st.raw.head == ep { st.raw.eof_pos =
None; return Some(0); } } before the existing empty/byte logic so a canonical
VEOF is observed once.
🪄 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: 1f8c77d7-809c-45e4-b5bc-61a37515624e

📥 Commits

Reviewing files that changed from the base of the PR and between a2a9671 and 312aa61.

📒 Files selected for processing (5)
  • base/vibix_abi/src/stdio.rs
  • kernel/src/fs/mod.rs
  • kernel/src/tty/ntty.rs
  • kernel/src/tty/ps2.rs
  • library/std/src/sys/stdio/vibix.rs

Comment thread kernel/src/fs/mod.rs
Comment on lines 635 to 668
fn read(&self, buf: &mut [u8]) -> Result<usize, i64> {
let caller = crate::process::current_pid();
let tty = crate::tty::console_tty();
if let Some(rc) = crate::tty::tty_check_sigttin(&tty, caller) {
// KERN_ERESTARTSYS is honoured by the syscall trampoline
// (see `signal::check_and_deliver_signals`): restart on
// SA_RESTART, otherwise -EINTR.
return Err(rc);
}
for (i, byte) in buf.iter_mut().enumerate() {
match crate::serial::try_read_byte() {
Some(b) => *byte = b,
None => {
return if i == 0 { Err(EAGAIN) } else { Ok(i) };
let ps2 = crate::tty::ps2::tty();
loop {
let mut i = 0;
while i < buf.len() {
match crate::tty::ps2::try_read_byte() {
Some(b) => {
buf[i] = b;
i += 1;
}
None => break,
}
}
if i > 0 {
return Ok(i);
}
if let Some(b) = crate::serial::try_read_byte() {
buf[0] = b;
return Ok(1);
}
let tid = crate::task::current_id();
let tok = ps2.read_wait.register_wait(tid);
if crate::tty::ps2::reader_len() > 0 {
ps2.read_wait.cancel(tok);
continue;
}
crate::task::block_current();
ps2.read_wait.cancel(tok);
}
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 | 🔴 Critical | ⚡ Quick win

Return immediately for zero-length reads.

read(&mut []) currently falls through into the fallback path. If COM1 already has a byte, Line 657 indexes buf[0] and panics; if not, the call blocks forever instead of returning Ok(0).

Suggested fix
 fn read(&self, buf: &mut [u8]) -> Result<usize, i64> {
+    if buf.is_empty() {
+        return Ok(0);
+    }
     let caller = crate::process::current_pid();
     let tty = crate::tty::console_tty();
📝 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
fn read(&self, buf: &mut [u8]) -> Result<usize, i64> {
let caller = crate::process::current_pid();
let tty = crate::tty::console_tty();
if let Some(rc) = crate::tty::tty_check_sigttin(&tty, caller) {
// KERN_ERESTARTSYS is honoured by the syscall trampoline
// (see `signal::check_and_deliver_signals`): restart on
// SA_RESTART, otherwise -EINTR.
return Err(rc);
}
for (i, byte) in buf.iter_mut().enumerate() {
match crate::serial::try_read_byte() {
Some(b) => *byte = b,
None => {
return if i == 0 { Err(EAGAIN) } else { Ok(i) };
let ps2 = crate::tty::ps2::tty();
loop {
let mut i = 0;
while i < buf.len() {
match crate::tty::ps2::try_read_byte() {
Some(b) => {
buf[i] = b;
i += 1;
}
None => break,
}
}
if i > 0 {
return Ok(i);
}
if let Some(b) = crate::serial::try_read_byte() {
buf[0] = b;
return Ok(1);
}
let tid = crate::task::current_id();
let tok = ps2.read_wait.register_wait(tid);
if crate::tty::ps2::reader_len() > 0 {
ps2.read_wait.cancel(tok);
continue;
}
crate::task::block_current();
ps2.read_wait.cancel(tok);
}
fn read(&self, buf: &mut [u8]) -> Result<usize, i64> {
if buf.is_empty() {
return Ok(0);
}
let caller = crate::process::current_pid();
let tty = crate::tty::console_tty();
if let Some(rc) = crate::tty::tty_check_sigttin(&tty, caller) {
return Err(rc);
}
let ps2 = crate::tty::ps2::tty();
loop {
let mut i = 0;
while i < buf.len() {
match crate::tty::ps2::try_read_byte() {
Some(b) => {
buf[i] = b;
i += 1;
}
None => break,
}
}
if i > 0 {
return Ok(i);
}
if let Some(b) = crate::serial::try_read_byte() {
buf[0] = b;
return Ok(1);
}
let tid = crate::task::current_id();
let tok = ps2.read_wait.register_wait(tid);
if crate::tty::ps2::reader_len() > 0 {
ps2.read_wait.cancel(tok);
continue;
}
crate::task::block_current();
ps2.read_wait.cancel(tok);
}
🤖 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 `@kernel/src/fs/mod.rs` around lines 635 - 668, The read method (fn read(&self,
buf: &mut [u8])) must return Ok(0) immediately for zero-length buffers to avoid
indexing buf[0] or blocking; add an early check like if buf.is_empty() { return
Ok(0); } at the start of read so the fallback path that calls
crate::serial::try_read_byte() or registers on ps2.read_wait never executes for
empty slices, preserving current behavior of try_read_byte(),
ps2.read_wait.register_wait(), and ps2.read_wait.cancel().

Comment thread kernel/src/fs/mod.rs
Comment on lines +656 to 668
if let Some(b) = crate::serial::try_read_byte() {
buf[0] = b;
return Ok(1);
}
let tid = crate::task::current_id();
let tok = ps2.read_wait.register_wait(tid);
if crate::tty::ps2::reader_len() > 0 {
ps2.read_wait.cancel(tok);
continue;
}
crate::task::block_current();
ps2.read_wait.cancel(tok);
}
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

Serial input can miss wakeups on the blocking path.

After the one-shot crate::serial::try_read_byte() poll, this code only waits on ps2.read_wait. A COM1 byte that arrives after Line 656 has no wake source here, so a serial-only stdin reader can sleep until unrelated PS/2 activity happens. This needs a serial wait queue, or a unified stdin wait source, before blocking.

🤖 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 `@kernel/src/fs/mod.rs` around lines 656 - 668, The code can miss COM1 wakeups
because it only registers on ps2.read_wait after a poll; modify the loop so you
register a serial wait (e.g., crate::serial::read_wait.register_wait) before
checking ps2::reader_len and before blocking, or register a unified stdin wait
that combines serial and ps2; specifically, in the loop around
crate::serial::try_read_byte() and ps2.read_wait.register_wait(tid) add
registration on the serial wait queue (or the unified stdin wait) into a tok
variable, check both reader_len() and crate::serial::has_bytes() (or equivalent)
after registering, cancel both tokens if waking or continuing, and only call
crate::task::block_current() when both queues report empty to ensure COM1 bytes
can wake the sleeper.

Comment thread kernel/src/tty/ntty.rs
Comment on lines +262 to +272
/// Pop a single committed byte from the raw ring, or `None` if empty.
pub fn try_read_byte(&self) -> Option<u8> {
let mut st = self.state.lock();
if st.raw.len() == 0 {
return None;
}
let idx = st.raw.head & (RAW_RING_CAP - 1);
let b = st.raw.buf[idx];
st.raw.head = st.raw.head.wrapping_add(1);
Some(b)
}
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

Expose EOF as a first-class read event.

RawRing::eof_pos is never observed or consumed here. After canonical VEOF, an empty line is indistinguishable from “no data yet”, so downstream readers block instead of returning 0; after draining a non-empty pre-EOF line, the one-shot EOF is lost for the same reason. This helper needs to surface EOF boundaries, not just bytes.

🤖 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 `@kernel/src/tty/ntty.rs` around lines 262 - 272, try_read_byte currently
ignores RawRing::eof_pos so EOF boundaries are lost; update try_read_byte to
first check st.raw.eof_pos and if it equals st.raw.head then consume that EOF
(clear st.raw.eof_pos) and return a dedicated EOF marker (e.g. Some(0) or
whatever the codebase uses to represent EOF), otherwise proceed to read a byte
as before; specifically, in try_read_byte check if let Some(ep) = st.raw.eof_pos
{ if st.raw.head == ep { st.raw.eof_pos = None; return Some(0); } } before the
existing empty/byte logic so a canonical VEOF is observed once.

Comment thread kernel/src/tty/ntty.rs
Comment on lines +529 to +542
// Echo the byte to the tty output if ECHO is set.
#[cfg(target_os = "none")]
if termios.c_lflag & ECHO != 0 {
if b == b'\n' && termios.c_oflag & ONLCR != 0 {
crate::serial::write_bytes(b"\r\n");
fb_echo_str("\n");
} else if matches_cc(&termios, VERASE, b) {
crate::serial::write_bytes(b"\x08 \x08");
fb_echo_str("\x08 \x08");
} else {
crate::serial::write_bytes(&[b]);
fb_echo_byte(b);
}
}
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

Reuse queue_echo() here instead of re-implementing echo rules.

This branch now diverges from the file’s own termios handling: VERASE gets visual erase even when ECHOE is clear, ECHONL is ignored when ECHO is clear, and \n is expanded even if OPOST is off. Routing echo through queue_echo()/process_output() and then mirroring those transformed bytes to serial/framebuffer keeps runtime behavior aligned with the tested rules above.

🤖 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 `@kernel/src/tty/ntty.rs` around lines 529 - 542, Replace the manual echo
handling in the ECHO branch with a call into the TTY's canonical echo pipeline:
instead of replicating ECHO/VERASE/OPOST/ECHOE/ECHONL logic here, call
queue_echo(...) (so the byte is enqueued and later transformed by
process_output()) and ensure the actual serial/framebuffer writes use the same
bytes emitted by process_output()/output queue (mirror those processed bytes to
crate::serial::write_bytes and fb_echo_str/fb_echo_byte) so visual erase and
newline expansion follow the existing termios rules.

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.

2 participants