Kernel Debugging, Testing, and Failure Analysis
LESSON
Kernel Debugging, Testing, and Failure Analysis
By the end of this lesson, you will be able to...
Turn a kernel panic into a small, falsifiable explanation of what failed.
Trace a fault from trap evidence through page-table and syscall state to a code path.
Design a regression test and invariant check that prevent the same bug from returning silently.
Idea in one sentence: Kernel debugging is a controlled feedback loop: reproduce one failure, capture the state at its boundary, test a narrow hypothesis, and turn the discovered invariant into a regression test.
writer calls write(fd, buf, 4). Its buffer starts two bytes before the end of a mapped 4096-byte page, so the final two bytes fall in an unmapped page. A correct kernel should reject the user range and return an error without changing the file. Instead, the teaching kernel panics with a supervisor-mode page fault while copying bytes.
The first bad instinct is to add prints everywhere. That can change timing, hide a race, overflow the console path, and leave you with more output than evidence. The better first move is smaller: preserve the exact reproducer, record the fault address and instruction, identify the execution context, and ask which invariant would have made this fault impossible.
This lesson uses one fault as a debugging laboratory. The same loop applies to a lost wakeup, stale buffer, DMA completion that never arrives, or packet queue leak: make the hidden state observable without confusing a symptom for a cause.
Core Insight
The debugging loop has a stable shape:
observable failure
-> minimal reproducer
-> boundary evidence
-> competing hypotheses
-> targeted inspection
-> fix the violated invariant
-> regression test and signal
| Stage | Question | Evidence that helps | Output |
|---|---|---|---|
| reproduce | Can the failure happen on demand? | exact command, image, CPU count, input bytes | stable failing case |
| classify | Which boundary failed? | trap cause, fault address, PC, process and mode | a small set of subsystems |
| inspect | Which state contradicts the expected invariant? | registers, page-table walk, lock/queue state, descriptor status | supported root-cause hypothesis |
| fix | What rule must the code enforce? | code path before side effects | narrow change and assertion |
| prevent | How will a future change reveal regression? | focused test and counter/log | repeatable proof of behavior |
Plain meaning:
Do not debug a kernel by guessing which room is haunted. Start where the alarm rang, find the door it crossed, and check the list of rules for that door.
In this scenario:
The alarm is a kernel page fault. The boundary is the user-to-kernel copy in write. The suspected invariant is: “the kernel accesses user memory only through a helper that validates each page of the requested range before it performs a privileged dereference or side effect.”
Technical name:
A panic stops the kernel because continuing would violate a critical invariant. On RISC-V, trap state such as scause, stval (faulting address), and sepc (faulting instruction address) helps classify a fault. A minimal reproducer is the smallest controlled input that triggers the failure. A regression test proves the corrected behavior stays true after later changes.
Start With a Reproducer, Not a Theory
Make the test intentionally small:
map one readable user page at 0x3000..0x3fff
leave 0x4000..0x4fff unmapped
put two valid bytes at 0x3ffe
call write(fd, 0x3ffe, 4)
Run it in the emulator with the same kernel image and test command each time. Record whether the test uses one CPU or several, the compile configuration, and the expected result. For this case, expected behavior is a controlled error such as -EFAULT or the teaching kernel's equivalent, with no file-offset advance and no changed cache buffer.
The naive test “run the whole user suite until a panic appears” has poor diagnostic value. It mixes scheduler timing, unrelated syscalls, and several possible pointers. A small failure makes every state transition easier to inspect.
If the failure is nondeterministic, reduce before instrumenting: one process instead of many, one device request instead of a workload, fixed input, one CPU when the race is not the subject. If the problem disappears only with one CPU, that is evidence for a synchronization hypothesis, not proof of one. Preserve both configurations as separate tests.
A Worked Investigation: a Page-Boundary write Panic
Assume the kernel prints this compact trap record:
panic: kernel page fault
scause = store/load page fault
stval = 0x4000
sepc = copyin + 0x24
pid = 7 (writer)
syscall = write(fd=3, buf=0x3ffe, n=4)
Input: write(3, 0x3ffe, 4) from the one-page test above.
1. Classify the failure before changing code
stval == 0x4000 is the first byte of the unmapped next page. sepc resolves, through kernel symbols, to the copy helper rather than the file driver or scheduler. The current process and syscall arguments match the reproducer.
Intermediate conclusion: this is most likely a user-memory range-validation or copy-path bug. It is not evidence that block writeback, the file descriptor table, or a device interrupt failed. Those systems may be downstream, but the trap occurred before the copy boundary was safely crossed.
2. Form two testable hypotheses
| Hypothesis | Prediction | Fast inspection |
|---|---|---|
| the helper validates only the starting address | page 0x3000 is present but 0x4000 has no valid user mapping |
walk both PTEs and inspect copy loop bounds |
| the helper validates the full range but dereferences the wrong address space | both expected PTEs look valid in the process table, but the kernel's access translation is wrong | inspect page-table argument, saved process, and address conversion |
This prevents a vague conclusion such as “page tables are broken.” Each hypothesis predicts a different observation.
3. Inspect at the fault boundary
In a debugger attached to the emulator, stop at the trap entry or copyin. Inspect the saved registers, the process page-table pointer, the copy source and remaining length, then walk the relevant virtual addresses. The expected observation for hypothesis one is:
VA 0x3ffe -> valid user-readable page at 0x3000
VA 0x4000 -> no valid user-readable mapping
copy loop -> checks 0x3ffe once, then increments through 0x4000
The bug is now specific: the helper accepted the start of the range but did not revalidate when the loop crossed a page boundary. A direct kernel load from 0x4000 triggered the panic instead of turning untrusted input into a syscall error.
4. Repair the invariant before side effects
Fix the copy helper so it processes the user range page by page. For each chunk, it checks arithmetic for overflow, finds a valid user mapping with the required permission, copies only to that page boundary, then continues with the next page. It returns an error if any page is invalid.
The syscall must perform this validation/copy before it mutates a file offset, dirties a cache buffer, or submits I/O. If partial write behavior is part of the API, define exactly which bytes and side effects may be accepted; do not let a kernel fault choose the semantics accidentally.
Add an assertion or debug-only diagnostic at the helper boundary: an attempted kernel copy must have a non-overflowing range and a valid mapping for the current chunk. Assertions should state the invariant, not merely print “copy failed.”
5. Turn the fix into evidence
Run the original page-boundary test. It should return the defined error and leave file state unchanged. Add nearby cases:
valid range entirely inside one page -> accepted
range crossing two valid pages -> accepted
range crossing into an unmapped page -> controlled error, no side effect
range with address + length overflow -> controlled error, no side effect
Output: four small tests establish both the valid path and the boundary. The panic is no longer the proof; the observable syscall result and untouched state are.
Naive contrast: a one-line if (buf == NULL) check misses almost every real invalid range. A broad “catch all page faults” handler can conceal a kernel bug. The correct fix is to make the copy boundary's required validation explicit and test its edges.
So far, the failure went from a PC and fault address to an invariant and a test suite. The same style transfers: for a lost wakeup, inspect condition plus sleeper publication; for a stale disk block, inspect dirty/in-flight/clean state; for DMA corruption, inspect descriptor, buffer owner, and completion record.
Make State Visible at the Right Boundary
Good kernel diagnostics are small and structured. Include the values needed to reconstruct the violated rule:
| Boundary | Useful evidence | Misleading shortcut |
|---|---|---|
| trap/syscall | cause, fault address, PC, privilege mode, syscall number and arguments | only “segmentation fault” |
| page translation | virtual address, PTE permissions, page-table root, physical result | dumping every page table on each syscall |
| scheduler/waiting | task state, wait channel, lock owner, runnable-queue membership | logging every scheduler tick forever |
| buffer cache/driver | block number, dirty/in-flight state, descriptor slot, completion status | treating submission as completion |
| packet/socket queue | buffer owner, length, destination, queue count, drop reason | assuming every interrupt equals delivery |
Use a serial console, a ring buffer, counters, trace points, or debugger breakpoints according to the question. QEMU's GDB stub can expose a repeatable guest state to a host debugger, while GDB's remote target support lets the host inspect symbols, registers, memory, and breakpoints. Logging must be safe in the context where it runs: a console lock or formatter can itself deadlock or perturb an interrupt path.
Trade-offs, Limits, and Signals
Instrumentation improves observability but costs time, memory, code paths, and sometimes timing stability. A verbose print inside a lock can create contention; a debugger breakpoint can make a race disappear; an assertion can turn a recoverable test failure into a panic. The trade-off is worth it when the diagnostic is narrow, removable or controllable, and tied to an invariant.
A passing regression suite is also not a proof that every kernel path is safe. It covers the specified cases under a particular configuration. Add stress tests for concurrency, fault injection for allocation and I/O errors, and emulator runs with repeatable seeds or configurations when those are relevant. Keep a fast, focused test close to the bug and a broader suite for interactions.
Signals that the feedback loop is healthy include: a panic record that identifies a boundary; a test that fails before the fix and passes after it; a counter for rejected user ranges or queue drops; and a known expected error rather than silent corruption. Signals of weak diagnosis include an unreproducible report, an unexplained panic without PC/fault state, or a “fix” that merely suppresses the symptom.
Common Confusions
Confusion: “The faulting instruction is the root cause.”
Why it is tempting:
The program stopped at that instruction.
Better model:
The PC tells you where the invariant became visible. The root cause can be an earlier unchecked input, ownership handoff, or state transition. Pair the PC with the relevant input and expected invariant.
Confusion: “More logs always mean better debugging.”
Why it is tempting:
Kernel state is hidden, so printing feels like visibility.
Better model:
Record the smallest evidence that distinguishes hypotheses. Excess logging can alter timing, add lock dependencies, and bury the causal sequence.
Confusion: “A test that avoids the panic proves the fix.”
Why it is tempting:
The visible failure disappeared.
Better model:
The test must assert the intended contract: return value, state changes, and safety boundary. Add valid neighboring cases so a fix that rejects everything cannot pass.
Confusion: “An emulator makes a concurrent bug deterministic.”
Why it is tempting:
The machine configuration is repeatable.
Better model:
Emulation improves control, but scheduling and device timing can still expose different interleavings. A reproducible configuration is evidence; targeted synchronization tests are still needed.
Check: A kernel panic reports stval = 0x4000, sepc inside copyin, and syscall arguments buf = 0x3ffe, n = 4. What is the first high-value hypothesis?
Think first, then reveal.
Answer: The copy path validated only the start or otherwise failed to validate the whole user range as it crossed into page 0x4000. Inspect the page mappings and per-page copy loop before investigating storage or driver code.
Check: A debug print inside an interrupt-shared lock makes a lost-wakeup test stop failing. Has the bug been fixed?
Think first, then reveal.
Answer: No. The print may change timing or locking enough to hide the interleaving. Remove or control the probe, capture condition/waiter/wakeup state, and use a test that asserts the queue and scheduler invariant.
Practice: write a kernel failure note
A test reports: “After a device write completion with error status, the cache buffer is marked clean and later reads return old storage bytes.” Write a failure note with:
- the minimal reproducer;
- the ownership or state invariant;
- three fields you would capture at submission and completion;
- the narrow repair boundary; and
- one regression test plus one operational signal.
Use this rubric:
| Criterion | A good answer includes |
|---|---|
| Reproducer | one writeback request with injected completion error and a later read |
| Invariant | only a successful, recorded completion can transition dirty/in-flight to clean |
| Evidence | block/buffer ID, descriptor/request slot, completion status, and dirty state |
| Repair | completion handler/cache boundary records error and preserves retry/recovery state |
| Prevention | a test asserts no false clean state; an I/O error or retry counter exposes recurrence |
Resources
- [DOC] QEMU GDB usage — Focus: attach a host debugger to a repeatable emulated guest and inspect the state near a fault.
- [DOC] GDB remote debugging — Focus: use remote targets, registers, memory inspection, and breakpoints for a kernel that cannot host a full debugger.
- [BOOK] MIT 6.1810 xv6 book — Focus: connect trap state, page tables, locks, filesystems, and device code to a small inspectable kernel.
Key Takeaways
- Start from a stable, minimal reproducer and use trap or subsystem evidence to classify the failed boundary.
- Debug hypotheses should predict observable state; inspect the smallest state that distinguishes them.
- Repair the violated invariant before side effects, then test valid, invalid, and edge cases of the contract.
- Diagnostics, assertions, and emulators are feedback tools with costs; keep them structured, scoped, and safe for their execution context.