Kernel Development: LLMs, Torvalds, and the Cost of Automation
On this page 5
My First Encounter: The Illusion of AI-Assisted Kernel Patches
A few months ago, I tried generating a Linux kernel patch using an LLM. The task was simple: add a pr_info message to a driver’s probe function, logging the device ID and revision. I fed the LLM the relevant driver file and the desired output format. It returned a patch that looked plausible on first glance.
The generated code modified drivers/pci/host/pcie-qcom.c. It correctly identified the qcom_pcie_probe function and inserted a dev_info call. The patch itself was formatted correctly, even including the --- a/... and +++ b/... headers. For a moment, it felt like a shortcut, a way to automate boilerplate.
--- a/drivers/pci/host/pcie-qcom.c
+++ b/drivers/pci/host/pcie-qcom.c
@@ -1077,6 +1077,9 @@
struct qcom_pcie *qcom_pcie = pci_get_drvdata(pci);
int ret;
+ dev_info(dev, "Probing QCOM PCIe device: ID %x, revision %x\n",
+ pdev->device, pdev->revision);
+
ret = qcom_pcie_parse_dt(qcom_pcie);
if (ret) {
dev_err(dev, "DT parsing failed: %d\n", ret);
The illusion broke down quickly. The kernel uses pr_info for driver-level messages, not dev_info in this context. More critically, the LLM chose to log pdev->device and pdev->revision, which are PCI device IDs and revisions, not specific to the QCOM PCIe controller being probed. The patch was syntactically valid but semantically incorrect for the intended purpose of logging controller initialization.
The real cost appeared in verification. I spent more time debugging the LLM’s output and correcting its assumptions than it would have taken to write the two-line patch from scratch. The LLM offered speed in generating text, but at the cost of introducing subtle functional errors and requiring a deeper review than if I had authored it myself. This isn’t just about coding style; it’s about understanding the specific kernel API usage and the context of the device being initialized. It highlighted the difference between generating code that looks right and code that is right within a complex, highly interdependent system like the kernel.
Linus’s Verdict: The Unforgiving Nature of Kernel Code
Linus Torvalds has been unequivocal: LLMs are not ready for kernel development. He views their current capabilities as insufficient for the precision required, often generating plausible-looking but subtly incorrect code. This skepticism stems from the kernel’s unique demands, where “almost right” is simply wrong.
Kernel code operates with zero tolerance for ambiguity or error. A misplaced bit, an off-by-one error in a memory allocation, or an incorrect synchronization primitive can lead to system crashes, data corruption, or security exploits. My own experience debugging an obscure race condition in a network driver taught me this. A single missed memory barrier in an ARM architecture-specific path caused intermittent packet drops under heavy load – a bug an LLM would struggle to diagnose or prevent.
The challenge for LLMs lies in their statistical nature. They predict the next most likely token, generating code that looks correct based on patterns in their training data. Kernel development, however, demands deterministic correctness, not statistical approximation. The implicit context of the kernel, spanning millions of lines across thousands of files and decades of architectural evolution, is beyond an LLM’s current grasp. It needs to understand not just syntax, but the intricate dance of hardware, memory models, and scheduler interactions.
Consider a patch to mm/page_alloc.c. An LLM might generate code that correctly allocates a page. But does it account for NUMA policies? GFP flags specific to a device driver? Potential reentrancy issues during interrupt handling? These are not trivial details; they determine if the system remains stable or deadlocks. The cost of getting these details wrong is a kernel panic.
While LLMs can generate boilerplate functions or simple script snippets, trusting them with core kernel logic is a different proposition. The tradeoff is clear: the marginal gain in development speed from auto-generated code is dwarfed by the potential cost of introducing a subtle, hard-to-find bug that could compromise an entire system. My position remains that for anything beyond trivial, isolated helper functions, LLMs introduce unacceptable risk into the kernel development process.
Beyond Generation: LLMs as Diagnostic Tools, Not Coders
Generating correct, production-ready kernel code remains beyond current LLM capabilities. The strict requirements for stability, performance, and security demand a level of deterministic accuracy and deep system understanding that these models do not possess. My experience confirms that direct code generation often yields plausible but fundamentally flawed results, unsuitable for integration into the kernel.
However, I’ve found limited utility in using LLMs for specific diagnostic tasks, particularly when faced with unfamiliar code paths or cryptic error messages. Consider a kernel panic trace. Dumping a dmesg output containing an Oops or BUG_ON into an LLM can sometimes highlight relevant function calls or potential race conditions faster than manual grep and source diving.
For example, given a simplified KASAN trace:
// Example of a simplified BUG_ON trace
[ 123.456789] BUG: KASAN: slab-out-of-bounds in my_driver_write+0x123/0x456 [my_driver]
[ 123.456795] Write of size 8 at addr ffff888000000000 by task foo/123
[ 123.456800] Call Trace:
[ 123.456805] my_driver_write+0x123/0x456 [my_driver]
[ 123.456810] vfs_write+0x78/0x100
[ 123.456815] ksys_write+0x50/0x90
[ 123.456820] do_syscall_64+0x60/0x100
[ 123.456825] entry_SYSCALL_64_after_hwframe+0x60/0x68
An LLM might correctly identify my_driver_write as the immediate culprit and point towards KASAN’s memory safety checks. The model’s suggestion is a starting point, not a definitive answer. I still need to verify its claims against the actual source code and system state. The cost of blindly trusting an LLM here is misdiagnosis, leading to wasted debugging cycles and potentially diverting attention from the true root cause.
Another area of limited use is in drafting documentation. Explaining complex kernel structures or API usage can be time-consuming. I’ve used LLMs to generate initial summaries of RFCs or to draft explanations for new driver interfaces based on source code comments. This often provides a passable first draft, but it requires thorough human review for accuracy and adherence to kernel documentation standards. LLMs can introduce subtle factual errors or omit crucial context, making direct publication unacceptable. The cost here is the time spent correcting and validating the output.
LLMs are assistants, not autonomous engineers. They can accelerate initial information synthesis or reduce the cognitive load of repetitive tasks. Their value lies in augmenting an engineer’s workflow by providing educated guesses or boilerplate, not in replacing the deep understanding required for kernel development.
The Hidden Cost of Convenience: Trust, Maintainability, and Security
Integrating LLM-generated code into the kernel codebase seems like a shortcut, but it introduces a host of problems that outweigh any perceived efficiency gains. My experience tells me that trust in kernel code is earned through rigorous review and a deep understanding of its implications. An LLM cannot earn that trust. Every line generated requires the same, if not more, scrutiny than code from an unknown human contributor.
The true cost emerges during debugging. LLMs often produce code that appears correct but contains subtle flaws. I’ve seen cases where a generated memory allocation, like kmalloc(size, GFP_KERNEL), looks plausible but misses a critical bounds check or misinterprets the size variable’s context. This leads to hard-to-trace panics or memory corruption that takes days to diagnose.
Consider this example of seemingly benign LLM output:
struct my_data {
char name[32];
int id;
// other fields
};
struct my_data *new_item = kmalloc(sizeof(*new_item), GFP_KERNEL);
if (!new_item)
return -ENOMEM;
// LLM-generated logic to copy user input
if (copy_from_user(new_item->name, user_input_name_ptr, user_input_name_len)) {
kfree(new_item);
return -EFAULT;
}
The LLM does not understand the intent behind user_input_name_len or the actual allocated size of new_item->name. If user_input_name_len exceeds 32, this copy_from_user call causes a buffer overflow. A human developer would explicitly clamp user_input_name_len to sizeof(new_item->name) - 1 and null-terminate. The LLM prioritizes statistical correctness over secure coding practices.
Security vulnerabilities are another significant concern. An LLM, trained on vast datasets, can replicate common coding patterns, including insecure ones. It might generate code that mishandles user input, leading to buffer overflows, or fails to properly validate permissions, creating privilege escalation vectors. The model prioritizes statistical correctness over security implications.
The perceived convenience of generating boilerplate code quickly trades off against a substantial increase in review burden and the risk of embedding latent bugs. What looks like faster initial development often translates to significantly longer debugging cycles and security audits down the line.
Ultimately, the effort required to verify, debug, and secure LLM-generated kernel code often exceeds the effort of writing it correctly from scratch. This makes the LLM a net negative for core kernel development, eroding confidence and increasing long-term maintenance costs.
My Position: Human Oversight Remains Irreplaceable
Despite the rapid advancements in large language models, my experience confirms that human oversight in kernel development remains non-negotiable. While LLMs can generate syntactically plausible code, this often masks deep semantic flaws. The cost is subtle bugs that manifest under specific load conditions or security vulnerabilities rooted in a lack of architectural understanding.
I’ve reviewed enough generated code snippets to see a recurring pattern: the output looks correct at a glance, but fails when confronted with the complex realities of hardware interaction, memory models, or concurrency primitives. An LLM might suggest a change to a locking mechanism, for instance, without truly understanding the implications of a specific CPU’s cache coherence protocol or the existing lock hierarchy. This isn’t a problem of minor refactoring; it’s a fundamental gap in comprehension.
Linus Torvalds’ consistent emphasis on “knowing what you’re doing” resonates here. Kernel development demands an engineer’s ability to reason about system state across multiple layers, from device drivers to virtual memory management. This requires intuition for edge cases, a deep historical context for why certain design decisions were made, and the critical judgment to identify non-obvious interactions. An LLM cannot replicate this depth of understanding; it merely predicts the next token based on its training data.
The real tradeoff with relying on LLMs for critical kernel code isn’t just the potential for errors, but the shift in the engineering burden. Instead of focusing on creation and initial validation, engineers would spend an inordinate amount of time debugging opaque issues introduced by generated code. Testing tools like kasan or syzkaller might catch some, but the most insidious bugs often require human insight to trace back to their conceptual origin.
For these reasons, LLMs serve best as specialized tools: assisting with boilerplate, generating initial drafts for well-understood patterns, or translating between APIs. They can simplify repetitive tasks. However, the ultimate responsibility for correctness, security, and performance rests with human engineers. Our expertise in navigating the intricate landscape of the kernel, understanding its historical context, and predicting its future behavior cannot be automated.
Spotted an error? Tell us via the corrections process — verified reports get fixed and credited.