
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Sun, 05 Apr 2026 03:51:17 GMT</lastBuildDate>
        <item>
            <title><![CDATA[How Cloudflare auto-mitigated world record 3.8 Tbps DDoS attack]]></title>
            <link>https://blog.cloudflare.com/how-cloudflare-auto-mitigated-world-record-3-8-tbps-ddos-attack/</link>
            <pubDate>Wed, 02 Oct 2024 13:00:00 GMT</pubDate>
            <description><![CDATA[ Over the past couple of weeks, Cloudflare's DDoS protection systems have automatically and successfully mitigated multiple hyper-volumetric L3/4 DDoS attacks exceeding 3 billion packets per second (Bpps). Our systems also automatically mitigated multiple attacks exceeding 3 terabits per second (Tbps), with the largest ones exceeding 3.65 Tbps. The scale of these attacks is unprecedented. ]]></description>
            <content:encoded><![CDATA[ <p>Since early September, <a href="https://developers.cloudflare.com/ddos-protection/about/components/#autonomous-edge"><u>Cloudflare's DDoS protection systems</u></a> have been combating a month-long campaign of hyper-volumetric L3/4 DDoS attacks. Cloudflare’s defenses mitigated over one hundred hyper-volumetric L3/4 DDoS attacks throughout the month, with many exceeding 2 billion packets per second (Bpps) and 3 terabits per second (Tbps). The largest attack peaked 3.8 Tbps — the largest ever disclosed publicly by any organization. Detection and mitigation was fully autonomous. The graphs below represent two separate attack events that targeted the same Cloudflare customer and were mitigated autonomously.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7hXUgmEd37kzZrMs0KPXLl/21a4779022bdff91f760dc66759eb955/BLOG-2586_2.png" />
          </figure><p><sup><i>A mitigated 3.8 Terabits per second DDoS attack that lasted 65 seconds</i></sup></p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1uxvtlkdFoH6KU6waXxK3o/d17fe7cf0e33c20e64a47b3ca0e6ca63/BLOG-2586_3.png" />
          </figure><p><sup><i>A mitigated 2.14 billion packet per second DDoS attack that lasted 60 seconds</i></sup></p>
    <div>
      <h2>Cloudflare customers are protected</h2>
      <a href="#cloudflare-customers-are-protected">
        
      </a>
    </div>
    <p>Cloudflare customers using Cloudflare’s HTTP reverse proxy services (e.g. <a href="https://developers.cloudflare.com/waf/"><u>Cloudflare WAF</u></a> and <a href="https://developers.cloudflare.com/cache/"><u>Cloudflare CDN</u></a>) are automatically protected.</p><p>Cloudflare customers using <a href="https://developers.cloudflare.com/spectrum/"><u>Spectrum</u></a> and <a href="https://developers.cloudflare.com/magic-transit/"><u>Magic Transit</u></a> are also automatically protected. Magic Transit customers can further optimize their protection by deploying <a href="https://developers.cloudflare.com/magic-firewall/"><u>Magic Firewall</u></a> rules to enforce a strict positive and negative security model at the packet layer.</p>
    <div>
      <h2>Other Internet properties may not be safe</h2>
      <a href="#other-internet-properties-may-not-be-safe">
        
      </a>
    </div>
    <p>The scale and frequency of these attacks are unprecedented. Due to their sheer size and bits/packets per second rates, these attacks have the ability to take down unprotected Internet properties, as well as Internet properties that are protected by on-premise equipment or by cloud providers that just don’t have sufficient network capacity or global coverage to be able to handle these volumes alongside legitimate traffic without impacting performance. </p><p>Cloudflare, however, does have the network capacity, global coverage, and intelligent systems needed to absorb and automatically mitigate these monstrous attacks. </p><p>In this blog post, we will review the attack campaign and why its attacks are so severe. We will describe the anatomy of a Layer 3/4 DDoS attack, its objectives, and how attacks are generated. We will then proceed to detail how Cloudflare’s systems were able to autonomously detect and mitigate these monstrous attacks without impacting performance for our customers. We will describe the key aspects of our defenses, starting with how our systems generate real-time (dynamic) signatures to match the attack traffic all the way to how we leverage kernel features to drop packets at wire-speed.</p>
    <div>
      <h2>Campaign analysis</h2>
      <a href="#campaign-analysis">
        
      </a>
    </div>
    <p>We have observed this attack campaign targeting multiple customers in the financial services, Internet, and telecommunication industries, among others. This attack campaign targets bandwidth saturation as well as resource exhaustion of in-line applications and devices.</p><p>The attacks predominantly leverage <a href="https://www.cloudflare.com/learning/ddos/glossary/user-datagram-protocol-udp/"><u>UDP</u></a> on a fixed port, and originated from across the globe with larger shares coming from Vietnam, Russia, Brazil, Spain, and the US. </p><p>The high packet rate attacks appear to originate from multiple types of compromised devices, including MikroTik devices, DVRs, and Web servers, orchestrated to work in tandem and flood the target with exceptionally large volumes of traffic. The high bitrate attacks appear to originate from a large number of compromised ASUS home routers, likely exploited using <a href="https://censys.com/june-20-improper-authentication-vulnerability-in-asus-routers/"><u>a CVE 9.8 (Critical) vulnerability that was recently discovered by Censys</u></a>.</p><div>
    <figure>
        <table>
            <colgroup>
                <col></col>
                <col></col>
            </colgroup>
            <tbody>
                <tr>
                    <td>
                        <p><span><span>Russia</span></span></p>
                    </td>
                    <td>
                        <p><span><span>12.1%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>Vietnam</span></span></p>
                    </td>
                    <td>
                        <p><span><span>11.6%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>United States</span></span></p>
                    </td>
                    <td>
                        <p><span><span>9.3%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>Spain</span></span></p>
                    </td>
                    <td>
                        <p><span><span>6.5%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>Brazil</span></span></p>
                    </td>
                    <td>
                        <p><span><span>4.7%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>France</span></span></p>
                    </td>
                    <td>
                        <p><span><span>4.7%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>Romania</span></span></p>
                    </td>
                    <td>
                        <p><span><span>4.4%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>Taiwan</span></span></p>
                    </td>
                    <td>
                        <p><span><span>3.4%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>United Kingdom</span></span></p>
                    </td>
                    <td>
                        <p><span><span>3.3%</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>Italy</span></span></p>
                    </td>
                    <td>
                        <p><span><span>2.8%</span></span></p>
                    </td>
                </tr>
            </tbody>
        </table>
    </figure>
</div><p><sup><i>Share of packets observed by source location</i></sup></p>
    <div>
      <h2>Anatomy of DDoS attacks</h2>
      <a href="#anatomy-of-ddos-attacks">
        
      </a>
    </div>
    <p>Before we discuss how Cloudflare automatically detected and mitigated the largest DDoS attacks ever seen, it‘s important to understand the basics of DDoS attacks. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7HLtAHnyjS33iiJAR1B9h9/2e1fc2cab871440fc95fa803989ae156/BLOG-2586_5.png" />
          </figure><p><sup><i>Simplified diagram of a DDoS attack</i></sup></p><p>The goal of a <a href="https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/"><u>Distributed Denial of Service (DDoS) attack</u></a> is to deny legitimate users access to a service. This is usually done by exhausting resources needed to provide the service. In the context of these recent Layer 3/4 DDoS attacks, that resource is CPU cycles and network bandwidth.</p>
    <div>
      <h3>Exhausting CPU cycles</h3>
      <a href="#exhausting-cpu-cycles">
        
      </a>
    </div>
    <p>Processing a packet consumes CPU cycles. In the case of regular (non-attack) traffic, a <i>legitimate</i> packet received by a service will cause that service to perform some action, and different actions require different amounts of CPU processing. However, before a packet is even delivered to a service there is per-packet work that needs to be done. Layer 3 packet headers need to be parsed and processed to deliver the packet to the correct machine and interface. Layer 4 packet headers need to be processed and routed to the correct socket (if any). There may be multiple additional processing steps that inspect every packet. Therefore, if an attacker sends at a high enough packet rate, then they can potentially saturate the available CPU resources, denying service to legitimate users.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3aBbNG4Fc7TY6MTugLl3BE/064fb17761aebbce737a5a4518a14235/BLOG-2586_6.png" />
          </figure><p>To defend against high packet rate attacks, you need to be able to inspect and discard the bad packets using as few CPU cycles as possible, leaving enough CPU to process the good packets. You can additionally acquire more, or faster, CPUs to perform the processing — but that can be a very lengthy process that bears high costs. </p>
    <div>
      <h3>Exhausting network bandwidth</h3>
      <a href="#exhausting-network-bandwidth">
        
      </a>
    </div>
    <p>Network bandwidth is the total amount of data per time that can be delivered to a server. You can think of bandwidth like a pipe to transport water. The amount of water we can deliver through a drinking straw is less than what we could deliver through a garden hose. If an attacker is able to push more garbage data into the pipe than it can deliver, then both the bad data <i>and</i> the good data will be discarded upstream, at the entrance to the pipe, and the DDoS is therefore successful.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/OehVItOhGfP9ApllRvDDM/fce73e6f375173a1a53bc22026e0b9b1/BLOG-2586_7.png" />
          </figure><p>
Defending against attacks that can saturate network bandwidth can be difficult because there is very little that can be done if you are on the downstream side of the saturated pipe. There are really only a few choices: you can get a bigger pipe, you can potentially find a way to move the good traffic to a new pipe that isn’t saturated, or you can hopefully ask the upstream side of the pipe to stop sending some or all of the data into the pipe.</p>
    <div>
      <h2>Generating DDoS attacks</h2>
      <a href="#generating-ddos-attacks">
        
      </a>
    </div>
    <p>If we think about what this means from the attackers point of view you realize there are similar constraints. Just as it takes CPU cycles to receive a packet, it also takes CPU cycles to create a packet. If, for example, the cost to send and receive a packet were equal, then the attacker would need an equal amount of CPU power to generate the attack as we would need to defend against it. In most cases this is not true — there is a cost asymmetry, as the attacker is able to generate packets using fewer CPU cycles than it takes to receive those packets. However, it is worth noting that generating attacks is not free and can require a large amount of CPU power.</p><p>Saturating network bandwidth can be even more difficult for an attacker. Here the attacker needs to be able to output more bandwidth than the target service has allocated. They actually need to be able to exceed the capacity of the receiving service. This is so difficult that the most common way to achieve a network bandwidth attack is to use a reflection/amplification attack method, for example a <a href="https://www.cloudflare.com/learning/ddos/dns-amplification-ddos-attack/"><u>DNS Amplification attack</u></a>. These attacks allow the attacker to send a small packet to an intermediate service, and the intermediate service will send a large packet to the victim.</p><p>In both scenarios, the attacker needs to acquire or gain access to many devices to generate the attack. These devices can be acquired in a number of different ways. They may be server class machines from cloud providers or hosting services, or they can be compromised devices like DVRs, routers, and webcams that have been infected with the attacker’s malware. These machines together form the botnet.</p>
    <div>
      <h2>How Cloudflare defends against large attacks</h2>
      <a href="#how-cloudflare-defends-against-large-attacks">
        
      </a>
    </div>
    <p>Now that we understand the fundamentals of how DDoS attacks work, we can explain how Cloudflare can defend against these attacks.</p>
    <div>
      <h3>Spreading the attack surface using global anycast</h3>
      <a href="#spreading-the-attack-surface-using-global-anycast">
        
      </a>
    </div>
    <p>The first not-so-secret ingredient is that Cloudflare’s network is built on <a href="https://www.cloudflare.com/learning/cdn/glossary/anycast-network/"><u>anycast</u></a>. In brief, anycast allows a single IP address to be advertised by multiple machines around the world. A packet sent to that IP address will be served by the closest machine. This means when an attacker uses their distributed botnet to launch an attack, the attack will be received in a distributed manner across the Cloudflare network. An infected DVR in Dallas, Texas will send packets to a Cloudflare server in Dallas. An infected webcam in London will send packets to a Cloudflare server in London.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/48tW6A6ACUT4Fd9h3l70F4/c6bcea8f037fc853477d0987c939e007/BLOG-2586_8.png" />
          </figure><p><sup><i>Anycast vs. Unicast networks</i></sup></p><p>Our anycast network additionally allows Cloudflare to allocate compute and bandwidth resources closest to the regions that need them the most. Densely populated regions will generate larger amounts of legitimate traffic, and the data centers placed in those regions will have more bandwidth and CPU resources to meet those needs. Sparsely populated regions will naturally generate less legitimate traffic, so Cloudflare data centers in those regions can be sized appropriately. Since attack traffic is mainly coming from compromised devices, those devices will tend to be distributed in a manner that matches normal traffic flows sending the attack traffic proportionally to datacenters that can handle it. And similarly, within the datacenter, traffic is distributed across multiple machines.</p><p>Additionally, for high bandwidth attacks, Cloudflare’s network has another advantage. A large proportion of traffic on the Cloudflare network does not consume bandwidth in a symmetrical manner. For example, an HTTP request to get a webpage from a site behind Cloudflare will be a relatively small incoming packet, but produce a larger amount of outgoing traffic back to the client. This means that the Cloudflare network tends to egress far more legitimate traffic than we receive. However, the network links and bandwidth allocated are symmetrical, meaning there is an abundance of ingress bandwidth available to receive volumetric attack traffic.</p>
    <div>
      <h3>Generating real-time signatures</h3>
      <a href="#generating-real-time-signatures">
        
      </a>
    </div>
    <p>By the time you’ve reached an individual server inside a datacenter, the bandwidth of the attack has been distributed enough that none of the upstream links are saturated. That doesn’t mean the attack has been fully stopped yet, since we haven’t dropped the bad packets. To do that, we need to sample traffic, qualify an attack, and create rules to block the bad packets. </p><p>Sampling traffic and dropping bad packets is the job of our <a href="https://blog.cloudflare.com/l4drop-xdp-ebpf-based-ddos-mitigations/"><u>l4drop</u></a> component, which uses <a href="https://netdevconf.info/2.1/papers/Gilberto_Bertin_XDP_in_practice.pdf"><u>XDP (eXpress Data Path)</u></a> and leverages an extended version of the Berkeley Packet Filter known as <a href="https://blog.cloudflare.com/tag/ebpf/"><u>eBPF (extended BPF)</u></a>. This enables us to execute custom code in kernel space and process (drop, forward, or modify) each packet directly at the network interface card (NIC) level. This component helps the system drop packets efficiently without consuming excessive CPU resources on the machine. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1NUxGTvKFs5l6G2UCSxvqw/3632f6728f9361123169d9694c807f0b/BLOG-2586_9.png" />
          </figure><p><sup><i>Cloudflare DDoS protection system overview</i></sup></p><p>We use XDP to sample packets to look for suspicious attributes that indicate an attack. The samples include fields such as the source IP, source port, destination IP, destination port, protocol, TCP flags, sequence number, options, packet rate and more. This analysis is conducted by the <i>denial of service daemon (dosd).</i> <i>Dosd</i> holds our secret sauce. It has many <i>filters</i> which instruct it, based on our curated heuristics, when to initiate mitigation. To our customers, these filters are logically grouped by attack vectors and exposed as the <a href="https://developers.cloudflare.com/ddos-protection/managed-rulesets/"><u>DDoS Managed Rules</u></a>. Our customers can <a href="https://developers.cloudflare.com/ddos-protection/managed-rulesets/adjust-rules/"><u>customize their behavior</u></a> to some extent, as needed.</p><p>As it receives samples from XDP, dosd will generate multiple permutations of fingerprints for suspicious traffic patterns. Then, using a data streaming algorithm, dosd will identify the most optimal fingerprints to mitigate the attack. Once an attack is qualified, dosd will push a mitigation rule inline as an eBPF program to surgically drop the attack traffic. </p><p>The detection and mitigation of attacks by dosd is done at the server level, at the data center level and at the global level — and it’s all software defined. This makes our network extremely resilient and leads to almost instant mitigation. There are no out-of-path “scrubbing centers” or “scrubbing devices”. Instead, each server runs the full stack of the Cloudflare product suite including the DDoS detection and mitigation component. And it is all done autonomously. Each server also <i>gossips (multicasts)</i> mitigation instructions within a data center between servers, and globally between data centers. This ensures that whether an attack is localized or globally distributed, dosd will have already installed mitigation rules inline to ensure a robust mitigation.</p>
    <div>
      <h2>Strong defenses against strong attacks</h2>
      <a href="#strong-defenses-against-strong-attacks">
        
      </a>
    </div>
    <p>Our software-defined, autonomous DDoS detection and mitigation systems run across our entire network. In this post we focused mainly on our dynamic fingerprinting capabilities, but our arsenal of defense systems is much larger. The <a href="https://developers.cloudflare.com/ddos-protection/tcp-protection/"><u>Advanced TCP Protection</u></a> system and <a href="https://developers.cloudflare.com/ddos-protection/dns-protection/"><u>Advanced DNS Protection</u></a> system work alongside our dynamic fingerprinting to identify sophisticated and highly randomized TCP-based DDoS attacks and also leverages statistical analysis to thwart complex DNS-based DDoS attacks. Our defenses also incorporate real-time threat intelligence, traffic profiling, and machine learning classification as part of our <a href="https://developers.cloudflare.com/ddos-protection/managed-rulesets/adaptive-protection/"><u>Adaptive DDoS Protection</u></a> to mitigate traffic anomalies. </p><p>Together, these systems, alongside the full breadth of the Cloudflare Security portfolio, are built atop of the Cloudflare network — one of the largest networks in the world — to ensure our customers are protected from the largest attacks in the world.</p> ]]></content:encoded>
            <category><![CDATA[DDoS]]></category>
            <category><![CDATA[Attacks]]></category>
            <category><![CDATA[Trends]]></category>
            <category><![CDATA[Security]]></category>
            <guid isPermaLink="false">4j6QWqeVFn6qzs0Md0LK7h</guid>
            <dc:creator>Manish Arora</dc:creator>
            <dc:creator>Shawn Bohrer</dc:creator>
            <dc:creator>Omer Yoachimik</dc:creator>
            <dc:creator>Cody Doucette</dc:creator>
            <dc:creator>Alex Forster</dc:creator>
            <dc:creator>Nick Wood</dc:creator>
        </item>
        <item>
            <title><![CDATA[A debugging story: corrupt packets in AF_XDP; a kernel bug or user error?]]></title>
            <link>https://blog.cloudflare.com/a-debugging-story-corrupt-packets-in-af_xdp-kernel-bug-or-user-error/</link>
            <pubDate>Mon, 16 Jan 2023 13:46:45 GMT</pubDate>
            <description><![CDATA[ A race condition in the virtual ethernet driver of the Linux kernel led to occasional packet content corruptions, which resulted in unwanted packet drops by one of our DDoS mitigation systems. This blogpost describes the thought process and technique we used to debug this complex issue. ]]></description>
            <content:encoded><![CDATA[ <p></p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6y8e5gSMEYuaTCmUVwscON/e7b52d75c044b155fbc6682379a6350b/image2-33.png" />
            
            </figure>
    <div>
      <h3>panic: Invalid TCP packet: Truncated</h3>
      <a href="#panic-invalid-tcp-packet-truncated">
        
      </a>
    </div>
    <p>A few months ago we started getting a handful of crash reports for flowtrackd, our <a href="https://developers.cloudflare.com/ddos-protection/tcp-protection/">Advanced TCP Protection</a> system that runs on our global network. The provided stack traces indicated that the panics occurred while parsing a TCP packet that was truncated.</p><p>What was most interesting wasn’t that we failed to parse the packet. It isn’t rare that we receive malformed packets from the Internet that are (deliberately or not) truncated. Those packets will be caught the first time we parse them and won’t make it to the latter processing stages. However, in our case, the panic occurred the second time we parsed the packet, indicating it had been truncated <b>after</b> we received it and successfully parsed it the first time. Both parse calls were made from a single green thread and referenced the same packet buffer in memory, and we made no attempts to mutate the packet in between.</p><p>It can be easy to dread discovering a bug like this. Is there a race condition? Is there memory corruption? Is this a kernel bug? A compiler bug? Our plan to get to the root cause of this potentially complex issue was to identify symptom(s) related to the bug, create theories on what may be occurring and create a way to test our theories or gather more information.</p><p>Before we get into the details we first need some background information about <code>AF_XDP</code> and our setup.</p>
    <div>
      <h3>AF_XDP overview</h3>
      <a href="#af_xdp-overview">
        
      </a>
    </div>
    <p><a href="https://www.kernel.org/doc/html/latest/networking/af_xdp.html">AF_XDP</a> is the high performance asynchronous user-space networking API in the Linux kernel. For network devices that support it, AF_XDP provides a way to perform extremely fast, zero-copy packet forwarding using a memory buffer that’s shared between the kernel and a user-space application.</p><p>A number of components need to be set up by the user-space application to start interacting with the packets entering a network device using AF_XDP.</p><p>First, a shared packet buffer (UMEM) is created. This UMEM is divided into equal-sized “frames” that are referenced by a “descriptor address,” which is just the offset from the start of the UMEM.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7sZldzVDmiFTIbWi5VROpW/799e655b50ce2288d748086a1c85bab4/image12-2.png" />
            
            </figure><p>Next, multiple AF_XDP sockets (XSKs) are created – one for each hardware queue on the network device – and bound to the UMEM. Each of these sockets provides four ring buffers (or “queues”) which are used to send descriptors back and forth between the kernel and user-space.</p><p>User-space sends packets by taking an unused descriptor and copying the packet into that descriptor (or rather, into the UMEM frame that the descriptor points to). It gives the descriptor to the kernel by enqueueing it on the <b>TX queue</b>. Some time later, the kernel dequeues the descriptor from the <b>TX queue</b> and transmits the packet that it points to out of the network device. Finally, the kernel gives the descriptor back to user-space by enqueueing it on the <b>COMPLETION queue</b>, so that user-space can reuse it later to send another packet.</p><p>To receive packets, user-space provides the kernel with unused descriptors by enqueueing them on the <b>FILL queue</b>. The kernel copies packets it receives into these unused descriptors, and then gives them to user-space by enqueueing them on the <b>RX queue</b>. Once user-space processes the packets it dequeues from the <b>RX queue</b>, it either transmits them back out of the network device by enqueueing them on the <b>TX queue</b>, or it gives them back to the kernel for later reuse by enqueueing them on the <b>FILL queue</b>.</p><table>
<thead>
  <tr>
    <th>Queue</th>
    <th>User space</th>
    <th>Kernel space</th>
    <th>Content description</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>COMPLETION</td>
    <td>Consumes</td>
    <td>Produces</td>
    <td>Descriptors containing a packet that was successfully transmitted by the kernel</td>
  </tr>
  <tr>
    <td>FILL</td>
    <td>Produces</td>
    <td>Consumes</td>
    <td>Descriptors that are empty and ready to be used by the kernel to receive packets</td>
  </tr>
  <tr>
    <td>RX</td>
    <td>Consumes</td>
    <td>Produces</td>
    <td>Descriptors containing a packet that was recently received by the kernel</td>
  </tr>
  <tr>
    <td>TX</td>
    <td>Produces</td>
    <td>Consumes</td>
    <td>Descriptors containing a packet that is ready to be transmitted by the kernel</td>
  </tr>
</tbody>
</table><p>Finally, a BPF program is attached to the network device. Its job is to direct incoming packets to whichever XSK is associated with the specific hardware queue that the packet was received on.</p><p>Here is an overview of the interactions between the kernel and user-space:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5g3TzP8qFYPWMxEXeXxhz3/798bc1db0ce5bdf894804039eee1b77d/Untitled-2.png" />
            
            </figure>
    <div>
      <h3>Our setup</h3>
      <a href="#our-setup">
        
      </a>
    </div>
    <p>Our application uses AF_XDP on a pair of multi-queue veth interfaces (“outer” and “inner”) that are each in different network namespaces. We follow the process outlined above to bind an XSK to each of the interfaces’ queues, forward packets from one interface to the other, send packets back out of the interface they were received on, or drop them. This functionality enables us to implement bidirectional traffic inspection to perform DDoS mitigation logic.</p><p>This setup is depicted in the following diagram:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/nAwNZF6NfDJK6sz1yymW2/45823d63ae47c22878796864d847187a/Untitled--1--1.png" />
            
            </figure>
    <div>
      <h3>Information gathering</h3>
      <a href="#information-gathering">
        
      </a>
    </div>
    <p>All we knew to start with was that our program was occasionally seeing corruption that seemed to be impossible. We didn’t know what these corrupt packets actually looked like. It was possible that their contents would reveal more details about the bug and how to reproduce it, so our first step was to log the packet bytes and discard the packet instead of panicking. We could then take the logs with packet bytes in them and create a PCAP file to analyze with <a href="https://www.wireshark.org/">Wireshark</a>. This showed us that the packets looked mostly normal, except for Wireshark’s TCP analyzer complaining that their “IPv4 total length exceeds packet length”. In other words, the “total length” IPv4 header field said the packet should be (for example) 60 bytes long, but the packet itself was only 56 bytes long.</p>
    <div>
      <h3>Lengths mismatch</h3>
      <a href="#lengths-mismatch">
        
      </a>
    </div>
    <p>Could it be possible that the number of bytes we read from the RX ring was incorrect? Let’s check.</p><p>An XDP descriptor has the following <a href="https://elixir.bootlin.com/linux/v5.15.77/source/include/uapi/linux/if_xdp.h#L103">C struct</a>:</p>
            <pre><code>struct xdp_desc {
	__u64 addr;
	__u32 len;
	__u32 options;
};</code></pre>
            <p>Here the len member tells us the total size of the packet pointed to by addr in the UMEM frame.</p><p>Our first interaction with the packet content happens in the BPF code attached to the network interfaces.</p><p>There our entrypoint function gets a pointer to a xdp_md C struct with <a href="https://elixir.bootlin.com/linux/v5.15.77/source/include/uapi/linux/bpf.h#L5442">the following definition</a>:</p>
            <pre><code>struct xdp_md {
	__u32 data;
	__u32 data_end;
	__u32 data_meta;
	/* Below access go through struct xdp_rxq_info */
	__u32 ingress_ifindex; /* rxq-&gt;dev-&gt;ifindex */
	__u32 rx_queue_index;  /* rxq-&gt;queue_index  */

	__u32 egress_ifindex;  /* txq-&gt;dev-&gt;ifindex */
};</code></pre>
            <p>This context structure contains two pointers (<code>as __u32</code>) referring to start and the end of the packet. Getting the packet length can be done by subtracting data from data_end.</p><p>If we compare that value with the one we get from the descriptors, we would surely find they are the same right?</p><p>We can use the BPF helper function <a href="https://man7.org/linux/man-pages/man7/bpf-helpers.7.html"><code>bpf_xdp_adjust_meta()</code></a> (since the veth driver supports it) to declare a metadata space that will hold the packet buffer length that we computed. We use it the same way <a href="https://elixir.bootlin.com/linux/v5.15.77/source/samples/bpf/xdp2skb_meta_kern.c#L41">this kernel sample code</a> does.</p><p>After deploying the new code in production, we saw the following lines in our logs:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2NG7NbMAC6kpbMT1mBZtwT/cbb254e24627be61ecba34d2783d069b/pasted-image-0-2.png" />
            
            </figure><p>Here you can see three interesting things:</p><ol><li><p>As we theorized, the length of the packet when first seen in XDP doesn’t match the length present in the descriptor.</p></li><li><p>We had already observed from our truncated packet panics that sometimes the descriptor length is shorter than the actual packet length, however the prints show that sometimes the descriptor length might be larger than the real packet bytes.</p></li><li><p>These often appeared to happen in “pairs” where the XDP length and descriptor length would swap between packets.</p></li></ol>
    <div>
      <h3>Two packets and one buffer?</h3>
      <a href="#two-packets-and-one-buffer">
        
      </a>
    </div>
    <p>Seeing the XDP and descriptor lengths swap in “pairs” was perhaps the first lightbulb moment. Are these two different packets being written to the same buffer? This also revealed a key piece of information that we failed to add to our debug prints, the descriptor address! We took this opportunity to print additional information like the packet bytes, and to print at multiple locations in the path to see if anything changed over time.</p><p>The real key piece of information that these debug prints revealed was that not only were each swapped “pair” sharing a descriptor address, but nearly every corrupt packet on a single server was always using the same descriptor address. Here you can see 49750 corrupt packets that all used descriptor address 69837056:</p>
            <pre><code>$ cat flowtrackd.service-2022-11-03.log | grep 87m237 | grep -o -E 'desc_addr: [[:digit:]]+' | sort | uniq -c
  49750 desc_addr: 69837056</code></pre>
            <p>This was the second lightbulb moment. Not only are we trying to copy two packets to the same buffer, but it is always the same buffer. Perhaps the problem is that this descriptor has been inserted into the AF_XDP rings twice? We tested this theory by updating our consumer code to test if a batch of descriptors read from the RX ring ever contained the same descriptor twice. This wouldn’t guarantee that the descriptor isn’t in the ring twice since there is no guarantee that the two descriptors will be in the same read batch, but we were lucky enough that it did catch the same descriptor twice in a single read proving this was our issue. In hindsight the <a href="https://www.kernel.org/doc/html/latest/networking/af_xdp.html">linux kernel AF_XDP documentation</a> points out this very issue:</p><blockquote><p><i>Q: My packets are sometimes corrupted. What is wrong?</i></p></blockquote><blockquote><p><i>A: Care has to be taken not to feed the same buffer in the UMEM into more than one ring at the same time. If you for example feed the same buffer into the FILL ring and the TX ring at the same time, the NIC might receive data into the buffer at the same time it is sending it. This will cause some packets to become corrupted. Same thing goes for feeding the same buffer into the FILL rings belonging to different queue ids or netdevs bound with the XDP_SHARED_UMEM flag.</i></p></blockquote><p>We now understand <i>why</i> we have corrupt packets, but we still don’t understand how a descriptor ever ends up in the AF_XDP rings twice. I would love to blame this on a kernel bug, but as the documentation points out this is more likely that we’ve placed the descriptor in the ring twice in our application. Additionally, since this is listed as a FAQ for AF_XDP we will need sufficient evidence proving that this is caused by a kernel bug and not user error before reporting to the kernel mailing list(s).</p>
    <div>
      <h3>Tracking descriptor transitions</h3>
      <a href="#tracking-descriptor-transitions">
        
      </a>
    </div>
    <p>Auditing our application code did not show any obvious location where we might be inserting the same descriptor address into either the FILL or TX ring twice. We do however know that descriptors transition through a set of known states, and we could track those transitions with a state machine. The below diagram shows all the possible valid transitions:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7c0EnogcWg13acmzNWQmiJ/6fc9f78824fc566a68da6893b24b7f0a/af_xdp_descriptor_transitions.png" />
            
            </figure><p>For example, a descriptor going from the RX ring to either the FILL or the TX ring is a perfectly valid transition. On the other hand, a descriptor going from the FILL ring to the COMP ring is an invalid transition.</p><p>To test the validity of the descriptor transitions, we added code to track their membership across the rings. This produced some of the following log messages:</p>
            <pre><code>Nov 16 23:49:01 fuzzer4 flowtrackd[45807]: thread 'flowtrackd-ZrBh' panicked at 'descriptor 26476800 transitioned from Fill to Tx'
Nov 17 02:09:01 fuzzer4 flowtrackd[45926]: thread 'flowtrackd-Ay0i' panicked at 'descriptor 18422016 transitioned from Comp to Rx'
Nov 29 10:52:08 fuzzer4 flowtrackd[83849]: thread 'flowtrackd-5UYF' panicked at 'descriptor 3154176 transitioned from Tx to Rx'</code></pre>
            <p>The first print shows a descriptor was put on the FILL ring and transitioned directly to the TX ring without being read from the RX ring first. This appears to hint at a bug in our application, perhaps indicating that our application duplicates the descriptor putting one copy in the FILL ring and the other copy in the TX ring.</p><p>The second invalid transition happened for a descriptor moving from the COMP ring to the RX ring without being put first on the FILL ring. This appears to hint at a kernel bug, perhaps indicating that the kernel duplicated a descriptor and put it both in the COMP ring and the RX ring.</p><p>The third invalid transition was from the TX to the RX ring without going through the FILL or COMP ring first. This seems like an extended case of the previous COMP to RX transition and again hints at a possible kernel bug.</p><p>Confused by the results we double-checked our tracking code and attempted to find any possible way our application could duplicate a descriptor putting it both in the FILL and TX rings. With no bugs found we felt we needed to gather more information.</p>
    <div>
      <h3>Using ftrace as a “flight recorder”</h3>
      <a href="#using-ftrace-as-a-flight-recorder">
        
      </a>
    </div>
    <p>While using a state machine to catch invalid descriptor transitions was able to catch these cases, it still lacked a number of important details which might help track down the ultimate cause of the bug. We still didn’t know if the bug was a kernel issue or an application issue. Confusingly the transition states seemed to indicate it was both.</p><p>To gather some more information we ideally wanted to be able to track the history of a descriptor. Since we were using a shared UMEM a descriptor could in theory transition between interfaces, and receive queues. Additionally, our application uses a single green thread to handle each XSK, so it might be interesting to track those descriptor transitions by XSK, CPU, and thread. A simple but unscalable way to achieve this would be to simply print this information at every transition point. This of course is not really an option for a production environment that needs to be able to process millions of packets per second. Both the amount of data produced and the overhead of printing that information will not work.</p><p>Up to this point we had been carefully debugging this issue in production systems. The issue was rare enough that even with our large production deployment it might take a day for some production machines to start to display the issue. If we did want to explore more resource intensive debugging techniques we needed to see if we could reproduce this in a test environment. For this we created 10 virtual machines that were continuously load testing our application with <a href="https://iperf.fr/">iperf</a>. Fortunately with this setup we were able to reproduce the issue about once a day, giving us some more freedom to try some more resource intensive debugging techniques.</p><p>Even using a virtual machine it still doesn’t scale to print logs at every descriptor transition, but do you really need to see every transition? In theory the most interesting events are the events right before the bug occurs. We could build something that internally keeps a log of the last N events and only dump that log when the bug occurs. Something like a black box flight recorder used in airplanes to track the events leading up to a crash. Fortunately for us, we don’t really need to build this, and instead can use the Linux kernel’s <a href="https://www.kernel.org/doc/html/latest/trace/ftrace.html">ftrace</a> feature, which has some additional features that might help us ultimately track down the cause of this bug.</p><p>ftrace is a kernel feature that operates by internally keeping a set of per-CPU ring buffers of trace events. Each event stored in the ring buffer is time-stamped and contains some additional information about the context where the event occurred, the CPU, and what process or thread was running at the time of the event. Since these events are stored in per-CPU ring buffers, once the ring is full, new events will overwrite the oldest events leaving a log of the most recent events on that CPU. Effectively we have our flight recorder that we desired, all we need to do is add our events to the ftrace ring buffers and disable tracing when the bug occurs.</p><p>ftrace is controlled using virtual files in the debugfs filesystem. Tracing can be enabled and disabled by writing either a 1 or a 0 to:</p><p><code>/sys/kernel/debug/tracing/tracing_on</code></p><p>We can update our application to insert our own events into the tracing ring buffer by writing our messages into the trace_marker file:</p><p><code>/sys/kernel/debug/tracing/trace_marker</code></p><p>And finally after we’ve reproduced the bug and our application has disabled tracing we can extract the contents of all the ring buffers into a single trace file by reading the trace file:</p><p><code>/sys/kernel/debug/tracing/trace</code></p><p>It is worth noting that writing messages to the trace_marker virtual file still involves making a system call and copying your message into the ring buffers. This can still add overhead and in our case where we are logging several prints per packet that overhead might be significant. Additionally, ftrace is a systemwide kernel tracing feature, so you may need to either adjust the permissions of virtual files, or run your application with the appropriate permissions.</p><p>There is of course one more big advantage of using ftrace to assist in debugging this issue. As shown above we can log our own application messages to ftrace using the trace_marker file, but at its core ftrace is a kernel tracing feature. This means that we can additionally use ftrace to log events from the kernel side of the AF_XDP packet processing. There are several ways to do this, but for our purposes we used kprobes so that we could target very specific lines of code and print some variables. kprobes can be created directly in ftrace, but I find it easier to create them using the “perf probe” command of perf tool in Linux. Using the “-L” and “-V” arguments you can find which lines of a function can be probed and which variables can be viewed at those probe points. Finally, you can add the probe with the “-a” argument. For example after examining the kernel code we insert the following probe in the receive path of a XSK:</p><p><code>perf probe -a '__xsk_rcv_zc:7 addr len xs xs-&gt;pool-&gt;fq xs-&gt;dev'</code></p><p>This will probe line 7 of __xsk_rcv_zc() and print the descriptor address, the packet length, the XSK address, the fill queue address and the net device address. For context here is what <a href="https://elixir.bootlin.com/linux/v5.15.77/source/net/xdp/xsk.c#L152"><code>__xsk_rcv_zc()</code></a> looks like from the perf probe command:</p>
            <pre><code>$ perf probe -L __xsk_rcv_zc
      0  static int __xsk_rcv_zc(struct xdp_sock *xs, struct xdp_buff *xdp, u32 len)
         {
                struct xdp_buff_xsk *xskb = container_of(xdp, struct xdp_buff_xsk, xdp);
                u64 addr;
                int err;
         
                addr = xp_get_handle(xskb);
      7         err = xskq_prod_reserve_desc(xs-&gt;rx, addr, len);
      8         if (err) {
                        xs-&gt;rx_queue_full++;
                        return err;
                }</code></pre>
            <p>In our case line 7 is the call to xskq_prod_reserve_desc(). At this point in the code the kernel has already removed a descriptor from the FILL queue and copied a packet into that descriptor. The call to xsk_prod_reserve_desc() will ensure that there is space in the RX queue, and if there is space will add that descriptor to the RX queue. It is important to note that while xskq_prod_reserve_desc() will put the descriptor in the RX queue it does not update the producer pointer of the RX ring or notify the XSK that packets are ready to be read because the kernel tries to batch these operations.</p><p>Similarly, we wanted to place a probe in the transmit path on the kernel side and ultimately placed the following probe:</p><p><code>perf probe -a 'xp_raw_get_data:0 addr'</code></p><p>There isn’t much interesting to show here in <a href="https://elixir.bootlin.com/linux/v5.15.77/source/net/xdp/xsk_buff_pool.c#L542">the code</a>, but this probe is placed at a location where descriptors have been removed from the TX queue but have not yet been put in the COMPLETION queue.</p><p>In both of these probes it would have been nice to put the probes at the earliest location where descriptors were added or removed from the XSK queues, and to print as much information as possible at these locations. However, in practice the locations where kprobes can be placed and the variables available at those locations limits what can be seen.</p><p>With the probes created we still need to enable them to be seen in ftrace. This can be done with:</p><p><code>echo 1 &gt; /sys/kernel/debug/tracing/events/probe/__xsk_rcv_zc_L7/enableecho 1 &gt; /sys/kernel/debug/tracing/events/probe/xp_raw_get_data/enable</code></p><p>With our application updated to trace the transition of every descriptor and stop tracing when an invalid transition occurred we were ready to test again.</p>
    <div>
      <h3>Tracking descriptor state is not enough</h3>
      <a href="#tracking-descriptor-state-is-not-enough">
        
      </a>
    </div>
    <p>Unfortunately our initial test of our “flight recorder” didn’t immediately tell us anything new. Instead, it mostly confirmed what we already knew, which was that somehow we would end up in a state with the same descriptor twice. It also highlighted the fact that catching an invalid descriptor transition doesn’t mean you have caught the earliest point where the duplicate descriptor appeared. For example assume we have our descriptor A and our duplicate A’. If these are already both present in the FILL queue it is perfectly valid to:</p><p><code>RX A -&gt; FILL ARX A’ -&gt; FILL A’</code></p><p>This can occur for many cycles, before an invalid transition eventually occurs when both descriptors are seen either in the same batch or between queues.</p><p>Instead, we needed to rethink our approach. We knew that the kernel removes descriptors from the FILL queue, fills them, and places them in the RX queue. This means that for any given XSK the order that descriptors are inserted into the FILL queue should match the order that they come out of the RX queue. If a descriptor was ever duplicated in this kernel RX path we should see the duplicate descriptor appear out-of-order. With this in mind we updated our application to independently track the order of the FILL queue using a double ended queue. As our application puts descriptors into the FILL queue we also push the descriptor address into the tail of our tracking queue and when we receive packets we pop the descriptor address from the head of our tracking queue and ensure the address matches. If it ever doesn’t match we again can log to trace_marker and stop ftrace.</p><p>Below is the end of the first trace we captured with the updated code tracking the order of the FILL to RX queues. The color has been added to improve readability:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/uSATZc25o3ESPjeXWQVUX/9967b0a78fd476266ca5daf42b5cc177/Screenshot-2023-01-16-at-14.16.47.png" />
            
            </figure><p>Here you can see the power of our ftrace flight recorder. For example, we can follow the full cycle of descriptor <span>0x16ce900</span> as it is first received in the kernel, received by our application which forwards the packet by adding to the TX queue, the kernel transmitting, and finally our application receiving the completion and placing the descriptor back in the FILL queue.</p>
<p>
The trace starts to get interesting on the next two packets received by the kernel. We can see <span>0x160a100</span> received first in the kernel and then by our application. However things go wrong when the kernel receives <span>0x13d3900</span> but our application receives <span>0x1229100</span>. The last print of the trace shows the result of our descriptor order tracking. We can see that the kernel side appears to match our next expected descriptor and the next two descriptors, yet unexpectedly we see <span>0x1229100</span> arrive out of nowhere. We do think that the descriptor is present in the FILL queue, but it is much further down the line in the queue. Another potentially interesting detail is that between <span>0x160a100</span> and <span>0x13d3900</span> the kernel’s softirq switches from CPU 1 to CPU 2.</p>
<p>If you recall, our __xsk_rcv_zc_L7 kprobe was placed on the call to xskq_prod_reserve_desc() which adds the descriptor to the RX queue. Below we can examine that function to see if there are any clues on how the descriptor address received by our application could be different from what we think should have been inserted by the kernel.</p>
            <pre><code>static inline int xskq_prod_reserve_desc(struct xsk_queue *q,
                                     	u64 addr, u32 len)
{
    	struct xdp_rxtx_ring *ring = (struct xdp_rxtx_ring *)q-&gt;ring;
    	u32 idx;
 
    	if (xskq_prod_is_full(q))
            	return -ENOBUFS;
 
    	/* A, matches D */
    	idx = q-&gt;cached_prod++ &amp; q-&gt;ring_mask;
    	ring-&gt;desc[idx].addr = addr;
    	ring-&gt;desc[idx].len = len;
 
    	return 0;
}</code></pre>
            <p>Here you can see that the queue’s cached_prod pointer is incremented first before we update the descriptor address and length. As the name implies the cached_prod pointer isn’t the actual producer pointer which means that at some point <code>xsk_flush()</code> must be called to sync the cached_prod pointer and the prod pointer to actually expose the newly received descriptors to user-mode. Perhaps there is a race where <code>xsk_flush()</code> is called after updating the <code>cached_prod</code> pointer, but before the actual descriptor address has been updated in the ring? If this were to occur our application would see the old descriptor address from that slot in the RX queue and would cause us to “duplicate” that descriptor.</p><p>We can test our theory by making two more changes. First we can update our application to write back a known “poisoned” descriptor address to each RX queue slot after we have received a packet. In this case we chose <code>0xdeadbeefdeadbeef</code> as our known invalid address and if we ever receive this value back out of the RX queue we know a race has occurred and exposed an uninitialized descriptor. The second change we can make is to add a kprobe on <code>xsk_flush()</code> to see if we can actually capture the race in the trace.</p><p><code>perf probe -a 'xsk_flush:0 xs'</code></p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1Xx2qaoywvGvlb0VsenLAw/c0ec0249ac0f1ca77829fd6a39228624/Screenshot-2023-01-16-at-14.16.05.png" />
            
            </figure><p>Here we appear to have our smoking gun. As we predicted we can see that <span><span>xsk_flush()</span></span> is called on CPU 0 while a softirq is currently in progress on CPU 2. After the flush our application sees the expected <span><span><span>0xff0900</span></span></span> filled in from the softirq on CPU 0, and then <span><span>0xdeadbeefdeadbeef</span></span> which is our poisoned uninitialized descriptor address.</p>
<p>We now have evidence that the following order of operations is happening:</p>
            <pre><code>CPU 2                                                   CPU 0
-----------------------------------                     --------------------------------
__xsk_rcv_zc(struct xdp_sock *xs):                      xsk_flush(struct xdp_sock *xs):
                                        
idx = xs-&gt;rx-&gt;cached_prod++ &amp; xs-&gt;rx-&gt;ring_mask; 
                                                        // Flush the cached pointer as the new head pointer of
                                                        // the RX ring.
                                                        smp_store_release(&amp;xs-&gt;rx-&gt;ring-&gt;producer, xs-&gt;rx-&gt;cached_prod);

                                                        // Notify user-side that new descriptors have been produced to
                                                        // the RX ring.
                                                        sock_def_readable(&amp;xs-&gt;sk);

                                                        // flowtrackd reads a descriptor "too soon" where the addr
                                                        // and/or len fields have not yet been updated.
xs-&gt;rx-&gt;ring-&gt;desc[idx].addr = addr;
xs-&gt;rx-&gt;ring-&gt;desc[idx].len = len;</code></pre>
            <p>The AF_XDP documentation states that: <i>“All rings are single-producer/single-consumer, so the user-space application needs explicit synchronization of multiple processes/threads are reading/writing to them.”</i> The explicit synchronization requirement must also apply on the kernel side. How can two operations on the RX ring of a socket run at the same time?</p><p>On Linux, a mechanism called <a href="https://wiki.linuxfoundation.org/networking/napi">NAPI</a> prevents CPU interrupts from occurring every time a packet is received by the network interface. It instructs the network driver to process a certain amount of packets at a frequent interval. For the veth driver that polling function is called <a href="https://elixir.bootlin.com/linux/v5.15.77/source/drivers/net/veth.c#L906">veth_poll</a>, and it is <a href="https://elixir.bootlin.com/linux/v5.15.77/source/drivers/net/veth.c#L1015">registered</a> as the function handler for each queue of the XDP enabled network device. A NAPI-compliant network driver provides the guarantee that the processing of the packets tied to a NAPI context (<code>struct napi_struct *napi</code>) will not be happening at the same time on multiple processors. In our case, a NAPI context exists for each queue of the device which means per AF_XDP socket and their associated set of ring buffers (RX, TX, FILL, COMPLETION).</p>
            <pre><code>static int veth_poll(struct napi_struct *napi, int budget)
{
	struct veth_rq *rq =
		container_of(napi, struct veth_rq, xdp_napi);
	struct veth_stats stats = {};
	struct veth_xdp_tx_bq bq;
	int done;

	bq.count = 0;

	xdp_set_return_frame_no_direct();
	done = veth_xdp_rcv(rq, budget, &amp;bq, &amp;stats);

	if (done &lt; budget &amp;&amp; napi_complete_done(napi, done)) {
		/* Write rx_notify_masked before reading ptr_ring */
		smp_store_mb(rq-&gt;rx_notify_masked, false);
		if (unlikely(!__ptr_ring_empty(&amp;rq-&gt;xdp_ring))) {
			if (napi_schedule_prep(&amp;rq-&gt;xdp_napi)) {
				WRITE_ONCE(rq-&gt;rx_notify_masked, true);
				__napi_schedule(&amp;rq-&gt;xdp_napi);
			}
		}
	}

	if (stats.xdp_tx &gt; 0)
		veth_xdp_flush(rq, &amp;bq);
	if (stats.xdp_redirect &gt; 0)
		xdp_do_flush();
	xdp_clear_return_frame_no_direct();

	return done;
}</code></pre>
            <p><code>veth_xdp_rcv()</code> processes as many packets as the budget variable is set to, marks the NAPI processing as complete, potentially reschedules a NAPI polling, and <b>then</b>, calls <code>xdp_do_flush()</code>, breaking the NAPI guarantee cited above. After the call to <code>napi_complete_done()</code>, any CPU is free to execute the <code>veth_poll()</code> function before all the flush operations of the previous call are complete, allowing the race on the RX ring.</p><p>The race condition can be fixed by completing all the packet processing before signaling the NAPI poll as complete. The patch as well as the discussion on the kernel mailing list that lead to the fix are available here: <a href="https://lore.kernel.org/bpf/20221220185903.1105011-1-sbohrer@cloudflare.com/">[PATCH] veth: Fix race with AF_XDP exposing old or uninitialized descriptors</a>. The patch was recently merged upstream.</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>We’ve found and fixed a race condition in the Linux virtual ethernet (veth) driver that was corrupting packets for AF_XDP enabled devices!</p><p>This issue was a tough one to find (and to reproduce) but logical iterations lead us all the way down to the internals of the Linux kernel where we saw that a few lines of code were not executed in the correct order.</p><p>A rigorous methodology and the knowledge of the right debugging tools are essential to go about tracking down the root cause of potentially complex bugs.</p><p>This was important for us to fix because while TCP was designed to recover from occasional packet drops, randomly dropping legitimate packets slightly increased the latency of connection establishments and data transfers across our network.</p><p>Interested about other deep dive kernel debugging journeys? Read more of them on <a href="/">our blog</a>!</p> ]]></content:encoded>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Linux]]></category>
            <guid isPermaLink="false">5bnyvGSAmPfUzfkmoNLY40</guid>
            <dc:creator>Shawn Bohrer</dc:creator>
            <dc:creator>Bastien Dhiver</dc:creator>
        </item>
    </channel>
</rss>