JohannesLks/CVE-2026-23398: The ICMP Packet Linux Forgot to Ignore

A tiny Scapy proof of concept, a forged inner header, and a kernel validation path that turns a routine network error into a crash.

8 min read • View on GitHub • More from JohannesLks

A small packet, drawn like a stamped envelope, collides with a towering mechanical Linux networking stack. One cabinet drawer in the protocol machinery is empty, and the packet is headed straight for that gap. The image explains how a tiny ICMP error can expose a missing kernel assumption.
A small error packet can still reach a missing assumption inside the kernel.
Key Takeaways

One packet, one missing check

The repo's whole trick fits in one sentence: a crafted ICMP Destination Unreachable, Fragmentation Needed message can drive Linux into a null pointer dereference if the inner packet is shaped to pass validation first. That is what makes this PoC interesting. The trigger is tiny, the code is readable, and the failure mode is dramatic.

This is not a noisy spray of malformed traffic. It is a standards-adjacent packet aimed at a trust boundary. The bug lives in the assumption that a protocol handler will always exist when the kernel asks for one.

‼️ CVE-2026-23398: Linux Kernel ICMP DoS Vulnerability PoC: https://t.co/qD3Vo8jHAF

Dark Web Informer, DarkWebInformer, 184,358 followers · @DarkWebInformer on X

The forged header that gets the packet inside

The clever part is not the outer ICMP shell. It is the inner IP header. The PoC forges that nested header so the kernel treats the error as if it belongs to traffic the target itself sent, which helps it survive the first validation gates.

UNREGISTERED_PROTO = 253  # RFC 3692 experimental protocol


def build_frag_needed(target):
    inner = IP(src=target, proto=UNREGISTERED_PROTO)
    return IP(dst=target) / ICMP(type=3, code=4) / inner
A close-up of nested packet layers opening like concentric shells. The outer ICMP error reveals an inner IP header built from transparent panels, and one address field has been mirrored to look legitimate. The image explains the spoofing trick that keeps the packet moving toward the dangerous branch.
The inner header is the pass that gets the packet to the dangerous branch.

Why protocol 253 is the empty slot

Protocol 253 is not a random number. It is an experimental protocol slot from RFC 3692, which means it can exist in the registry without having a normal handler behind it. That matters because the strict PMTU path eventually reaches into the protocol table and expects a live function pointer.

The sysctl setting net.ipv4.ip_no_pmtu_disc = 3 nudges Linux onto that stricter path. Without it, the packet does not reach the same validation logic. With it, the empty slot becomes a crash surface.

icmp_rcv()
  -> icmp_unreach()
  -> icmp_tag_validation()
  -> inet_protos[253]
  -> null dereference

The packet only becomes fatal after PMTU validation asks the kernel to resolve a protocol handler that does not exist.

The path from ICMP error to kernel panic

The flow is short, which is part of the point. icmp_rcv() accepts the packet, icmp_unreach() handles the destination-unreachable case, and icmp_tag_validation() inspects the embedded header. Under the right sysctl setting, the kernel takes the strict branch and tries to validate the inner protocol against inet_protos[253].

  1. icmp_rcv() receives the outer packet.
  2. icmp_unreach() routes the fragmentation-needed case.
  3. icmp_tag_validation() inspects the inner header.
  4. inet_protos[253] is empty, so the dereference falls through.
  5. The kernel crashes in softirq context.

The important thing is not the exact line number. It is the shape of the assumption. The kernel treats a reserved protocol slot like a safe lookup, then discovers that the lookup itself is the bug.

Why this PoC is unusually clean

The repository is almost aggressively small. One Python file, Scapy, a README, and enough comments to map the packet structure back to the kernel path. That minimalism makes it better than many bigger PoCs. You can inspect the whole idea without guessing where the clever part is hidden.

Built to explain, not to spread

JohannesLks presents the bug as a focused proof of concept, not a toolkit. That keeps the artifact legible, and it makes the README feel like a lab note rather than a product page.

A hedcut-style portrait derived from JohannesLks's GitHub avatar on a white background. It serves as a small origin cue rather than a personality piece, reinforcing that the repository is a compact research artifact.
DimensionTypical kernel DoS PoCJohannesLks/CVE-2026-23398
Trigger sizeOften a chain of packets, state, or timingOne crafted ICMP error
PrerequisitesComplex setup or unstable conditionsRoot privileges and a specific sysctl setting
Bug classMemory corruption or state machine abuseNull pointer dereference in validation
Code complexityExploit framework with many moving partsA short Scapy script
ObservabilityHard to trace by inspectionEasy to map back to the kernel path
Failure modeCrash, hang, or attempted code executionKernel panic or denial of service
What the reader learnsExploit mechanicsProtocol trust boundaries

What it says about kernel error handling

This is not a memory safety puzzle. It is an assumption puzzle. The bug shows what happens when a mature networking stack assumes that every protocol lookup will land on a real handler, even inside a validation path that was supposed to make the system safer.

That is why the PoC matters. It turns a narrow edge case into something you can reason about, test, and patch against. Small protocol corners are where large systems still get embarrassed.