Semantic Realignment: The Class of Kernel Changes That Silently Break Drivers

Some kernel changes don't change function signatures at all. The compiler stays quiet. Sparse stays clean. The driver loads, probes its hardware, and appears to function normally. Then something goes wrong at runtime under specific hardware conditions — a device fails to resume from suspend, a DMA transfer corrupts memory after hours of operation, or an interrupt handler runs in the wrong context.

These are semantic realignment changes: modifications to the kernel that alter the behavioral contract of an API without changing its syntactic interface. They are the hardest class of kernel evolution to detect, the hardest to migrate, and the most dangerous to miss. In DRIFTBENCH, they account for 23% of all cases and have the lowest automated resolution rate of any change class.

This post explains what semantic realignment is, walks through a real example, and describes how KDRIFT approaches detection and migration for this class of changes.

What Is Semantic Realignment?

In the KDRIFT taxonomy, we classify kernel API changes into nine categories. Classes 1 through 4 are syntactic changes — they alter the interface itself. A function is renamed, an argument is added, a return type changes, a struct is reorganized. These are visible to the compiler. A driver that doesn't adapt to them won't build.

Classes 5 through 9 include changes that may or may not alter the syntax but fundamentally alter the behavioral semantics. Semantic realignment specifically refers to cases where the syntax is unchanged or trivially changed, but the expected behavior of the API has shifted. Examples from real kernel evolution include:

In every one of these cases, the function signature is identical between kernel versions. The compiler sees no difference. Static analysis sees no type violation. The driver builds, loads, and may even pass basic functional tests. The failure only manifests under specific conditions — suspend/resume cycles, high interrupt rates, sustained DMA traffic, or particular error recovery paths.

A Real Example — PM Runtime

Let's walk through a concrete case. The function pm_runtime_get_sync() is used by hundreds of drivers to resume a device from runtime suspend. Its signature has remained stable for years:

int pm_runtime_get_sync(struct device *dev);

The function increments the device's runtime PM usage counter and attempts to resume the device. The return value has always been an int. But its semantics shifted between kernel versions in a subtle way.

In older kernels, pm_runtime_get_sync() returned 0 on success when the device was already active, 1 on success when the device was resumed, and a negative errno on failure. Many drivers checked for failure like this:

ret = pm_runtime_get_sync(dev);
if (ret < 0) {
    pm_runtime_put_noidle(dev);
    return ret;
}

This worked. If the device was already active, ret was 0. If it needed resuming, ret was 1. Both are "not less than zero," so both pass the check. Failures return negative values and are caught.

Then the kernel changed how pm_runtime_get_sync() handles the case where the device's resume callback itself returns an error. In the new semantics, the function still increments the usage counter even on failure. This means the error path must always call pm_runtime_put_noidle() to decrement the counter — not just when ret < 0, but always when the operation didn't achieve the desired state.

Drivers that only call pm_runtime_put_noidle() on negative return values now leak a usage count reference when the resume callback fails after the counter was incremented. The device can never enter runtime suspend again. Battery life degrades. The system slowly accumulates permanently-awake devices.

drivers/example/pm_fix.c semantic: pm_runtime_get_sync error handling
@@ -87,7 +87,7 @@ static int example_open(struct inode *inode, struct file *file)
ret = pm_runtime_get_sync(&pdev->dev);
- if (ret < 0) {
+ if (ret < 0) { /* must also put on failure to balance refcount */
pm_runtime_put_noidle(&pdev->dev);
return ret;
}

This is a simplified illustration, but the real fix is more nuanced. The correct migration often involves switching to pm_runtime_resume_and_get(), a newer wrapper that was introduced precisely because the pm_runtime_get_sync() semantics were a source of persistent bugs. The wrapper handles the refcount correctly on failure, making the caller's job simpler. But recognizing that this substitution is needed requires understanding the semantic shift — something no compiler or static analyzer can do from the code alone.

Why Static Analysis Misses This

Sparse and Smatch analyze syntax and type flow. They can verify that you're not dereferencing a null pointer, that you're using the right type annotations, that your lock ordering is consistent. But they cannot model behavioral contracts that exist only in documentation, commit messages, and maintainer knowledge.

The pm_runtime_get_sync() example is instructive. Both the old and new versions have the same signature. The return type is int in both cases. The values returned are valid ints. The bug is not in the types — it's in the interpretation of the return value and its interaction with the refcount side effect. This is a semantic property, not a syntactic one.

Static analysis tools could theoretically be extended with API-specific models that encode these behavioral contracts. Some work has been done in this direction — the kernel's own documentation infrastructure has been slowly adding structured contract annotations. But the coverage is incomplete, and the rate of API evolution outpaces the rate of annotation.

The fundamental problem is that behavioral contracts are implicit. They live in the collective understanding of subsystem maintainers. They are communicated through commit messages, mailing list discussions, and code review comments. Extracting them requires reading natural language, not parsing C.

How KDRIFT Detects Semantic Changes

KDRIFT uses a multi-signal approach to identify semantic realignment. No single signal is sufficient, but the combination provides high-confidence detection:

The 9-class taxonomy used by KDRIFT explicitly separates syntactic from semantic changes. Classes 5 (lifecycle/ordering), 6 (concurrency/locking), 7 (error handling semantics), and 8 (security/permissions) are all semantic in nature. This classification drives the choice of migration strategy — syntactic changes go to the LPA (fast-path agent), while semantic changes are escalated to the GMA (global migration agent).

The GMA Approach

For semantic changes, the Global Migration Agent (GMA) uses full repository context to understand the behavioral shift before generating a patch. Unlike the Local Patch Agent, which works from a classification and an AST delta alone, the GMA has access to:

The GMA's approach to semantic changes is fundamentally different from its approach to syntactic changes. For a syntactic change, the task is mechanical transformation — update the call site to match the new signature. For a semantic change, the task is behavioral reasoning — understand what the old code was trying to do, understand how the API's behavior has shifted, and determine what the new code must do to preserve the original intent.

This often requires changes in multiple locations within a driver. The pm_runtime_get_sync() example might require updating error handling in every function that calls it, adding cleanup code in error paths, and potentially restructuring the driver's power management flow to use the newer pm_runtime_resume_and_get() wrapper.

The GMA iterates with compiler and validation feedback. For semantic changes, it typically takes 3-5 iterations to converge on a correct patch, compared to 1-2 for syntactic changes. This is expected — behavioral reasoning is harder than syntactic transformation.

Frequency in DRIFTBENCH

Semantic realignment is not rare. In the DRIFTBENCH corpus:

23%
Semantic cases
38%
Resolution rate
3.4x
Harder than syntactic

23% of all migration cases in DRIFTBENCH involve semantic realignment as the primary change type. The automated resolution rate for these cases is 38%, compared to 67% for purely syntactic changes — roughly 3.4 times harder. The patches that fail tend to fail at Stage 4 (runtime analysis), which is consistent with the behavioral nature of the bugs.

Across the 20 kernel versions in DRIFTBENCH (v5.10 through v6.10), semantic changes are concentrated in certain subsystems. Power management, DMA/IOMMU, and the networking stack have the highest rates of semantic realignment. These are subsystems where behavioral contracts are complex and evolving — exactly the subsystems where out-of-tree drivers are most common.

The trend is also notable: the rate of semantic changes has been increasing over recent kernel versions. As the kernel matures, the easy syntactic cleanups (renaming, deduplication, argument reordering) are increasingly done. What remains are the harder refactorings that change how subsystems behave. This means the problem of semantic realignment will become more important over time, not less.

Understanding semantic realignment is essential for anyone building or using kernel migration tools. It defines the ceiling of what purely syntactic approaches can achieve and motivates the need for tools that reason about behavior, not just structure. It is the reason KDRIFT exists as a multi-agent system with staged validation, rather than a simple code transformation tool.

The DRIFTBENCH dataset includes labels for semantic vs. syntactic changes. See Building DRIFTBENCH for details on the extraction methodology.
← All posts Next: Building DRIFTBENCH →