Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bad0643
docs: design spec for SIGINT handling
bboe May 7, 2026
3ecd5f5
docs: implementation plan for SIGINT handling
bboe May 7, 2026
53eb1ce
kernel+libc: SIGINT/SYS_SIGNAL constants + EINTR/ERROR_INTERRUPTED
bboe May 7, 2026
6855cac
kernel: per-program SIGINT state in BSS, reset by program_enter
bboe May 7, 2026
473eadb
kernel: signal_dispatch_kill — teleport-to-kernel-ESP teardown
bboe May 7, 2026
7da50ca
drivers/ps2: set pending_sigint when cooked Ctrl+C is detected
bboe May 7, 2026
f3b01a2
kernel: SIGINT_TAIL_CHECK in all IRQ epilogues; default-kill on Ctrl+C
bboe May 7, 2026
934a1e6
drivers: extend SIGINT_TAIL_CHECK to ps2_irq1_handler + fdc_irq6_handler
bboe May 7, 2026
1e80055
kernel: SIGINT_TAIL_CHECK in syscall iret epilogues
bboe May 7, 2026
08f7e1e
kernel: SYS_SYS_SIGNAL — DFL/IGN/user-virt registration
bboe May 7, 2026
1fdea6a
libc: signal() wrapper around SYS_SYS_SIGNAL
bboe May 7, 2026
92455b3
shell: install SIG_IGN at startup so its own Ctrl+C is benign
bboe May 7, 2026
892cbe5
kernel: vDSO sigreturn trampoline at VDSO_VIRT + 0x450
bboe May 7, 2026
47f2c78
kernel: signal_dispatch_user — sigcontext build + iret-frame rewrite
bboe May 7, 2026
237d1a4
kernel: SYS_SYS_SIGRETURN — restore sigcontext, redeliver pending SIGINT
bboe May 7, 2026
de2f55a
fs/fd/console: cooperative SIGINT bail in read; detect serial 0x03
bboe May 7, 2026
cd55694
fs/fd/midi: cooperative SIGINT bail in MIDI_IOCTL_DRAIN sti+hlt loop
bboe May 7, 2026
9cd99ab
libc: map ERROR_INTERRUPTED (0x08) -> EINTR in syscall wrappers
bboe May 7, 2026
13caab5
tests: SIGINT handler delivery + sigreturn end-to-end
bboe May 7, 2026
48e5361
docs: SIGINT handling — syscalls, architecture, changelog
bboe May 7, 2026
05f31c7
kernel: sanitize EFLAGS in SYS_SYS_SIGRETURN (drop IOPL/VM/NT/RF, for…
bboe May 7, 2026
58f5c80
docs: correct SIGINT architecture section (file paths, dispatch order…
bboe May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ at the time.

## [Unreleased](https://github.com/bboe/BBoeOS/compare/0.10.0...main)

- **SIGINT handling**: Ctrl+C on PS/2 kills runaway user programs by default (`SIG_DFL`); programs may register a handler via `signal(SIGINT, handler)` using `sys_signal` (INT 30h AH=F5h). Delivery builds a 48-byte sigcontext on the user stack and redirects the IRET frame to the handler; a two-instruction vDSO trampoline (`__kernel_sigreturn` at user-virt `0x10450`) issues `sys_sigreturn` (AH=F6h) to restore the saved context. Cooperative-interruption convention added to `fd_read_console` and `MIDI_IOCTL_DRAIN`: both return early with `CF=1, AL=ERROR_INTERRUPTED` (maps to `EINTR` in libc) when `pending_sigint` is set mid-wait. See `docs/architecture.md` for the two-axis detection/delivery model and `docs/superpowers/specs/2026-05-06-sigint-handling-design.md` for the full design spec.
- **drivers/sb16**: switch SB16 PCM playback from synchronous single-cycle DMA to auto-init double-buffering with a kernel-side software ring. `sb16_open` programs the 8237 in auto-init mode (mode byte `0x59`) and starts the DSP via `0x48` (set block size) + `0x1C` (8-bit auto-init PCM, no args), so the DSP loops the 4 KB DMA buffer indefinitely and fires IRQ 5 every `AUDIO_HALF_SIZE` (2 KB) bytes — at which point `sb16_refill` (called from `pmode_irq5_handler`) drains the 4 KB software ring into the just-finished DMA half and pads with silence on underrun. `fd_write_audio` becomes a non-blocking ring producer: it returns as soon as user bytes are queued instead of blocking for the chunk's playback duration. Doom's per-tick audio write (~315 bytes) used to pin the engine at the SB16's chunk rate (~28 ms blocking per write, magnified by the 1 kHz PIT's hlt thrashing), starving the renderer; with the auto-init ring it's a single memcpy into the ring and Doom hits frame 30 in ~0.5 s instead of dragging through 3 frames in 12 s.
- **drivers/rtc**: fix `date` returning a wildly off month/day that drifted forward by ~50 ms of `system_ticks` per call. The C-port `rtc_read` (`drivers: port rtc.asm to C`, 2026-04-28) compiled `kernel_outb(0x70, reg); return kernel_inb(0x71)` into `mov edx, 0x70 / out dx, al / mov edx, 0x71 / in al, dx`, clobbering `EDX` — but `rtc_read_date_internal` writes the month into `DH` and then calls `rtc_read` again for the day, so the second call's `mov edx, 0x71` zeroed `DH` before `mov dl, al` landed. C then read `epoch_month = 0`, computed `month_index = -1`, and `rtc_month_days[-1]` indexed exactly onto `_g_system_ticks` in the global layout. Restore `rtc_read` to the original immediate-port asm shape (`out 0x70, al / in al, 0x71 / ret`) so it preserves `EDX` again. `tests/test_programs.py`'s `date` test now runs the command three times and requires the dates to agree.
- **kernel**: widen `SYS_RTC_DATETIME`, `SYS_RTC_MILLIS`, `SYS_RTC_UPTIME` to return the full 32-bit value in `EAX` (was `DX:AX` for the first two, sign-extended `AX` for uptime — which silently truncated past 9 h and zero-wrapped at 18 h). `SYS_RTC_SLEEP` now reads the full duration from `ECX` (was `CX`, capped at 65 535 ms). vDSO `shared_print_datetime`, the libc `gettimeofday`, and the Doom `DG_GetTicksMs` / `DG_SleepMs` helpers updated to match.
Expand Down
55 changes: 55 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,58 @@ disk:
## Build-time derivation

- Kernel sector count and reserved-region base are both derived at build time: `make_os.sh` measures `kernel.bin`, passes the sector count to `boot.asm` as `-DKERNEL_SECTORS=N`, computes `KERNEL_RESERVED_BASE = page_align(0x20000 + sizeof(kernel.bin))`, then re-assembles `kernel.asm` and `boot.asm` with `-DKERNEL_RESERVED_BASE=N`. A size-invariant check between the two `kernel.asm` passes confirms the change cannot shift the binary. A separate VGA-hole assert verifies that `KERNEL_RESERVED_BASE + reserved-region-size < 0xA0000` so the kernel-side fixed-phys regions never cross the VGA aperture (which is what lets the OS boot under QEMU `-m 1`). The boot-time `kernel_bytes` word at MBR offset 508 holds `(BOOT_SECTORS + KERNEL_SECTORS) * 512` so `add_file.py`'s host-side `compute_directory_sector` arithmetic still works.

## SIGINT handling

SIGINT delivery is split into two independent axes — detection and delivery — and three dispatch modes depending on the handler registered by the program.

### Detection

Two paths set the kernel global `pending_sigint` (a single byte in kernel BSS):

- **PS/2 IRQ 1** (`src/drivers/ps2.c`): the cooked-byte path recognises the Ctrl+C scancode sequence and sets `pending_sigint` before returning from the IRQ handler. Because IRQ 1 fires for every keypress regardless of what the CPU is executing, this path works unconditionally — even a tight compute loop in user code is interrupted.
- **Serial 0x03 read** (`src/fs/fd/console.c`, `fd_read_console`): the serial poll branch checks each received byte; if it equals `0x03` (ASCII ETX, the byte a terminal sends for Ctrl+C) it sets `pending_sigint` and does not enqueue the byte into the line buffer.

### Delivery

Every interrupt and syscall return path passes through the `SIGINT_TAIL_CHECK` macro (defined in `src/include/irq_tail.inc`, inlined into the IRQ 0/5/6 handlers in `src/arch/x86/entry.asm`, the IRQ 1 handler `ps2_irq1_handler` in `src/drivers/ps2.c`, the IRQ 6 handler `fdc_irq6_handler` in `src/drivers/fdc.c`, and the INT 30h handler in `src/arch/x86/syscall.asm`). The macro:

1. Checks the IRET frame's CS: if `RPL != 3` the signal is suppressed (the kernel itself does not receive SIGINT — only user programs do).
2. Tests `pending_sigint`; if clear, falls through to popad + IRET.
3. Tests `in_sigint_handler`; if set, falls through to popad + IRET (block re-entry until SYS_SYS_SIGRETURN clears the flag).
4. Reads `sigint_handler` (entry.asm BSS, one dword per program, reset to `SIG_DFL` on each `shell_reload`).
5. Branches: SIG_DFL → `signal_dispatch_kill` (does NOT clear `pending_sigint` — the program is dying anyway); SIG_IGN → clear `pending_sigint`, fall through; user-virt → `signal_dispatch_user` (which clears `pending_sigint` itself).

### Dispatch modes

- **`SIG_DFL` (0)** — `signal_dispatch_kill`: calls `address_space_destroy` on the current program's PD, prints `^C` to the console, and falls into `shell_reload`. This is the out-of-the-box Ctrl+C behaviour: a runaway program is terminated and the shell prompt reappears.
- **`SIG_IGN` (1)** — clear `pending_sigint` (already done by `SIGINT_TAIL_CHECK`), resume the IRET path unchanged. The signal is silently discarded; the program continues as if nothing happened.
- **User handler (virt addr ≥ `PROGRAM_BASE`)** — `signal_dispatch_user`: builds a 48-byte `sigcontext` record on the user stack (pushed below the current user ESP), rewrites the IRET frame so the CPU returns to the handler address at ring 3, and leaves `[user_esp]` pointing at the `sigcontext`. The saved context captures EIP, EFLAGS, ESP (pre-signal), EAX, EBX, ECX, EDX, ESI, EDI, and EBP so the handler can be transparent to the interrupted code.

### Handler resume via vDSO trampoline

The vDSO page (mapped read-only at user-virt `0x10000`) contains a two-instruction trampoline `__kernel_sigreturn` at user-virt `0x10450` (`FUNCTION_TABLE + VDSO_SIGRETURN_OFFSET`):

```nasm
mov ah, SYS_SYS_SIGRETURN ; AH = F6h
int 30h
```

`signal_dispatch_user` writes the trampoline address as the first dword of the on-stack sigcontext so the handler executes a plain `ret` to reach it. After the trampoline pops that return address, the user ESP points one dword into the sigcontext (so saved_eip lives at `[user_esp + 4]`, saved_eflags at `[user_esp + 8]`, saved_esp at `[user_esp + 12]`, etc.). `sys_sigreturn` (INT 30h AH=F6h) then:

1. Validates that the sigcontext's saved_eip and saved_esp are both within the user address space (`PROGRAM_BASE..KERNEL_VIRT_BASE`); failure routes to `signal_dispatch_kill`.
2. Restores EIP, ESP, and a sanitized subset of EFLAGS (arithmetic flags + DF + TF; IF forced on, IOPL/VM/NT/RF cleared per `USER_EFLAGS_MASK` in `src/include/constants.asm`), plus the general-purpose registers from the sigcontext.
3. Clears `in_sigint_handler` and (if a SIGINT arrived during the handler) re-dispatches it before the final `iretd` so back-to-back Ctrl+Cs are not lost.

### Cooperative interruption of blocking syscalls

Long-blocking syscalls poll `pending_sigint` between iterations and bail out early rather than forcing a delivery through the IRET path:

- **`fd_read_console`** (the console read loop in `src/fs/fd/console.c`): checks `pending_sigint` after each character poll cycle. If set, it returns immediately with `CF=1, AL=ERROR_INTERRUPTED` without consuming the flag (the `SIGINT_TAIL_CHECK` epilogue handles final delivery).
- **`MIDI_IOCTL_DRAIN`** (the `sti`/`hlt` drain loop in `src/fs/fd/midi.c`): checks `pending_sigint` after each `hlt` wakeup. Same early-exit convention: `CF=1, AL=ERROR_INTERRUPTED`.

The libc `errno` layer in `tools/libc/syscall.c` maps `ERROR_INTERRUPTED` to `EINTR`, so portable C programs using `read()` get the standard POSIX interrupted-call semantics.

### Known limitation (v1)

Serial Ctrl+C is detected only while `fd_read_console` is actively polling the serial port — a program that never calls `read(0, ...)` over serial cannot be killed via serial Ctrl+C. PS/2 Ctrl+C has no such restriction because IRQ 1 fires unconditionally from hardware.
Loading
Loading