# A 15-Year-Old Out-of-Bounds Read in the Linux Kernel's NFC Stack

> An unauthenticated NFC peer could crash the kernel with one malformed frame. The bug shipped in 2011 and sat in every kernel since. Automated source analysis surfaced it. The fix is accepted upstream in the NFC maintainer tree and heading to mainline (ed85d4cbbfaa).

Published: 2026-07-09

Canonical article: https://0.security/blog/nfc-llcp-15-year-kernel-bug/

*An unauthenticated NFC peer could crash the kernel with one malformed frame. The bug shipped in 2011 and sat in every kernel since. Our automated source analysis surfaced it. The fix is now accepted upstream, heading to mainline.*

Some bugs are interesting because they are novel. This one is interesting for the opposite reason. It was old, quiet, and reachable over the air by anyone standing next to your phone. It lives in code that ships in effectively every Linux kernel. It survived roughly fifteen years of review, static analysis, and fuzzing before an autonomous engine reading source picked it out.

The NFC maintainer reviewed and accepted the fix. It now sits in the NFC subsystem tree (in linux-next) as commit [`ed85d4cbbfaa`](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=ed85d4cbbfaa4e630c5aa0d607348b42620d976b), "nfc: llcp: bound SNL TLV parsing to the skb and add length checks", on its way to Linus's mainline tree in an upcoming merge window. No CVE has been assigned. The accepted commit is the citable proof.

## The bug

NFC is the short-range radio in phones, transit cards, and payment terminals. It runs a link-layer protocol called LLCP (Logical Link Control Protocol). Part of LLCP is a service-discovery mechanism. Peers exchange "SNL" (Service Name Lookup) messages to ask each other "do you offer service X?" Those messages are encoded as a list of TLVs: type/length/value triples, walked one after another.

The kernel function that parses incoming SNL messages is `nfc_llcp_recv_snl()`, in `net/nfc/llcp_core.c`. For each TLV it read a type, a length, and then `length` bytes of value. Two things were missing.

1. **The walk was unbounded.** Nothing checked that the next TLV, its two-byte header, or its declared value length actually fit inside the received packet (`skb`). A crafted message could declare lengths that ran the parser off the end of the buffer.

2. **A length of zero underflowed.** For a service-discovery request (SDREQ), the code computed the service-name length as `service_name_len = length - 1`. `length` is unsigned. Send a TLV with `length == 0`, and `length - 1` wraps to `SIZE_MAX`, a service name several exabytes long as far as the parser is concerned.

Put together, a malicious NFC peer could send one malformed SNL frame and walk the parser straight past the end of the socket buffer. The result is an out-of-bounds read. At best a kernel oops (denial of service). At worst reading adjacent kernel memory into the parse path.

![NFC OOB Unbounded Read](/blog/nfc-oob-unbounded.png)

The important detail is the trust boundary. LLCP link activation happens automatically once two NFC devices associate. There is no pairing step, no authentication, no user tap-to-confirm. Any NFC-capable device in range can reach this code, whether an emulated tag or a hostile phone. This is a remote, unauthenticated, zero-interaction path into the kernel.

## Why it survived fifteen years

The vulnerable code is not obscure recent work. It dates to the *initial* NFC LLCP support: commit `d646960f7986` ("NFC: Initial LLCP support"), merged on 2011-12-14. The SNL path arrived shortly after in `19cfe5843e86` ("NFC: Initial SNL support") in 2012. Every kernel built since then carried it.

So why did it last? A few honest reasons.

- **It is a driver/subsystem periphery, not the hot core.** NFC LLCP is compiled into many kernels but exercised by very few people relative to, say, TCP. Attention follows attack surface that attackers are known to hammer. This quietly wasn't.
- **Fuzzers need the right harness.** Reaching this parser means driving the NFC-DEP peer state machine and feeding it crafted LLCP frames. That is not where most fuzzing effort has historically pointed. Code you cannot easily reach, you cannot easily fuzz.
- **The bug is a *shape*, not a crash.** An unsigned `length - 1` underflow and an unbounded TLV walk look like ordinary parsing code. There is no obviously wrong line. You have to reason about the range of `length` and about what bounds the loop. That is exactly the kind of latent, boring, high-consequence pattern that human reviewers skim past.

That last point is the one worth sitting with. A parser that trusts a length field it never validated against the actual buffer is one of the most durable bug classes in systems software. It hides in plain sight precisely because each individual line looks fine.

## How an autonomous engine surfaces this

0sec's automated security-research tooling ([0sec.ai](https://0.security)) found this doing source-level variant analysis: reading kernel source and reasoning about it directly, rather than waiting for a crash.

Reading source means you are not gated on reachability. A fuzzer has to *get to* `nfc_llcp_recv_snl()` and *provoke* a crash before it learns anything. A source-level analyzer can look at the parser in isolation and ask the structural questions. What is the range of this length field? What bounds this loop? Does every read stay inside the buffer the caller handed us? Those questions have answers whether or not anyone has ever built a harness that reaches the code.

That is how a dormant, fifteen-year-old bug in a rarely-exercised subsystem becomes findable. The engine does not need NFC to be a popular fuzz target. It needs the source, and the patience to check every length against every bound.

Then comes the part that keeps this honest: verification. A source-level hunch is a hypothesis, not a finding. We verified the candidate against the real kernel trees before it went anywhere near a maintainer, and compile-checked the fix against mainline.

## The fix, and the disclosure

The patch does the two things the original parser skipped. It bounds the TLV walk to the actual packet, using `skb_tail_pointer()` to mark the end of the buffer and refusing to read a header or a value that would cross it. It also validates each TLV's declared length before use (an SDREQ must carry at least one byte; an SDRES exactly two). The `length - 1` underflow disappears because a zero-length SDREQ is now rejected instead of trusted.

We reported it upstream through normal coordinated disclosure. The NFC maintainer, David Heidelberg, reviewed the patch and applied it ("Applied, thanks!"). It now sits in the NFC subsystem tree as `ed85d4cbbfaa`, queued for mainline, authored by our co-founder Doruk Tan Ozturk. No marketing name, no logo. A parser got a bounds check it should have had in 2011, and every kernel downstream is a little safer for it.

That is the whole loop we care about: an autonomous engine reads code no one was fuzzing, surfaces a latent bug by structure rather than by crash, a human verifies it, and it lands upstream where it protects real users. Old bugs in code everyone runs are exactly the ones worth finding.

---

*Found by 0sec's automated security-research tooling ([https://0.security](https://0.security)). Fix accepted upstream in the NFC maintainer tree as commit `ed85d4cbbfaa`, heading to mainline.*
