#DF: deliver via a task gate on a dedicated stack (closes #119) - #122
Merged
Conversation
Vector 8 was an ordinary interrupt gate, so a double fault took the same
path as every other exception: isr8 -> isr_common -> isr_common_handler,
whose first act is pushing a register frame onto the CURRENT stack.
double_fault_c_handler() -- written for a dedicated stack, and saying so in
its own comment -- had no call site in the linked binary, and the 8 KB
double_fault_stack plus tss_get_double_fault_stack_top() were allocated and
never read.
This only mattered in one case, but it is the common one. For a #DF on a
HEALTHY stack the generic path was already fine and printed more than the
dead handler would. The gap was kernel STACK EXHAUSTION: there the stack is
already gone, so isr_common's pushes fault again and the CPU escalates to a
triple fault -- instant reset, no output. TinyOS has hit exactly this (the
signed-exec chain overflowing a 64 KB stack is why KERNEL_TASK_STACK_PAGES
is 32 today), and a recurrence produced nothing to debug.
i386 has no IST -- that is x86-64. The only construct that switches stacks
on an exception is a hardware task switch, so vector 8 becomes a TASK GATE
(type 0x85, selector = a dedicated TSS, offset ignored) pointing at a second
TSS whose esp/eip/cr3/segments the CPU loads wholesale. The handler starts
on a known-good stack whatever the faulting one looks like, and reads the
interrupted context out of the outgoing TSS.
tss.c df_tss + tss_init_double_fault(); captures CR3 after paging is
live, so a #DF inside a user process still finds the handler
mapped. Uses the KERNEL pdpt, valid in every address space.
gdt.c gdt_grow_for_df_tss() extends gdtr.limit to cover the new
descriptor and re-lgdts. Bounds-checked: a descriptor past the
limit would #GP inside the gate, i.e. the failure being fixed.
idt.c idt_install_double_fault_gate() rewrites entry 8 as a task gate.
interrupts.c double_fault_c_handler -> double_fault_task_entry: no args,
never returns (there is nothing to return to), reads the faulting
state from the main TSS and the backlink from df_tss.prev_tss.
The stack-exhaustion tell measures DISTANCE FROM esp0, not page equality.
esp0 is the TOP of a stack that grows DOWN, so an exhausted stack puts ESP
as far below esp0 as possible; a same-page test fires only on a nearly EMPTY
stack, i.e. exactly backwards. The first draft had it backwards and the
harness caught it. "Used" can exceed capacity once ESP runs past the base --
labelled, so it is not read as a quota.
Not addressed: a hardware task switch sets the busy bit in the DF TSS
descriptor, so a SECOND #DF would #GP. Acceptable here because the handler
halts and never returns, so there is no second one by construction.
Harness: verify/verify-double-fault.sh (needs -DTINYOS_FAULT_INJECT for the
`dftest` command, which recurses until the stack is gone). Asserts the gate
is installed, the handler prints, the dump carries the interrupted state,
the diagnosis names stack exhaustion, and -- independently, from QEMU rather
than from our own kprintf -- that the CPU never triple-faults.
Both polarities were run. Fixed tree: PASS, with ESP 852 KB below esp0 on a
128 KB stack (through the guard page and onward). Suppressing only the
idt_install_double_fault_gate() call: FAIL, with the trace showing
v=0e -> v=08 -> Triple fault and serial ending at the [FAULT] line. The
watchdog exists because that unfixed kernel REBOOTS and the typist would
otherwise log in and re-run dftest forever, reporting as a hang and not the
FAIL it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEkhAhgTxbE5TgifyYf8v4
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.
What
Vector 8 was an ordinary interrupt gate, so a double fault took the same path as every other exception:
isr8→isr_common→isr_common_handler, whose first act is pushing a register frame onto the current stack.That is fine for every cause of #DF except kernel stack exhaustion — the one case where the current stack has no room for those pushes. There the push itself re-faults, and the CPU triple-faults and resets. The dedicated handler
double_fault_c_handler()— written for a dedicated stack, and saying so in its own comment — had no call site in the linked binary, and the 8 KBdouble_fault_stackplustss_get_double_fault_stack_top()were allocated and never used.i386 has no IST, so the fix is the architectural one: a task gate with its own TSS, which switches ESP as part of delivery rather than after it.
Verified
verify-double-fault.shdrivesdftest, which exhausts the kernel stack by construction. The handler now runs and dumps the interrupted task state read from the outgoing TSS:Closes #119.
🤖 Generated with Claude Code