Skip to content

fix: make vi (and other full-screen apps) usable, plus assorted CPU/bus correctness fixes - #18

Merged
bokuweb merged 3 commits into
mainfrom
fix/terminal-raw-mode
Aug 6, 2026
Merged

bokuweb merged 3 commits into
mainfrom
fix/terminal-raw-mode

Conversation

@bokuweb

@bokuweb bokuweb commented Aug 6, 2026 •

Copy link
Copy Markdown
Owner

Problem

Running vi inside the guest killed the emulator, while non-interactive commands such as ls worked fine.

The host terminal was left in canonical mode for the entire session. That has two consequences:

  • Input was only delivered a line at a time, so vi never saw the individual keystrokes it waits for.
  • Ctrl-C was interpreted by the host line discipline and raised SIGINT against the emulator process itself, so the very first attempt to interrupt something inside the guest terminated the emulator.

While tracking this down, a few unrelated correctness bugs in the CPU and the bus turned up, and they are fixed here too.

Changes

Terminal handling

  • Add devices::terminal::RawMode, a guard that puts the host terminal into raw mode via cfmakeraw and restores the saved termios on drop. It also installs a panic hook so an unexpected panic does not leave the user with a broken terminal.
  • Uart owns a RawMode, so raw mode is active exactly while the guest owns the serial port.
  • Because Ctrl-C now belongs to the guest, add a Ctrl-A escape prefix: Ctrl-A x quits the emulator cleanly (restoring the terminal first) and Ctrl-A Ctrl-A sends a literal Ctrl-A to the guest.
  • Rework the keyboard input path around a pending-byte queue so escape sequences can be buffered and replayed one byte at a time.
  • Write UART output as raw bytes instead of going through char::from_u32, which mangled anything the guest emitted that was not valid UTF-8 on its own.

8250 divisor latch

The UART model ignored the DLAB bit in the LCR. When the guest's 8250 driver programmed the baud rate divisor, those writes to registers 0 and 1 were treated as characters and showed up as stray output on the console. The bus now tracks lcr and divisor and routes register 0/1 accesses to the divisor latches while DLAB is set.

CPU and bus correctness

  • DIV performed unsigned division; the operands are now sign-extended before dividing.
  • Loads wrote straight into x[rd], so a load targeting x0 could clobber the zero register. They go through write_back now.
  • Out-of-range RAM accesses panicked the emulator. They now raise LoadAccessFault / StoreAccessFault so the guest can handle them.
  • Instruction fetches from unmapped memory reported InstructionAddressMisaligned instead of InstructionAccessFault.
  • pc and mepc arithmetic used plain +/-, which panics on overflow in debug builds. These are wrapping_* now.
  • MRET did not mask the MPP field, so a mstatus with higher bits set could transition the hart into PrivilegeMode::Reserved.

CI

CI was already red on main. rust-toolchain pinned 1.80.0 while Cargo.lock is gitignored, so every run resolved the newest clap, which requires edition2024 and cannot even be parsed by Cargo 1.80.0. The pin moves to 1.96.1.

That bump exposed a second issue: the workspace contains a local crate literally named core, which shadows the built-in core crate inside macro expansions — clap's derive emits core::env! and failed to resolve it. The dependency is aliased as r2_core in app, the only crate that uses derive macros.

Testing

  • cargo check and cargo build -p app (the two CI steps) pass locally on 1.96.1.
  • Booted fixtures/linux.bin under a pty, logged in, ran ls / and confirmed the colour output is clean, then opened vi /etc/passwd and confirmed the full-screen UI draws correctly and :q! returns to the shell.
  • Verified Ctrl-A x exits and leaves the host terminal in its original state.

Note

One issue found during the investigation is deliberately left alone: after cating certain files the console can be flooded with NULs. This traces back to ftruncate64 in the guest image misreading its 64-bit argument and producing a file of size 22 << 32. The same behaviour reproduces on the reference mini-rv32ima emulator, so it is an image bug rather than an emulator bug.

bokuweb and others added 3 commits August 6, 2026 11:13
…l port

The host line discipline kept canonical mode, echo and signal generation
enabled, so keystrokes only reached the guest once a newline was typed and
Ctrl-C terminated the emulator instead of being delivered to the guest. Line
oriented commands happened to work, but full screen applications such as vi
were unusable and died together with the emulator on the first Ctrl-C.

Since ISIG is now off, Ctrl-A x is needed to leave the emulator. The terminal
is restored on drop and from a panic hook so it is never left unusable.

Bytes are also written to stdout untouched rather than being re-encoded as
UTF-8, which corrupted everything the guest emitted above 0x7f.

Co-authored-by: Cursor <cursoragent@cursor.com>
…or latch

Found while tracking down the vi failure:

- DIV performed an unsigned division, so -1 / 1000 yielded 4294967 instead
  of 0. It is executed constantly by the guest's poll() timeout conversion.
- Loads assigned self.x[rd] directly, letting a load with rd == 0 clobber x0.
- Out-of-range RAM accesses indexed the backing Vec and panicked, taking the
  emulator down on any stray guest pointer. They now raise an access fault so
  the guest can handle it. Instruction fetch reports an access fault as well
  instead of misreporting it as a misaligned address.
- The CLINT was handed an unmasked address on 8 and 16 bit reads, which hit
  unreachable!().
- DLAB was not modelled, so the 8250 driver's baud divisor writes were printed
  as characters. This put stray bytes on the terminal every time the guest
  called tcsetattr, which vi does on entry and exit.
- pc and mepc arithmetic wraps, and MRET masks MPP to two bits so a stale
  mstatus bit cannot leave the CPU in the reserved privilege mode.

Co-authored-by: Cursor <cursoragent@cursor.com>
`rust-toolchain` pinned 1.80.0, but `Cargo.lock` is gitignored so CI always
resolves the latest clap, which now needs edition2024 and fails to even parse
its manifest on 1.80.0.

Bumping the pin exposes a second problem: the workspace has a local crate
literally named `core`, so clap's derive expansion of `core::env!` resolved to
it instead of the built-in crate. Alias the dependency as `r2_core` in `app`,
which is the only crate using derive macros.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bokuweb bokuweb changed the title fix: run the host terminal in raw mode so full screen guest apps work fix: make vi (and other full-screen apps) usable, plus assorted CPU/bus correctness fixes Aug 6, 2026
@bokuweb
bokuweb merged commit f350a23 into main Aug 6, 2026
2 checks passed
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.

1 participant