Kernel Debugging, Testing, and Failure Analysis

LESSON

Operating Systems Implementation

015 30 min intermediate

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:

  1. the minimal reproducer;
  2. the ownership or state invariant;
  3. three fields you would capture at submission and completion;
  4. the narrow repair boundary; and
  5. 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

Key Takeaways

  1. Start from a stable, minimal reproducer and use trap or subsystem evidence to classify the failed boundary.
  2. Debug hypotheses should predict observable state; inspect the smallest state that distinguishes them.
  3. Repair the violated invariant before side effects, then test valid, invalid, and edge cases of the contract.
  4. Diagnostics, assertions, and emulators are feedback tools with costs; keep them structured, scoped, and safe for their execution context.
PREVIOUS Containers as Kernel Namespace and Resource-Control Mechanisms NEXT OS Implementation Capstone: Build and Explain a Small Kernel