CVE-2024-42096 - Dangerous Stack Assumptions in profile_pc()—Now Fixed in Linux Kernel

A subtle but persistent issue was identified and resolved in the Linux kernel’s profile_pc() function for x86 architectures. This flaw, tracked as CVE-2024-42096, revolved around outdated assumptions about function stack layouts. While timer-based profiling is now mostly obsolete, this part of the code could misinterpret stack contents, leading to unreliable profiling data, confusing kernel address identification, and years of KASAN memory-safety warnings. Ultimately, the code was removed to end years of trouble and remove pointless complexity.

Let’s break down what happened, why it mattered (even as a seemingly low-risk bug), show you the crux of the code, and explain how this issue was discovered and finally resolved.

What was the function supposed to do?

The profile_pc() function was designed for timer-based profiling on x86 Linux systems. Its main job was to determine which piece of code should be “blamed” for time spent in spinlocks, so the time could be posted to the right code location in profiling output.

Where did it go wrong?

The function made bold assumptions about the stack—namely, that after entering a spinlock, the stack frame would look the same every time (either no extra frame, or a simple PUSHF of the CPU flags). Based on this, it attempted to "look up" the real caller’s instruction pointer (return PC) either just after the flags or straight off the stack.

If the kernel had additional debugging features turned on, or if there were any deviations in how functions set up their stack frames, these assumptions could break. The code might then treat totally unrelated values as addresses or flags, potentially causing bad data or, worse, triggering KASAN memory safety errors.

Example of the offending code

unsigned long profile_pc(struct pt_regs *regs)
{
    unsigned long pc = instruction_pointer(regs);

    if (in_spinlock(pc)) {
        unsigned long *sp = (unsigned long *)regs->sp;
        unsigned long maybe_eflags = *sp++;
        if ((maybe_eflags & ~(x3FFFFF)) == )
            pc = *sp;
    }
    return pc;
}

*The code above blindly reads two words off the stack pointer. If the stack doesn’t look as expected, this can be dangerous!*

How Severe Was CVE-2024-42096?

This wasn't a security-critical crash or arbitrary code execution bug in modern usage, but rather a “chronic injury” to kernel reliability and difference between supposed and real profiling outputs.

Why is it classified as a CVE?
Any function that incorrectly reads or interprets kernel memory can, in theory, be used to infer kernel layout or—under unusual circumstances—lead to out-of-bounds reads. When coupled with debugging or special build configurations, these issues could be amplified, potentially turning into a denial-of-service or information-leak situation. The real-world impact was mostly with misattributed profiling, unpredictable kernel behavior, and years of false memory bugs (thanks to KASAN and syzkaller fuzzing tools).

Syzkaller’s Endless Reports and Pressure to Fix

Syzkaller, a well-known kernel fuzzer, repeatedly triggered KASAN alerts due to bad stack accesses in profile_pc(). Here's an example of a syzkaller bug report.

Similar reports went on for years, creating developer frustration and masking more important memory safety issues.

The Fix: Removing Outdated Code

The fix, merged in this commit, was refreshingly simple: just remove the “stack games” logic—that is, the sketchy code that probed the stack. Since timer-based profiling is largely a thing of the past, there is no more value in the gymnastics, and kernel safety became instantly better.

Patched version

unsigned long profile_pc(struct pt_regs *regs)
{
    return instruction_pointer(regs);
}

No more tricks—just report the current instruction pointer.

Exploit Details and Potential Abuse

This vulnerability is subtle and not considered a direct attack vector under default setups. However, if a kernel were built with custom debugging, or if profiling outputs were used to feed other tools, there was a non-zero risk of:

- OOB (Out of Bounds) Stack Reads: Under debugging builds, the stack would not look like the code expects. This could cause the kernel to “read” past intended boundaries.
- Profiling Misdirection: Malicious code (or noisy user code) could possibly skew profiling by triggering odd stack states, causing kernel maintainers to chase “phantom” hot spots.
- Potential Info Leaks: In theory, if crafted just right, an attacker could poke at kernel addresses to disclose minor details about kernel layout (though syzkaller did not show real info leak PoCs).

Researchers or kernel fuzzers could trigger KASAN like this

void trigger_profile_pc_kasan() {
    struct pt_regs fake_regs = { .sp = (unsigned long)malicious_stack_ptr };
    // Call profile_pc(fake_regs) in a context where spinlock is active and stack frame is non-conventional
}

This would result in KASAN “out of bounds” stack access alerts, showing the bug.

References and Further Reading

- Linux kernel commit removing faulty logic (a271cec5e68)
- syzkaller bug ID: 723dc4ed8f9b1c98e46e
- KASAN: Kernel Address Sanitizer docs
- History: cb91a229364, 31679f38d886, ef4512882dbe

Conclusion

CVE-2024-42096 is a great example of why it’s important to revisit and question “legacy” assumptions in kernel code—especially those dealing with memory and stack access. Thanks to persistent fuzzing by syzkaller and cooperation among kernel maintainers, this subtle flaw is now gone. Profiling code may not get much love these days, but removing historical landmines keeps the Linux kernel safer for everyone.

If you maintain an older or customized Linux kernel, make sure to backport this fix, or use a recent kernel version!


*Original and further references:*
- Upstream kernel fix
- CVE page on Mitre (pending)

Timeline

Published on: 07/29/2024 18:15:12 UTC
Last modified on: 05/04/2025 09:22:55 UTC