kernel: SIGINT handling (Ctrl+C → kill / SIG_IGN / user handler)#331
Closed
kernel: SIGINT handling (Ctrl+C → kill / SIG_IGN / user handler)#331
Conversation
Linux-shaped, single-signal SIGINT delivery with cooperative
syscall interruption. Detection in IRQ 1 (PS/2) and serial-read
paths sets a per-program pending bit; every IRET-to-user epilogue
checks it and dispatches to one of {SIG_DFL kill, SIG_IGN, user
handler via stack-built sigcontext + vDSO trampoline + sigreturn}.
Establishes the IRET-frame-rewrite primitive that a future async
timer-callback PR will reuse.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17-task TDD plan across 6 phases: foundation (constants + state), default-kill path (PS/2 detection + IRQ epilogue dispatch), SIG_IGN + shell, real handler delivery (vDSO trampoline + sigcontext + sigreturn), cooperative interruption (read + MIDI drain + EINTR), tests + docs. Each phase produces a manually-verifiable slice; the final phase adds the automated test_programs.py entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the SIGINT_TAIL_CHECK macro from entry.asm into a new shared header src/include/irq_tail.inc so it is visible before driver kasm files are included (kernel.asm includes drivers before entry.asm). Add the macro invocation between popad/pop-eax and iretd in ps2_irq1_handler (ps2.c) and fdc_irq6_handler (fdc.c), completing SIGINT dispatch coverage for all live IRQ handlers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add SYS_SYS_SIGNAL (F5h) handler with full input validation: signum must be SIGINT, handler must be SIG_DFL/SIG_IGN or a user-virt address in [PROGRAM_BASE, KERNEL_VIRT_BASE). Returns previous handler in EAX (CF=0) so callers can restore state, or ERROR_INVALID (CF=1) on bad args. Add stub SYS_SYS_SIGRETURN (F6h) returning ERROR_INVALID until Task 12. Update SYSCALL_COUNT bound and add ERROR_INVALID (09h) to constants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Place __kernel_sigreturn (mov eax, SYS_SYS_SIGRETURN; int 0x30) at a fixed offset in vdso.asm via a times-padded block. Offset 0x100 is occupied by shared_print_datetime, so VDSO_SIGRETURN_OFFSET is updated to 0x450 (first 16-byte-aligned slot past the existing helpers). Task 11 will wire signal_dispatch_user to point the iret return-address here. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructure SIGINT_TAIL_CHECK so it runs BEFORE popad with the pushad slots still on the kernel stack. The macro now takes ownership of popad: in the no-dispatch path it pops; in the dispatch path it routes to a kernel routine that either captures the pushad slots into a sigcontext on the user stack (user handler) or doesn't need them (SIG_DFL kill). Each IRQ + syscall handler that uses the macro drops its own popad and ends with `SIGINT_TAIL_CHECK / iretd`. pmode_irq6_handler and fdc_irq6_handler — previously `push eax / pop eax` — are converted to pushad/popad so the macro's pushad-shape stack contract holds. signal_dispatch_user builds a 48-byte sigcontext at user_esp - 48 (trampoline addr, signum, saved EIP/EFLAGS/ESP, then pushad-order registers minus the redundant ESP slot), rewrites the IRET EIP to the registered handler and the IRET ESP to the sigcontext base, sets in_sigint_handler = 1 / pending_sigint = 0, drops the pushad slots, and iretds. SYS_SYS_SIGRETURN (Task 12) will consume the sigcontext to resume the interrupted state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires .sys_sigreturn to a new signal_resume_after_handler asm thunk in signal.c. After the user handler's `ret` pops the trampoline address off the sigcontext, the vDSO trampoline re-enters the kernel via int 0x30 with EAX = 0xF6. At syscall entry user ESP = sigcontext_base + 4, so signal_resume_after_handler reads saved_eip / saved_eflags / saved_esp / saved_eax..edi at offsets [edi + 4..40], validates eip + esp are user-virt, rewrites the syscall iret frame and pushad slots, clears in_sigint_handler, and either popad+iretds back to interrupted user code or, if a SIGINT arrived during the handler, hands off to signal_dispatch_kill / signal_dispatch_user / clears for SIG_IGN. Build clean; test_programs (29) + test_asm (20) pass; manual boot smoke test reaches the shell prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Check pending_sigint before each polling iteration in fd_read_console; return CF set / ERROR_INTERRUPTED (0x08) so the syscall epilogue's SIGINT_TAIL_CHECK delivers the signal on iret. Also detect 0x03 (Ctrl+C) from the serial port and set pending_sigint so serial users get the same signal path as PS/2 keyboard users. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests/programs/sigint_test.c: registers on_sigint, calls SYS_IO_READ which detects serial 0x03, sets pending_sigint, and returns the byte; the syscall epilogue dispatches via signal_dispatch_user; on_sigint sets got_sigint and returns through the vDSO sigreturn trampoline; signal_resume_after_handler restores the interrupted state; main checks got_sigint and prints CAUGHT. Fix the vDSO sigreturn trampoline to use `mov ah, SYS_SYS_SIGRETURN` instead of `mov eax, SYS_SYS_SIGRETURN` — the INT 30h dispatcher reads AH for the syscall number, so the mov-eax form left AH=0 and dispatched SYS_FS_CHMOD (0x00) instead of SYS_SYS_SIGRETURN (0xF6), causing an EXC0E page fault in the trampoline. Add sigint_test to tests/test_programs.py with command `"sigint_test\n\x03"`: the \n terminates the shell input so the program launches, then the 0x03 sits in the serial FIFO for the program's SYS_IO_READ to consume. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ce IF=1) A user-installed SIGINT handler controls every byte of the on-stack sigcontext, including saved_eflags. signal_resume_after_handler loaded that dword verbatim into the iret frame, so a handler could return through the vDSO trampoline with IOPL=3 (granting ring-3 unrestricted in/out), VM=1 (Virtual-8086 entry), NT=1, RF=1, etc. Mask saved_eflags down to CF/PF/AF/ZF/SF/TF/DF/OF (USER_EFLAGS_MASK = 0xDD5) and force IF=1 before reloading. Mirrors Linux's restore_sigcontext FIX_EFLAGS rationale.
…, EFLAGS) The SIGINT section had several errors caught in code review: - Macro lives in src/include/irq_tail.inc, not src/arch/x86/sigint.asm. - Insertion sites are entry.asm (IRQ 0/5/6), ps2.c (IRQ 1), fdc.c (IRQ 6), syscall.asm (INT 30h) — not idt.asm. - Dispatch order is CS-check first, then pending_sigint, then in_sigint_handler, then read sigint_handler. pending_sigint is cleared by SIG_IGN and signal_dispatch_user; the SIG_DFL kill path does not (the program is dying). - BSS slot is sigint_handler, not signal_handler. - vDSO trampoline lives at user-virt 0x10450 (FUNCTION_TABLE + VDSO_SIGRETURN_OFFSET). - sys_sigreturn validates saved_eip and saved_esp ranges, not a separate "saved sigcontext pointer at [user_esp + 4]" — the sigcontext IS the on-stack frame; saved_eip lives at +4 because the trampoline's `ret` already popped the trampoline address. - After the EFLAGS sanitize fix, restate the EFLAGS-restore wording: arithmetic flags + DF + TF preserved, IF forced on, IOPL/VM/NT/RF cleared via USER_EFLAGS_MASK.
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pending_sigintflag. Every kernel-to-user IRET epilogue (5 IRQ handlers + the syscall iret tail) checks the flag via the newSIGINT_TAIL_CHECKmacro and dispatches to one ofSIG_DFL(kill viaaddress_space_destroy+shell_reload),SIG_IGN(clear and resume), or a user handler delivered via a 48-byte sigcontext on the user stack + vDSO trampoline +SYS_SYS_SIGRETURNresume.while(1)user loop is now killable from PS/2 within ~1 ms (IRQ 0 epilogue catches it). Shell installsSIG_IGNso its own Ctrl+C is benign; children inheritSIG_DFLfromprogram_enterand are killable by default.fd_read_consoleandMIDI_IOCTL_DRAIN:pending_sigintchecked in their wait loops, bail withCF=1, AL=ERROR_INTERRUPTED. libc maps toerrno = EINTR.SYS_SYS_SIGNAL(0xF5) andSYS_SYS_SIGRETURN(0xF6); libcsignal()wrapper.SYS_SYS_SIGRETURN(mask0xDD5, forceIF=1) prevents user handlers from escalating IOPL via a forged sigcontext — the one privilege issue the design naturally introduces.Design spec:
docs/superpowers/specs/2026-05-06-sigint-handling-design.mdImplementation plan:
docs/superpowers/plans/2026-05-06-sigint-handling.mdKnown limitations (documented)
Test plan
tests/test_programs.py— 30 / 30 pass under bbfs (includes newsigint_testexercising handler install + sigcontext build + sigreturn end-to-end via serial Ctrl+C)tests/test_asm.py— 20 / 20 pass (no regression in self-hosted assembler suite)$cleanlywhile(1)program, press Ctrl+C, confirm shell prompt returns. Cannot be automated (requires real keyboard input)Commits (20)
Atomic per-task series following the implementation plan: constants → BSS state → kill path → detection → IRQ epilogues → syscall epilogue → SYS_SYS_SIGNAL → libc signal() → shell SIG_IGN → vDSO trampoline → signal_dispatch_user → SYS_SYS_SIGRETURN → cooperative interruption → libc EINTR → automated test → docs → EFLAGS sanitization fix.
🤖 Generated with Claude Code