Blog / / 5 min read

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 — surfaced by automated source analysis, now accepted upstream in the NFC maintainer tree and heading to mainline (ed85d4cbbfaa).

An unauthenticated NFC peer could crash the kernel with a single malformed frame. The bug shipped in 2011 and sat in every kernel since. Here is how our automated source analysis surfaced it — and the fix that is now accepted upstream, heading to mainline.

Some bugs are exciting 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 lived in code that ships in effectively every Linux kernel, and it survived roughly fifteen years of review, static analysis, and fuzzing before an autonomous engine reading source picked it out.

The fix has been reviewed and accepted by the NFC maintainer and now sits in the NFC subsystem tree (in linux-next) as commit ed85d4cbbfaa — “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 — the short-range radio in phones, transit cards, and payment terminals — 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 a single 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

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 — an emulated tag, a hostile phone, a rogue reader — can reach this code. 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 — with the SNL path arriving shortly after in 19cfe5843e86 (“NFC: Initial SNL support”) in 2012. Every kernel built since then carried it.

So why did it last? A few reasons that are worth being honest about:

  • 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, and 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, and 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 — exactly the kind of latent, boring, high-consequence pattern that human reviewers skim past.

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

How an autonomous engine surfaces this

This was found by 0sec’s automated security-research tooling (0sec.ai) doing source-level variant analysis — reading kernel source and reasoning about it directly, rather than waiting for a crash.

The advantage of reading source is that 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. The candidate was verified against the real kernel trees before it went anywhere near a maintainer, and the fix was compile-checked 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 — and it 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!” — and it now sits in the NFC subsystem tree as ed85d4cbbfaa, queued for mainline, authored by our co-founder Doruk Tan Ozturk. No drama, 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://0sec.ai). Fix accepted upstream in the NFC maintainer tree as commit ed85d4cbbfaa, heading to mainline.