A successful build, a warning-free run, and a SKIP test case each represent different outcomes. Kernel verification requires keeping inputs, execution paths, environmental prerequisites, and results in the same record.
Goal of Review
Where to Look
Locate execution paths
Logs and Dynamic Debug
Verify various boundaries
KUnit, kselftest, sanitizers, fuzzing
Failure branches and delivery evidence
Fault injection, reproduction records
printk and Console Log Levels
pr_err, pr_info, pr_debug, etc., provide logging entry points, while the dev_* forms include device context. Logging to the kernel buffer and final output to the console are different stages; console_loglevel filters console output and does not equate to deleting all lower-level records.
Smaller log level numbers indicate higher severity. console_loglevel=4 typically outputs messages with a number less than 4, i.e., error and more severe levels; this setting should not be described as meaning the system only has these logs. High-frequency path logging can also alter timing and performance, requiring rate limiting or switching to appropriate tracepoints.
Dynamic Debug Controls Existing Call Sites
Dynamic debug enables pr_debug/dev_dbg and other messages based on module, function, file, line number, or other conditions, provided the call sites are supported and compiled in. It cannot create logging entry points for arbitrary functions that lack debug statements.
This document provides /proc/dynamic_debug/control; when debugfs is enabled and mounted, /sys/kernel/debug/dynamic_debug/control may also be available. First, read the actual existing control file to confirm entries and original flags, then make limited-range modifications. Dynamic debug
For example, the command string module my_driver +p enables the corresponding module entry; -p disables printing, and flags like f/l/t add function, line number, and thread information. When restoring, revert to the previous state; do not unconditionally clear debug points that others have enabled.
KUnit and kselftest Check Different Boundaries
KUnit verifies internal functions or subsystem logic within the kernel environment and can be executed via UML, QEMU, or a test kernel. kselftest typically checks kernel interfaces and behavior from user space, with some suites cooperating with test modules; the two are complementary. KUnit Introduction, kselftest
KUnit involves more than just a KUNIT_CASE array; it requires registering a suite and integrating it into Kconfig/Kbuild. The following uses a buffer range check as an example: capacity is capacity, access is [offset, offset+length); length zero is allowed if offset exactly equals capacity, but out-of-bounds access is prohibited. If you directly judge offset + length <= capacity, unsigned integer overflow might incorrectly treat an illegal range as valid.
The first group checks exclusive upper bounds and empty interval contracts; the second group checks addition risks near the maximum integer. offset <= capacity also ensures the subsequent subtraction does not underflow; C's && short-circuit evaluation ensures the second term is only calculated if this premise holds. This helper only verifies numerical ranges; it does not guarantee pointer validity, page accessibility, or device DMA execution rights.
When integrating the test source code into the directory, add the corresponding Kconfig entry and Kbuild rules, and ensure the upper-level configuration and build include this directory:
config KB_RANGE_KUNIT_TEST
tristate "Range boundary tests" if !KUNIT_ALL_TESTS
depends on KUNIT
default KUNIT_ALL_TESTS
For a clean source tree with this test integrated, you can use .kunitconfig in an independent build directory to enable CONFIG_KUNIT=y and CONFIG_KB_RANGE_KUNIT_TEST=y, then filter by suite name. Filters only select compiled tests and cannot replace configuration enabling.
./tools/testing/kunit/kunit.py run --build_dir=.kunit-range --kunitconfig=.kunitconfig kb-range
The default UML path builds and runs a user-space Linux test kernel, rather than loading the test module in the current host. If the source tree already has incompatible build artifacts, use a separate clean source copy; do not execute mrproper on the actively maintained kernel tree just for tutorial commands. KUnit Configuration, Build, and Filtering
Built-in tests and module tests have different execution timings. kunit.py can help configure, build, and execute tests; the first run may require a full test kernel build; merely compiling this C file does not mean the test has been executed.
Result Interpretation: Record SKIP and PASS Separately
TARGETS in selftests can filter suites, but network, BPF, and other tests require corresponding kernel configurations, permissions, and dependencies, and may alter the test environment. Prioritize running suites related to modifications and record SKIP reasons; "all skipped" is not "all passed."
Sanitizers, Lock Checks, and Breakpoints Provide Different Evidence
KASAN focuses on applicable out-of-bounds and use-after-free errors; KCSAN helps discover data races; lockdep checks lock dependency constraints. They are influenced by configuration, execution paths, and sampling mechanisms; do not treat unreported issues as proof of no errors.
KGDB allows debugging the kernel with GDB under appropriate configuration and debug link. SysRq-g may halt the target kernel waiting for the debugger, suitable for test environments with recovery means; it should not be executed directly as a daily read-only diagnostic command.
Breakpoints and logs both alter scheduling timing. Hard-to-reproduce races can be combined with tracepoints, minimal inputs, and repeated triggers to save the order of occurrence, rather than just keeping the final stack trace.
syzkaller and LTP
syzkaller uses system call descriptions, resource relationships, and coverage feedback to generate and mutate programs, looking for crashes, hangs, and other anomalies in a configured target environment, then attempting to minimize and reproduce them. QEMU is a common execution backend, not the only one; automatic bisect also requires corresponding infrastructure and cannot be claimed to be included in any arbitrary run. syzkaller
LTP provides regression test suites for system interfaces, suitable for supplementing verification of kernel and libc interaction behavior. Test case failures may stem from the kernel, environment, permissions, or test prerequisites; specific results and versions should be preserved, rather than just reporting a total failure count. LTP
Fault Injection: Turning Uncontrollable Failures into Verifiable Inputs
Injection points such as memory allocation, I/O, and function return values help check cleanup, retry, and rollback paths. The framework's probability, interval, times, task-filter, and filtering conditions jointly affect triggering; setting failslab/interval to 100 alone does not guarantee that any allocation fails once every 100 times. Fault Injection
Stage
Facts to Confirm
Before Injection
Target task/path is limited, baseline behavior is clear
During Injection
Expected failure actually occurs, cleanup and rollback are verifiable
After Injection
Resources are reclaimed, related parameters restored
Theoretical examples do not need to enable global injection on the current host; a test VM is more convenient for reproduction and recovery.
Fail One by One in Acquisition Order, Check if Only Owned Resources Are Released
Assume initialization acquires context, buffer, registration in sequence, and only after all succeed are the objects published to the caller. Each step's failure should be tested separately, rather than just letting the first allocation fail once and calling it "error path covered."
Failure Location
Owned Before Failure
Rollback Order
context
None
Do not release unacquired resources
buffer
context
context
registration
context, buffer
buffer → context
Exit after all succeed
Three resources, and object published
Stop external publishing first, then clean up according to resource dependencies
Below is a deterministic local lifecycle model. The fail_at-th acquisition fails before establishing the resource; ownership of successful objects is transferred to the caller; on exit, publishing is revoked first, then resources are released. It does not enable host failslab, nor does it simulate true device asynchronous callbacks.
classResources:def__init__(self,fail_at=None):self.fail_at=fail_atself.attempts=0self.live=set()self.freed=[]self.published=Nonedefacquire(self,name):self.attempts+=1ifself.attempts==self.fail_at:raiseMemoryError(name)assertnamenotinself.liveself.live.add(name)returnnamedefrelease(self,name):assertself.publishedisNoneandnameinself.liveself.live.remove(name)self.freed.append(name)defcreate(self):acquired=[]try:fornamein("context","buffer","registration"):acquired.append(self.acquire(name))exceptMemoryError:fornameinreversed(acquired):self.release(name)raiseself.published=tuple(acquired)returnself.publisheddefdestroy(self,owned):assertself.publishedisownedself.published=Nonefornameinreversed(owned):self.release(name)forfailure,expectedin[(1,[]),(2,["context"]),(3,["buffer","context"])]:resources=Resources(failure)try:resources.create()exceptMemoryError:passelse:raiseAssertionError("Target failure did not occur")assertresources.attempts==failureassertnotresources.liveandresources.publishedisNoneassertresources.freed==expectedresources=Resources()owned=resources.create()assertresources.live==set(owned)andnotresources.freedresources.destroy(owned)assertnotresources.liveandresources.publishedisNoneassertresources.freed==["registration","buffer","context"]
Here, we not only check for function return failures, but also verify that the target injection point was indeed reached, subsequent acquisitions did not continue, resources were not leaked or double-freed, and incomplete objects were not visible externally. These are the verifiable invariants of error paths.
Real driver registration operations might start work before returning, or external objects might still hold references; after revoking publishing, new requests must be canceled, in-flight operations and callbacks must be waited for, and then the buffers/contexts they used must be released. Mechanically reversing a few free statements is not enough. The probability and filtering parameters of the injection API are used to trigger paths; resource and visibility checks are used to judge whether that path is correct; the two cannot replace each other.
Move the failure point through acquisition, then compare cleanup policies. Count both retained resources and premature releases while dependents are still live.
Preparing the visual
How does cleanup change with the failure point?
Failure cleanup releases only acquired resources in dependency order. Ending with zero resources does not prove that a buffer was never freed while still in use.
{"id":"failure-injection-rollback","title":"How does cleanup change with the failure point?","summary":"Failure cleanup releases only acquired resources in dependency order. Ending with zero resources does not prove that a buffer was never freed while still in use.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label for=\"failAt\" data-i18n=\"failAt\"></label><select id=\"failAt\"><option value=\"0\" data-i18n=\"buffer\"></option><option value=\"1\" data-i18n=\"irq\"></option><option value=\"2\" selected data-i18n=\"interface\"></option><option value=\"3\" data-i18n=\"success\"></option></select><label for=\"policy\" class=\"caption\" data-i18n=\"policy\"></label><select id=\"policy\"><option value=\"correct\" data-i18n=\"correct\"></option><option value=\"leak\" data-i18n=\"leak\"></option><option value=\"forward\" data-i18n=\"forward\"></option></select><div class=\"actions\"><button id=\"run\" data-i18n=\"run\"></button><button id=\"remove\" data-i18n=\"remove\"></button></div><div class=\"track-row\"><span data-i18n=\"resources\"></span><div id=\"resources\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"hazards\"></span><strong id=\"hazards\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"trace\"></span><div id=\"trace\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.caption{display:block;margin:14px 0 5px}#trace{display:grid;grid-template-columns:1fr}#trace .cell{min-height:28px;font:11px var(--viz-font-ui)}.track#resources .cell{min-height:60px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let live=[false,false,false],hazards=0,trace=[],success=false;const names=['buffer','IRQ','interface'];function cleanup(){const policy=$('#policy').value,order=policy==='forward'?[0,1,2]:[2,1,0];for(const i of order){if(!live[i])continue;if(policy==='leak'&&i===0){trace.push('KEEP buffer');continue;}if((i===0&&live[1])||(i===1&&live[2])){hazards++;trace.push('FREE '+names[i]+' !');}else trace.push('FREE '+names[i]);live[i]=false;}}function draw(){cells('resources',live.map((v,i)=>({text:names[i]+' '+(v?'●':'×'),cls:v?'gold':'empty'})));cells('trace',Array.from({length:7},(_,i)=>({text:trace[i]??'—',cls:trace[i]?.includes('!')?'gold':trace[i]?'on':'empty'})));set('remaining',live.filter(Boolean).length);set('hazards',hazards);$('#remove').disabled=!success;set('explanation',t('note'));}$('#run').onclick=()=>{live=[false,false,false];hazards=0;trace=[];const fail=+$('#failAt').value;for(let i=0;i<3;i++){if(i===fail){trace.push('FAIL '+names[i]);break;}live[i]=true;trace.push('GET '+names[i]);}success=fail===3;if(!success)cleanup();draw();};$('#remove').onclick=()=>{cleanup();success=false;draw();};draw();","audio":false,"strings":{"buffer":"Before buffer allocation","correct":"Reverse dependency cleanup","failAt":"Where failure occurs","forward":"Free in allocation order","hazards":"Release-order hazards","heading":"How does cleanup change with the failure point?","interface":"Before interface registration","intro":"Choose a probe failure point and rollback policy, then run the model. Compare leaked resources and unsafe release order; successful probes can also be torn down.","irq":"Before IRQ registration","leak":"Omit buffer release","note":"Try a successful probe and teardown, or fail before interface registration. Reverse cleanup, a missing free, and wrong order have different results; testing successful probe alone is insufficient.","policy":"Rollback policy","remaining":"Remaining resources","remove":"Tear down successful probe","reset":"Reset","resources":"Resources and dependencies still active","run":"Run selected scenario","scope":"Toy probe: buffer → IRQ using buffer → public interface depending on IRQ. Failure occurs before acquiring the selected resource. Incorrect policies run only in the model, not as real fault injection. Concurrency is omitted; an order hazard means releasing a resource while its dependent user remains active.","scopeLabel":"Model scope and assumptions","success":"All succeed","trace":"Acquisition and cleanup trace"}}
Deliverable Reproduction Records
Record the accurate kernel version or commit, .config, boot parameters, module and taint status, as well as minimal input, actual behavior, expected behavior, and original logs. If the fix is only verified under a specific configuration, this boundary should be explicitly stated.
Connecting this evidence to the artifacts of Build and Modules allows confirming that the test ran the target code, not another kernel with the same name.