
<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>Sat, 11 Apr 2026 11:06:50 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Searching for the cause of hung tasks in the Linux kernel]]></title>
            <link>https://blog.cloudflare.com/searching-for-the-cause-of-hung-tasks-in-the-linux-kernel/</link>
            <pubDate>Fri, 14 Feb 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ The Linux kernel can produce a hung task warning. Searching the Internet and the kernel docs, you can find a brief explanation that the process is stuck in the uninterruptible state. ]]></description>
            <content:encoded><![CDATA[ <p>Depending on your configuration, the Linux kernel can produce a hung task warning message in its log. Searching the Internet and the kernel documentation, you can find a brief explanation that the kernel process is stuck in the uninterruptable state and hasn’t been scheduled on the CPU for an unexpectedly long period of time. That explains the warning’s meaning, but doesn’t provide the reason it occurred. In this blog post we’re going to explore how the hung task warning works, why it happens, whether it is a bug in the Linux kernel or application itself, and whether it is worth monitoring at all.</p>
    <div>
      <h3>INFO: task XXX:1495882 blocked for more than YYY seconds.</h3>
      <a href="#info-task-xxx-1495882-blocked-for-more-than-yyy-seconds">
        
      </a>
    </div>
    <p>The hung task message in the kernel log looks like this:</p>
            <pre><code>INFO: task XXX:1495882 blocked for more than YYY seconds.
     Tainted: G          O       6.6.39-cloudflare-2024.7.3 #1
"echo 0 &gt; /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:XXX         state:D stack:0     pid:1495882 ppid:1      flags:0x00004002
. . .</code></pre>
            <p>Processes in Linux can be in different states. Some of them are running or ready to run on the CPU — they are in the <a href="https://elixir.bootlin.com/linux/v6.12.6/source/include/linux/sched.h#L99"><code><u>TASK_RUNNING</u></code></a> state. Others are waiting for some signal or event to happen, e.g. network packets to arrive or terminal input from a user. They are in a <code>TASK_INTERRUPTIBLE</code> state and can spend an arbitrary length of time in this state until being woken up by a signal. The most important thing about these states is that they still can receive signals, and be terminated by a signal. In contrast, a process in the <code>TASK_UNINTERRUPTIBLE</code> state is waiting only for certain special classes of events to wake them up, and can’t be interrupted by a signal. The signals are not delivered until the process emerges from this state and only a system reboot can clear the process. It’s marked with the letter <code>D</code> in the log shown above.</p><p>What if this wake up event doesn’t happen or happens with a significant delay? (A “significant delay” may be on the order of seconds or minutes, depending on the system.) Then our dependent process is hung in this state. What if this dependent process holds some lock and prevents other processes from acquiring it? Or if we see many processes in the D state? Then it might tell us that some of the system resources are overwhelmed or are not working correctly. At the same time, this state is very valuable, especially if we want to preserve the process memory. It might be useful if part of the data is written to disk and another part is still in the process memory — we don’t want inconsistent data on a disk. Or maybe we want a snapshot of the process memory when the bug is hit. To preserve this behaviour, but make it more controlled, a new state was introduced in the kernel: <a href="https://lwn.net/Articles/288056/"><code><u>TASK_KILLABLE</u></code></a> — it still protects a process, but allows termination with a fatal signal. </p>
    <div>
      <h3>How Linux identifies the hung process</h3>
      <a href="#how-linux-identifies-the-hung-process">
        
      </a>
    </div>
    <p>The Linux kernel has a special thread called <code>khungtaskd</code>. It runs regularly depending on the settings, iterating over all processes in the <code>D</code> state. If a process is in this state for more than YYY seconds, we’ll see a message in the kernel log. There are settings for this daemon that can be changed according to your wishes:</p>
            <pre><code>$ sudo sysctl -a --pattern hung
kernel.hung_task_all_cpu_backtrace = 0
kernel.hung_task_check_count = 4194304
kernel.hung_task_check_interval_secs = 0
kernel.hung_task_panic = 0
kernel.hung_task_timeout_secs = 10
kernel.hung_task_warnings = 200</code></pre>
            <p>At Cloudflare, we changed the notification threshold <code>kernel.hung_task_timeout_secs</code> from the default 120 seconds to 10 seconds. You can adjust the value for your system depending on configuration and how critical this delay is for you. If the process spends more than <code>hung_task_timeout_secs</code> seconds in the D state, a log entry is written, and our internal monitoring system emits an alert based on this log. Another important setting here is <code>kernel.hung_task_warnings</code> — the total number of messages that will be sent to the log. We limit it to 200 messages and reset it every 15 minutes. It allows us not to be overwhelmed by the same issue, and at the same time doesn’t stop our monitoring for too long. You can make it unlimited by <a href="https://docs.kernel.org/admin-guide/sysctl/kernel.html#hung-task-warnings"><u>setting the value to "-1"</u></a>.</p><p>To better understand the root causes of the hung tasks and how a system can be affected, we’re going to review more detailed examples. </p>
    <div>
      <h3>Example #1 or XFS</h3>
      <a href="#example-1-or-xfs">
        
      </a>
    </div>
    <p>Typically, there is a meaningful process or application name in the log, but sometimes you might see something like this:</p>
            <pre><code>INFO: task kworker/13:0:834409 blocked for more than 11 seconds.
 	Tainted: G      	O   	6.6.39-cloudflare-2024.7.3 #1
"echo 0 &gt; /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:kworker/13:0	state:D stack:0 	pid:834409 ppid:2   flags:0x00004000
Workqueue: xfs-sync/dm-6 xfs_log_worker</code></pre>
            <p>In this log, <code>kworker</code> is the kernel thread. It’s used as a deferring mechanism, meaning a piece of work will be scheduled to be executed in the future. Under <code>kworker</code>, the work is aggregated from different tasks, which makes it difficult to tell which application is experiencing a delay. Luckily, the <code>kworker</code> is accompanied by the <a href="https://docs.kernel.org/core-api/workqueue.html"><code><u>Workqueue</u></code></a> line. <code>Workqueue</code> is a linked list, usually predefined in the kernel, where these pieces of work are added and performed by the <code>kworker</code> in the order they were added to the queue. The <code>Workqueue</code> name <code>xfs-sync</code> and <a href="https://elixir.bootlin.com/linux/v6.12.6/source/kernel/workqueue.c#L6096"><u>the function which it points to</u></a>, <code>xfs_log_worker</code>, might give a good clue where to look. Here we can make an assumption that the <a href="https://en.wikipedia.org/wiki/XFS"><u>XFS</u></a> is under pressure and check the relevant metrics. It helped us to discover that due to some configuration changes, we forgot <code>no_read_workqueue</code> / <code>no_write_workqueue</code> flags that were introduced some time ago to <a href="https://blog.cloudflare.com/speeding-up-linux-disk-encryption/"><u>speed up Linux disk encryption</u></a>.</p><p><i>Summary</i>: In this case, nothing critical happened to the system, but the hung tasks warnings gave us an alert that our file system had slowed down.</p>
    <div>
      <h3>Example #2 or Coredump</h3>
      <a href="#example-2-or-coredump">
        
      </a>
    </div>
    <p>Let’s take a look at the next hung task log and its decoded stack trace:</p>
            <pre><code>INFO: task test:964 blocked for more than 5 seconds.
      Not tainted 6.6.72-cloudflare-2025.1.7 #1
"echo 0 &gt; /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:test            state:D stack:0     pid:964   ppid:916    flags:0x00004000
Call Trace:
&lt;TASK&gt;
__schedule (linux/kernel/sched/core.c:5378 linux/kernel/sched/core.c:6697) 
schedule (linux/arch/x86/include/asm/preempt.h:85 (discriminator 13) linux/kernel/sched/core.c:6772 (discriminator 13)) 
[do_exit (linux/kernel/exit.c:433 (discriminator 4) linux/kernel/exit.c:825 (discriminator 4)) 
? finish_task_switch.isra.0 (linux/arch/x86/include/asm/irqflags.h:42 linux/arch/x86/include/asm/irqflags.h:77 linux/kernel/sched/sched.h:1385 linux/kernel/sched/core.c:5132 linux/kernel/sched/core.c:5250) 
do_group_exit (linux/kernel/exit.c:1005) 
get_signal (linux/kernel/signal.c:2869) 
? srso_return_thunk (linux/arch/x86/lib/retpoline.S:217) 
? hrtimer_try_to_cancel.part.0 (linux/kernel/time/hrtimer.c:1347) 
arch_do_signal_or_restart (linux/arch/x86/kernel/signal.c:310) 
? srso_return_thunk (linux/arch/x86/lib/retpoline.S:217) 
? hrtimer_nanosleep (linux/kernel/time/hrtimer.c:2105) 
exit_to_user_mode_prepare (linux/kernel/entry/common.c:176 linux/kernel/entry/common.c:210) 
syscall_exit_to_user_mode (linux/arch/x86/include/asm/entry-common.h:91 linux/kernel/entry/common.c:141 linux/kernel/entry/common.c:304) 
? srso_return_thunk (linux/arch/x86/lib/retpoline.S:217) 
do_syscall_64 (linux/arch/x86/entry/common.c:88) 
entry_SYSCALL_64_after_hwframe (linux/arch/x86/entry/entry_64.S:121) 
&lt;/TASK&gt;</code></pre>
            <p>The stack trace says that the process or application <code>test</code> was blocked <code>for more than 5 seconds</code>. We might recognise this user space application by the name, but why is it blocked? It’s always helpful to check the stack trace when looking for a cause. The most interesting line here is <code>do_exit (linux/kernel/exit.c:433 (discriminator 4) linux/kernel/exit.c:825 (discriminator 4))</code>. The <a href="https://elixir.bootlin.com/linux/v6.6.67/source/kernel/exit.c#L825"><u>source code</u></a> points to the <code>coredump_task_exit</code> function. Additionally, checking the process metrics revealed that the application crashed during the time when the warning message appeared in the log. When a process is terminated based on some set of signals (abnormally), <a href="https://man7.org/linux/man-pages/man5/core.5.html"><u>the Linux kernel can provide a core dump file</u></a>, if enabled. The mechanism — when a process terminates, the kernel makes a snapshot of the process memory before exiting and either writes it to a file or sends it through the socket to another handler — can be <a href="https://systemd.io/COREDUMP/"><u>systemd-coredump</u></a> or your custom one. When it happens, the kernel moves the process to the <code>D</code> state to preserve its memory and early termination. The higher the process memory usage, the longer it takes to get a core dump file, and the higher the chance of getting a hung task warning.</p><p>Let’s check our hypothesis by triggering it with a small Go program. We’ll use the default Linux coredump handler and will decrease the hung task threshold to 1 second.</p><p>Coredump settings:</p>
            <pre><code>$ sudo sysctl -a --pattern kernel.core
kernel.core_pattern = core
kernel.core_pipe_limit = 16
kernel.core_uses_pid = 1</code></pre>
            <p>You can make changes with <a href="https://man7.org/linux/man-pages/man8/sysctl.8.html"><u>sysctl</u></a>:</p>
            <pre><code>$ sudo sysctl -w kernel.core_uses_pid=1</code></pre>
            <p>Hung task settings:</p>
            <pre><code>$ sudo sysctl -a --pattern hung
kernel.hung_task_all_cpu_backtrace = 0
kernel.hung_task_check_count = 4194304
kernel.hung_task_check_interval_secs = 0
kernel.hung_task_panic = 0
kernel.hung_task_timeout_secs = 1
kernel.hung_task_warnings = -1</code></pre>
            <p>Go program:</p>
            <pre><code>$ cat main.go
package main

import (
	"os"
	"time"
)

func main() {
	_, err := os.ReadFile("test.file")
	if err != nil {
		panic(err)
	}
	time.Sleep(8 * time.Minute) 
}</code></pre>
            <p>This program reads a 10 GB file into process memory. Let’s create the file:</p>
            <pre><code>$ yes this is 10GB file | head -c 10GB &gt; test.file</code></pre>
            <p>The last step is to build the Go program, crash it, and watch our kernel log:</p>
            <pre><code>$ go mod init test
$ go build .
$ GOTRACEBACK=crash ./test
$ (Ctrl+\)</code></pre>
            <p>Hooray! We can see our hung task warning:</p>
            <pre><code>$ sudo dmesg -T | tail -n 31
INFO: task test:8734 blocked for more than 22 seconds.
      Not tainted 6.6.72-cloudflare-2025.1.7 #1
      Blocked by coredump.
"echo 0 &gt; /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:test            state:D stack:0     pid:8734  ppid:8406   task_flags:0x400448 flags:0x00004000</code></pre>
            <p>By the way, have you noticed the <code>Blocked by coredump.</code> line in the log? It was recently added to the <a href="https://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm.git/commit/?h=mm-nonmm-stable&amp;id=23f3f7625cfb55f92e950950e70899312f54afb7"><u>upstream</u></a> code to improve visibility and remove the blame from the process itself. The patch also added the <code>task_flags</code> information, as <code>Blocked by coredump</code> is detected via the flag <a href="https://elixir.bootlin.com/linux/v6.13.1/source/include/linux/sched.h#L1675"><code><u>PF_POSTCOREDUMP</u></code></a>, and knowing all the task flags is useful for further root-cause analysis.</p><p><i>Summary</i>: This example showed that even if everything suggests that the application is the problem, the real root cause can be something else — in this case, <code>coredump</code>.</p>
    <div>
      <h3>Example #3 or rtnl_mutex</h3>
      <a href="#example-3-or-rtnl_mutex">
        
      </a>
    </div>
    <p>This one was tricky to debug. Usually, the alerts are limited by one or two different processes, meaning only a certain application or subsystem experiences an issue. In this case, we saw dozens of unrelated tasks hanging for minutes with no improvements over time. Nothing else was in the log, most of the system metrics were fine, and existing traffic was being served, but it was not possible to ssh to the server. New Kubernetes container creations were also stalling. Analyzing the stack traces of different tasks initially revealed that all the traces were limited to just three functions:</p>
            <pre><code>rtnetlink_rcv_msg+0x9/0x3c0
dev_ethtool+0xc6/0x2db0 
bonding_show_bonds+0x20/0xb0</code></pre>
            <p>Further investigation showed that all of these functions were waiting for <a href="https://elixir.bootlin.com/linux/v6.6.74/source/net/core/rtnetlink.c#L76"><code><u>rtnl_lock</u></code></a> to be acquired. It looked like some application acquired the <code>rtnl_mutex</code> and didn’t release it. All other processes were in the <code>D</code> state waiting for this lock.</p><p>The RTNL lock is primarily used by the kernel networking subsystem for any network-related config, for both writing and reading. The RTNL is a global <b>mutex</b> lock, although <a href="https://lpc.events/event/18/contributions/1959/"><u>upstream efforts</u></a> are being made for splitting up RTNL per network namespace (netns).</p><p>From the hung task reports, we can observe the “victims” that are being stalled waiting for the lock, but how do we identify the task that is holding this lock for too long? For troubleshooting this, we leveraged <code>BPF</code> via a <code>bpftrace</code> script, as this allows us to inspect the running kernel state. The <a href="https://elixir.bootlin.com/linux/v6.6.75/source/include/linux/mutex.h#L67"><u>kernel's mutex implementation</u></a> has a struct member called <code>owner</code>. It contains a pointer to the <a href="https://elixir.bootlin.com/linux/v6.6.75/source/include/linux/sched.h#L746"><code><u>task_struct</u></code></a> from the mutex-owning process, except it is encoded as type <code>atomic_long_t</code>. This is because the mutex implementation stores some state information in the lower 3-bits (mask 0x7) of this pointer. Thus, to read and dereference this <code>task_struct</code> pointer, we must first mask off the lower bits (0x7).</p><p>Our <code>bpftrace</code> script to determine who holds the mutex is as follows:</p>
            <pre><code>#!/usr/bin/env bpftrace
interval:s:10 {
  $rtnl_mutex = (struct mutex *) kaddr("rtnl_mutex");
  $owner = (struct task_struct *) ($rtnl_mutex-&gt;owner.counter &amp; ~0x07);
  if ($owner != 0) {
    printf("rtnl_mutex-&gt;owner = %u %s\n", $owner-&gt;pid, $owner-&gt;comm);
  }
}</code></pre>
            <p>In this script, the <code>rtnl_mutex</code> lock is a global lock whose address can be exposed via <code>/proc/kallsyms</code> – using <code>bpftrace</code> helper function <code>kaddr()</code>, we can access the struct mutex pointer from the <code>kallsyms</code>. Thus, we can periodically (via <code>interval:s:10</code>) check if someone is holding this lock.</p><p>In the output we had this:</p>
            <pre><code>rtnl_mutex-&gt;owner = 3895365 calico-node</code></pre>
            <p>This allowed us to quickly identify <code>calico-node</code> as the process holding the RTNL lock for too long. To quickly observe where this process itself is stalled, the call stack is available via <code>/proc/3895365/stack</code>. This showed us that the root cause was a Wireguard config change, with function <code>wg_set_device()</code> holding the RTNL lock, and <code>peer_remove_after_dead()</code> waiting too long for a <code>napi_disable()</code> call. We continued debugging via a tool called <a href="https://drgn.readthedocs.io/en/latest/user_guide.html#stack-traces"><code><u>drgn</u></code></a>, which is a programmable debugger that can debug a running kernel via a Python-like interactive shell. We still haven’t discovered the root cause for the Wireguard issue and have <a href="https://lore.kernel.org/lkml/CALrw=nGoSW=M-SApcvkP4cfYwWRj=z7WonKi6fEksWjMZTs81A@mail.gmail.com/"><u>asked the upstream</u></a> for help, but that is another story.</p><p><i>Summary</i>: The hung task messages were the only ones which we had in the kernel log. Each stack trace of these messages was unique, but by carefully analyzing them, we could spot similarities and continue debugging with other instruments.</p>
    <div>
      <h3>Epilogue</h3>
      <a href="#epilogue">
        
      </a>
    </div>
    <p>Your system might have different hung task warnings, and we have many others not mentioned here. Each case is unique, and there is no standard approach to debug them. But hopefully this blog post helps you better understand why it’s good to have these warnings enabled, how they work, and what the meaning is behind them. We tried to provide some navigation guidance for the debugging process as well:</p><ul><li><p>analyzing the stack trace might be a good starting point for debugging it, even if all the messages look unrelated, like we saw in example #3</p></li><li><p>keep in mind that the alert might be misleading, pointing to the victim and not the offender, as we saw in example #2 and example #3</p></li><li><p>if the kernel doesn’t schedule your application on the CPU, puts it in the D state, and emits the warning – the real problem might exist in the application code</p></li></ul><p>Good luck with your debugging, and hopefully this material will help you on this journey!</p> ]]></content:encoded>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Monitoring]]></category>
            <guid isPermaLink="false">3UHNgpNPKn2IAwDUzD4m3a</guid>
            <dc:creator>Oxana Kharitonova</dc:creator>
            <dc:creator>Jesper Brouer</dc:creator>
        </item>
        <item>
            <title><![CDATA[Linux kernel security tunables everyone should consider adopting]]></title>
            <link>https://blog.cloudflare.com/linux-kernel-hardening/</link>
            <pubDate>Wed, 06 Mar 2024 14:00:43 GMT</pubDate>
            <description><![CDATA[ This post illustrates some of the Linux Kernel features, which are helping us to keep our production systems more secure. We will deep dive into how they work and why you may consider enabling them as well ]]></description>
            <content:encoded><![CDATA[ 
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1MwDdpmFexb2YBRb6Y4VkD/f8fe7a101729ee9fa518a457b8bb170a/Technical-deep-dive-for-Security-week.png" />
            
            </figure><p>The Linux kernel is the heart of many modern production systems. It decides when any code is allowed to run and which programs/users can access which resources. It manages memory, mediates access to hardware, and does a bulk of work under the hood on behalf of programs running on top. Since the kernel is always involved in any code execution, it is in the best position to protect the system from malicious programs, enforce the desired system security policy, and provide security features for safer production environments.</p><p>In this post, we will review some Linux kernel security configurations we use at Cloudflare and how they help to block or minimize a potential system compromise.</p>
    <div>
      <h2>Secure boot</h2>
      <a href="#secure-boot">
        
      </a>
    </div>
    <p>When a machine (either a laptop or a server) boots, it goes through several boot stages:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7LULhRh5qh02zFnil5ZPnl/b2e1867ba56492628bbb95ba6850448e/image3-17.png" />
            
            </figure><p>Within a secure boot architecture each stage from the above diagram verifies the integrity of the next stage before passing execution to it, thus forming a so-called secure boot chain. This way “trustworthiness” is extended to every component in the boot chain, because if we verified the code integrity of a particular stage, we can trust this code to verify the integrity of the next stage.</p><p>We <a href="/anchoring-trust-a-hardware-secure-boot-story">have previously covered</a> how Cloudflare implements secure boot in the initial stages of the boot process. In this post, we will focus on the Linux kernel.</p><p>Secure boot is the cornerstone of any operating system security mechanism. The Linux kernel is the primary enforcer of the operating system security configuration and policy, so we have to be sure that the Linux kernel itself has not been tampered with. In our previous <a href="/anchoring-trust-a-hardware-secure-boot-story">post about secure boot</a> we showed how we use UEFI Secure Boot to ensure the integrity of the Linux kernel.</p><p>But what happens next? After the kernel gets executed, it may try to load additional drivers, or as they are called in the Linux world, kernel modules. And kernel module loading is not confined just to the boot process. A module can be loaded at any time during runtime — a new device being plugged in and a driver is needed, some additional extensions in the networking stack are required (for example, for fine-grained firewall rules), or just manually by the system administrator.</p><p>However, uncontrolled kernel module loading might pose a significant risk to system integrity. Unlike regular programs, which get executed as user space processes, kernel modules are pieces of code which get injected and executed directly in the Linux kernel address space. There is no separation between the code and data in different kernel modules and core kernel subsystems, so everything can access everything. This means that a rogue kernel module can completely nullify the trustworthiness of the operating system and make secure boot useless. As an example, consider a simple Debian 12 (Bookworm installation), but with <a href="https://selinuxproject.org/">SELinux</a> configured and enforced:</p>
            <pre><code>ignat@dev:~$ lsb_release --all
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 12 (bookworm)
Release:	12
Codename:	bookworm
ignat@dev:~$ uname -a
Linux dev 6.1.0-18-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64 GNU/Linux
ignat@dev:~$ sudo getenforce
Enforcing</code></pre>
            <p>Now we need to do some research. First, we see that we’re running 6.1.76 Linux Kernel. If we explore the source code, we would see that <a href="https://elixir.bootlin.com/linux/v6.1.76/source/security/selinux/hooks.c#L107">inside the kernel, the SELinux configuration is stored in a singleton structure</a>, which <a href="https://elixir.bootlin.com/linux/v6.1.76/source/security/selinux/include/security.h#L92">is defined</a> as follows:</p>
            <pre><code>struct selinux_state {
#ifdef CONFIG_SECURITY_SELINUX_DISABLE
	bool disabled;
#endif
#ifdef CONFIG_SECURITY_SELINUX_DEVELOP
	bool enforcing;
#endif
	bool checkreqprot;
	bool initialized;
	bool policycap[__POLICYDB_CAP_MAX];

	struct page *status_page;
	struct mutex status_lock;

	struct selinux_avc *avc;
	struct selinux_policy __rcu *policy;
	struct mutex policy_mutex;
} __randomize_layout;</code></pre>
            <p>From the above, we can see that if the kernel configuration has <code>CONFIG_SECURITY_SELINUX_DEVELOP</code> enabled, the structure would have a boolean variable <code>enforcing</code>, which controls the enforcement status of SELinux at runtime. This is exactly what the above <code>$ sudo getenforce</code> command returns. We can double check that the Debian kernel indeed has the configuration option enabled:</p>
            <pre><code>ignat@dev:~$ grep CONFIG_SECURITY_SELINUX_DEVELOP /boot/config-`uname -r`
CONFIG_SECURITY_SELINUX_DEVELOP=y</code></pre>
            <p>Good! Now that we have a variable in the kernel, which is responsible for some security enforcement, we can try to attack it. One problem though is the <code>__randomize_layout</code> attribute: since <code>CONFIG_SECURITY_SELINUX_DISABLE</code> is actually not set for our Debian kernel, normally <code>enforcing</code> would be the first member of the struct. Thus if we know where the struct is, we immediately know the position of the <code>enforcing</code> flag. With <code>__randomize_layout</code>, during kernel compilation the compiler might place members at arbitrary positions within the struct, so it is harder to create generic exploits. But arbitrary struct randomization within the kernel <a href="https://elixir.bootlin.com/linux/v6.1.76/source/security/Kconfig.hardening#L301">may introduce performance impact</a>, so is often disabled and it is disabled for the Debian kernel:</p>
            <pre><code>ignat@dev:~$ grep RANDSTRUCT /boot/config-`uname -r`
CONFIG_RANDSTRUCT_NONE=y</code></pre>
            <p>We can also confirm the compiled position of the <code>enforcing</code> flag using the <a href="https://git.kernel.org/pub/scm/devel/pahole/pahole.git/">pahole tool</a> and either kernel debug symbols, if available, or (on modern kernels, if enabled) in-kernel <a href="https://www.kernel.org/doc/html/next/bpf/btf.html">BTF</a> information. We will use the latter:</p>
            <pre><code>ignat@dev:~$ pahole -C selinux_state /sys/kernel/btf/vmlinux
struct selinux_state {
	bool                       enforcing;            /*     0     1 */
	bool                       checkreqprot;         /*     1     1 */
	bool                       initialized;          /*     2     1 */
	bool                       policycap[8];         /*     3     8 */

	/* XXX 5 bytes hole, try to pack */

	struct page *              status_page;          /*    16     8 */
	struct mutex               status_lock;          /*    24    32 */
	struct selinux_avc *       avc;                  /*    56     8 */
	/* --- cacheline 1 boundary (64 bytes) --- */
	struct selinux_policy *    policy;               /*    64     8 */
	struct mutex               policy_mutex;         /*    72    32 */

	/* size: 104, cachelines: 2, members: 9 */
	/* sum members: 99, holes: 1, sum holes: 5 */
	/* last cacheline: 40 bytes */
};</code></pre>
            <p>So <code>enforcing</code> is indeed located at the start of the structure and we don’t even have to be a privileged user to confirm this.</p><p>Great! All we need is the runtime address of the <code>selinux_state</code> variable inside the kernel:(shell/bash)</p>
            <pre><code>ignat@dev:~$ sudo grep selinux_state /proc/kallsyms
ffffffffbc3bcae0 B selinux_state</code></pre>
            <p>With all the information, we can write an almost textbook simple kernel module to manipulate the SELinux state:</p><p>Mymod.c:</p>
            <pre><code>#include &lt;linux/module.h&gt;

static int __init mod_init(void)
{
	bool *selinux_enforce = (bool *)0xffffffffbc3bcae0;
	*selinux_enforce = false;
	return 0;
}

static void mod_fini(void)
{
}

module_init(mod_init);
module_exit(mod_fini);

MODULE_DESCRIPTION("A somewhat malicious module");
MODULE_AUTHOR("Ignat Korchagin &lt;ignat@cloudflare.com&gt;");
MODULE_LICENSE("GPL");</code></pre>
            <p>And the respective <code>Kbuild</code> file:</p>
            <pre><code>obj-m := mymod.o</code></pre>
            <p>With these two files we can build a full fledged kernel module according to <a href="https://docs.kernel.org/kbuild/modules.html">the official kernel docs</a>:</p>
            <pre><code>ignat@dev:~$ cd mymod/
ignat@dev:~/mymod$ ls
Kbuild  mymod.c
ignat@dev:~/mymod$ make -C /lib/modules/`uname -r`/build M=$PWD
make: Entering directory '/usr/src/linux-headers-6.1.0-18-cloud-amd64'
  CC [M]  /home/ignat/mymod/mymod.o
  MODPOST /home/ignat/mymod/Module.symvers
  CC [M]  /home/ignat/mymod/mymod.mod.o
  LD [M]  /home/ignat/mymod/mymod.ko
  BTF [M] /home/ignat/mymod/mymod.ko
Skipping BTF generation for /home/ignat/mymod/mymod.ko due to unavailability of vmlinux
make: Leaving directory '/usr/src/linux-headers-6.1.0-18-cloud-amd64'</code></pre>
            <p>If we try to load this module now, the system may not allow it due to the SELinux policy:</p>
            <pre><code>ignat@dev:~/mymod$ sudo insmod mymod.ko
insmod: ERROR: could not load module mymod.ko: Permission denied</code></pre>
            <p>We can workaround it by copying the module into the standard module path somewhere:</p>
            <pre><code>ignat@dev:~/mymod$ sudo cp mymod.ko /lib/modules/`uname -r`/kernel/crypto/</code></pre>
            <p>Now let’s try it out:</p>
            <pre><code>ignat@dev:~/mymod$ sudo getenforce
Enforcing
ignat@dev:~/mymod$ sudo insmod /lib/modules/`uname -r`/kernel/crypto/mymod.ko
ignat@dev:~/mymod$ sudo getenforce
Permissive</code></pre>
            <p>Not only did we disable the SELinux protection via a malicious kernel module, we did it quietly. Normal <code>sudo setenforce 0</code>, even if allowed, would go through the official <a href="https://elixir.bootlin.com/linux/v6.1.76/source/security/selinux/selinuxfs.c#L173">selinuxfs interface and would emit an audit message</a>. Our code manipulated the kernel memory directly, so no one was alerted. This illustrates why uncontrolled kernel module loading is very dangerous and that is why most security standards and commercial security monitoring products advocate for close monitoring of kernel module loading.</p><p>But we don’t need to monitor kernel modules at Cloudflare. Let’s repeat the exercise on a Cloudflare production kernel (module recompilation skipped for brevity):</p>
            <pre><code>ignat@dev:~/mymod$ uname -a
Linux dev 6.6.17-cloudflare-2024.2.9 #1 SMP PREEMPT_DYNAMIC Mon Sep 27 00:00:00 UTC 2010 x86_64 GNU/Linux
ignat@dev:~/mymod$ sudo insmod /lib/modules/`uname -r`/kernel/crypto/mymod.ko
insmod: ERROR: could not insert module /lib/modules/6.6.17-cloudflare-2024.2.9/kernel/crypto/mymod.ko: Key was rejected by service</code></pre>
            <p>We get a <code>Key was rejected by service</code> error when trying to load a module, and the kernel log will have the following message:</p>
            <pre><code>ignat@dev:~/mymod$ sudo dmesg | tail -n 1
[41515.037031] Loading of unsigned module is rejected</code></pre>
            <p>This is because the Cloudflare kernel <a href="https://elixir.bootlin.com/linux/v6.6.17/source/kernel/module/Kconfig#L211">requires all the kernel modules to have a valid signature</a>, so we don’t even have to worry about a malicious module being loaded at some point:</p>
            <pre><code>ignat@dev:~$ grep MODULE_SIG_FORCE /boot/config-`uname -r`
CONFIG_MODULE_SIG_FORCE=y</code></pre>
            <p>For completeness it is worth noting that the Debian stock kernel also supports module signatures, but does not enforce it:</p>
            <pre><code>ignat@dev:~$ grep MODULE_SIG /boot/config-6.1.0-18-cloud-amd64
CONFIG_MODULE_SIG_FORMAT=y
CONFIG_MODULE_SIG=y
# CONFIG_MODULE_SIG_FORCE is not set
…</code></pre>
            <p>The above configuration means that the kernel will validate a module signature, if available. But if not - the module will be loaded anyway with a warning message emitted and the <a href="https://docs.kernel.org/admin-guide/tainted-kernels.html">kernel will be tainted</a>.</p>
    <div>
      <h3>Key management for kernel module signing</h3>
      <a href="#key-management-for-kernel-module-signing">
        
      </a>
    </div>
    <p>Signed kernel modules are great, but it creates a key management problem: to sign a module we need a signing keypair that is trusted by the kernel. The public key of the keypair is usually directly embedded into the kernel binary, so the kernel can easily use it to verify module signatures. The private key of the pair needs to be protected and secure, because if it is leaked, anyone could compile and sign a potentially malicious kernel module which would be accepted by our kernel.</p><p>But what is the best way to eliminate the risk of losing something? Not to have it in the first place! Luckily the kernel build system <a href="https://elixir.bootlin.com/linux/v6.6.17/source/certs/Makefile#L36">will generate a random keypair</a> for module signing, if none is provided. At Cloudflare, we use that feature to sign all the kernel modules during the kernel compilation stage. When the compilation and signing is done though, instead of storing the key in a secure place, we just destroy the private key:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1EWN5Kh5NebZTW3kAzK9uM/0dfb98bc095710e5b317a128d3efb1c0/image1-19.png" />
            
            </figure><p>So with the above process:</p><ol><li><p>The kernel build system generated a random keypair, compiles the kernel and modules</p></li><li><p>The public key is embedded into the kernel image, the private key is used to sign all the modules</p></li><li><p>The private key is destroyed</p></li></ol><p>With this scheme not only do we not have to worry about module signing key management, we also use a different key for each kernel we release to production. So even if a particular build process is hijacked and the signing key is not destroyed and potentially leaked, the key will no longer be valid when a kernel update is released.</p><p>There are some flexibility downsides though, as we can’t “retrofit” a new kernel module for an already released kernel (for example, for <a href="https://www.cloudflare.com/en-gb/press-releases/2023/cloudflare-powers-hyper-local-ai-inference-with-nvidia/">a new piece of hardware we are adopting</a>). However, it is not a practical limitation for us as we release kernels often (roughly every week) to keep up with a steady stream of bug fixes and vulnerability patches in the Linux Kernel.</p>
    <div>
      <h2>KEXEC</h2>
      <a href="#kexec">
        
      </a>
    </div>
    <p><a href="https://en.wikipedia.org/wiki/Kexec">KEXEC</a> (or <code>kexec_load()</code>) is an interesting system call in Linux, which allows for one kernel to directly execute (or jump to) another kernel. The idea behind this is to switch/update/downgrade kernels faster without going through a full reboot cycle to minimize the potential system downtime. However, it was developed quite a while ago, when secure boot and system integrity was not quite a concern. Therefore its original design has security flaws and is known to be able to <a href="https://mjg59.dreamwidth.org/28746.html">bypass secure boot and potentially compromise system integrity</a>.</p><p>We can see the problems just based on the <a href="https://man7.org/linux/man-pages/man2/kexec_load.2.html">definition of the system call itself</a>:</p>
            <pre><code>struct kexec_segment {
	const void *buf;
	size_t bufsz;
	const void *mem;
	size_t memsz;
};
...
long kexec_load(unsigned long entry, unsigned long nr_segments, struct kexec_segment *segments, unsigned long flags);</code></pre>
            <p>So the kernel expects just a collection of buffers with code to execute. Back in those days there was not much desire to do a lot of data parsing inside the kernel, so the idea was to parse the to-be-executed kernel image in user space and provide the kernel with only the data it needs. Also, to switch kernels live, we need an intermediate program which would take over while the old kernel is shutting down and the new kernel has not yet been executed. In the kexec world this program is called <a href="https://git.kernel.org/pub/scm/utils/kernel/kexec/kexec-tools.git/tree/purgatory">purgatory</a>. Thus the problem is evident: we give the kernel a bunch of code and it will happily execute it at the highest privilege level. But instead of the original kernel or purgatory code, we can easily provide code similar to the one demonstrated earlier in this post, which disables SELinux (or does something else to the kernel).</p><p>At Cloudflare we have had <code>kexec_load()</code> disabled for some time now just because of this. The advantage of faster reboots with kexec comes with <a href="https://elixir.bootlin.com/linux/v6.6.17/source/kernel/Kconfig.kexec#L30">a (small) risk of improperly initialized hardware</a>, so it was not worth using it even without the security concerns. However, kexec does provide one useful feature — it is the foundation of the Linux kernel <a href="https://docs.kernel.org/admin-guide/kdump/kdump.html">crashdumping solution</a>. In a nutshell, if a kernel crashes in production (due to a bug or some other error), a backup kernel (previously loaded with kexec) can take over, collect and save the memory dump for further investigation. This allows to more effectively investigate kernel and other issues in production, so it is a powerful tool to have.</p><p>Luckily, since the <a href="https://mjg59.dreamwidth.org/28746.html">original problems with kexec were outlined</a>, Linux developed an alternative <a href="https://elixir.bootlin.com/linux/v6.6.17/source/kernel/Kconfig.kexec#L36">secure interface for kexec</a>: instead of buffers with code it expects file descriptors with the to-be-executed kernel image and initrd and does parsing inside the kernel. Thus, only a valid kernel image can be supplied. On top of this, we can <a href="https://elixir.bootlin.com/linux/v6.6.17/source/kernel/Kconfig.kexec#L48">configure</a> and <a href="https://elixir.bootlin.com/linux/v6.6.17/source/kernel/Kconfig.kexec#L62">require</a> kexec to ensure the provided images are properly signed, so only authorized code can be executed in the kexec scenario. A secure configuration for kexec looks something like this:</p>
            <pre><code>ignat@dev:~$ grep KEXEC /boot/config-`uname -r`
CONFIG_KEXEC_CORE=y
CONFIG_HAVE_IMA_KEXEC=y
# CONFIG_KEXEC is not set
CONFIG_KEXEC_FILE=y
CONFIG_KEXEC_SIG=y
CONFIG_KEXEC_SIG_FORCE=y
CONFIG_KEXEC_BZIMAGE_VERIFY_SIG=y
…</code></pre>
            <p>Above we ensure that the legacy <code>kexec_load()</code> system call is disabled by disabling <code>CONFIG_KEXEC</code>, but still can configure Linux Kernel crashdumping via the new <code>kexec_file_load()</code> system call via <code>CONFIG_KEXEC_FILE=y</code> with enforced signature checks (<code>CONFIG_KEXEC_SIG=y</code> and <code>CONFIG_KEXEC_SIG_FORCE=y</code>).</p><p>Note that stock Debian kernel has the legacy <code>kexec_load()</code> system call enabled and does not enforce signature checks for <code>kexec_file_load()</code> (similar to module signature checks):</p>
            <pre><code>ignat@dev:~$ grep KEXEC /boot/config-6.1.0-18-cloud-amd64
CONFIG_KEXEC=y
CONFIG_KEXEC_FILE=y
CONFIG_ARCH_HAS_KEXEC_PURGATORY=y
CONFIG_KEXEC_SIG=y
# CONFIG_KEXEC_SIG_FORCE is not set
CONFIG_KEXEC_BZIMAGE_VERIFY_SIG=y
…</code></pre>
            
    <div>
      <h2>Kernel Address Space Layout Randomization (KASLR)</h2>
      <a href="#kernel-address-space-layout-randomization-kaslr">
        
      </a>
    </div>
    <p>Even on the stock Debian kernel if you try to repeat the exercise we described in the “Secure boot” section of this post after a system reboot, you will likely see it would fail to disable SELinux now. This is because we hardcoded the kernel address of the <code>selinux_state</code> structure in our malicious kernel module, but the address changed now:</p>
            <pre><code>ignat@dev:~$ sudo grep selinux_state /proc/kallsyms
ffffffffb41bcae0 B selinux_state</code></pre>
            <p><a href="https://docs.kernel.org/security/self-protection.html#kernel-address-space-layout-randomization-kaslr">Kernel Address Space Layout Randomization (or KASLR)</a> is a simple concept: it slightly and randomly shifts the kernel code and data on each boot:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6e7sTU0q25lHmBG5b4Q8if/d19a762047547d6f14dff4f9ab8f2d81/Screenshot-2024-03-06-at-13.53.23-2.png" />
            
            </figure><p>This is to combat targeted exploitation (like the malicious module in this post) based on the knowledge of the location of internal kernel structures and code. It is especially useful for popular Linux distribution kernels, like the Debian one, because most users use the same binary and anyone can download the debug symbols and the System.map file with all the addresses of the kernel internals. Just to note: it will not prevent the module loading and doing harm, but it will likely not achieve the targeted effect of disabling SELinux. Instead, it will modify a random piece of kernel memory potentially causing the kernel to crash.</p><p>Both the Cloudflare kernel and the Debian one have this feature enabled:</p>
            <pre><code>ignat@dev:~$ grep RANDOMIZE_BASE /boot/config-`uname -r`
CONFIG_RANDOMIZE_BASE=y</code></pre>
            
    <div>
      <h3>Restricted kernel pointers</h3>
      <a href="#restricted-kernel-pointers">
        
      </a>
    </div>
    <p>While KASLR helps with targeted exploits, it is quite easy to bypass since everything is shifted by a single random offset as shown on the diagram above. Thus if the attacker knows at least one runtime kernel address, they can recover this offset by subtracting the runtime address from the compile time address of the same symbol (function or data structure) from the kernel’s System.map file. Once they know the offset, they can recover the addresses of all other symbols by adjusting them by this offset.</p><p>Therefore, modern kernels take precautions not to leak kernel addresses at least to unprivileged users. One of the main tunables for this is the <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#kptr-restrict">kptr_restrict sysctl</a>. It is a good idea to set it at least to <code>1</code> to not allow regular users to see kernel pointers:(shell/bash)</p>
            <pre><code>ignat@dev:~$ sudo sysctl -w kernel.kptr_restrict=1
kernel.kptr_restrict = 1
ignat@dev:~$ grep selinux_state /proc/kallsyms
0000000000000000 B selinux_state</code></pre>
            <p>Privileged users can still see the pointers:</p>
            <pre><code>ignat@dev:~$ sudo grep selinux_state /proc/kallsyms
ffffffffb41bcae0 B selinux_state</code></pre>
            <p>Similar to <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#kptr-restrict">kptr_restrict sysctl</a> there is also <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#dmesg-restrict">dmesg_restrict</a>, which if set, would prevent regular users from reading the kernel log (which may also leak kernel pointers via its messages). While you need to explicitly set <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#kptr-restrict">kptr_restrict sysctl</a> to a non-zero value on each boot (or use some system sysctl configuration utility, like <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-sysctl.service.html">this one</a>), you can configure <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#dmesg-restrict">dmesg_restrict</a> initial value via the <code>CONFIG_SECURITY_DMESG_RESTRICT</code> kernel configuration option. Both the Cloudflare kernel and the Debian one enforce <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#dmesg-restrict">dmesg_restrict</a> this way:</p>
            <pre><code>ignat@dev:~$ grep CONFIG_SECURITY_DMESG_RESTRICT /boot/config-`uname -r`
CONFIG_SECURITY_DMESG_RESTRICT=y</code></pre>
            <p>Worth noting that <code>/proc/kallsyms</code> and the kernel log are not the only sources of potential kernel pointer leaks. There is a lot of legacy in the Linux kernel and [new sources are continuously being found and patched]. That’s why it is very important to stay up to date with the latest kernel bugfix releases.</p>
    <div>
      <h2>Lockdown LSM</h2>
      <a href="#lockdown-lsm">
        
      </a>
    </div>
    <p><a href="https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html">Linux Security Modules (LSM)</a> is a hook-based framework for implementing security policies and Mandatory Access Control in the Linux Kernel. We have [covered our usage of another LSM module, BPF-LSM, previously].</p><p>BPF-LSM is a useful foundational piece for our kernel security, but in this post we want to mention another useful LSM module we use — <a href="https://man7.org/linux/man-pages/man7/kernel_lockdown.7.html">the Lockdown LSM</a>. Lockdown can be in three states (controlled by the <code>/sys/kernel/security/lockdown</code> special file):</p>
            <pre><code>ignat@dev:~$ cat /sys/kernel/security/lockdown
[none] integrity confidentiality</code></pre>
            <p><code>none</code> is the state where nothing is enforced and the module is effectively disabled. When Lockdown is in the <code>integrity</code> state, the kernel tries to prevent any operation, which may compromise its integrity. We already covered some examples of these in this post: loading unsigned modules and executing unsigned code via KEXEC. But there are other potential ways (which are mentioned in <a href="https://man7.org/linux/man-pages/man7/kernel_lockdown.7.html">the LSM’s man page</a>), all of which this LSM tries to block. <code>confidentiality</code> is the most restrictive mode, where Lockdown will also try to prevent any information leakage from the kernel. In practice this may be too restrictive for server workloads as it blocks all runtime debugging capabilities, like <code>perf</code> or eBPF.</p><p>Let’s see the Lockdown LSM in action. On a barebones Debian system the initial state is <code>none</code> meaning nothing is locked down:</p>
            <pre><code>ignat@dev:~$ uname -a
Linux dev 6.1.0-18-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64 GNU/Linux
ignat@dev:~$ cat /sys/kernel/security/lockdown
[none] integrity confidentiality</code></pre>
            <p>We can switch the system into the <code>integrity</code> mode:</p>
            <pre><code>ignat@dev:~$ echo integrity | sudo tee /sys/kernel/security/lockdown
integrity
ignat@dev:~$ cat /sys/kernel/security/lockdown
none [integrity] confidentiality</code></pre>
            <p>It is worth noting that we can only put the system into a more restrictive state, but not back. That is, once in <code>integrity</code> mode we can only switch to <code>confidentiality</code> mode, but not back to <code>none</code>:</p>
            <pre><code>ignat@dev:~$ echo none | sudo tee /sys/kernel/security/lockdown
none
tee: /sys/kernel/security/lockdown: Operation not permitted</code></pre>
            <p>Now we can see that even on a stock Debian kernel, which as we discovered above, does not enforce module signatures by default, we cannot load a potentially malicious unsigned kernel module anymore:</p>
            <pre><code>ignat@dev:~$ sudo insmod mymod/mymod.ko
insmod: ERROR: could not insert module mymod/mymod.ko: Operation not permitted</code></pre>
            <p>And the kernel log will helpfully point out that this is due to Lockdown LSM:</p>
            <pre><code>ignat@dev:~$ sudo dmesg | tail -n 1
[21728.820129] Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7</code></pre>
            <p>As we can see, Lockdown LSM helps to tighten the security of a kernel, which otherwise may not have other enforcing bits enabled, like the stock Debian one.</p><p>If you compile your own kernel, you can go one step further and set <a href="https://elixir.bootlin.com/linux/v6.6.17/source/security/lockdown/Kconfig#L33">the initial state of the Lockdown LSM to be more restrictive than none from the start</a>. This is exactly what we did for the Cloudflare production kernel:</p>
            <pre><code>ignat@dev:~$ grep LOCK_DOWN /boot/config-6.6.17-cloudflare-2024.2.9
# CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE is not set
CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY is not set</code></pre>
            
    <div>
      <h2>Conclusion</h2>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>In this post we reviewed some useful Linux kernel security configuration options we use at Cloudflare. This is only a small subset, and there are many more available and even more are being constantly developed, reviewed, and improved by the Linux kernel community. We hope that this post will shed some light on these security features and that, if you haven’t already, you may consider enabling them in your Linux systems.</p>
    <div>
      <h2>Watch on Cloudflare TV</h2>
      <a href="#watch-on-cloudflare-tv">
        
      </a>
    </div>
    <div>
  
</div><p>Tune in for more news, announcements and thought-provoking discussions! Don't miss the full <a href="https://cloudflare.tv/shows/security-week">Security Week hub page</a>.</p> ]]></content:encoded>
            <category><![CDATA[Security Week]]></category>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Security]]></category>
            <guid isPermaLink="false">3ySkqS53T1nhzX61XzJFEG</guid>
            <dc:creator>Ignat Korchagin</dc:creator>
        </item>
        <item>
            <title><![CDATA[The Linux Crypto API for user applications]]></title>
            <link>https://blog.cloudflare.com/the-linux-crypto-api-for-user-applications/</link>
            <pubDate>Thu, 11 May 2023 13:00:58 GMT</pubDate>
            <description><![CDATA[ If you run your software on Linux, the Linux Kernel itself can satisfy all your cryptographic needs! In this post we will explore Linux Crypto API for user applications and try to understand its pros and cons ]]></description>
            <content:encoded><![CDATA[ 
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6o7ZLXKVXmuq5yaRC7sdbe/cef8a48e2dead5815f187b829103622d/Screenshot_2024-08-26_at_6.21.48_PM.png" />
            
            </figure><p>In this post we will explore Linux Crypto API for user applications and try to understand its pros and cons.</p><p>The Linux Kernel Crypto API was introduced in <a href="https://lwn.net/Articles/14197/">October 2002</a>. It was initially designed to satisfy internal needs, mostly for <a href="https://www.cloudflare.com/learning/network-layer/what-is-ipsec/">IPsec</a>. However, in addition to the kernel itself, user space applications can benefit from it.</p><p>If we apply the basic definition of an <a href="https://www.cloudflare.com/learning/security/api/what-is-an-api/">API</a> to our case, we will have the kernel on one side and our application on the other. The application needs to send data, i.e. plaintext or ciphertext, and get encrypted/decrypted text in response from the kernel. To communicate with the kernel we need to make a system call. Also, before starting the data exchange, we need to agree on some cryptographic parameters, at least the selected crypto algorithm and key length. These constraints, along with all supported algorithms, can be found in the <code>/proc/crypto</code> virtual file.</p><p>Below is a short excerpt from my <code>/proc/crypto</code> looking at <code>ctr(aes)</code>. In the examples, we will use the AES cipher in CTR mode, further we will give more details about the algorithm itself.</p>
            <pre><code>name         : ctr(aes)
driver       : ctr(aes-generic)
module       : ctr
priority     : 100
refcnt       : 1
selftest     : passed
internal     : no
type         : skcipher
async        : no
blocksize    : 1
min keysize  : 16
max keysize  : 32
ivsize       : 16
chunksize    : 16
walksize     : 16


name         : ctr(aes)
driver       : ctr(aes-aesni)
module       : ctr
priority     : 300
refcnt       : 1
selftest     : passed
internal     : no
type         : skcipher
async        : no
blocksize    : 1
min keysize  : 16
max keysize  : 32
ivsize       : 16
chunksize    : 16
walksize     : 16


name         : ctr(aes)
driver       : ctr-aes-aesni
module       : aesni_intel
priority     : 400
refcnt       : 1
selftest     : passed
internal     : no
type         : skcipher
async        : yes
blocksize    : 1
min keysize  : 16
max keysize  : 32
ivsize       : 16
chunksize    : 16
walksize     : 16</code></pre>
            <p>In the output above, there are three config blocks. The kernel may provide several implementations of the same algorithm depending on the CPU architecture, available hardware, presence of crypto accelerators etc.</p><p>We can pick the implementation based on the algorithm name or the driver name. The algorithm name is not unique, but the driver name is. If we use the algorithm name, the driver with the highest priority will be chosen for us, which in theory should provide the best cryptographic performance in this context. Let’s see the performance of different implementations of AES-CTR encryption. I use the <a href="https://github.com/smuellerDD/libkcapi">libkcapi library</a>: it’s a lightweight wrapper for the kernel crypto API which also provides built-in speed tests. We will examine <a href="https://github.com/smuellerDD/libkcapi/blob/master/speed-test/cryptoperf-skcipher.c#L228-L238">these tests</a>.</p>
            <pre><code>$ kcapi-speed -c "AES(G) CTR(G) 128" -b 1024 -t 10
AES(G) CTR(G) 128   	|d|	1024 bytes|          	149.80 MB/s|153361 ops/s
AES(G) CTR(G) 128   	|e|	1024 bytes|          	159.76 MB/s|163567 ops/s
 
$ kcapi-speed -c "AES(AESNI) CTR(ASM) 128" -b 1024 -t 10
AES(AESNI) CTR(ASM) 128 |d|	1024 bytes|          	343.10 MB/s|351332 ops/s
AES(AESNI) CTR(ASM) 128 |e|	1024 bytes|         	310.100 MB/s|318425 ops/s
 
$ kcapi-speed -c "AES(AESNI) CTR(G) 128" -b 1024 -t 10
AES(AESNI) CTR(G) 128   |d|	1024 bytes|          	155.37 MB/s|159088 ops/s
AES(AESNI) CTR(G) 128   |e|	1024 bytes|          	172.94 MB/s|177054 ops/s</code></pre>
            <p>Here and later ignore the absolute numbers, as they depend on the environment where the tests were running. Rather look at the relationship between the numbers.</p><p>The <a href="https://en.wikipedia.org/wiki/AES_instruction_set">x86 AES instructions</a> showed the best results, twice as fast vs the generic portable C implementation. As expected, this implementation has the highest priority in the <code>/proc/crypto</code>. We will use only this one later.</p><p>This brief introduction can be rephrased as: “I can ask the kernel to encrypt or decrypt data from my application”. But, why do I need it?</p>
    <div>
      <h2>Why do I need it?</h2>
      <a href="#why-do-i-need-it">
        
      </a>
    </div>
    <p>In our previous blog post <a href="/the-linux-kernel-key-retention-service-and-why-you-should-use-it-in-your-next-application/">Linux Kernel Key Retention Service</a> we talked a lot about cryptographic key protection. We concluded that the best Linux option is to store <a href="https://www.cloudflare.com/learning/ssl/what-is-a-cryptographic-key/">cryptographic keys</a> in the kernel space and restrict the access to a limited number of applications. However, if all our cryptography is processed in user space, potentially damaging code still has access to the raw key material. We have to think wisely about using the key: what part of the code has access to it, don’t log it accidentally, how the open-source libraries manage it and if the memory is purged after using it. We may need to support a dedicated process to not have a key in network-facing code. Thus, many things need to be done for security, and for each application which works with cryptography. And even after all these precautionary measures, the best of the best are subject to bugs and vulnerabilities. <a href="https://en.wikipedia.org/wiki/OpenSSL">OpenSSL</a>, the most known and widely used cryptographic library in user space, <a href="/cloudflare-is-not-affected-by-the-openssl-vulnerabilities-cve-2022-3602-and-cve-2022-37/">has had a few problems in its security</a>.</p><p>Can we move all the cryptography to the kernel and help solve these problems? Looks like it! Our <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7984ceb134bf31aa9a597f10ed52d831d5aede14">recent patch</a> to upstream extended the key types which can be used in symmetric encryption in the Crypto API directly from the Linux Kernel Key Retention Service.</p><p>But nothing is free. There will be some overhead for the system calls and data copying between user and kernel spaces. So, the next question is how fast it is.</p>
    <div>
      <h2>Is it fast?</h2>
      <a href="#is-it-fast">
        
      </a>
    </div>
    <p>To answer this question we need to have some baseline to compare with. OpenSSL would be the best as it’s used all around the Internet. OpenSSL provides a good composite of toolkits, including C-functions, a console utility and various speed tests. For the sake of equality, we will ignore the built-in tests and write our own tests using OpenSSL C-functions. We want the same data to be processed and the same logic parts to be measured in both cases (Kernel versus OpenSSL).</p><p>So, the task: write a benchmark for AES-CTR-128 encrypting data split in chunks. Make implementations for the Kernel Crypto API and OpenSSL.</p>
    <div>
      <h3>About AES-CTR-128</h3>
      <a href="#about-aes-ctr-128">
        
      </a>
    </div>
    <p>AES stands for <a href="https://en.wikipedia.org/wiki/Advanced_Encryption_Standard">Advanced Encryption Standard</a>. It is a block cipher algorithm, which means the whole plaintext is split into blocks and two operations are applied: substitution and permutation. There are two parameters characterizing a block cipher: the block size and the key size. AES processes a block of 128 bits using a key of either 128, 192 or 256 bits. Each 128 bits or 16 bytes block is presented as a 4x4 two-dimensional array (matrix), where one element of the matrix presents one byte of the plaintext. To change the plaintext to ciphertext several rounds of transformation are applied: the bits of the block XORs with a key derived from the main key and substitution with permutation are applied to rows and columns of the matrix. There can be 10, 12 or 14 rounds depending on the key size (the key size determines how many keys can be derived from it).</p><p>AES is a secure cipher, but there is one nuance - the same plaintext/block of text will produce the same result. Look at <a href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_(ECB)">Linux’s mascot Tux</a>. To avoid this, a <a href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation">mode of operation</a> (or just mode) has to be applied. It determines how the text changes, so the same input doesn't result in the same output. Tux was encrypted using ECB mode, there is no text transformation at all. Another mode example is CBC, where the ciphertext from the previously encrypted block is added to the next block, for the first block an initial value (IV) is added. This mode guarantees that for the same input and different IV the output will be different. However, this mode is slow as each block depends on the previous one and so encryption can’t be parallelized. CTR is a counter mode, instead of using previously encrypted blocks it uses a counter and a nonce. A counter is an integer which is incremented for each block. A nonce is just a random number similar to the IV. The nonce, and IV, should be different for each message and can be transferred openly with the encrypted text. So, the title AES-CTR-128 means AES used in CTR mode with the key size of 128 bits.</p>
    <div>
      <h3>Implementing AES-CTR-128 with the Kernel Crypto API</h3>
      <a href="#implementing-aes-ctr-128-with-the-kernel-crypto-api">
        
      </a>
    </div>
    <p>The kernel and user spaces are isolated for security reasons and each time data needs to be transferred between them, it’s copied. In our case, it would add a significant overhead - copying a big bunch of plain or encrypted text to the kernel and back. However, the crypto API supports a zero-copy interface. Instead of transferring the actual data, a file descriptor is passed. But it has a limitation - the maximum size is only <a href="https://www.kernel.org/doc/html/latest/crypto/userspace-if.html#zero-copy-interface">16 pages</a>. So for our tests we picked the number closest to the maximum limit - 63KB (16 pages of 4KB minus 1KB to avoid any potential edge cases).</p><p>The code below is the exact implementation of what is written in the <a href="https://www.kernel.org/doc/html/latest/crypto/userspace-if.html">kernel documentation</a>. Firstly we created a socket of AF_ALG type. The <code>salg_type</code> and <code>salg_name</code> parameters can be taken from the <code>/proc/crypto</code> file. Instead of a generic name we used the driver name <code>ctr-aes-aesni</code>. We might put just a name <code>ctr(aes)</code> and the driver with the highest priority (<code>ctr-aes-aesni</code> in our context) will be picked for us by the Kernel. Further we put the key length and accepted the socket. The IV size is provided before the payload as ancillary data. Constraints of the key and IV sizes can be found in <code>/proc/crypto</code> too.</p><p>Now we are ready to start communication. We excluded all pre-set up steps from the measurements. In a loop we send plaintext for encryption with the flag <code>SPLICE_F_MORE</code> to inform the kernel that more data will be provided. And here in the loop we <code>read</code> the cipher text from the kernel. The last plaintext should be sent without the flag thus saying that we are done, and the kernel can finalize the encryption.</p><p>In favor of brevity, error handling is omitted in both examples.</p><p>kernel.c</p>
            <pre><code>#define _GNU_SOURCE

#include &lt;stdint.h&gt;
#include &lt;string.h&gt;
#include &lt;stdio.h&gt;

#include &lt;unistd.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;time.h&gt;
#include &lt;sys/random.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;linux/if_alg.h&gt;

#define PT_LEN (63 * 1024)
#define CT_LEN PT_LEN
#define IV_LEN 16
#define KEY_LEN 16
#define ITER_COUNT 100000

static uint8_t pt[PT_LEN];
static uint8_t ct[CT_LEN];
static uint8_t key[KEY_LEN];
static uint8_t iv[IV_LEN];

static void time_diff(struct timespec *res, const struct timespec *start, const struct timespec *end)
{
    res-&gt;tv_sec = end-&gt;tv_sec - start-&gt;tv_sec;
    res-&gt;tv_nsec = end-&gt;tv_nsec - start-&gt;tv_nsec;
    if (res-&gt;tv_nsec &lt; 0) {
        res-&gt;tv_sec--;
        res-&gt;tv_nsec += 1000000000;
    }
}

int main(void)
{
    // Fill the test data
    getrandom(key, sizeof(key), GRND_NONBLOCK);
    getrandom(iv, sizeof(iv), GRND_NONBLOCK);
    getrandom(pt, sizeof(pt), GRND_NONBLOCK);

    // Set up AF_ALG socket
    int alg_s, aes_ctr;
    struct sockaddr_alg sa = { .salg_family = AF_ALG };
    strcpy(sa.salg_type, "skcipher");
    strcpy(sa.salg_name, "ctr-aes-aesni");

    alg_s = socket(AF_ALG, SOCK_SEQPACKET, 0);
    bind(alg_s, (const struct sockaddr *)&amp;sa, sizeof(sa));
    setsockopt(alg_s, SOL_ALG, ALG_SET_KEY, key, KEY_LEN);
    aes_ctr = accept(alg_s, NULL, NULL);
    close(alg_s);

    // Set up IV
    uint8_t cmsg_buf[CMSG_SPACE(sizeof(uint32_t)) + CMSG_SPACE(sizeof(struct af_alg_iv) + IV_LEN)] = {0};
    struct msghdr msg = {
	.msg_control = cmsg_buf,
	.msg_controllen = sizeof(cmsg_buf)
    };

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&amp;msg);
    cmsg-&gt;cmsg_len = CMSG_LEN(sizeof(uint32_t));
    cmsg-&gt;cmsg_level = SOL_ALG;
    cmsg-&gt;cmsg_type = ALG_SET_OP;
    *((uint32_t *)CMSG_DATA(cmsg)) = ALG_OP_ENCRYPT;
    
    cmsg = CMSG_NXTHDR(&amp;msg, cmsg);
    cmsg-&gt;cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + IV_LEN);
    cmsg-&gt;cmsg_level = SOL_ALG;
    cmsg-&gt;cmsg_type = ALG_SET_IV;
    ((struct af_alg_iv *)CMSG_DATA(cmsg))-&gt;ivlen = IV_LEN;
    memcpy(((struct af_alg_iv *)CMSG_DATA(cmsg))-&gt;iv, iv, IV_LEN);
    sendmsg(aes_ctr, &amp;msg, 0);

    // Set up pipes for using zero-copying interface
    int pipes[2];
    pipe(pipes);

    struct iovec pt_iov = {
        .iov_base = pt,
        .iov_len = sizeof(pt)
    };

    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &amp;start);
    
    int i;
    for (i = 0; i &lt; ITER_COUNT; i++) {
        vmsplice(pipes[1], &amp;pt_iov, 1, SPLICE_F_GIFT);
        // SPLICE_F_MORE means more data will be coming
        splice(pipes[0], NULL, aes_ctr, NULL, sizeof(pt), SPLICE_F_MORE);
        read(aes_ctr, ct, sizeof(ct));
    }
    vmsplice(pipes[1], &amp;pt_iov, 1, SPLICE_F_GIFT);
    // A final call without SPLICE_F_MORE
    splice(pipes[0], NULL, aes_ctr, NULL, sizeof(pt), 0);
    read(aes_ctr, ct, sizeof(ct));
    
    clock_gettime(CLOCK_MONOTONIC, &amp;end);

    close(pipes[0]);
    close(pipes[1]);
    close(aes_ctr);

    struct timespec diff;
    time_diff(&amp;diff, &amp;start, &amp;end);
    double tput_krn = ((double)ITER_COUNT * PT_LEN) / (diff.tv_sec + (diff.tv_nsec * 0.000000001 ));
    printf("Kernel: %.02f Mb/s\n", tput_krn / (1024 * 1024));
    
    return 0;
}</code></pre>
            <p>Compile and run:</p>
            <pre><code>$ gcc -o kernel kernel.c
$ ./kernel
Kernel: 2112.49 Mb/s</code></pre>
            
    <div>
      <h3>Implementing AES-CTR-128 with OpenSSL</h3>
      <a href="#implementing-aes-ctr-128-with-openssl">
        
      </a>
    </div>
    <p>With OpenSSL everything is straight forward, we just repeated an example from the <a href="https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption#Encrypting_the_message">official documentation</a>.</p><p>openssl.c</p>
            <pre><code>#include &lt;time.h&gt;
#include &lt;sys/random.h&gt;
#include &lt;openssl/evp.h&gt;

#define PT_LEN (63 * 1024)
#define CT_LEN PT_LEN
#define IV_LEN 16
#define KEY_LEN 16
#define ITER_COUNT 100000

static uint8_t pt[PT_LEN];
static uint8_t ct[CT_LEN];
static uint8_t key[KEY_LEN];
static uint8_t iv[IV_LEN];

static void time_diff(struct timespec *res, const struct timespec *start, const struct timespec *end)
{
    res-&gt;tv_sec = end-&gt;tv_sec - start-&gt;tv_sec;
    res-&gt;tv_nsec = end-&gt;tv_nsec - start-&gt;tv_nsec;
    if (res-&gt;tv_nsec &lt; 0) {
        res-&gt;tv_sec--;
        res-&gt;tv_nsec += 1000000000;
    }
}

int main(void)
{
    // Fill the test data
    getrandom(key, sizeof(key), GRND_NONBLOCK);
    getrandom(iv, sizeof(iv), GRND_NONBLOCK);
    getrandom(pt, sizeof(pt), GRND_NONBLOCK);

    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
    EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), NULL, key, iv);

    int outl = sizeof(ct);
    
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &amp;start);

    int i;
    for (i = 0; i &lt; ITER_COUNT; i++) {
        EVP_EncryptUpdate(ctx, ct, &amp;outl, pt, sizeof(pt));
    }
    uint8_t *ct_final = ct + outl;
    outl = sizeof(ct) - outl;
    EVP_EncryptFinal_ex(ctx, ct_final, &amp;outl);

    clock_gettime(CLOCK_MONOTONIC, &amp;end);

    EVP_CIPHER_CTX_free(ctx);

    struct timespec diff;
    time_diff(&amp;diff, &amp;start, &amp;end);
    double tput_ossl = ((double)ITER_COUNT * PT_LEN) / (diff.tv_sec + (diff.tv_nsec * 0.000000001 ));
    printf("OpenSSL: %.02f Mb/s\n", tput_ossl / (1024 * 1024));

    return 0;
}</code></pre>
            <p>Compile and run:</p>
            <pre><code>$ gcc -o openssl openssl.c -lcrypto
$ ./openssl
OpenSSL: 3758.60 Mb/s</code></pre>
            
    <div>
      <h3>Results of OpenSSL vs Crypto API</h3>
      <a href="#results-of-openssl-vs-crypto-api">
        
      </a>
    </div>
    
            <pre><code>OpenSSL: 3758.60 Mb/s
Kernel: 2112.49 Mb/s</code></pre>
            <p>Don’t pay attention to the absolute values, look at the relationship.</p><p>The numbers look pessimistic. But why? Can't the kernel implement AES-CTR similar to OpenSSL? We used <a href="https://github.com/iovisor/bpftrace/blob/master/docs/tutorial_one_liners.md">bpftrace</a> to understand this better. The encryption function is called on the <code>read()</code> system call. Trying to be as close to the encryption code as possible, we put a probe on the <a href="https://elixir.bootlin.com/linux/v5.15.90/source/arch/x86/crypto/aesni-intel_glue.c#L1027">ctr_crypt function</a> instead of the whole <code>read</code> call.</p>
            <pre><code>$ sudo bpftrace -e 'kprobe:ctr_crypt { @start=nsecs; @count+=1; } kretprobe:ctr_crypt /@start!=0/ { @total+=nsecs-@start; }'</code></pre>
            <p>We took the same plaintext, encrypted it in chunks of 63KB and measured how much time it took for both cases to encrypt it with <code>bpftrace</code> attached to the kernel:</p>
            <pre><code>OpenSSL: 1 sec 650532178 nsec
Kernel: 3 sec 120442931 nsec // 3120442931 ns
OpenSSL: 3727.49 Mb/s
Kernel: 1971.63 Mb/s

@total: 2031169756     //  2031169756 / 3120442931 = 0.6509235390339526</code></pre>
            <p>The <code>@total</code> number is output from bpftrace, which tells us how much time the kernel spent in the encryption function. To compare plain kernel encryption vs OpenSSL we need to say how many Mb/s kernel would have done if only encryption had been involved (excluding all system calls and data copy/ manipulation). We need to apply some math:</p><ol><li><p>The correlation between the total time and the time which the kernel spent in the encryption is <code>2031169756 / 3120442931 = 0.6509235390339526</code> or 65%.</p></li><li><p>So throughput would be <code>1971.63 / 0.650923539033952</code> - 3028.97 Mb/s. Comparing this to OpenSSL Mb/s we get <code>3028.97 / 3727.49</code>, so around 81%.</p></li></ol><p>It would be fair to say that <code>bpftrace</code> adds some overhead and our numbers for the kernel are less than they could be. So, we can safely say that while the Kernel Crypto API is two times slower than OpenSSL, the crypto part itself is almost equal.</p>
    <div>
      <h2>Conclusion</h2>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>In this post we reviewed the Linux Kernel Crypto API and its user space interface. We reiterated some security benefits of doing encryption through the Kernel vs using some sort of cryptographic library. We also measured the performance overhead of doing data encryption/decryption through the Kernel Crypto API, confirmed that in-kernel crypto is likely as good as in OpenSSL, but a better user space interface is needed to make Kernel Crypto API as fast as using a cryptographic library. Using Crypto API is a subjective decision depending on your circumstances, it’s a trade-off in speed vs. security.</p> ]]></content:encoded>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Kernel]]></category>
            <guid isPermaLink="false">TZcwKRTxODYQJEEXgDegx</guid>
            <dc:creator>Oxana Kharitonova</dc:creator>
        </item>
        <item>
            <title><![CDATA[The quantum state of a TCP port]]></title>
            <link>https://blog.cloudflare.com/the-quantum-state-of-a-tcp-port/</link>
            <pubDate>Mon, 20 Mar 2023 13:00:00 GMT</pubDate>
            <description><![CDATA[ If I navigate to https://blog.cloudflare.com/, my browser will connect to a remote TCP address from the local IP address assigned to my machine, and a randomly chosen local TCP port. What happens if I then decide to head to another site? ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Have you noticed how simple questions sometimes lead to complex answers? Today we will tackle one such question. Category: our favorite - Linux networking.</p>
    <div>
      <h2>When can two TCP sockets share a local address?</h2>
      <a href="#when-can-two-tcp-sockets-share-a-local-address">
        
      </a>
    </div>
    <p>If I navigate to <a href="/">https://blog.cloudflare.com/</a>, my browser will connect to a remote TCP address, might be 104.16.132.229:443 in this case, from the local IP address assigned to my Linux machine, and a randomly chosen local TCP port, say 192.0.2.42:54321. What happens if I then decide to head to a different site? Is it possible to establish another TCP connection from the same local IP address and port?</p><p>To find the answer let's do a bit of <a href="https://en.wikipedia.org/wiki/Discovery_learning">learning by discovering</a>. We have prepared eight quiz questions. Each will let you discover one aspect of the rules that govern local address sharing between TCP sockets under Linux. Fair warning, it might get a bit mind-boggling.</p><p>Questions are split into two groups by test scenario:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3Fu0occJtMgjz3Rd7xGJCo/9bf63f71e754ccbcf47c1fdd801d8f8f/image4-15.png" />
            
            </figure><p>In the first test scenario, two sockets connect from the same local port to the same remote IP and port. However, the local IP is different for each socket.</p><p>While, in the second scenario, the local IP and port is the same for all sockets, but the remote address, or actually just the IP address, differs.</p><p>In our quiz questions, we will either:</p><ol><li><p>let the OS automatically select the the local IP and/or port for the socket, or</p></li><li><p>we will explicitly assign the local address with <a href="https://man7.org/linux/man-pages/man2/bind.2.html"><code>bind()</code></a> before <a href="https://man7.org/linux/man-pages/man2/connect.2.html"><code>connect()</code></a>’ing the socket; a method also known as <a href="https://idea.popcount.org/2014-04-03-bind-before-connect/">bind-before-connect</a>.</p></li></ol><p>Because we will be examining corner cases in the bind() logic, we need a way to exhaust available local addresses, that is (IP, port) pairs. We could just create lots of sockets, but it will be easier to <a href="https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html?#ip-variables">tweak the system configuration</a> and pretend that there is just one ephemeral local port, which the OS can assign to sockets:</p><p><code>sysctl -w net.ipv4.ip_local_port_range='60000 60000'</code></p><p>Each quiz question is a short Python snippet. Your task is to predict the outcome of running the code. Does it succeed? Does it fail? If so, what fails? Asking ChatGPT is not allowed ?</p><p>There is always a common setup procedure to keep in mind. We will omit it from the quiz snippets to keep them short:</p>
            <pre><code>from os import system
from socket import *

# Missing constants
IP_BIND_ADDRESS_NO_PORT = 24

# Our network namespace has just *one* ephemeral port
system("sysctl -w net.ipv4.ip_local_port_range='60000 60000'")

# Open a listening socket at *:1234. We will connect to it.
ln = socket(AF_INET, SOCK_STREAM)
ln.bind(("", 1234))
ln.listen(SOMAXCONN)</code></pre>
            <p>With the formalities out of the way, let us begin. Ready. Set. Go!</p>
    <div>
      <h3>Scenario #1: When the local IP is unique, but the local port is the same</h3>
      <a href="#scenario-1-when-the-local-ip-is-unique-but-the-local-port-is-the-same">
        
      </a>
    </div>
    <p>In Scenario #1 we connect two sockets to the same remote address - 127.9.9.9:1234. The sockets will use different local IP addresses, but is it enough to share the local port?</p>
<table>
<thead>
  <tr>
    <th><span>local IP</span></th>
    <th><span>local port</span></th>
    <th><span>remote IP</span></th>
    <th><span>remote port</span></th>
  </tr>
</thead>
<tbody>
  <tr>
    <td><span>unique</span></td>
    <td><span>same</span></td>
    <td><span>same</span></td>
    <td><span>same</span></td>
  </tr>
  <tr>
    <td><span>127.0.0.1<br />127.1.1.1<br />127.2.2.2</span></td>
    <td><span>60_000</span></td>
    <td><span>127.9.9.9</span></td>
    <td><span>1234</span></td>
  </tr>
</tbody>
</table>
    <div>
      <h3>Quiz #1</h3>
      <a href="#quiz-1">
        
      </a>
    </div>
    <p>On the local side, we bind two sockets to distinct, explicitly specified IP addresses. We will allow the OS to select the local port. Remember: our local ephemeral port range contains just one port (60,000).</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.bind(('127.1.1.1', 0))
s1.connect(('127.9.9.9', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.bind(('127.2.2.2', 0))
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_1.py">Answer #1</a></p>
    <div>
      <h3>Quiz #2</h3>
      <a href="#quiz-2">
        
      </a>
    </div>
    <p>Here, the setup is almost identical as before. However, we ask the OS to select the local IP address and port for the first socket. Do you think the result will differ from the previous question?</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.connect(('127.9.9.9', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.bind(('127.2.2.2', 0))
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_2.py">Answer #2</a></p>
    <div>
      <h3>Quiz #3</h3>
      <a href="#quiz-3">
        
      </a>
    </div>
    <p>This quiz question is just like  the one above. We just changed the ordering. First, we connect a socket from an explicitly specified local address. Then we ask the system to select a local address for us. Obviously, such an ordering change should not make any difference, right?</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.bind(('127.1.1.1', 0))
s1.connect(('127.9.9.9', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_3.py">Answer #3</a></p>
    <div>
      <h3>Scenario #2: When the local IP and port are the same, but the remote IP differs</h3>
      <a href="#scenario-2-when-the-local-ip-and-port-are-the-same-but-the-remote-ip-differs">
        
      </a>
    </div>
    <p>In Scenario #2 we reverse our setup. Instead of multiple local IP's and one remote address, we now have one local address <code>127.0.0.1:60000</code> and two distinct remote addresses. The question remains the same - can two sockets share the local port? Reminder: ephemeral port range is still of size one.</p>
<table>
<thead>
  <tr>
    <th><span>local IP</span></th>
    <th><span>local port</span></th>
    <th><span>remote IP</span></th>
    <th><span>remote port</span></th>
  </tr>
</thead>
<tbody>
  <tr>
    <td><span>same</span></td>
    <td><span>same</span></td>
    <td><span>unique</span></td>
    <td><span>same</span></td>
  </tr>
  <tr>
    <td><span>127.0.0.1</span></td>
    <td><span>60_000</span></td>
    <td><span>127.8.8.8<br />127.9.9.9</span></td>
    <td><span>1234</span></td>
  </tr>
</tbody>
</table>
    <div>
      <h3>Quiz #4</h3>
      <a href="#quiz-4">
        
      </a>
    </div>
    <p>Let’s start from the basics. We <code>connect()</code> to two distinct remote addresses. This is a warm up ?</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.connect(('127.8.8.8', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_4.py">Answer #4</a></p>
    <div>
      <h3>Quiz #5</h3>
      <a href="#quiz-5">
        
      </a>
    </div>
    <p>What if we <code>bind()</code> to a local IP explicitly but let the OS select the port - does anything change?</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.bind(('127.0.0.1', 0))
s1.connect(('127.8.8.8', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.bind(('127.0.0.1', 0))
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_5.py">Answer #5</a></p>
    <div>
      <h3>Quiz #6</h3>
      <a href="#quiz-6">
        
      </a>
    </div>
    <p>This time we explicitly specify the local address and port. Sometimes there is a need to specify the local port.</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.bind(('127.0.0.1', 60_000))
s1.connect(('127.8.8.8', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.bind(('127.0.0.1', 60_000))
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_6.py">Answer #6</a></p>
    <div>
      <h3>Quiz #7</h3>
      <a href="#quiz-7">
        
      </a>
    </div>
    <p>Just when you thought it couldn’t get any weirder, we add <a href="https://manpages.debian.org/unstable/manpages/socket.7.en.html#SO_REUSEADDR"><code>SO_REUSEADDR</code></a> into the mix.</p><p>First, we ask the OS to allocate a local address for us. Then we explicitly bind to the same local address, which we know the OS must have assigned to the first socket. We enable local address reuse for both sockets. Is this allowed?</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s1.connect(('127.8.8.8', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s2.bind(('127.0.0.1', 60_000))
s2.connect(('127.9.9.9', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_7.py">Answer #7</a></p>
    <div>
      <h3>Quiz #8</h3>
      <a href="#quiz-8">
        
      </a>
    </div>
    <p>Finally, a cherry on top. This is Quiz #7 but in reverse. Common sense dictates that the outcome should be the same, but is it?</p>
            <pre><code>s1 = socket(AF_INET, SOCK_STREAM)
s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s1.bind(('127.0.0.1', 60_000))
s1.connect(('127.9.9.9', 1234))
s1.getsockname(), s1.getpeername()

s2 = socket(AF_INET, SOCK_STREAM)
s2.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s2.connect(('127.8.8.8', 1234))
s2.getsockname(), s2.getpeername()</code></pre>
            <p>GOTO <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/quiz_8.py">Answer #8</a></p>
    <div>
      <h2>The secret tri-state life of a local TCP port</h2>
      <a href="#the-secret-tri-state-life-of-a-local-tcp-port">
        
      </a>
    </div>
    <p>Is it all clear now? Well, probably no. It feels like reverse engineering a black box. So what is happening behind the scenes? Let's take a look.</p><p>Linux tracks all TCP <b>ports</b> in use in a hash table named <a href="https://elixir.bootlin.com/linux/v6.2/source/include/net/inet_hashtables.h#L166">bhash</a>. Not to be confused with with <a href="https://elixir.bootlin.com/linux/v6.2/source/include/net/inet_hashtables.h#L156">ehash</a> table, which tracks <b>sockets</b> with both local and remote address already assigned.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3OJ1M8Zu9lgEZoEJdJCNgr/d50e8f331dc994366b2ff749ed10b519/Untitled.png" />
            
            </figure><p>Each hash table entry points to a chain of so-called bind buckets, which group together sockets which share a local port. To be precise, sockets are grouped into buckets by:</p><ul><li><p>the <a href="https://man7.org/linux/man-pages/man7/network_namespaces.7.html">network namespace</a> they belong to, and</p></li><li><p>the <a href="https://docs.kernel.org/networking/vrf.html">VRF</a> device they are bound to, and</p></li><li><p>the local port number they are bound to.</p></li></ul><p>But in the simplest possible setup - single network namespace, no VRFs - we can say that sockets in a bind bucket are grouped by their local port number.</p><p>The set of sockets in each bind bucket, that is sharing a local port, is backed by a linked list of named owners.</p><p>When we ask the kernel to assign a local address to a socket, its task is to check for a conflict with any existing socket. That is because a local port number can be shared only <a href="https://elixir.bootlin.com/linux/v6.2/source/include/net/inet_hashtables.h#L43">under some conditions</a>:</p>
            <pre><code>/* There are a few simple rules, which allow for local port reuse by
 * an application.  In essence:
 *
 *   1) Sockets bound to different interfaces may share a local port.
 *      Failing that, goto test 2.
 *   2) If all sockets have sk-&gt;sk_reuse set, and none of them are in
 *      TCP_LISTEN state, the port may be shared.
 *      Failing that, goto test 3.
 *   3) If all sockets are bound to a specific inet_sk(sk)-&gt;rcv_saddr local
 *      address, and none of them are the same, the port may be
 *      shared.
 *      Failing this, the port cannot be shared.
 *
 * The interesting point, is test #2.  This is what an FTP server does
 * all day.  To optimize this case we use a specific flag bit defined
 * below.  As we add sockets to a bind bucket list, we perform a
 * check of: (newsk-&gt;sk_reuse &amp;&amp; (newsk-&gt;sk_state != TCP_LISTEN))
 * As long as all sockets added to a bind bucket pass this test,
 * the flag bit will be set.
 * ...
 */</code></pre>
            <p>The comment above hints that the kernel tries to optimize for the happy case of no conflict. To this end the bind bucket holds additional state which aggregates the properties of the sockets it holds:</p>
            <pre><code>struct inet_bind_bucket {
        /* ... */
        signed char          fastreuse;
        signed char          fastreuseport;
        kuid_t               fastuid;
#if IS_ENABLED(CONFIG_IPV6)
        struct in6_addr      fast_v6_rcv_saddr;
#endif
        __be32               fast_rcv_saddr;
        unsigned short       fast_sk_family;
        bool                 fast_ipv6_only;
        /* ... */
};</code></pre>
            <p>Let's focus our attention just on the first aggregate property - <code>fastreuse</code>. It has existed since, now prehistoric, Linux 2.1.90pre1. Initially in the form of a <a href="https://git.kernel.org/pub/scm/linux/kernel/git/history/history.git/tree/include/net/tcp.h?h=2.1.90pre1&amp;id=9d11a5176cc5b9609542b1bd5a827b8618efe681#n76">bit flag</a>, as the comment says, only to evolve to a byte-sized field over time.</p><p>The other six fields came on much later with the introduction of <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=da5e36308d9f7151845018369148201a5d28b46d"><code>SO_REUSEPORT</code> in Linux 3.9</a>. Because they play a role only when there are sockets with the <a href="https://manpages.debian.org/unstable/manpages/socket.7.en.html#SO_REUSEPORT"><code>SO_REUSEPORT</code></a> flag set. We are going to ignore them today.</p><p>Whenever the Linux kernel needs to bind a socket to a local port, it first has to look for the bind bucket for that port. What makes life a bit more complicated is the fact that the search for a TCP bind bucket exists in two places in the kernel. The bind bucket lookup can happen early - <code>at bind()</code> time - or late - <code>at connect()</code> - time. Which one gets called depends on how the connected socket has been set up:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3KOEGkTF7HEH7qku86bufY/8560316429d383b25add8c9f0b2bab3b/image5-5.png" />
            
            </figure><p>However, whether we land in <a href="https://elixir.bootlin.com/linux/v6.2/source/net/ipv4/inet_connection_sock.c#L486"><code>inet_csk_get_port</code></a> or <a href="https://elixir.bootlin.com/linux/v6.2/source/net/ipv4/inet_hashtables.c#L992"><code>__inet_hash_connect</code></a>, we always end up walking the bucket chain in the bhash looking for the bucket with a matching port number. The bucket might already exist or we might have to create it first. But once it exists, its fastreuse field is in one of three possible states: <code>-1</code>, <code>0</code>, or <code>+1</code>. As if Linux developers were inspired by <a href="https://en.wikipedia.org/wiki/Triplet_state">quantum mechanics</a>.</p><p>That state reflects two aspects of the bind bucket:</p><ol><li><p>What sockets are in the bucket?</p></li><li><p>When can the local port be shared?</p></li></ol><p>So let us try to decipher the three possible fastreuse states then, and what they mean in each case.</p><p>First, what does the fastreuse property say about the owners of the bucket, that is the sockets using that local port?</p>
<table>
<thead>
  <tr>
    <th><span>fastreuse is</span></th>
    <th><span>owners list contains</span></th>
  </tr>
</thead>
<tbody>
  <tr>
    <td><span>-1</span></td>
    <td><span>sockets connect()'ed from an ephemeral port</span></td>
  </tr>
  <tr>
    <td><span>0</span></td>
    <td><span>sockets bound without SO_REUSEADDR</span></td>
  </tr>
  <tr>
    <td><span>+1</span></td>
    <td><span>sockets bound with SO_REUSEADDR</span></td>
  </tr>
</tbody>
</table><p>While this is not the whole truth, it is close enough for now. We will soon get to the bottom of it.</p><p>When it comes port sharing, the situation is far less straightforward:</p>
<table>
<thead>
  <tr>
    <th><span>Can I … when …</span></th>
    <th><span>fastreuse = -1</span></th>
    <th><span>fastreuse = 0</span></th>
    <th><span>fastreuse = +1</span></th>
  </tr>
</thead>
<tbody>
  <tr>
    <td><span>bind() to the same port (ephemeral or specified)</span></td>
    <td><span>yes</span><span> IFF local IP is unique ①</span></td>
    <td><span>← </span><a href="https://en.wiktionary.org/wiki/idem#Pronoun"><span>idem</span></a></td>
    <td><span>← idem</span></td>
  </tr>
  <tr>
    <td><span>bind() to the specific port with SO_REUSEADDR</span></td>
    <td><span>yes</span><span> IFF local IP is unique OR conflicting socket uses SO_REUSEADDR ①</span></td>
    <td><span>← idem</span></td>
    <td><span>yes</span><span> ②</span></td>
  </tr>
  <tr>
    <td><span>connect() from the same ephemeral port to the same remote (IP, port)</span></td>
    <td><span>yes</span><span> IFF local IP unique ③</span></td>
    <td><span>no</span><span> ③</span></td>
    <td><span>no</span><span> ③</span></td>
  </tr>
  <tr>
    <td><span>connect() from the same ephemeral port to a unique remote (IP, port)</span></td>
    <td><span>yes</span><span> ③</span></td>
    <td><span>no</span><span> ③</span></td>
    <td><span>no</span><span> ③</span></td>
  </tr>
</tbody>
</table><p>① Determined by <a href="https://elixir.bootlin.com/linux/v6.2/source/net/ipv4/inet_connection_sock.c#L214"><code>inet_csk_bind_conflict()</code></a> called from <code>inet_csk_get_port()</code> (specific port bind) or <code>inet_csk_get_port()</code> → <code>inet_csk_find_open_port()</code> (ephemeral port bind).</p><p>② Because <code>inet_csk_get_port()</code> <a href="https://elixir.bootlin.com/linux/v6.2/source/net/ipv4/inet_connection_sock.c#L531">skips conflict check</a> for <code>fastreuse == 1 buckets</code>.</p><p>③ Because <code>inet_hash_connect()</code> → <code>__inet_hash_connect()</code> <a href="https://elixir.bootlin.com/linux/v6.2/source/net/ipv4/inet_hashtables.c#L1062">skips buckets</a> with <code>fastreuse != -1</code>.</p><p>While it all looks rather complicated at first sight, we can distill the table above into a few statements that hold true, and are a bit easier to digest:</p><ul><li><p><code>bind()</code>, or early local address allocation, always succeeds if there is no local IP address conflict with any existing socket,</p></li><li><p><code>connect()</code>, or late local address allocation, always fails when TCP bind bucket for a local port is in any state other than <code>fastreuse = -1</code>,</p></li><li><p><code>connect()</code> only succeeds if there is no local and remote address conflict,</p></li><li><p><code>SO_REUSEADDR</code> socket option allows local address sharing, if all conflicting sockets also use it (and none of them is in the listening state).</p></li></ul>
    <div>
      <h3>This is crazy. I don’t believe you.</h3>
      <a href="#this-is-crazy-i-dont-believe-you">
        
      </a>
    </div>
    <p>Fortunately, you don't have to. With <a href="https://drgn.readthedocs.io/en/latest/index.html">drgn</a>, the programmable debugger, we can examine the bind bucket state on a live kernel:</p>
            <pre><code>#!/usr/bin/env drgn

"""
dump_bhash.py - List all TCP bind buckets in the current netns.

Script is not aware of VRF.
"""

import os

from drgn.helpers.linux.list import hlist_for_each, hlist_for_each_entry
from drgn.helpers.linux.net import get_net_ns_by_fd
from drgn.helpers.linux.pid import find_task


def dump_bind_bucket(head, net):
    for tb in hlist_for_each_entry("struct inet_bind_bucket", head, "node"):
        # Skip buckets not from this netns
        if tb.ib_net.net != net:
            continue

        port = tb.port.value_()
        fastreuse = tb.fastreuse.value_()
        owners_len = len(list(hlist_for_each(tb.owners)))

        print(
            "{:8d}  {:{sign}9d}  {:7d}".format(
                port,
                fastreuse,
                owners_len,
                sign="+" if fastreuse != 0 else " ",
            )
        )


def get_netns():
    pid = os.getpid()
    task = find_task(prog, pid)
    with open(f"/proc/{pid}/ns/net") as f:
        return get_net_ns_by_fd(task, f.fileno())


def main():
    print("{:8}  {:9}  {:7}".format("TCP-PORT", "FASTREUSE", "#OWNERS"))

    tcp_hashinfo = prog.object("tcp_hashinfo")
    net = get_netns()

    # Iterate over all bhash slots
    for i in range(0, tcp_hashinfo.bhash_size):
        head = tcp_hashinfo.bhash[i].chain
        # Iterate over bind buckets in the slot
        dump_bind_bucket(head, net)


main()</code></pre>
            <p>Let's take <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/dump_bhash.py">this script</a> for a spin and try to confirm what <i>Table 1</i> claims to be true. Keep in mind that to produce the <code>ipython --classic</code> session snippets below I've used the same setup as for the quiz questions.</p><p>Two connected sockets sharing ephemeral port 60,000:</p>
            <pre><code>&gt;&gt;&gt; s1 = socket(AF_INET, SOCK_STREAM)
&gt;&gt;&gt; s1.connect(('127.1.1.1', 1234))
&gt;&gt;&gt; s2 = socket(AF_INET, SOCK_STREAM)
&gt;&gt;&gt; s2.connect(('127.2.2.2', 1234))
&gt;&gt;&gt; !./dump_bhash.py
TCP-PORT  FASTREUSE  #OWNERS
    1234          0        3
   60000         -1        2
&gt;&gt;&gt;</code></pre>
            <p>Two bound sockets reusing port 60,000:</p>
            <pre><code>&gt;&gt;&gt; s1 = socket(AF_INET, SOCK_STREAM)
&gt;&gt;&gt; s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
&gt;&gt;&gt; s1.bind(('127.1.1.1', 60_000))
&gt;&gt;&gt; s2 = socket(AF_INET, SOCK_STREAM)
&gt;&gt;&gt; s2.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
&gt;&gt;&gt; s2.bind(('127.1.1.1', 60_000))
&gt;&gt;&gt; !./dump_bhash.py
TCP-PORT  FASTREUSE  #OWNERS
    1234          0        1
   60000         +1        2
&gt;&gt;&gt; </code></pre>
            <p>A mix of bound sockets with and without REUSEADDR sharing port 60,000:</p>
            <pre><code>&gt;&gt;&gt; s1 = socket(AF_INET, SOCK_STREAM)
&gt;&gt;&gt; s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
&gt;&gt;&gt; s1.bind(('127.1.1.1', 60_000))
&gt;&gt;&gt; !./dump_bhash.py
TCP-PORT  FASTREUSE  #OWNERS
    1234          0        1
   60000         +1        1
&gt;&gt;&gt; s2 = socket(AF_INET, SOCK_STREAM)
&gt;&gt;&gt; s2.bind(('127.2.2.2', 60_000))
&gt;&gt;&gt; !./dump_bhash.py
TCP-PORT  FASTREUSE  #OWNERS
    1234          0        1
   60000          0        2
&gt;&gt;&gt;</code></pre>
            <p>With such tooling, proving that <i>Table 2</i> holds true is just a matter of writing a bunch of <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/test_fastreuse.py">exploratory tests</a>.</p><p>But what has happened in that last snippet? The bind bucket has clearly transitioned from one fastreuse state to another. This is what <i>Table 1</i> fails to capture. And it means that we still don't have the full picture.</p><p>We have yet to find out when the bucket's fastreuse state can change. This calls for a state machine.</p>
    <div>
      <h3>Das State Machine</h3>
      <a href="#das-state-machine">
        
      </a>
    </div>
    <p>As we have just seen, a bind bucket does not need to stay in the initial fastreuse state throughout its lifetime. Adding sockets to the bucket can trigger a state change. As it turns out, it can only transition into <code>fastreuse = 0</code>, if we happen to bind() a socket that:</p><ol><li><p>doesn't conflict existing owners, and</p></li><li><p>doesn't have the <code>SO_REUSEADDR</code> option enabled.</p></li></ol>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7awSZQJp00EDbGbvcpBNv9/cca9b2c49a3db5f3d1db62ce142ac46a/Untitled--1-.png" />
            
            </figure><p>And while we could have figured it all out by carefully reading the code in <a href="https://elixir.bootlin.com/linux/v6.2/source/net/ipv4/inet_connection_sock.c#L431"><code>inet_csk_get_port → inet_csk_update_fastreuse</code></a>, it certainly doesn't hurt to confirm our understanding with <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/test_fastreuse_states.py">a few more tests</a>.</p><p>Now that we have the full picture, this begs the question...</p>
    <div>
      <h3>Why are you telling me all this?</h3>
      <a href="#why-are-you-telling-me-all-this">
        
      </a>
    </div>
    <p>Firstly, so that the next time <code>bind()</code> syscall rejects your request with <code>EADDRINUSE</code>, or <code>connect()</code> refuses to cooperate by throwing the <code>EADDRNOTAVAIL</code> error, you will know what is happening, or at least have the tools to find out.</p><p>Secondly, because we have previously <a href="/how-to-stop-running-out-of-ephemeral-ports-and-start-to-love-long-lived-connections/">advertised a technique</a> for opening connections from a specific range of ports which involves bind()'ing sockets with the SO_REUSEADDR option. What we did not realize back then, is that there exists <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2023-03-quantum-state-of-tcp-port/test_fastreuse.py#L300">a corner case</a> when the same port can't be shared with the regular, <code>connect()</code>'ed sockets. While that is not a deal-breaker, it is good to understand the consequences.</p><p>To make things better, we have worked with the Linux community to extend the kernel API with a new socket option that lets the user <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=91d0b78c5177">specify the local port range</a>. The new option will be available in the upcoming Linux 6.3. With it we no longer have to resort to bind()-tricks. This makes it possible to yet again share a local port with regular <code>connect()</code>'ed sockets.</p>
    <div>
      <h2>Closing thoughts</h2>
      <a href="#closing-thoughts">
        
      </a>
    </div>
    <p>Today we posed a relatively straightforward question - when can two TCP sockets share a local address? - and worked our way towards an answer. An answer that is too complex to compress it into a single sentence. What is more, it's not even the full answer. After all, we have decided to ignore the existence of the SO_REUSEPORT feature, and did not consider conflicts with TCP listening sockets.</p><p>If there is a simple takeaway, though, it is that bind()'ing a socket can have tricky consequences. When using bind() to select an egress IP address, it is best to combine it with IP_BIND_ADDRESS_NO_PORT socket option, and leave the port assignment to the kernel. Otherwise we might unintentionally block local TCP ports from being reused.</p><p>It is too bad that the same advice does not apply to UDP, where IP_BIND_ADDRESS_NO_PORT does not really work today. But that is another story.</p><p>Until next time ?.</p><p>If you enjoy scratching your head while reading the Linux kernel source code, <a href="https://www.cloudflare.com/careers/">we are hiring</a>.</p> ]]></content:encoded>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <guid isPermaLink="false">74q2VGXmBazVsIZUpVUD8o</guid>
            <dc:creator>Jakub Sitnicki</dc:creator>
        </item>
        <item>
            <title><![CDATA[The Linux Kernel Key Retention Service and why you should use it in your next application]]></title>
            <link>https://blog.cloudflare.com/the-linux-kernel-key-retention-service-and-why-you-should-use-it-in-your-next-application/</link>
            <pubDate>Mon, 28 Nov 2022 14:57:20 GMT</pubDate>
            <description><![CDATA[ Many leaks happen because of software bugs and security vulnerabilities. In this post we will learn how the Linux kernel can help protect cryptographic keys from a whole class of potential security vulnerabilities: memory access violations. ]]></description>
            <content:encoded><![CDATA[ <p><i></i></p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2LKKOrcwGlRDUpSWhMkxe4/406961181fbe307f99573e8fbc13a0b0/unnamed-5.png" />
            
            </figure><p>We want our digital data to be safe. We want to visit websites, send bank details, type passwords, sign documents online, login into remote computers, encrypt data before storing it in databases and be sure that nobody can tamper with it. Cryptography can provide a high degree of data security, but we need to protect cryptographic keys.</p><p>At the same time, we can’t have our key written somewhere securely and just access it occasionally. Quite the opposite, it’s involved in every request where we do crypto-operations. If a site supports TLS, then the private key is used to establish each connection.</p><p>Unfortunately cryptographic keys sometimes leak and when it happens, it is a big problem. Many leaks happen because of software bugs and security vulnerabilities. In this post we will learn how the Linux kernel can help protect cryptographic keys from a whole class of potential security vulnerabilities: memory access violations.</p>
    <div>
      <h3>Memory access violations</h3>
      <a href="#memory-access-violations">
        
      </a>
    </div>
    <p>According to the <a href="https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/3215760/nsa-releases-guidance-on-how-to-protect-against-software-memory-safety-issues/">NSA</a>, around 70% of vulnerabilities in both Microsoft's and Google's code were related to memory safety issues. One of the consequences of incorrect memory accesses is leaking security data (including cryptographic keys). Cryptographic keys are just some (mostly random) data stored in memory, so they may be subject to memory leaks like any other in-memory data. The below example shows how a cryptographic key may accidentally leak via stack memory reuse:</p><p>broken.c</p>
            <pre><code>#include &lt;stdio.h&gt;
#include &lt;stdint.h&gt;

static void encrypt(void)
{
    uint8_t key[] = "hunter2";
    printf("encrypting with super secret key: %s\n", key);
}

static void log_completion(void)
{
    /* oh no, we forgot to init the msg */
    char msg[8];
    printf("not important, just fyi: %s\n", msg);
}

int main(void)
{
    encrypt();
    /* notify that we're done */
    log_completion();
    return 0;
}</code></pre>
            <p>Compile and run our program:</p>
            <pre><code>$ gcc -o broken broken.c
$ ./broken 
encrypting with super secret key: hunter2
not important, just fyi: hunter2</code></pre>
            <p>Oops, we printed the secret key in the “fyi” logger instead of the intended log message! There are two problems with the code above:</p><ul><li><p>we didn’t securely destroy the key in our pseudo-encryption function (by overwriting the key data with zeroes, for example), when we finished using it</p></li><li><p>our buggy logging function has access to any memory within our process</p></li></ul><p>And while we can probably easily fix the first problem with some additional code, the second problem is the inherent result of how software runs inside the operating system.</p><p>Each process is given a block of contiguous virtual memory by the operating system. It allows the kernel to share limited computer resources among several simultaneously running processes. This approach is called <a href="https://en.wikipedia.org/wiki/Virtual_memory">virtual memory management</a>. Inside the virtual memory a process has its own address space and doesn’t have access to the memory of other processes, but it can access any memory within its address space. In our example we are interested in a piece of process memory called the stack.</p><p>The stack consists of stack frames. A stack frame is dynamically allocated space for the currently running function. It contains the function’s local variables, arguments and return address. When compiling a function the compiler calculates how much memory needs to be allocated and requests a stack frame of this size. Once a function finishes execution the stack frame is marked as free and can be used again. A stack frame is a logical block, it doesn’t provide any boundary checks, it’s not erased, just marked as free. Additionally, the virtual memory is a contiguous block of addresses. Both of these statements give the possibility for malware/buggy code to access data from anywhere within virtual memory.</p><p>The stack of our program <code>broken.c</code> will look like:</p><img src="https://imagedelivery.net/52R3oh4H-57qkVChwuo3Ag/3526edee-ce7e-4f98-a2bf-ff1efd2fc800/public" /><p>At the beginning we have a stack frame of the main function. Further, the <code>main()</code> function calls <code>encrypt()</code> which will be placed on the stack immediately below the <code>main()</code> (the code stack grows downwards). Inside <code>encrypt()</code> the compiler requests 8 bytes for the <code>key</code> variable (7 bytes of data + C-null character). When <code>encrypt()</code> finishes execution, the same memory addresses are taken by <code>log_completion()</code>. Inside the <code>log_completion()</code> the compiler allocates eight bytes for the <code>msg</code> variable. Accidentally, it was put on the stack at the same place where our private key was stored before. The memory for <code>msg</code> was only allocated, but not initialized, the data from the previous function left as is.</p><p>Additionally, to the code bugs, programming languages provide unsafe functions known for the safe-memory vulnerabilities. For example, for C such functions are <code>printf()</code>, <code>strcpy()</code>, <code>gets()</code>. The function <code>printf()</code> doesn’t check how many arguments must be passed to replace all placeholders in the format string. The function arguments are placed on the stack above the function stack frame, <code>printf()</code> fetches arguments according to the numbers and type of placeholders, easily going off its arguments and accessing data from the stack frame of the previous function.</p><p>The NSA advises us to use safety-memory languages like Python, Go, Rust. But will it completely protect us?</p><p>The Python compiler will definitely check boundaries in many cases for you and notify with an error:</p>
            <pre><code>&gt;&gt;&gt; print("x: {}, y: {}, {}".format(1, 2))
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
IndexError: Replacement index 2 out of range for positional args tuple</code></pre>
            <p>However, this is a quote from one of 36 (for now) <a href="https://www.cvedetails.com/vulnerability-list/vendor_id-10210/opov-1/Python.html">vulnerabilities</a>:</p><blockquote><p><i>Python 2.7.14 is vulnerable to a Heap-Buffer-Overflow as well as a Heap-Use-After-Free.</i></p></blockquote><p>Golang has its own list of <a href="https://www.cvedetails.com/vulnerability-list/vendor_id-14185/opov-1/Golang.html">overflow vulnerabilities</a>, and has an <a href="https://pkg.go.dev/unsafe">unsafe package</a>. The name of the package speaks for itself, usual rules and checks don’t work inside this package.</p>
    <div>
      <h3>Heartbleed</h3>
      <a href="#heartbleed">
        
      </a>
    </div>
    <p>In 2014, the Heartbleed bug was discovered. The (at the time) most used cryptography library OpenSSL leaked private keys. We experienced it <a href="/answering-the-critical-question-can-you-get-private-ssl-keys-using-heartbleed/">too</a>.</p>
    <div>
      <h3>Mitigation</h3>
      <a href="#mitigation">
        
      </a>
    </div>
    <p>So memory bugs are a fact of life, and we can’t really fully protect ourselves from them. But, given the fact that cryptographic keys are much more valuable than the other data, can we do better protecting the keys at least?</p><p>As we already said, a memory address space is normally associated with a process. And two different processes don’t share memory by default, so are naturally isolated from each other. Therefore, a potential memory bug in one of the processes will not accidentally leak a cryptographic key from another process. The security of ssh-agent builds on this principle. There are always two processes involved: a client/requester and the <a href="https://linux.die.net/man/1/ssh-agent">agent</a>.</p><blockquote><p><i>The agent will never send a private key over its request channel. Instead, operations that require a private key will be performed by the agent, and the result will be returned to the requester. This way, private keys are not exposed to clients using the agent.</i></p></blockquote><p>A requester is usually a network-facing process and/or processing untrusted input. Therefore, the requester is much more likely to be susceptible to memory-related vulnerabilities but in this scheme it would never have access to cryptographic keys (because keys reside in a separate process address space) and, thus, can never leak them.</p><p>At Cloudflare, we employ the same principle in <a href="/heartbleed-revisited/">Keyless SSL</a>. Customer private keys are stored in an isolated environment and protected from Internet-facing connections.</p>
    <div>
      <h3>Linux Kernel Key Retention Service</h3>
      <a href="#linux-kernel-key-retention-service">
        
      </a>
    </div>
    <p>The client/requester and agent approach provides better protection for secrets or cryptographic keys, but it brings some drawbacks:</p><ul><li><p>we need to develop and maintain two different programs instead of one</p></li><li><p>we also need to design a well-defined-interface for communication between the two processes</p></li><li><p>we need to implement the communication support between two processes (Unix sockets, shared memory, etc.)</p></li><li><p>we might need to authenticate and support ACLs between the processes, as we don’t want any requester on our system to be able to use our cryptographic keys stored inside the agent</p></li><li><p>we need to ensure the agent process is up and running, when working with the client/requester process</p></li></ul><p>What if we replace the agent process with the Linux kernel itself?</p><ul><li><p>it is already running on our system (otherwise our software would not work)</p></li><li><p>it has a well-defined interface for communication (system calls)</p></li><li><p>it can enforce various ACLs on kernel objects</p></li><li><p>and it runs in a separate address space!</p></li></ul><p>Fortunately, the <a href="https://www.kernel.org/doc/html/v6.0/security/keys/core.html">Linux Kernel Key Retention Service</a> can perform all the functions of a typical agent process and probably even more!</p><p>Initially it was designed for kernel services like dm-crypt/ecryptfs, but later was opened to use by userspace programs. It gives us some advantages:</p><ul><li><p>the keys are stored outside the process address space</p></li><li><p>the well-defined-interface and the communication layer is implemented via syscalls</p></li><li><p>the keys are kernel objects and so have associated permissions and ACLs</p></li><li><p>the keys lifecycle can be implicitly bound to the process lifecycle</p></li></ul><p>The Linux Kernel Key Retention Service operates with two types of entities: keys and keyrings, where a keyring is a key of a special type. If we put it into analogy with files and directories, we can say a key is a file and a keyring is a directory. Moreover, they represent a key hierarchy similar to a filesystem tree hierarchy: keyrings reference keys and other keyrings, but only keys can hold the actual cryptographic material similar to files holding the actual data.</p><p>Keys have types. The type of key determines which operations can be performed over the keys. For example, keys of user and logon types can hold arbitrary blobs of data, but logon keys can never be read back into userspace, they are exclusively used by the in-kernel services.</p><p>For the purposes of using the kernel instead of an agent process the most interesting type of keys is the <a href="https://man7.org/linux/man-pages/man7/asymmetric.7.html">asymmetric type</a>. It can hold a private key inside the kernel and provides the ability for the allowed applications to either decrypt or sign some data with the key. Currently, only RSA keys are supported, but work is underway to add <a href="https://www.cloudflare.com/learning/dns/dnssec/ecdsa-and-dnssec/">ECDSA key support</a>.</p><p>While keys are responsible for safeguarding the cryptographic material inside the kernel, keyrings determine key lifetime and shared access. In its simplest form, when a particular keyring is destroyed, all the keys that are linked only to that keyring are securely destroyed as well. We can create custom keyrings manually, but probably one the most powerful features of the service are the “special keyrings”.</p><p>These keyrings are created implicitly by the kernel and their lifetime is bound to the lifetime of a different kernel object, like a process or a user. (Currently there are four categories of “implicit” <a href="https://man7.org/linux/man-pages/man7/keyrings.7.html">keyrings</a>), but for the purposes of this post we’re interested in two most widely used ones: process keyrings and user keyrings.</p><p>User keyring lifetime is bound to the existence of a particular user and this keyring is shared between all the processes of the same UID. Thus, one process, for example, can store a key in a user keyring and another process running as the same user can retrieve/use the key. When the UID is removed from the system, all the keys (and other keyrings) under the associated user keyring will be securely destroyed by the kernel.</p><p>Process keyrings are bound to some processes and may be of three types differing in semantics: process, thread and session. A process keyring is bound and private to a particular process. Thus, any code within the process can store/use keys in the keyring, but other processes (even with the same user id or child processes) cannot get access. And when the process dies, the keyring and the associated keys are securely destroyed. Besides the advantage of storing our secrets/keys in an isolated address space, the process keyring gives us the guarantee that the keys will be destroyed regardless of the reason for the process termination: even if our application crashed hard without being given an opportunity to execute any clean up code - our keys will still be securely destroyed by the kernel.</p><p>A thread keyring is similar to a process keyring, but it is private and bound to a particular thread. For example, we can build a multithreaded web server, which can serve TLS connections using multiple private keys, and we can be sure that connections/code in one thread can never use a private key, which is associated with another thread (for example, serving a different domain name).</p><p>A session keyring makes its keys available to the current process and all its children. It is destroyed when the topmost process terminates and child processes can store/access keys, while the topmost process exists. It is mostly useful in shell and interactive environments, when we employ the <a href="https://man7.org/linux/man-pages/man1/keyctl.1.html">keyctl tool</a> to access the Linux Kernel Key Retention Service, rather than using the kernel system call interface. In the shell, we generally can’t use the process keyring as every executed command creates a new process. Thus, if we add a key to the process keyring from the command line - that key will be immediately destroyed, because the “adding” process terminates, when the command finishes executing. Let’s actually confirm this with <code>[bpftrace](https://github.com/iovisor/bpftrace)</code>.</p><p>In one terminal we will trace the <code>[user_destroy](https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L146)</code> function, which is responsible for deleting a user key:</p>
            <pre><code>$ sudo bpftrace -e 'kprobe:user_destroy { printf("destroying key %d\n", ((struct key *)arg0)-&gt;serial) }'
Att</code></pre>
            <p>And in another terminal let’s try to add a key to the process keyring:</p>
            <pre><code>$ keyctl add user mykey hunter2 @p
742524855</code></pre>
            <p>Going back to the first terminal we can immediately see:</p>
            <pre><code>…
Attaching 1 probe...
destroying key 742524855</code></pre>
            <p>And we can confirm the key is not available by trying to access it:</p>
            <pre><code>$ keyctl print 742524855
keyctl_read_alloc: Required key not available</code></pre>
            <p>So in the above example, the key “mykey” was added to the process keyring of the subshell executing <code>keyctl add user mykey hunter2 @p</code>. But since the subshell process terminated the moment the command was executed, both its process keyring and the added key were destroyed.</p><p>Instead, the session keyring allows our interactive commands to add keys to our current shell environment and subsequent commands to consume them. The keys will still be securely destroyed, when our main shell process terminates (likely, when we log out from the system).</p><p>So by selecting the appropriate keyring type we can ensure the keys will be securely destroyed, when not needed. Even if the application crashes! This is a very brief introduction, but it will allow you to play with our examples, for the whole context, please, reach the <a href="https://www.kernel.org/doc/html/v5.8/security/keys/core.html">official documentation</a>.</p>
    <div>
      <h3>Replacing the ssh-agent with the Linux Kernel Key Retention Service</h3>
      <a href="#replacing-the-ssh-agent-with-the-linux-kernel-key-retention-service">
        
      </a>
    </div>
    <p>We gave a long description of how we can replace two isolated processes with the Linux Kernel Retention Service. It’s time to put our words into code. We talked about ssh-agent as well, so it will be a good exercise to replace our private key stored in memory of the agent with an in-kernel one. We picked the most popular SSH implementation <a href="https://github.com/openssh/openssh-portable.git">OpenSSH</a> as our target.</p><p>Some minor changes need to be added to the code to add functionality to retrieve a key from the kernel:</p><p>openssh.patch</p>
            <pre><code>diff --git a/ssh-rsa.c b/ssh-rsa.c
index 6516ddc1..797739bb 100644
--- a/ssh-rsa.c
+++ b/ssh-rsa.c
@@ -26,6 +26,7 @@
 
 #include &lt;stdarg.h&gt;
 #include &lt;string.h&gt;
+#include &lt;stdbool.h&gt;
 
 #include "sshbuf.h"
 #include "compat.h"
@@ -63,6 +64,7 @@ ssh_rsa_cleanup(struct sshkey *k)
 {
 	RSA_free(k-&gt;rsa);
 	k-&gt;rsa = NULL;
+	k-&gt;serial = 0;
 }
 
 static int
@@ -220,9 +222,14 @@ ssh_rsa_deserialize_private(const char *ktype, struct sshbuf *b,
 	int r;
 	BIGNUM *rsa_n = NULL, *rsa_e = NULL, *rsa_d = NULL;
 	BIGNUM *rsa_iqmp = NULL, *rsa_p = NULL, *rsa_q = NULL;
+	bool is_keyring = (strncmp(ktype, "ssh-rsa-keyring", strlen("ssh-rsa-keyring")) == 0);
 
+	if (is_keyring) {
+		if ((r = ssh_rsa_deserialize_public(ktype, b, key)) != 0)
+			goto out;
+	}
 	/* Note: can't reuse ssh_rsa_deserialize_public: e, n vs. n, e */
-	if (!sshkey_is_cert(key)) {
+	else if (!sshkey_is_cert(key)) {
 		if ((r = sshbuf_get_bignum2(b, &amp;rsa_n)) != 0 ||
 		    (r = sshbuf_get_bignum2(b, &amp;rsa_e)) != 0)
 			goto out;
@@ -232,28 +239,46 @@ ssh_rsa_deserialize_private(const char *ktype, struct sshbuf *b,
 		}
 		rsa_n = rsa_e = NULL; /* transferred */
 	}
-	if ((r = sshbuf_get_bignum2(b, &amp;rsa_d)) != 0 ||
-	    (r = sshbuf_get_bignum2(b, &amp;rsa_iqmp)) != 0 ||
-	    (r = sshbuf_get_bignum2(b, &amp;rsa_p)) != 0 ||
-	    (r = sshbuf_get_bignum2(b, &amp;rsa_q)) != 0)
-		goto out;
-	if (!RSA_set0_key(key-&gt;rsa, NULL, NULL, rsa_d)) {
-		r = SSH_ERR_LIBCRYPTO_ERROR;
-		goto out;
-	}
-	rsa_d = NULL; /* transferred */
-	if (!RSA_set0_factors(key-&gt;rsa, rsa_p, rsa_q)) {
-		r = SSH_ERR_LIBCRYPTO_ERROR;
-		goto out;
-	}
-	rsa_p = rsa_q = NULL; /* transferred */
 	if ((r = sshkey_check_rsa_length(key, 0)) != 0)
 		goto out;
-	if ((r = ssh_rsa_complete_crt_parameters(key, rsa_iqmp)) != 0)
-		goto out;
-	if (RSA_blinding_on(key-&gt;rsa, NULL) != 1) {
-		r = SSH_ERR_LIBCRYPTO_ERROR;
-		goto out;
+
+	if (is_keyring) {
+		char *name;
+		size_t len;
+
+		if ((r = sshbuf_get_cstring(b, &amp;name, &amp;len)) != 0)
+			goto out;
+
+		key-&gt;serial = request_key("asymmetric", name, NULL, KEY_SPEC_PROCESS_KEYRING);
+		free(name);
+
+		if (key-&gt;serial == -1) {
+			key-&gt;serial = 0;
+			r = SSH_ERR_KEY_NOT_FOUND;
+			goto out;
+		}
+	} else {
+		if ((r = sshbuf_get_bignum2(b, &amp;rsa_d)) != 0 ||
+			(r = sshbuf_get_bignum2(b, &amp;rsa_iqmp)) != 0 ||
+			(r = sshbuf_get_bignum2(b, &amp;rsa_p)) != 0 ||
+			(r = sshbuf_get_bignum2(b, &amp;rsa_q)) != 0)
+			goto out;
+		if (!RSA_set0_key(key-&gt;rsa, NULL, NULL, rsa_d)) {
+			r = SSH_ERR_LIBCRYPTO_ERROR;
+			goto out;
+		}
+		rsa_d = NULL; /* transferred */
+		if (!RSA_set0_factors(key-&gt;rsa, rsa_p, rsa_q)) {
+			r = SSH_ERR_LIBCRYPTO_ERROR;
+			goto out;
+		}
+		rsa_p = rsa_q = NULL; /* transferred */
+		if ((r = ssh_rsa_complete_crt_parameters(key, rsa_iqmp)) != 0)
+			goto out;
+		if (RSA_blinding_on(key-&gt;rsa, NULL) != 1) {
+			r = SSH_ERR_LIBCRYPTO_ERROR;
+			goto out;
+		}
 	}
 	/* success */
 	r = 0;
@@ -333,6 +358,21 @@ rsa_hash_alg_nid(int type)
 	}
 }
 
+static const char *
+rsa_hash_alg_keyctl_info(int type)
+{
+	switch (type) {
+	case SSH_DIGEST_SHA1:
+		return "enc=pkcs1 hash=sha1";
+	case SSH_DIGEST_SHA256:
+		return "enc=pkcs1 hash=sha256";
+	case SSH_DIGEST_SHA512:
+		return "enc=pkcs1 hash=sha512";
+	default:
+		return NULL;
+	}
+}
+
 int
 ssh_rsa_complete_crt_parameters(struct sshkey *key, const BIGNUM *iqmp)
 {
@@ -433,7 +473,14 @@ ssh_rsa_sign(struct sshkey *key,
 		goto out;
 	}
 
-	if (RSA_sign(nid, digest, hlen, sig, &amp;len, key-&gt;rsa) != 1) {
+	if (key-&gt;serial &gt; 0) {
+		len = keyctl_pkey_sign(key-&gt;serial, rsa_hash_alg_keyctl_info(hash_alg), digest, hlen, sig, slen);
+		if ((long)len == -1) {
+			ret = SSH_ERR_LIBCRYPTO_ERROR;
+			goto out;
+		}
+	}
+	else if (RSA_sign(nid, digest, hlen, sig, &amp;len, key-&gt;rsa) != 1) {
 		ret = SSH_ERR_LIBCRYPTO_ERROR;
 		goto out;
 	}
@@ -705,6 +752,18 @@ const struct sshkey_impl sshkey_rsa_impl = {
 	/* .funcs = */		&amp;sshkey_rsa_funcs,
 };
 
+const struct sshkey_impl sshkey_rsa_keyring_impl = {
+	/* .name = */		"ssh-rsa-keyring",
+	/* .shortname = */	"RSA",
+	/* .sigalg = */		NULL,
+	/* .type = */		KEY_RSA,
+	/* .nid = */		0,
+	/* .cert = */		0,
+	/* .sigonly = */	0,
+	/* .keybits = */	0,
+	/* .funcs = */		&amp;sshkey_rsa_funcs,
+};
+
 const struct sshkey_impl sshkey_rsa_cert_impl = {
 	/* .name = */		"ssh-rsa-cert-v01@openssh.com",
 	/* .shortname = */	"RSA-CERT",
diff --git a/sshkey.c b/sshkey.c
index 43712253..3524ad37 100644
--- a/sshkey.c
+++ b/sshkey.c
@@ -115,6 +115,7 @@ extern const struct sshkey_impl sshkey_ecdsa_nistp521_cert_impl;
 #  endif /* OPENSSL_HAS_NISTP521 */
 # endif /* OPENSSL_HAS_ECC */
 extern const struct sshkey_impl sshkey_rsa_impl;
+extern const struct sshkey_impl sshkey_rsa_keyring_impl;
 extern const struct sshkey_impl sshkey_rsa_cert_impl;
 extern const struct sshkey_impl sshkey_rsa_sha256_impl;
 extern const struct sshkey_impl sshkey_rsa_sha256_cert_impl;
@@ -154,6 +155,7 @@ const struct sshkey_impl * const keyimpls[] = {
 	&amp;sshkey_dss_impl,
 	&amp;sshkey_dsa_cert_impl,
 	&amp;sshkey_rsa_impl,
+	&amp;sshkey_rsa_keyring_impl,
 	&amp;sshkey_rsa_cert_impl,
 	&amp;sshkey_rsa_sha256_impl,
 	&amp;sshkey_rsa_sha256_cert_impl,
diff --git a/sshkey.h b/sshkey.h
index 771c4bce..a7ae45f6 100644
--- a/sshkey.h
+++ b/sshkey.h
@@ -29,6 +29,7 @@
 #include &lt;sys/types.h&gt;
 
 #ifdef WITH_OPENSSL
+#include &lt;keyutils.h&gt;
 #include &lt;openssl/rsa.h&gt;
 #include &lt;openssl/dsa.h&gt;
 # ifdef OPENSSL_HAS_ECC
@@ -153,6 +154,7 @@ struct sshkey {
 	size_t	shielded_len;
 	u_char	*shield_prekey;
 	size_t	shield_prekey_len;
+	key_serial_t serial;
 };
 
 #define	ED25519_SK_SZ	crypto_sign_ed25519_SECRETKEYBYTES</code></pre>
            <p>We need to download and patch OpenSSH from the latest git as the above patch won’t work on the latest release (<code>V_9_1_P1</code> at the time of this writing):</p>
            <pre><code>$ git clone https://github.com/openssh/openssh-portable.git
…
$ cd openssl-portable
$ $ patch -p1 &lt; ../openssh.patch
patching file ssh-rsa.c
patching file sshkey.c
patching file sshkey.h</code></pre>
            <p>Now compile and build the patched OpenSSH</p>
            <pre><code>$ autoreconf
$ ./configure --with-libs=-lkeyutils --disable-pkcs11
…
$ make
…</code></pre>
            <p>Note that we instruct the build system to additionally link with <code>[libkeyutils](https://man7.org/linux/man-pages/man3/keyctl.3.html)</code>, which provides convenient wrappers to access the Linux Kernel Key Retention Service. Additionally, we had to disable PKCS11 support as the code has a function with the same name as in `libkeyutils`, so there is a naming conflict. There might be a better fix for this, but it is out of scope for this post.</p><p>Now that we have the patched OpenSSH - let’s test it. Firstly, we need to generate a new SSH RSA key that we will use to access the system. Because the Linux kernel only supports private keys in the PKCS8 format, we’ll use it from the start (instead of the default OpenSSH format):</p>
            <pre><code>$ ./ssh-keygen -b 4096 -m PKCS8
Generating public/private rsa key pair.
…</code></pre>
            <p>Normally, we would be using `ssh-add` to add this key to our ssh agent. In our case we need to use a replacement script, which would add the key to our current session keyring:</p><p>ssh-add-keyring.sh</p>
            <pre><code>#/bin/bash -e

in=$1
key_desc=$2
keyring=$3

in_pub=$in.pub
key=$(mktemp)
out="${in}_keyring"

function finish {
    rm -rf $key
}
trap finish EXIT

# https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
# null-terminanted openssh-key-v1
printf 'openssh-key-v1\0' &gt; $key
# cipher: none
echo '00000004' | xxd -r -p &gt;&gt; $key
echo -n 'none' &gt;&gt; $key
# kdf: none
echo '00000004' | xxd -r -p &gt;&gt; $key
echo -n 'none' &gt;&gt; $key
# no kdf options
echo '00000000' | xxd -r -p &gt;&gt; $key
# one key in the blob
echo '00000001' | xxd -r -p &gt;&gt; $key

# grab the hex public key without the (00000007 || ssh-rsa) preamble
pub_key=$(awk '{ print $2 }' $in_pub | base64 -d | xxd -s 11 -p | tr -d '\n')
# size of the following public key with the (0000000f || ssh-rsa-keyring) preamble
printf '%08x' $(( ${#pub_key} / 2 + 19 )) | xxd -r -p &gt;&gt; $key
# preamble for the public key
# ssh-rsa-keyring in prepended with length of the string
echo '0000000f' | xxd -r -p &gt;&gt; $key
echo -n 'ssh-rsa-keyring' &gt;&gt; $key
# the public key itself
echo $pub_key | xxd -r -p &gt;&gt; $key

# the private key is just a key description in the Linux keyring
# ssh will use it to actually find the corresponding key serial
# grab the comment from the public key
comment=$(awk '{ print $3 }' $in_pub)
# so the total size of the private key is
# two times the same 4 byte int +
# (0000000f || ssh-rsa-keyring) preamble +
# a copy of the public key (without preamble) +
# (size || key_desc) +
# (size || comment )
priv_sz=$(( 8 + 19 + ${#pub_key} / 2 + 4 + ${#key_desc} + 4 + ${#comment} ))
# we need to pad the size to 8 bytes
pad=$(( 8 - $(( priv_sz % 8 )) ))
# so, total private key size
printf '%08x' $(( $priv_sz + $pad )) | xxd -r -p &gt;&gt; $key
# repeated 4-byte int
echo '0102030401020304' | xxd -r -p &gt;&gt; $key
# preamble for the private key
echo '0000000f' | xxd -r -p &gt;&gt; $key
echo -n 'ssh-rsa-keyring' &gt;&gt; $key
# public key
echo $pub_key | xxd -r -p &gt;&gt; $key
# private key description in the keyring
printf '%08x' ${#key_desc} | xxd -r -p &gt;&gt; $key
echo -n $key_desc &gt;&gt; $key
# comment
printf '%08x' ${#comment} | xxd -r -p &gt;&gt; $key
echo -n $comment &gt;&gt; $key
# padding
for (( i = 1; i &lt;= $pad; i++ )); do
    echo 0$i | xxd -r -p &gt;&gt; $key
done

echo '-----BEGIN OPENSSH PRIVATE KEY-----' &gt; $out
base64 $key &gt;&gt; $out
echo '-----END OPENSSH PRIVATE KEY-----' &gt;&gt; $out
chmod 600 $out

# load the PKCS8 private key into the designated keyring
openssl pkcs8 -in $in -topk8 -outform DER -nocrypt | keyctl padd asymmetric $key_desc $keyring
</code></pre>
            <p>Depending on how our kernel was compiled, we might also need to load some kernel modules for asymmetric private key support:</p>
            <pre><code>$ sudo modprobe pkcs8_key_parser
$ ./ssh-add-keyring.sh ~/.ssh/id_rsa myssh @s
Enter pass phrase for ~/.ssh/id_rsa:
723263309</code></pre>
            <p>Finally, our private ssh key is added to the current session keyring with the name “myssh”. In addition, the <code>ssh-add-keyring.sh</code> will create a pseudo-private key file in <code>~/.ssh/id_rsa_keyring</code>, which needs to be passed to the main <code>ssh</code> process. It is a pseudo-private key, because it doesn’t have any sensitive cryptographic material. Instead, it only has the “myssh” identifier in a native OpenSSH format. If we use multiple SSH keys, we have to tell the main <code>ssh</code> process somehow which in-kernel key name should be requested from the system.</p><p>Before we start testing it, let’s make sure our SSH server (running locally) will accept the newly generated key as a valid authentication:</p>
            <pre><code>$ cat ~/.ssh/id_rsa.pub &gt;&gt; ~/.ssh/authorized_keys</code></pre>
            <p>Now we can try to SSH into the system:</p>
            <pre><code>$ SSH_AUTH_SOCK="" ./ssh -i ~/.ssh/id_rsa_keyring localhost
The authenticity of host 'localhost (::1)' can't be established.
ED25519 key fingerprint is SHA256:3zk7Z3i9qZZrSdHvBp2aUYtxHACmZNeLLEqsXltynAY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
Linux dev 5.15.79-cloudflare-2022.11.6 #1 SMP Mon Sep 27 00:00:00 UTC 2010 x86_64
…</code></pre>
            <p>It worked! Notice that we’re resetting the `SSH_AUTH_SOCK` environment variable to make sure we don’t use any keys from an ssh-agent running on the system. Still the login flow does not request any password for our private key, the key itself is resident of the kernel address space, and we reference it using its serial for signature operations.</p>
    <div>
      <h3>User or session keyring?</h3>
      <a href="#user-or-session-keyring">
        
      </a>
    </div>
    <p>In the example above, we set up our SSH private key into the session keyring. We can check if it is there:</p>
            <pre><code>$ keyctl show
Session Keyring
 577779279 --alswrv   1000  1000  keyring: _ses
 846694921 --alswrv   1000 65534   \_ keyring: _uid.1000
 723263309 --als--v   1000  1000   \_ asymmetric: myssh</code></pre>
            <p>We might have used user keyring as well. What is the difference? Currently, the “myssh” key lifetime is limited to the current login session. That is, if we log out and login again, the key will be gone, and we would have to run the <code>ssh-add-keyring.sh</code> script again. Similarly, if we log in to a second terminal, we won’t see this key:</p>
            <pre><code>$ keyctl show
Session Keyring
 333158329 --alswrv   1000  1000  keyring: _ses
 846694921 --alswrv   1000 65534   \_ keyring: _uid.1000</code></pre>
            <p>Notice that the serial number of the session keyring <code>_ses</code> in the second terminal is different. A new keyring was created and  “myssh” key along with the previous session keyring doesn’t exist anymore:</p>
            <pre><code>$ SSH_AUTH_SOCK="" ./ssh -i ~/.ssh/id_rsa_keyring localhost
Load key "/home/ignat/.ssh/id_rsa_keyring": key not found
…</code></pre>
            <p>If instead we tell <code>ssh-add-keyring.sh</code> to load the private key into the user keyring (replace <code>@s</code> with <code>@u</code> in the command line parameters), it will be available and accessible from both login sessions. In this case, during logout and re-login, the same key will be presented. Although, this has a security downside - any process running as our user id will be able to access and use the key.</p>
    <div>
      <h3>Summary</h3>
      <a href="#summary">
        
      </a>
    </div>
    <p>In this post we learned about one of the most common ways that data, including highly valuable cryptographic keys, can leak. We talked about some real examples, which impacted many users around the world, including Cloudflare. Finally, we learned how the Linux Kernel Retention Service can help us to protect our cryptographic keys and secrets.</p><p>We also introduced a working patch for OpenSSH to use this cool feature of the Linux kernel, so you can easily try it yourself. There are still many Linux Kernel Key Retention Service features left untold, which might be a topic for another blog post. Stay tuned!</p> ]]></content:encoded>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <guid isPermaLink="false">1w40uuzCCDLItmJBNEkbkq</guid>
            <dc:creator>Oxana Kharitonova</dc:creator>
            <dc:creator>Ignat Korchagin</dc:creator>
        </item>
        <item>
            <title><![CDATA[Assembly within! BPF tail calls on x86 and ARM]]></title>
            <link>https://blog.cloudflare.com/assembly-within-bpf-tail-calls-on-x86-and-arm/</link>
            <pubDate>Mon, 10 Oct 2022 13:00:00 GMT</pubDate>
            <description><![CDATA[ We have first adopted the BPF tail calls when building our XDP-based packet processing pipeline. BPF tail calls have served us well since then. But they do have their caveats ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Early on when we learn to program, we get introduced to the concept of <a href="https://ocw.mit.edu/courses/6-00sc-introduction-to-computer-science-and-programming-spring-2011/resources/lecture-6-recursion/">recursion</a>. And that it is handy for computing, among other things, sequences defined in terms of recurrences. Such as the famous <a href="https://en.wikipedia.org/wiki/Fibonacci_number">Fibonnaci numbers</a> - <i>Fn = Fn-1 + Fn-2</i>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5Zkbmk81PBl2lx62bzcNz4/ce5eb7474578577dd0c5d4c3284d73f2/Screenshot-2022-10-10-at-10.13.32.png" />
            
            </figure><p>Later on, perhaps when diving into multithreaded programming, we come to terms with the fact that <a href="https://textbook.cs161.org/memory-safety/x86.html#26-stack-pushing-and-popping">the stack space</a> for call frames is finite. And that there is an “okay” way and a “cool” way to calculate the Fibonacci numbers using recursion:</p>
            <pre><code>// fib_okay.c

#include &lt;stdint.h&gt;

uint64_t fib(uint64_t n)
{
        if (n == 0 || n == 1)
                return 1;

        return fib(n - 1) + fib(n - 2);
}</code></pre>
            <p>Listing 1. An okay Fibonacci number generator implementation</p>
            <pre><code>// fib_cool.c

#include &lt;stdint.h&gt;

static uint64_t fib_tail(uint64_t n, uint64_t a, uint64_t b)
{
    if (n == 0)
        return a;
    if (n == 1)
        return b;

    return fib_tail(n - 1, b, a + b);
}

uint64_t fib(uint64_t n)
{
    return fib_tail(n, 1, 1);
}</code></pre>
            <p>Listing 2. A better version of the same</p><p>If we take a look at the machine code the compiler produces, the “cool” variant translates to a nice and tight sequence of instructions:</p><p>⚠ DISCLAIMER: This blog post is assembly-heavy. We will be looking at assembly code for x86-64, arm64 and BPF architectures. If you need an introduction or a refresher, I can recommend <a href="https://github.com/Apress/low-level-programming">“Low-Level Programming”</a> by Igor Zhirkov for x86-64, and <a href="https://github.com/Apress/programming-with-64-bit-ARM-assembly-language">“Programming with 64-Bit ARM Assembly Language”</a> by Stephen Smith for arm64. For BPF, see the <a href="https://www.kernel.org/doc/html/latest/bpf/standardization/instruction-set.html">Linux kernel documentation</a>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7bahmmfAErsC7aO9LUTzTa/e685101ca35f092cd482993e3b86405b/Screenshot-2022-10-10-at-10.25.23.png" />
            
            </figure><p>Listing 3. <code>fib_cool.c</code> compiled for x86-64 and arm64</p><p>The “okay” variant, disappointingly, leads to <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2022-10-bpf-tail-call/fib_okay.x86-64.disasm">more instructions</a> than a listing can fit. It is a spaghetti of <a href="https://en.wikipedia.org/wiki/Basic_block">basic blocks</a>.</p><p><a href="https://raw.githubusercontent.com/cloudflare/cloudflare-blog/master/2022-10-bpf-tail-call/fib_okay.dot.png"><img src="http://staging.blog.mrk.cfdata.org/content/images/2022/10/image6.png" /></a></p><p>But more importantly, it is not free of <a href="https://textbook.cs161.org/memory-safety/x86.html#29-x86-function-call-in-assembly">x86 call instructions</a>.</p>
            <pre><code>$ objdump -d fib_okay.o | grep call
 10c:   e8 00 00 00 00          call   111 &lt;fib+0x111&gt;
$ objdump -d fib_cool.o | grep call
$</code></pre>
            <p>This has an important consequence - as fib recursively calls itself, the stacks keep growing. We can observe it with a bit of <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2022-10-bpf-tail-call/trace_rsp.gdb">help from the debugger</a>.</p>
            <pre><code>$ gdb --quiet --batch --command=trace_rsp.gdb --args ./fib_okay 6
Breakpoint 1 at 0x401188: file fib_okay.c, line 3.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
n = 6, %rsp = 0xffffd920
n = 5, %rsp = 0xffffd900
n = 4, %rsp = 0xffffd8e0
n = 3, %rsp = 0xffffd8c0
n = 2, %rsp = 0xffffd8a0
n = 1, %rsp = 0xffffd880
n = 1, %rsp = 0xffffd8c0
n = 2, %rsp = 0xffffd8e0
n = 1, %rsp = 0xffffd8c0
n = 3, %rsp = 0xffffd900
n = 2, %rsp = 0xffffd8e0
n = 1, %rsp = 0xffffd8c0
n = 1, %rsp = 0xffffd900
13
[Inferior 1 (process 50904) exited normally]
$</code></pre>
            <p>While the “cool” variant makes no use of the stack.</p>
            <pre><code>$ gdb --quiet --batch --command=trace_rsp.gdb --args ./fib_cool 6
Breakpoint 1 at 0x40118a: file fib_cool.c, line 13.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
n = 6, %rsp = 0xffffd938
13
[Inferior 1 (process 50949) exited normally]
$</code></pre>
            
    <div>
      <h2>Where did the <code>calls</code> go?</h2>
      <a href="#where-did-the-calls-go">
        
      </a>
    </div>
    <p>The smart compiler turned the last function call in the body into a regular jump. Why was it allowed to do that?</p><p>It is the last instruction in the function body we are talking about. The caller stack frame is going to be destroyed right after we return anyway. So why keep it around when we can reuse it for the callee’s <a href="https://eli.thegreenplace.net/2011/02/04/where-the-top-of-the-stack-is-on-x86/">stack frame</a>?</p><p>This optimization, known as <a href="https://en.wikipedia.org/wiki/Tail_call#In_assembly">tail call elimination</a>, leaves us with no function calls in the “cool” variant of our fib implementation. There was only one call to eliminate - right at the end.</p><p>Once applied, the call becomes a jump (loop). If assembly is not your second language, decompiling the fib_cool.o object file with <a href="https://ghidra-sre.org/">Ghidra</a> helps see the transformation:</p>
            <pre><code>long fib(ulong param_1)

{
  long lVar1;
  long lVar2;
  long lVar3;
  
  if (param_1 &lt; 2) {
    lVar3 = 1;
  }
  else {
    lVar3 = 1;
    lVar2 = 1;
    do {
      lVar1 = lVar3;
      param_1 = param_1 - 1;
      lVar3 = lVar2 + lVar1;
      lVar2 = lVar1;
    } while (param_1 != 1);
  }
  return lVar3;
}</code></pre>
            <p>Listing 4. <code>fib_cool.o</code> decompiled by Ghidra</p><p>This is very much desired. Not only is the generated machine code much shorter. It is also way faster due to lack of calls, which pop up on the <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2022-10-bpf-tail-call/fib_okay_50.perf.txt#L85">profile</a> for fib_okay.</p><p>But I am no <a href="https://github.com/dendibakh/perf-ninja">performance ninja</a> and this blog post is not about compiler optimizations. So why am I telling you about it?</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4pi98zHx1blOehbqwhB1LP/049a1532b192261cdfedade320e871a1/image5.jpg" />
            
            </figure><p><a href="https://commons.wikimedia.org/wiki/File:Lemur_catta_-_tail_length_01.jpg">Alex Dunkel (Maky), CC BY-SA 3.0</a>, via <a href="https://creativecommons.org/licenses/by-sa/3.0">Wikimedia Commons</a></p>
    <div>
      <h2>Tail calls in BPF</h2>
      <a href="#tail-calls-in-bpf">
        
      </a>
    </div>
    <p>The concept of tail call elimination made its way into the BPF world. Although not in the way you might expect. Yes, the LLVM compiler does get rid of the trailing function calls when building for -target bpf. The transformation happens at the intermediate representation level, so it is backend agnostic. This can save you some <a href="https://docs.cilium.io/en/stable/bpf/#bpf-to-bpf-calls">BPF-to-BPF function calls</a>, which you can spot by looking for call -N instructions in the BPF assembly.</p><p>However, when we talk about tail calls in the BPF context, we usually have something else in mind. And that is a mechanism, built into the BPF JIT compiler, for <a href="https://docs.cilium.io/en/stable/bpf/#tail-calls">chaining BPF programs</a>.</p><p>We first adopted BPF tail calls when building our <a href="https://legacy.netdevconf.info/0x13/session.html?talk-XDP-based-DDoS-mitigation">XDP-based packet processing pipeline</a>. Thanks to it, we were able to divide the processing logic into several XDP programs. Each responsible for doing one thing.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3ANc6el4eTdaklbTHnlzPH/688282a0c8534d50fd1d8c1143c53058/image8.png" />
            
            </figure><p>Slide from “<a href="https://legacy.netdevconf.info/0x13/session.html?talk-XDP-based-DDoS-mitigation">XDP based DDoS Mitigation</a>” talk by <a href="/author/arthur/">Arthur Fabre</a></p><p>BPF tail calls have served us well since then. But they do have their caveats. Until recently it was impossible to have both BPF tails calls and BPF-to-BPF function calls in the same XDP program on arm64, which is one of the supported architectures for us.</p><p>Why? Before we get to that, we have to clarify what a BPF tail call actually does.</p>
    <div>
      <h2>A tail call is a tail call is a tail call</h2>
      <a href="#a-tail-call-is-a-tail-call-is-a-tail-call">
        
      </a>
    </div>
    <p>BPF exposes the tail call mechanism through the <a href="https://elixir.bootlin.com/linux/v5.15.63/source/include/uapi/linux/bpf.h#L1712">bpf_tail_call helper</a>, which we can invoke from our BPF code. We don’t directly point out which BPF program we would like to call. Instead, we pass it a BPF map (a container) capable of holding references to BPF programs (BPF_MAP_TYPE_PROG_ARRAY), and an index into the map.</p>
            <pre><code>long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)

       Description
              This  special  helper is used to trigger a "tail call", or
              in other words, to jump into  another  eBPF  program.  The
              same  stack frame is used (but values on stack and in reg‐
              isters for the caller are not accessible to  the  callee).
              This  mechanism  allows  for  program chaining, either for
              raising the maximum number of available eBPF instructions,
              or  to  execute  given programs in conditional blocks. For
              security reasons, there is an upper limit to the number of
              successive tail calls that can be performed.</code></pre>
            <p><a href="https://man7.org/linux/man-pages/man7/bpf-helpers.7.html">bpf-helpers(7) man page</a></p><p>At first glance, this looks somewhat similar to the <a href="https://man7.org/linux/man-pages/man2/execve.2.html">execve(2) syscall</a>. It is easy to mistake it for a way to execute a new program from the current program context. To quote the excellent <a href="https://docs.cilium.io/en/stable/bpf/#tail-calls">BPF and XDP Reference Guide</a> from the Cilium project documentation:</p><blockquote><p><i>Tail calls can be seen as a mechanism that allows one BPF program to call another, without returning to the old program. Such a call has minimal overhead as unlike function calls, it is implemented as a long jump, reusing the same stack frame.</i></p></blockquote><p>But once we add <a href="https://docs.cilium.io/en/stable/bpf/#bpf-to-bpf-calls">BPF function calls</a> into the mix, it becomes clear that the BPF tail call mechanism is indeed an implementation of tail call elimination, rather than a way to replace one program with another:</p><blockquote><p><i>Tail calls, before the actual jump to the target program, will unwind only its current stack frame. As we can see in the example above, if a tail call occurs from within the sub-function, the function’s (func1) stack frame will be present on the stack when a program execution is at func2. Once the final function (func3) function terminates, all the previous stack frames will be unwinded and control will get back to the caller of BPF program caller.</i></p></blockquote><p>Alas, one with sometimes slightly surprising semantics. Consider the code <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2022-10-bpf-tail-call/tail_call_ex3.bpf.c">like below</a>, where a BPF function calls the <code>bpf_tail_call()</code> helper:</p>
            <pre><code>struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 1);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
} bar SEC(".maps");

SEC("tc")
int serve_drink(struct __sk_buff *skb __unused)
{
    return 0xcafe;
}

static __noinline
int bring_order(struct __sk_buff *skb)
{
    bpf_tail_call(skb, &amp;bar, 0);
    return 0xf00d;
}

SEC("tc")
int server1(struct __sk_buff *skb)
{
    return bring_order(skb);    
}

SEC("tc")
int server2(struct __sk_buff *skb)
{
    __attribute__((musttail)) return bring_order(skb);  
}</code></pre>
            <p>We have two seemingly not so different BPF programs - <code>server1()</code> and <code>server2()</code>. They both call the same BPF function <code>bring_order()</code>. The function tail calls into the <code>serve_drink()</code> program, if the <code>bar[0]</code> map entry points to it (let’s assume that).</p><p>Do both <code>server1</code> and <code>server2</code> return the same value? Turns out that - no, they don’t. We get a hex ? from <code>server1</code>, and a ☕ from <code>server2</code>. How so?</p><p>First thing to notice is that a BPF tail call unwinds just the current function stack frame. Code past the <code>bpf_tail_call()</code> invocation in the function body never executes, providing the tail call is successful (the map entry was set, and the tail call limit has not been reached).</p><p>When the tail call finishes, control returns to the caller of the function which made the tail call. Applying this to our example, the control flow is <code>serverX() --&gt; bring_order() --&gt; bpf_tail_call() --&gt; serve_drink() -return-&gt; serverX()</code> for both programs.</p><p>The second thing to keep in mind is that the compiler does not know that the bpf_tail_call() helper changes the control flow. Hence, the unsuspecting compiler optimizes the code as if the execution would continue past the BPF tail call.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/57RsaISELMEErCz7js2C0w/70efe8757f435fb6450ae26c50ff0f2b/image2-8.png" />
            
            </figure><p>The call graph for <code>server1()</code> and <code>server2()</code> is the same, but the return value differs due to build time optimizations.</p><p>In our case, the compiler thinks it is okay to propagate the constant which <code>bring_order()</code> returns to <code>server1()</code>. Possibly catching us by surprise, if we didn’t check the <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2022-10-bpf-tail-call/tail_call_ex3.bpf.disasm">generated BPF assembly</a>.</p><p>We can prevent it by <a href="https://clang.llvm.org/docs/AttributeReference.html#musttail">forcing the compiler</a> to make a tail call to <code>bring_order()</code>. This way we ensure that whatever <code>bring_order()</code> returns will be used as the <code>server2()</code> program result.</p><p>? General rule - for least surprising results, use <a href="https://clang.llvm.org/docs/AttributeReference.html#musttail"><code>musttail attribute</code></a> when calling a function that contain a BPF tail call.</p><p>How does the <code>bpf_tail_call()</code> work underneath then? And why the BPF verifier wouldn’t let us mix the function calls with tail calls on arm64? Time to dig deeper.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5rp6nKgRPwuuXUzAyolOA9/1b740786194fceb11c2fdf26e1c5a1d7/image7.jpg" />
            
            </figure><p>Public Domain <a href="https://www.rawpixel.com/image/3578996/free-photo-image-bulldozer-construction-site">image</a></p>
    <div>
      <h2>BPF tail call on x86-64</h2>
      <a href="#bpf-tail-call-on-x86-64">
        
      </a>
    </div>
    <p>What does a <code>bpf_tail_call()</code> helper call translate to after BPF JIT for x86-64 has compiled it? How does the implementation guarantee that we don’t end up in a tail call loop forever?</p><p>To find out we will need to piece together a few things.</p><p>First, there is the BPF JIT compiler source code, which lives in <a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c"><code>arch/x86/net/bpf_jit_comp.c</code></a>. Its code is annotated with helpful comments. We will focus our attention on the following call chain within the JIT:</p><p><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c#L894"><span>do_jit()</span><span> ?</span><span><br /></span></a><span>   </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c#L292"><span>emit_prologue()</span><span> ?</span><span><br /></span></a><span>   </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c#L257"><span>push_callee_regs()</span><span> ?</span><span><br /></span></a><span>   </span><span>for (i = 1; i &lt;= insn_cnt; i++, insn++) {</span><span><br /></span><span>     </span><span>switch (insn-&gt;code) {</span><span><br /></span><span>     </span><span>case BPF_JMP | BPF_CALL:</span><span><br /></span><span>       </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c#L1434"><span>/* emit function call */</span><span> ?</span><span><br /></span></a><span>     </span><span>case BPF_JMP | BPF_TAIL_CALL:</span><span><br /></span><span>       </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c#L531"><span>emit_bpf_tail_call_direct()</span><span> ?</span><span><br /></span></a><span>     </span><span>case BPF_JMP | BPF_EXIT:</span><span><br /></span><span>       </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/x86/net/bpf_jit_comp.c#L1693"><span>/* emit epilogue */</span><span> ?</span><span><br /></span></a><span>     </span><span>}</span> <span><br /></span><span>  </span><span>}</span></p><p>It is sometimes hard to visualize the generated instruction stream just from reading the compiler code. Hence, we will also want to inspect the input - BPF instructions - and the output - x86-64 instructions - of the JIT compiler.</p><p>To inspect BPF and x86-64 instructions of a loaded BPF program, we can use <code>bpftool prog dump</code>. However, first we must populate the BPF map used as the tail call jump table. Otherwise, we might not be able to see the tail call jump!</p><p>This is due to <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=428d5df1fa4f28daf622c48dd19da35585c9053c">optimizations</a> that use instruction patching when the index into the program array is known at load time.</p>
            <pre><code># bpftool prog loadall ./tail_call_ex1.o /sys/fs/bpf pinmaps /sys/fs/bpf
# bpftool map update pinned /sys/fs/bpf/jmp_table key 0 0 0 0 value pinned /sys/fs/bpf/target_prog
# bpftool prog dump xlated pinned /sys/fs/bpf/entry_prog
int entry_prog(struct __sk_buff * skb):
; bpf_tail_call(skb, &amp;jmp_table, 0);
   0: (18) r2 = map[id:24]
   2: (b7) r3 = 0
   3: (85) call bpf_tail_call#12
; return 0xf00d;
   4: (b7) r0 = 61453
   5: (95) exit
# bpftool prog dump jited pinned /sys/fs/bpf/entry_prog
int entry_prog(struct __sk_buff * skb):
bpf_prog_4f697d723aa87765_entry_prog:
; bpf_tail_call(skb, &amp;jmp_table, 0);
   0:   nopl   0x0(%rax,%rax,1)
   5:   xor    %eax,%eax
   7:   push   %rbp
   8:   mov    %rsp,%rbp
   b:   push   %rax
   c:   movabs $0xffff888102764800,%rsi
  16:   xor    %edx,%edx
  18:   mov    -0x4(%rbp),%eax
  1e:   cmp    $0x21,%eax
  21:   jae    0x0000000000000037
  23:   add    $0x1,%eax
  26:   mov    %eax,-0x4(%rbp)
  2c:   nopl   0x0(%rax,%rax,1)
  31:   pop    %rax
  32:   jmp    0xffffffffffffffe3   // bug? ?
; return 0xf00d;
  37:   mov    $0xf00d,%eax
  3c:   leave
  3d:   ret</code></pre>
            <p>There is a caveat. The target addresses for tail call jumps in <code>bpftool prog dump jited</code> output will not make any sense. To discover the real jump targets, we have to peek into the kernel memory. That can be done with <code>gdb</code> after we find the address of our JIT’ed BPF programs in <code>/proc/kallsyms</code>:</p>
            <pre><code># tail -2 /proc/kallsyms
ffffffffa0000720 t bpf_prog_f85b2547b00cbbe9_target_prog        [bpf]
ffffffffa0000748 t bpf_prog_4f697d723aa87765_entry_prog [bpf]
# gdb -q -c /proc/kcore -ex 'x/18i 0xffffffffa0000748' -ex 'quit'
[New process 1]
Core was generated by `earlyprintk=serial,ttyS0,115200 console=ttyS0 psmouse.proto=exps "virtme_stty_c'.
#0  0x0000000000000000 in ?? ()
   0xffffffffa0000748:  nopl   0x0(%rax,%rax,1)
   0xffffffffa000074d:  xor    %eax,%eax
   0xffffffffa000074f:  push   %rbp
   0xffffffffa0000750:  mov    %rsp,%rbp
   0xffffffffa0000753:  push   %rax
   0xffffffffa0000754:  movabs $0xffff888102764800,%rsi
   0xffffffffa000075e:  xor    %edx,%edx
   0xffffffffa0000760:  mov    -0x4(%rbp),%eax
   0xffffffffa0000766:  cmp    $0x21,%eax
   0xffffffffa0000769:  jae    0xffffffffa000077f
   0xffffffffa000076b:  add    $0x1,%eax
   0xffffffffa000076e:  mov    %eax,-0x4(%rbp)
   0xffffffffa0000774:  nopl   0x0(%rax,%rax,1)
   0xffffffffa0000779:  pop    %rax
   0xffffffffa000077a:  jmp    0xffffffffa000072b
   0xffffffffa000077f:  mov    $0xf00d,%eax
   0xffffffffa0000784:  leave
   0xffffffffa0000785:  ret
# gdb -q -c /proc/kcore -ex 'x/7i 0xffffffffa0000720' -ex 'quit'
[New process 1]
Core was generated by `earlyprintk=serial,ttyS0,115200 console=ttyS0 psmouse.proto=exps "virtme_stty_c'.
#0  0x0000000000000000 in ?? ()
   0xffffffffa0000720:  nopl   0x0(%rax,%rax,1)
   0xffffffffa0000725:  xchg   %ax,%ax
   0xffffffffa0000727:  push   %rbp
   0xffffffffa0000728:  mov    %rsp,%rbp
   0xffffffffa000072b:  mov    $0xcafe,%eax
   0xffffffffa0000730:  leave
   0xffffffffa0000731:  ret
#</code></pre>
            <p>Lastly, it will be handy to have a cheat sheet of <a href="https://elixir.bootlin.com/linux/v5.15.63/source/arch/x86/net/bpf_jit_comp.c#L104">mapping</a> between BPF registers (<code>r0</code>, <code>r1</code>, …) to hardware registers (<code>rax</code>, <code>rdi</code>, …) that the JIT compiler uses.</p>
<table>
<thead>
  <tr>
    <th>BPF</th>
    <th>x86-64</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>r0</td>
    <td>rax</td>
  </tr>
  <tr>
    <td>r1</td>
    <td>rdi</td>
  </tr>
  <tr>
    <td>r2</td>
    <td>rsi</td>
  </tr>
  <tr>
    <td>r3</td>
    <td>rdx</td>
  </tr>
  <tr>
    <td>r4</td>
    <td>rcx</td>
  </tr>
  <tr>
    <td>r5</td>
    <td>r8</td>
  </tr>
  <tr>
    <td>r6</td>
    <td>rbx</td>
  </tr>
  <tr>
    <td>r7</td>
    <td>r13</td>
  </tr>
  <tr>
    <td>r8</td>
    <td>r14</td>
  </tr>
  <tr>
    <td>r9</td>
    <td>r15</td>
  </tr>
  <tr>
    <td>r10</td>
    <td>rbp</td>
  </tr>
  <tr>
    <td>internal</td>
    <td>r9-r12</td>
  </tr>
</tbody>
</table><p>Now we are prepared to work out what happens when we use a BPF tail call.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3cyhfC6M0zUcXpRmaGDf6b/26595d9ce05e788d01a23fc0c6836d96/image9.png" />
            
            </figure><p>In essence, <code>bpf_tail_call()</code> emits a jump into another function, reusing the current stack frame. It is just like a regular optimized tail call, but with a twist.</p><p>Because of the BPF security guarantees - execution terminates, no stack overflows - there is a limit on the number of tail calls we can have (<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ebf7f6f0a6cdcc17a3da52b81e4b3a98c4005028"><code>MAX_TAIL_CALL_CNT = 33</code></a>).</p><p>Counting the tail calls across BPF programs is not something we can do at load-time. The jump table (BPF program array) contents can change after the program has been verified. Our only option is to keep track of tail calls at run-time. That is why the JIT’ed code for the <code>bpf_tail_call()</code> helper checks and updates the <code>tail_call_cnt</code> counter.</p><p>The updated count is then passed from one BPF program to another, and from one BPF function to another, as we will see, through the <code>rax register</code> (<code>r0</code> in BPF).</p><p>Luckily for us, the x86-64 calling convention dictates that the <code>rax</code> register does not partake in passing function arguments, but rather holds the function return value. The JIT can repurpose it to pass an additional - hidden - argument.</p><p>The function body is, however, free to make use of the <code>r0/rax</code> register in any way it pleases. This explains why we want to save the <code>tail_call_cnt</code> passed via <code>rax</code> onto stack right after we jump to another program. <code>bpf_tail_call()</code> can later load the value from a known location on the stack.</p><p>This way, the <a href="https://elixir.bootlin.com/linux/v5.15.63/source/arch/x86/net/bpf_jit_comp.c#L1437">code emitted</a> for each <code>bpf_tail_call()</code> invocation, and the <a href="https://elixir.bootlin.com/linux/v5.15.63/source/arch/x86/net/bpf_jit_comp.c#L281">BPF function prologue</a> work in tandem, keeping track of tail call count across BPF program boundaries.</p><p>But what if our BPF program is split up into several BPF functions, each with its own stack frame? What if these functions perform BPF tail calls? How is the tail call count tracked then?</p>
    <div>
      <h3>Mixing BPF function calls with BPF tail calls</h3>
      <a href="#mixing-bpf-function-calls-with-bpf-tail-calls">
        
      </a>
    </div>
    <p>BPF has its own terminology when it comes to functions and calling them, which is influenced by the internal implementation. Function calls are referred to as <a href="https://docs.cilium.io/en/stable/bpf/#bpf-to-bpf-calls">BPF to BPF calls</a>. Also, the main/entry function in your BPF code is called “the program”, while all other functions are known as “subprograms”.</p><p>Each call to subprogram allocates a stack frame for local state, which persists until the function returns. Naturally, BPF subprogram calls can be nested creating a call chain. Just like nested function calls in user-space.</p><p>BPF subprograms are also allowed to make BPF tail calls. This, effectively, is a mechanism for extending the call chain to another BPF program and its subprograms.</p><p>If we cannot track how long the call chain can be, and how much stack space each function uses, we put ourselves at risk of <a href="https://en.wikipedia.org/wiki/Stack_overflow">overflowing the stack</a>. We cannot let this happen, so BPF enforces limitations on <a href="https://elixir.bootlin.com/linux/v5.15.62/source/kernel/bpf/verifier.c#L3600">when and how many BPF tail calls can be done</a>:</p>
            <pre><code>static int check_max_stack_depth(struct bpf_verifier_env *env)
{
        …
        /* protect against potential stack overflow that might happen when
         * bpf2bpf calls get combined with tailcalls. Limit the caller's stack
         * depth for such case down to 256 so that the worst case scenario
         * would result in 8k stack size (32 which is tailcall limit * 256 =
         * 8k).
         *
         * To get the idea what might happen, see an example:
         * func1 -&gt; sub rsp, 128
         *  subfunc1 -&gt; sub rsp, 256
         *  tailcall1 -&gt; add rsp, 256
         *   func2 -&gt; sub rsp, 192 (total stack size = 128 + 192 = 320)
         *   subfunc2 -&gt; sub rsp, 64
         *   subfunc22 -&gt; sub rsp, 128
         *   tailcall2 -&gt; add rsp, 128
         *    func3 -&gt; sub rsp, 32 (total stack size 128 + 192 + 64 + 32 = 416)
         *
         * tailcall will unwind the current stack frame but it will not get rid
         * of caller's stack as shown on the example above.
         */
        if (idx &amp;&amp; subprog[idx].has_tail_call &amp;&amp; depth &gt;= 256) {
                verbose(env,
                        "tail_calls are not allowed when call stack of previous frames is %d bytes. Too large\n",
                        depth);
                return -EACCES;
        }
        …
}</code></pre>
            <p>While the stack depth can be calculated by the BPF verifier at load-time, we still need to keep count of tail call jumps at run-time. Even when subprograms are involved.</p><p>This means that we have to pass the tail call count from one BPF subprogram to another, just like we did when making a BPF tail call, so we yet again turn to value passing through the <code>rax register</code>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3cRz7vr40Hl4l65tgG15gV/09d1d3980f2f4c3e61ed2ff5cb5021f0/image1-11.png" />
            
            </figure><p>Control flow in a BPF program with a function call followed by a tail call.</p><p>? To keep things simple, BPF code in our examples does not allocate anything on stack. I encourage you to check how the JIT’ed code changes when you <a href="https://elixir.bootlin.com/linux/v5.19.11/source/tools/testing/selftests/bpf/progs/tailcall_bpf2bpf6.c#L37">add some local variables</a>. Just make sure the compiler does not optimize them out.</p><p>To make it work, we need to:</p><p>① load the tail call count saved on stack into <code>rax</code> before <code>call</code>’ing the subprogram,② adjust the subprogram prologue, so that it does not reset the <code>rax</code> like the main program does,③ save the passed tail call count on subprogram’s stack for the <code>bpf_tail_call()</code> helper to consume it.</p><p>A <code>bpf_tail_call()</code> within our suprogram will then:</p><p>④ load the tail call count from stack,⑤ unwind the BPF stack, but keep the current subprogram’s stack frame in tact, and⑥ jump to the target BPF program.</p><p>Now we have seen how all the pieces of the puzzle fit together to make BPF tail work on x86-64 safely. The only open question is does it work the same way on other platforms like arm64? Time to shift gears and dive into a completely different BPF JIT implementation.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4WJ7PZxCCS8jaahx6fGfu/a3c1568774c9c47fbf902f1959b3ec17/image10.jpg" />
            
            </figure><p>Based on an image by <a href="https://www.flickr.com/photos/pockethifi/48303582037/">Wutthichai Charoenburi</a>, <a href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a></p>
    <div>
      <h2>Tail calls on arm64</h2>
      <a href="#tail-calls-on-arm64">
        
      </a>
    </div>
    <p>If you try loading a BPF program that uses both BPF function calls (aka BPF to BPF calls) and BPF tail calls on an arm64 machine running the latest 5.15 LTS kernel, or even the latest 5.19 stable kernel, the BPF verifier will kindly ask you to reconsider your choice:</p>
            <pre><code># uname -rm
5.19.12 aarch64
# bpftool prog loadall tail_call_ex2.o /sys/fs/bpf
libbpf: prog 'entry_prog': BPF program load failed: Invalid argument
libbpf: prog 'entry_prog': -- BEGIN PROG LOAD LOG --
0: R1=ctx(off=0,imm=0) R10=fp0
; __attribute__((musttail)) return sub_func(skb);
0: (85) call pc+1
caller:
 R10=fp0
callee:
 frame1: R1=ctx(off=0,imm=0) R10=fp0
; bpf_tail_call(skb, &amp;jmp_table, 0);
2: (18) r2 = 0xffffff80c38c7200       ; frame1: R2_w=map_ptr(off=0,ks=4,vs=4,imm=0)
4: (b7) r3 = 0                        ; frame1: R3_w=P0
5: (85) call bpf_tail_call#12
tail_calls are not allowed in non-JITed programs with bpf-to-bpf calls
processed 4 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
-- END PROG LOAD LOG --
…
#</code></pre>
            <p>That is a pity! We have been looking forward to reaping the benefits of code sharing with BPF to BPF calls in our lengthy machine generated BPF programs. So we asked - how hard could it be to make it work?</p><p>After all, BPF <a href="https://elixir.bootlin.com/linux/v5.15.70/source/arch/arm64/net/bpf_jit_comp.c">JIT for arm64</a> already can handle BPF tail calls and BPF to BPF calls, when used in isolation.</p><p>It is “just” a matter of understanding the existing JIT implementation, which lives in <code>arch/arm64/net/bpf_jit_comp.c</code>, and identifying the missing pieces.</p><p>To understand how BPF JIT for arm64 works, we will use the same method as before - look at its code together with sample input (BPF instructions) and output (arm64 instructions).</p><p>We don’t have to read the whole source code. It is enough to zero in on a few particular code paths:</p><p><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L1356"><span>bpf_int_jit_compile()</span><span> ?</span><span><br /></span></a><span>   </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L246"><span>build_prologue()</span><span> ?</span><span><br /></span></a><span>   </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L1287"><span>build_body()</span><span> ?</span><span><br /></span></a><span>     </span><span>for (i = 0; i &lt; prog-&gt;len; i++) {</span><span><br /></span><span>        </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L663"><span>build_insn()</span><span> ?</span><span><br /></span></a><span>          </span><span>switch (code) {</span><span><br /></span><span>          </span><span>case BPF_JMP | BPF_CALL:</span><span><br /></span><span>            </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L983"><span>/* emit function call */</span><span> ?</span><span><br /></span></a><span>          </span><span>case BPF_JMP | BPF_TAIL_CALL:</span><span><br /></span><span>            </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L329"><span>emit_bpf_tail_call()</span><span> ?</span><span><br /></span></a><span>          </span><span>}</span><span><br /></span><span>     </span><span>}</span><span><br /></span><span>   </span><a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L559"><span>build_epilogue()</span><span> ?</span></a></p><p>One thing that the arm64 architecture, and RISC architectures in general, are known for is that it has a plethora of general purpose registers (<code>x0-x30</code>). This is a good thing. We have more registers to allocate to JIT internal state, like the tail call count. A cheat sheet of what <a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L42">roles</a> the hardware registers play in the BPF JIT will be helpful:</p>
<table>
<thead>
  <tr>
    <th>BPF</th>
    <th>arm64</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>r0</td>
    <td>x7</td>
  </tr>
  <tr>
    <td>r1</td>
    <td>x0</td>
  </tr>
  <tr>
    <td>r2</td>
    <td>x1</td>
  </tr>
  <tr>
    <td>r3</td>
    <td>x2</td>
  </tr>
  <tr>
    <td>r4</td>
    <td>x3</td>
  </tr>
  <tr>
    <td>r5</td>
    <td>x4</td>
  </tr>
  <tr>
    <td>r6</td>
    <td>x19</td>
  </tr>
  <tr>
    <td>r7</td>
    <td>x20</td>
  </tr>
  <tr>
    <td>r8</td>
    <td>x21</td>
  </tr>
  <tr>
    <td>r9</td>
    <td>x22</td>
  </tr>
  <tr>
    <td>r10</td>
    <td>x25</td>
  </tr>
  <tr>
    <td>internal</td>
    <td>x9-x12, x26 (tail_call_cnt), x27</td>
  </tr>
</tbody>
</table><p>Now let’s try to understand the state of things by looking at the JIT’s input and output for two particular scenarios: (1) a BPF tail call, and (2) a BPF to BPF call.</p><p>It is hard to read assembly code selectively. We will have to go through all instructions one by one, and understand what each one is doing.</p><p>⚠ Brace yourself. Time to decipher a bit of ARM64 assembly. If this will be your first time reading ARM64 assembly, you might want to at least skim through this <a href="https://modexp.wordpress.com/2018/10/30/arm64-assembly/">Guide to ARM64 / AArch64 Assembly on Linux</a> before diving in.</p><p>Scenario #1: A single BPF tail call - <a href="https://github.com/jsitnicki/cloudflare-blog/blob/jakub/2022-10-bpf-tail-calls/2022-10-bpf-tail-call/tail_call_ex1.bpf.c"><code>tail_call_ex1.bpf.c</code></a></p><p>Input: BPF assembly (<code>bpftool prog dump xlated</code>)</p>
            <pre><code>   0: (18) r2 = map[id:4]           // jmp_table map
   2: (b7) r3 = 0
   3: (85) call bpf_tail_call#12
   4: (b7) r0 = 61453               // 0xf00d
   5: (95) exit</code></pre>
            <p>Output: ARM64 assembly (<code>bpftool prog dump jited</code>)</p>
            <pre><code> 0:   paciasp                            // Sign LR (ROP protection) ①
 4:   stp     x29, x30, [sp, #-16]!      // Save FP and LR registers ②
 8:   mov     x29, sp                    // Set up Frame Pointer
 c:   stp     x19, x20, [sp, #-16]!      // Save callee-saved registers ③
10:   stp     x21, x22, [sp, #-16]!      // ⋮ 
14:   stp     x25, x26, [sp, #-16]!      // ⋮ 
18:   stp     x27, x28, [sp, #-16]!      // ⋮ 
1c:   mov     x25, sp                    // Set up BPF stack base register (r10)
20:   mov     x26, #0x0                  // Initialize tail_call_cnt ④
24:   sub     x27, x25, #0x0             // Calculate FP bottom ⑤
28:   sub     sp, sp, #0x200             // Set up BPF program stack ⑥
2c:   mov     x1, #0xffffff80ffffffff    // r2 = map[id:4] ⑦
30:   movk    x1, #0xc38c, lsl #16       // ⋮ 
34:   movk    x1, #0x7200                // ⋮
38:   mov     x2, #0x0                   // r3 = 0
3c:   mov     w10, #0x24                 // = offsetof(struct bpf_array, map.max_entries) ⑧
40:   ldr     w10, [x1, x10]             // Load array-&gt;map.max_entries
44:   add     w2, w2, #0x0               // = index (0)
48:   cmp     w2, w10                    // if (index &gt;= array-&gt;map.max_entries)
4c:   b.cs    0x0000000000000088         //     goto out;
50:   mov     w10, #0x21                 // = MAX_TAIL_CALL_CNT (33)
54:   cmp     x26, x10                   // if (tail_call_cnt &gt;= MAX_TAIL_CALL_CNT)
58:   b.cs    0x0000000000000088         //     goto out;
5c:   add     x26, x26, #0x1             // tail_call_cnt++;
60:   mov     w10, #0x110                // = offsetof(struct bpf_array, ptrs)
64:   add     x10, x1, x10               // = &amp;array-&gt;ptrs
68:   lsl     x11, x2, #3                // = index * sizeof(array-&gt;ptrs[0])
6c:   ldr     x11, [x10, x11]            // prog = array-&gt;ptrs[index];
70:   cbz     x11, 0x0000000000000088    // if (prog == NULL) goto out;
74:   mov     w10, #0x30                 // = offsetof(struct bpf_prog, bpf_func)
78:   ldr     x10, [x11, x10]            // Load prog-&gt;bpf_func
7c:   add     x10, x10, #0x24            // += PROLOGUE_OFFSET * AARCH64_INSN_SIZE (4)
80:   add     sp, sp, #0x200             // Unwind BPF stack
84:   br      x10                        // goto *(prog-&gt;bpf_func + prologue_offset)
88:   mov     x7, #0xf00d                // r0 = 0xf00d
8c:   add     sp, sp, #0x200             // Unwind BPF stack ⑨
90:   ldp     x27, x28, [sp], #16        // Restore used callee-saved registers
94:   ldp     x25, x26, [sp], #16        // ⋮
98:   ldp     x21, x22, [sp], #16        // ⋮
9c:   ldp     x19, x20, [sp], #16        // ⋮
a0:   ldp     x29, x30, [sp], #16        // ⋮
a4:   add     x0, x7, #0x0               // Set return value
a8:   autiasp                            // Authenticate LR
ac:   ret                                // Return to caller</code></pre>
            <p>① BPF program prologue starts with Pointer Authentication Code (PAC), which protects against Return <a href="https://developer.arm.com/documentation/102433/0100/Return-oriented-programming">Oriented Programming attacks</a>. PAC instructions are emitted by JIT only if CONFIG_ARM64_PTR_AUTH_KERNEL is enabled.</p><p>② <a href="https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst">Arm 64 Architecture Procedure Call Standard</a> <a href="https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#the-frame-pointer">mandates</a> that the Frame Pointer (register X29) and the Link Register (register X30), aka the return address, of the caller should be recorded onto the stack.</p><p>③ Registers X19 to X28, and X29 (FP) plus X30 (LR), are callee saved. ARM64 BPF JIT does not use registers X23 and X24 currently, so they are not saved.</p><p>④ We track the tail call depth in X26. No need to save it onto stack since we use a register dedicated just for this purpose.</p><p>⑤ FP bottom is an optimization that allows store/loads to BPF stack <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5b3d19b9bd4080d7f5e260f91ce8f639e19eb499">with a single instruction and an immediate offset value</a>.</p><p>⑥ Reserve space for the BPF program stack. The stack layout is now as shown in a <a href="https://elixir.bootlin.com/linux/v5.19.12/source/arch/arm64/net/bpf_jit_comp.c#L260">diagram in <code>build_prologue()</code></a> source code.</p><p>⑦ The BPF function body starts here.</p><p>⑧ <code>bpf_tail_call()</code> instructions start here.</p><p>⑨ The epilogue starts here.</p><p>Whew! That was a handful ?.</p><p>Notice that the BPF tail call implementation on arm64 is not as optimized as on x86-64. There is no code patching to make direct jumps when the target program index is known at the JIT-compilation time. Instead, the target address is always loaded from the BPF program array.</p><p>Ready for the second scenario? I promise it will be shorter. Function prologue and epilogue instructions will look familiar, so we are going to keep annotations down to a minimum.</p><p>Scenario #2: A BPF to BPF call - <a href="https://github.com/jsitnicki/cloudflare-blog/blob/jakub/2022-10-bpf-tail-calls/2022-10-bpf-tail-call/sub_call_ex1.bpf.c"><code>sub_call_ex1.bpf.c</code></a></p><p>Input: BPF assembly (<code>bpftool prog dump xlated</code>)</p>
            <pre><code>int entry_prog(struct __sk_buff * skb):
   0: (85) call pc+1#bpf_prog_a84919ecd878b8f3_sub_func
   1: (95) exit
int sub_func(struct __sk_buff * skb):
   2: (b7) r0 = 61453                   // 0xf00d
   3: (95) exit</code></pre>
            <p>Output: ARM64 assembly</p>
            <pre><code>int entry_prog(struct __sk_buff * skb):
bpf_prog_163e74e7188910f2_entry_prog:
   0:   paciasp                                 // Begin prologue
   4:   stp     x29, x30, [sp, #-16]!           // ⋮
   8:   mov     x29, sp                         // ⋮
   c:   stp     x19, x20, [sp, #-16]!           // ⋮
  10:   stp     x21, x22, [sp, #-16]!           // ⋮
  14:   stp     x25, x26, [sp, #-16]!           // ⋮
  18:   stp     x27, x28, [sp, #-16]!           // ⋮
  1c:   mov     x25, sp                         // ⋮
  20:   mov     x26, #0x0                       // ⋮
  24:   sub     x27, x25, #0x0                  // ⋮
  28:   sub     sp, sp, #0x0                    // End prologue
  2c:   mov     x10, #0xffffffffffff5420        // Build sub_func()+0x0 address
  30:   movk    x10, #0x8ff, lsl #16            // ⋮
  34:   movk    x10, #0xffc0, lsl #32           // ⋮
  38:   blr     x10 ------------------.         // Call sub_func()+0x0 
  3c:   add     x7, x0, #0x0 &lt;----------.       // r0 = sub_func()
  40:   mov     sp, sp                | |       // Begin epilogue
  44:   ldp     x27, x28, [sp], #16   | |       // ⋮
  48:   ldp     x25, x26, [sp], #16   | |       // ⋮
  4c:   ldp     x21, x22, [sp], #16   | |       // ⋮
  50:   ldp     x19, x20, [sp], #16   | |       // ⋮
  54:   ldp     x29, x30, [sp], #16   | |       // ⋮
  58:   add     x0, x7, #0x0          | |       // ⋮
  5c:   autiasp                       | |       // ⋮
  60:   ret                           | |       // End epilogue
                                      | |
int sub_func(struct __sk_buff * skb): | |
bpf_prog_a84919ecd878b8f3_sub_func:   | |
   0:   paciasp &lt;---------------------' |       // Begin prologue
   4:   stp     x29, x30, [sp, #-16]!   |       // ⋮
   8:   mov     x29, sp                 |       // ⋮
   c:   stp     x19, x20, [sp, #-16]!   |       // ⋮
  10:   stp     x21, x22, [sp, #-16]!   |       // ⋮
  14:   stp     x25, x26, [sp, #-16]!   |       // ⋮
  18:   stp     x27, x28, [sp, #-16]!   |       // ⋮
  1c:   mov     x25, sp                 |       // ⋮
  20:   mov     x26, #0x0               |       // ⋮
  24:   sub     x27, x25, #0x0          |       // ⋮
  28:   sub     sp, sp, #0x0            |       // End prologue
  2c:   mov     x7, #0xf00d             |       // r0 = 0xf00d
  30:   mov     sp, sp                  |       // Begin epilogue
  34:   ldp     x27, x28, [sp], #16     |       // ⋮
  38:   ldp     x25, x26, [sp], #16     |       // ⋮
  3c:   ldp     x21, x22, [sp], #16     |       // ⋮
  40:   ldp     x19, x20, [sp], #16     |       // ⋮
  44:   ldp     x29, x30, [sp], #16     |       // ⋮
  48:   add     x0, x7, #0x0            |       // ⋮
  4c:   autiasp                         |       // ⋮
  50:   ret ----------------------------'       // End epilogue</code></pre>
            <p>We have now seen what a BPF tail call and a BPF function/subprogram call compiles down to. Can you already spot what would go wrong if mixing the two was allowed?</p><p>That’s right! Every time we enter a BPF subprogram, we reset the X26 register, which holds the tail call count, to zero (<code>mov x26</code>, <code>#0x0</code>). This is bad. It would let users create program chains longer than the <code>MAX_TAIL_CALL_CNT</code> limit.</p><p>How about we just skip this step when emitting the prologue for BPF subprograms?</p>
            <pre><code>@@ -246,6 +246,7 @@ static bool is_lsi_offset(int offset, int scale)
 static int build_prologue(struct jit_ctx *ctx, bool ebpf_from_cbpf)
 {
        const struct bpf_prog *prog = ctx-&gt;prog;
+       const bool is_main_prog = prog-&gt;aux-&gt;func_idx == 0;
        const u8 r6 = bpf2a64[BPF_REG_6];
        const u8 r7 = bpf2a64[BPF_REG_7];
        const u8 r8 = bpf2a64[BPF_REG_8];
@@ -299,7 +300,7 @@ static int build_prologue(struct jit_ctx *ctx, bool ebpf_from_cbpf)
        /* Set up BPF prog stack base register */
        emit(A64_MOV(1, fp, A64_SP), ctx);

-       if (!ebpf_from_cbpf) {
+       if (!ebpf_from_cbpf &amp;&amp; is_main_prog) {
                /* Initialize tail_call_cnt */
                emit(A64_MOVZ(1, tcc, 0, 0), ctx);</code></pre>
            <p>Believe it or not. This is everything that <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d4609a5d8c70d21b4a3f801cf896a3c16c613fe1">was missing</a> to get BPF tail calls working with function calls on arm64. The feature will be enabled in the upcoming Linux 6.0 release.</p>
    <div>
      <h2>Outro</h2>
      <a href="#outro">
        
      </a>
    </div>
    <p>From recursion to tweaking the BPF JIT. How did we get here? Not important. It’s all about the journey.</p><p>Along the way we have unveiled a few secrets behind BPF tails calls, and hopefully quenched your thirst for low-level programming. At least for today.</p><p>All that is left is to sit back and watch the fruits of our work. With GDB hooked up to a VM, we can observe how a BPF program calls into a BPF function, and from there tail calls to another BPF program:</p><p><a href="https://demo-gdb-step-thru-bpf.pages.dev/">https://demo-gdb-step-thru-bpf.pages.dev/</a></p><p>Until next time ?.</p> ]]></content:encoded>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <guid isPermaLink="false">2mPM5QrHXSfrNhO9mxBcWD</guid>
            <dc:creator>Jakub Sitnicki</dc:creator>
        </item>
        <item>
            <title><![CDATA[Missing Manuals - io_uring worker pool]]></title>
            <link>https://blog.cloudflare.com/missing-manuals-io_uring-worker-pool/</link>
            <pubDate>Fri, 04 Feb 2022 13:58:05 GMT</pubDate>
            <description><![CDATA[ Chances are you might have heard of io_uring. It first appeared in Linux 5.1, back in 2019, and was advertised as the new API for asynchronous I/O. Its goal was to be an alternative to the deemed-to-be-broken-beyond-repair AIO, the “old” asynchronous I/O API ]]></description>
            <content:encoded><![CDATA[ <p>Chances are you might have heard of <code>io_uring</code>. It first appeared in <a href="https://kernelnewbies.org/Linux_5.1#High-performance_asynchronous_I.2FO_with_io_uring">Linux 5.1</a>, back in 2019, and was <a href="https://lwn.net/Articles/776703/">advertised as the new API for asynchronous I/O</a>. Its goal was to be an alternative to the deemed-to-be-broken-beyond-repair <a href="/io_submit-the-epoll-alternative-youve-never-heard-about/">AIO</a>, the “old” asynchronous I/O API.</p><p>Calling <code>io_uring</code> just an asynchronous I/O API doesn’t do it justice, though. Underneath the API calls, io_uring is a full-blown runtime for processing I/O requests. One that spawns threads, sets up work queues, and dispatches requests for processing. All this happens “in the background” so that the user space process doesn’t have to, but can, block while waiting for its I/O requests to complete.</p><p>A runtime that spawns threads and manages the worker pool for the developer makes life easier, but using it in a project begs the questions:</p><p>1. How many threads will be created for my workload by default?</p><p>2. How can I monitor and control the thread pool size?</p><p>I could not find the answers to these questions in either the <a href="https://kernel.dk/io_uring.pdf">Efficient I/O with io_uring</a> article, or the <a href="https://unixism.net/loti/">Lord of the io_uring</a> guide – two well-known pieces of available documentation.</p><p>And while a recent enough <a href="https://manpages.debian.org/unstable/liburing-dev/io_uring_register.2.en.html"><code>io_uring</code> man page</a> touches on the topic:</p><blockquote><p>By default, <code>io_uring</code> limits the unbounded workers created to the maximum processor count set by <code>RLIMIT_NPROC</code> and the bounded workers is a function of the SQ ring size and the number of CPUs in the system.</p></blockquote><p>… it also leads to more questions:</p><p>3. What is an unbounded worker?</p><p>4. How does it differ from a bounded worker?</p><p>Things seem a bit under-documented as is, hence this blog post. Hopefully, it will provide the clarity needed to put <code>io_uring</code> to work in your project when the time comes.</p><p>Before we dig in, a word of warning. This post is not meant to be an introduction to <code>io_uring</code>. The existing documentation does a much better job at showing you the ropes than I ever could. Please give it a read first, if you are not familiar yet with the io_uring API.</p>
    <div>
      <h2>Not all I/O requests are created equal</h2>
      <a href="#not-all-i-o-requests-are-created-equal">
        
      </a>
    </div>
    <p><code>io_uring</code> can perform I/O on any kind of file descriptor; be it a regular file or a special file, like a socket. However, the kind of file descriptor that it operates on makes a difference when it comes to the size of the worker pool.</p><p>You see, <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2e480058ddc21ec53a10e8b41623e245e908bdbc">I/O requests get classified into two categories</a> by <code>io_uring</code>:</p><blockquote><p><code>io-wq</code> divides work into two categories:1. Work that completes in a bounded time, like reading from a regular file or a block device. This type of work is limited based on the size of the SQ ring.2. Work that may never complete, we call this unbounded work. The amount of workers here is limited by <code>RLIMIT_NPROC</code>.</p></blockquote><p>This answers the latter two of our open questions. Unbounded workers handle I/O requests that operate on neither regular files (<code>S_IFREG</code>) nor block devices (<code>S_ISBLK</code>). This is the case for network I/O, where we work with sockets (<code>S_IFSOCK</code>), and other special files like character devices (e.g. <code>/dev/null</code>).</p><p>We now also know that there are different limits in place for how many bounded vs unbounded workers there can be running. So we have to pick one before we dig further.</p>
    <div>
      <h2>Capping the unbounded worker pool size</h2>
      <a href="#capping-the-unbounded-worker-pool-size">
        
      </a>
    </div>
    <p>Pushing data through sockets is Cloudflare’s bread and butter, so this is what we are going to base our test workload around. To put it in <code>io_uring</code> lingo – we will be submitting unbounded work requests.</p><p>While doing that, we will observe how <code>io_uring</code> goes about creating workers.</p><p>To observe how <code>io_uring</code> goes about creating workers we will ask it to read from a UDP socket multiple times. No packets will arrive on the socket, so we will have full control over when the requests complete.</p><p>Here is our test workload - <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2022-02-io_uring-worker-pool/src/bin/udp_read.rs">udp_read.rs</a>.</p>
            <pre><code>$ ./target/debug/udp-read -h
udp-read 0.1.0
read from UDP socket with io_uring

USAGE:
    udp-read [FLAGS] [OPTIONS]

FLAGS:
    -a, --async      Set IOSQE_ASYNC flag on submitted SQEs
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -c, --cpu &lt;cpu&gt;...                     CPU to run on when invoking io_uring_enter for Nth ring (specify multiple
                                           times) [default: 0]
    -w, --workers &lt;max-unbound-workers&gt;    Maximum number of unbound workers per NUMA node (0 - default, that is
                                           RLIMIT_NPROC) [default: 0]
    -r, --rings &lt;num-rings&gt;                Number io_ring instances to create per thread [default: 1]
    -t, --threads &lt;num-threads&gt;            Number of threads creating io_uring instances [default: 1]
    -s, --sqes &lt;sqes&gt;                      Number of read requests to submit per io_uring (0 - fill the whole queue)
                                           [default: 0]</code></pre>
            <p>While it is parametrized for easy experimentation, at its core it doesn’t do much. We fill the submission queue with read requests from a UDP socket and then wait for them to complete. But because data doesn’t arrive on the socket out of nowhere, and there are no timeouts set up, nothing happens. As a bonus, we have complete control over when requests complete, which will come in handy later.</p><p>Let’s run the test workload to convince ourselves that things are working as expected. <code>strace</code> won’t be very helpful when using <code>io_uring</code>. We won’t be able to tie I/O requests to system calls. Instead, we will have to turn to in-kernel tracing.</p><p>Thankfully, <code>io_uring</code> comes with a set of ready to use static tracepoints, which save us the trouble of digging through the source code to decide where to hook up dynamic tracepoints, known as <a href="https://docs.kernel.org/trace/kprobes.html">kprobes</a>.</p><p>We can discover the tracepoints with <a href="https://man7.org/linux/man-pages/man1/perf-list.1.html"><code>perf list</code></a> or <a href="https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#4--l-listing-probes"><code>bpftrace -l</code></a>, or by browsing the <code>events/</code> directory on the <a href="https://www.kernel.org/doc/Documentation/trace/ftrace.txt"><code>tracefs filesystem</code></a>, usually mounted under <code>/sys/kernel/tracing</code>.</p>
            <pre><code>$ sudo perf list 'io_uring:*'

List of pre-defined events (to be used in -e):

  io_uring:io_uring_complete                         [Tracepoint event]
  io_uring:io_uring_cqring_wait                      [Tracepoint event]
  io_uring:io_uring_create                           [Tracepoint event]
  io_uring:io_uring_defer                            [Tracepoint event]
  io_uring:io_uring_fail_link                        [Tracepoint event]
  io_uring:io_uring_file_get                         [Tracepoint event]
  io_uring:io_uring_link                             [Tracepoint event]
  io_uring:io_uring_poll_arm                         [Tracepoint event]
  io_uring:io_uring_poll_wake                        [Tracepoint event]
  io_uring:io_uring_queue_async_work                 [Tracepoint event]
  io_uring:io_uring_register                         [Tracepoint event]
  io_uring:io_uring_submit_sqe                       [Tracepoint event]
  io_uring:io_uring_task_add                         [Tracepoint event]
  io_uring:io_uring_task_run                         [Tracepoint event]</code></pre>
            <p>Judging by the number of tracepoints to choose from, <code>io_uring</code> takes visibility seriously. To help us get our bearings, here is a diagram that maps out paths an I/O request can take inside io_uring code annotated with tracepoint names – not all of them, just those which will be useful to us.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6MeUxepPVh1riK3jpiWQ0m/e29927eafad9fc96d30473af1f637f5b/image3-8.png" />
            
            </figure><p>Starting on the left, we expect our toy workload to push entries onto the submission queue. When we publish submitted entries by calling <a href="https://manpages.debian.org/unstable/liburing-dev/io_uring_enter.2.en.html"><code>io_uring_enter()</code></a>, the kernel consumes the submission queue and constructs internal request objects. A side effect we can observe is a hit on the <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L7193"><code>io_uring:io_uring_submit_sqe</code></a> tracepoint.</p>
            <pre><code>$ sudo perf stat -e io_uring:io_uring_submit_sqe -- timeout 1 ./udp-read

 Performance counter stats for 'timeout 1 ./udp-read':

              4096      io_uring:io_uring_submit_sqe

       1.049016083 seconds time elapsed

       0.003747000 seconds user
       0.013720000 seconds sys</code></pre>
            <p>But, as it turns out, submitting entries is not enough to make <code>io_uring</code> spawn worker threads. Our process remains single-threaded:</p>
            <pre><code>$ ./udp-read &amp; p=$!; sleep 1; ps -o thcount $p; kill $p; wait $p
[1] 25229
THCNT
    1
[1]+  Terminated              ./udp-read</code></pre>
            <p>This shows that <code>io_uring</code> is smart. It knows that sockets support non-blocking I/O, and they <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L2837">can be polled for readiness to read</a>.</p><p>So, by default, <code>io_uring</code> performs a non-blocking read on sockets. This is bound to fail with <code>-EAGAIN</code> in our case. What follows is that <code>io_uring</code> registers a wake-up call (<a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L5570"><code>io_async_wake()</code></a>) for when the socket becomes readable. There is no need to perform a blocking read, when we can wait to be notified.</p><p>This resembles polling the socket with <code>select()</code> or <code>[e]poll()</code> from user space. There is no timeout, if we didn’t ask for it explicitly by submitting an <code>IORING_OP_LINK_TIMEOUT</code> request. <code>io_uring</code> will simply wait indefinitely.</p><p>We can observe <code>io_uring</code> when it calls <a href="https://elixir.bootlin.com/linux/v5.15.16/source/include/linux/poll.h#L86"><code>vfs_poll</code></a>, the machinery behind non-blocking I/O, to monitor the sockets. If that happens, we will be hitting the <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L5691"><code>io_uring:io_uring_poll_arm</code></a> tracepoint. Meanwhile, the wake-ups that follow, if the polled file becomes ready for I/O, can be recorded with the <code>io_uring:io_uring_poll_wake</code> tracepoint embedded in <code>io_async_wake()</code> wake-up call.</p><p>This is what we are experiencing. <code>io_uring</code> is polling the socket for read-readiness:</p>
            <pre><code>$ sudo bpftrace -lv t:io_uring:io_uring_poll_arm
tracepoint:io_uring:io_uring_poll_arm
    void * ctx
    void * req
    u8 opcode
    u64 user_data
    int mask
    int events      
$ sudo bpftrace -e 't:io_uring:io_uring_poll_arm { @[probe, args-&gt;opcode] = count(); } i:s:1 { exit(); }' -c ./udp-read
Attaching 2 probes...


@[tracepoint:io_uring:io_uring_poll_arm, 22]: 4096
$ sudo bpftool btf dump id 1 format c | grep 'IORING_OP_.*22'
        IORING_OP_READ = 22,
$</code></pre>
            <p>To make <code>io_uring</code> spawn worker threads, we have to force the read requests to be processed concurrently in a blocking fashion. We can do this by marking the I/O requests as asynchronous. As <a href="https://manpages.debian.org/bullseye/liburing-dev/io_uring_enter.2.en.html"><code>io_uring_enter(2) man-page</code></a> says:</p>
            <pre><code>  IOSQE_ASYNC
         Normal operation for io_uring is to try and  issue  an
         sqe  as non-blocking first, and if that fails, execute
         it in an async manner. To support more efficient over‐
         lapped  operation  of  requests  that  the application
         knows/assumes will always (or most of the time) block,
         the  application can ask for an sqe to be issued async
         from the start. Available since 5.6.</code></pre>
            <p>This will trigger a call to <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L1482"><code>io_queue_sqe() → io_queue_async_work()</code></a>, which deep down invokes <a href="https://elixir.bootlin.com/linux/v5.15.16/source/kernel/fork.c#L2520"><code>create_io_worker() → create_io_thread()</code></a> to spawn a new task to process work. Remember that last function, <code>create_io_thread()</code> – it will come up again later.</p><p>Our toy program sets the <code>IOSQE_ASYNC</code> flag on requests when we pass the <code>--async</code> command line option to it. Let’s give it a try:</p>
            <pre><code>$ ./udp-read --async &amp; pid=$!; sleep 1; ps -o pid,thcount $pid; kill $pid; wait $pid
[2] 3457597
    PID THCNT
3457597  4097
[2]+  Terminated              ./udp-read --async
$</code></pre>
            <p>The thread count went up by the number of submitted I/O requests (4,096). And there is one extra thread - the main thread. <code>io_uring</code> has spawned workers.</p><p>If we trace it again, we see that requests are now taking the blocking-read path, and we are hitting the <code>io_uring:io_uring_queue_async_work</code> tracepoint on the way.</p>
            <pre><code>$ sudo perf stat -a -e io_uring:io_uring_poll_arm,io_uring:io_uring_queue_async_work -- ./udp-read --async
^C./udp-read: Interrupt

 Performance counter stats for 'system wide':

                 0      io_uring:io_uring_poll_arm
              4096      io_uring:io_uring_queue_async_work

       1.335559294 seconds time elapsed

$</code></pre>
            <p>In the code, the fork happens in the <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L7046"><code>io_queue_sqe()</code> function</a>, where we are now branching off to <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io_uring.c#L1482"><code>io_queue_async_work()</code></a>, which contains the corresponding tracepoint.</p><p>We got what we wanted. We are now using the worker thread pool.</p><p>However, having 4,096 threads just for reading one socket sounds like overkill. If we were to limit the number of worker threads, how would we go about that? There are four ways I know of.</p>
    <div>
      <h3>Method 1 - Limit the number of in-flight requests</h3>
      <a href="#method-1-limit-the-number-of-in-flight-requests">
        
      </a>
    </div>
    <p>If we take care to never have more than some number of in-flight blocking I/O requests, then we will have more or less the same number of workers. This is because:</p><ol><li><p><code>io_uring</code> spawns workers only when there is work to process. We control how many requests we submit and can throttle new submissions based on completion notifications.</p></li><li><p><code>io_uring</code> retires workers when there is no more pending work in the queue. Although, there is a grace period before a worker dies.</p></li></ol><p>The downside of this approach is that by throttling submissions, we reduce batching. We will have to drain the completion queue, refill the submission queue, and switch context with <code>io_uring_enter()</code> syscall more often.</p><p>We can convince ourselves that this method works by tweaking the number of submitted requests, and observing the thread count as the requests complete. The <code>--sqes &lt;n&gt;</code> option (<b>s</b>ubmission <b>q</b>ueue <b>e</b>ntrie<b>s</b>) controls how many read requests get queued by our workload. If we want a request to complete, we simply need to send a packet toward the UDP socket we are reading from. The workload does not refill the submission queue.</p>
            <pre><code>$ ./udp-read --async --sqes 8 &amp; pid=$!
[1] 7264
$ ss -ulnp | fgrep pid=$pid
UNCONN 0      0          127.0.0.1:52763      0.0.0.0:*    users:(("udp-read",pid=7264,fd=3))
$ ps -o thcount $pid; nc -zu 127.0.0.1 52763; echo -e '\U1F634'; sleep 5; ps -o thcount $pid
THCNT
    9
?
THCNT
    8
$</code></pre>
            <p>After sending one packet, the run queue length shrinks by one, and the thread count soon follows.</p><p>This works, but we can do better.</p>
    <div>
      <h3>Method 2 - Configure IORING_REGISTER_IOWQ_MAX_WORKERS</h3>
      <a href="#method-2-configure-ioring_register_iowq_max_workers">
        
      </a>
    </div>
    <p>In 5.15 the <a href="https://manpages.debian.org/unstable/liburing-dev/io_uring_register.2.en.html"><code>io_uring_register()</code> syscall</a> gained a new command for setting the maximum number of bound and unbound workers.</p>
            <pre><code>  IORING_REGISTER_IOWQ_MAX_WORKERS
         By default, io_uring limits the unbounded workers cre‐
         ated   to   the   maximum   processor   count  set  by
         RLIMIT_NPROC and the bounded workers is a function  of
         the SQ ring size and the number of CPUs in the system.
         Sometimes this can be excessive (or  too  little,  for
         bounded),  and  this  command provides a way to change
         the count per ring (per NUMA node) instead.

         arg must be set to an unsigned int pointer to an array
         of  two values, with the values in the array being set
         to the maximum count of workers per NUMA node. Index 0
         holds  the bounded worker count, and index 1 holds the
         unbounded worker  count.  On  successful  return,  the
         passed  in array will contain the previous maximum va‐
         lyes for each type. If the count being passed in is 0,
         then  this  command returns the current maximum values
         and doesn't modify the current setting.  nr_args  must
         be set to 2, as the command takes two values.

         Available since 5.15.</code></pre>
            <p>By the way, if you would like to grep through the <code>io_uring</code> man pages, they live in the <a href="https://github.com/axboe/liburing">liburing</a> repo maintained by <a href="https://twitter.com/axboe">Jens Axboe</a> – not the go-to repo for Linux API <a href="https://github.com/mkerrisk/man-pages">man-pages</a> maintained by <a href="https://twitter.com/mkerrisk">Michael Kerrisk</a>.</p><p>Since it is a fresh addition to the <code>io_uring</code> API, the <a href="https://docs.rs/io-uring/latest/io_uring/"><code>io-uring</code></a> Rust library we are using has not caught up yet. But with <a href="https://github.com/tokio-rs/io-uring/pull/121">a bit of patching</a>, we can make it work.</p><p>We can tell our toy program to set <code>IORING_REGISTER_IOWQ_MAX_WORKERS (= 19 = 0x13)</code> by running it with the <code>--workers &lt;N&gt;</code> option:</p>
            <pre><code>$ strace -o strace.out -e io_uring_register ./udp-read --async --workers 8 &amp;
[1] 3555377
$ pstree -pt $!
strace(3555377)───udp-read(3555380)─┬─{iou-wrk-3555380}(3555381)
                                    ├─{iou-wrk-3555380}(3555382)
                                    ├─{iou-wrk-3555380}(3555383)
                                    ├─{iou-wrk-3555380}(3555384)
                                    ├─{iou-wrk-3555380}(3555385)
                                    ├─{iou-wrk-3555380}(3555386)
                                    ├─{iou-wrk-3555380}(3555387)
                                    └─{iou-wrk-3555380}(3555388)
$ cat strace.out
io_uring_register(4, 0x13 /* IORING_REGISTER_??? */, 0x7ffd9b2e3048, 2) = 0
$</code></pre>
            <p>This works perfectly. We have spawned just eight <code>io_uring</code> worker threads to handle 4k of submitted read requests.</p><p>Question remains - is the set limit per io_uring instance? Per thread? Per process? Per UID? Read on to find out.</p>
    <div>
      <h3>Method 3 - Set RLIMIT_NPROC resource limit</h3>
      <a href="#method-3-set-rlimit_nproc-resource-limit">
        
      </a>
    </div>
    <p>A resource limit for the maximum number of new processes is another way to cap the worker pool size. The documentation for the <code>IORING_REGISTER_IOWQ_MAX_WORKERS</code> command mentions this.</p><p>This resource limit overrides the <code>IORING_REGISTER_IOWQ_MAX_WORKERS</code> setting, which makes sense because bumping <code>RLIMIT_NPROC</code> above the configured hard maximum requires <code>CAP_SYS_RESOURCE</code> capability.</p><p>The catch is that the limit is tracked <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=21d1c5e386bc751f1953b371d72cd5b7d9c9e270">per UID within a user namespace</a>.</p><p>Setting the new process limit without using a dedicated UID or outside a dedicated user namespace, where other processes are running under the same UID, can have surprising effects.</p><p>Why? io_uring will try over and over again to scale up the worker pool, only to generate a bunch of <code>-EAGAIN</code> errors from <code>create_io_worker()</code> if it can’t reach the configured <code>RLIMIT_NPROC</code> limit:</p>
            <pre><code>$ prlimit --nproc=8 ./udp-read --async &amp;
[1] 26348
$ ps -o thcount $!
THCNT
    3
$ sudo bpftrace --btf -e 'kr:create_io_thread { @[retval] = count(); } i:s:1 { print(@); clear(@); } END { clear(@); }' -c '/usr/bin/sleep 3' | cat -s
Attaching 3 probes...
@[-11]: 293631
@[-11]: 306150
@[-11]: 311959

$ mpstat 1 3
Linux 5.15.9-cloudflare-2021.12.8 (bullseye)    01/04/22        _x86_64_        (4 CPU)
                                   ???
02:52:46     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
02:52:47     all    0.00    0.00   25.00    0.00    0.00    0.00    0.00    0.00    0.00   75.00
02:52:48     all    0.00    0.00   25.13    0.00    0.00    0.00    0.00    0.00    0.00   74.87
02:52:49     all    0.00    0.00   25.30    0.00    0.00    0.00    0.00    0.00    0.00   74.70
Average:     all    0.00    0.00   25.14    0.00    0.00    0.00    0.00    0.00    0.00   74.86
$</code></pre>
            <p>We are hogging one core trying to spawn new workers. This is not the best use of CPU time.</p><p>So, if you want to use <code>RLIMIT_NPROC</code> as a safety cap over the <code>IORING_REGISTER_IOWQ_MAX_WORKERS</code> limit, you better use a “fresh” UID or a throw-away user namespace:</p>
            <pre><code>$ unshare -U prlimit --nproc=8 ./udp-read --async --workers 16 &amp;
[1] 3555870
$ ps -o thcount $!
THCNT
    9</code></pre>
            
    <div>
      <h3>Anti-Method 4 - cgroup process limit - pids.max file</h3>
      <a href="#anti-method-4-cgroup-process-limit-pids-max-file">
        
      </a>
    </div>
    <p>There is also one other way to cap the worker pool size – <a href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#pid-interface-files">limit the number of tasks</a> (that is, processes and their threads) in a control group.</p><p>It is an anti-example and a potential misconfiguration to watch out for, because just like with <code>RLIMIT_NPROC</code>, we can fall into the same trap where <code>io_uring</code> will burn CPU:</p>
            <pre><code>$ systemd-run --user -p TasksMax=128 --same-dir --collect --service-type=exec ./udp-read --async
Running as unit: run-ra0336ff405f54ad29726f1e48d6a3237.service
$ systemd-cgls --user-unit run-ra0336ff405f54ad29726f1e48d6a3237.service
Unit run-ra0336ff405f54ad29726f1e48d6a3237.service (/user.slice/user-1000.slice/user@1000.service/app.slice/run-ra0336ff405f54ad29726f1e48d6a3237.service):
└─823727 /blog/io-uring-worker-pool/./udp-read --async
$ cat /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-ra0336ff405f54ad29726f1e48d6a3237.service/pids.max
128
$ ps -o thcount 823727
THCNT
  128
$ sudo bpftrace --btf -e 'kr:create_io_thread { @[retval] = count(); } i:s:1 { print(@); clear(@); }'
Attaching 2 probes...
@[-11]: 163494
@[-11]: 173134
@[-11]: 184887
^C

@[-11]: 76680
$ systemctl --user stop run-ra0336ff405f54ad29726f1e48d6a3237.service
$</code></pre>
            <p>Here, we again see <code>io_uring</code> wasting time trying to spawn more workers without success. The kernel does not let the number of tasks within the service’s control group go over the limit.</p><p>Okay, so we know what is the best and the worst way to put a limit on the number of <code>io_uring</code> workers. But is the limit per <code>io_uring</code> instance? Per user? Or something else?</p>
    <div>
      <h2>One ring, two ring, three ring, four …</h2>
      <a href="#one-ring-two-ring-three-ring-four">
        
      </a>
    </div>
    <p>Your process is not limited to one instance of io_uring, naturally. In the case of a network proxy, where we push data from one socket to another, we could have one instance of io_uring servicing each half of the proxy.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2TXCzhgUiUGocx8WP9rJ8B/b77b4b9d89414398674d069dd80ae749/image2-3.png" />
            
            </figure><p>How many worker threads will be created in the presence of multiple <code>io_urings</code>? That depends on whether your program is single- or multithreaded.</p><p>In the single-threaded case, if the main thread creates two io_urings, and configures each io_uring to have a maximum of two unbound workers, then:</p>
            <pre><code>$ unshare -U ./udp-read --async --threads 1 --rings 2 --workers 2 &amp;
[3] 3838456
$ pstree -pt $!
udp-read(3838456)─┬─{iou-wrk-3838456}(3838457)
                  └─{iou-wrk-3838456}(3838458)
$ ls -l /proc/3838456/fd
total 0
lrwx------ 1 vagrant vagrant 64 Dec 26 03:32 0 -&gt; /dev/pts/0
lrwx------ 1 vagrant vagrant 64 Dec 26 03:32 1 -&gt; /dev/pts/0
lrwx------ 1 vagrant vagrant 64 Dec 26 03:32 2 -&gt; /dev/pts/0
lrwx------ 1 vagrant vagrant 64 Dec 26 03:32 3 -&gt; 'socket:[279241]'
lrwx------ 1 vagrant vagrant 64 Dec 26 03:32 4 -&gt; 'anon_inode:[io_uring]'
lrwx------ 1 vagrant vagrant 64 Dec 26 03:32 5 -&gt; 'anon_inode:[io_uring]'</code></pre>
            <p>… a total of two worker threads will be spawned.</p><p>While in the case of a multithreaded program, where two threads create one <code>io_uring</code> each, with a maximum of two unbound workers per ring:</p>
            <pre><code>$ unshare -U ./udp-read --async --threads 2 --rings 1 --workers 2 &amp;
[2] 3838223
$ pstree -pt $!
udp-read(3838223)─┬─{iou-wrk-3838224}(3838227)
                  ├─{iou-wrk-3838224}(3838228)
                  ├─{iou-wrk-3838225}(3838226)
                  ├─{iou-wrk-3838225}(3838229)
                  ├─{udp-read}(3838224)
                  └─{udp-read}(3838225)
$ ls -l /proc/3838223/fd
total 0
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 0 -&gt; /dev/pts/0
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 1 -&gt; /dev/pts/0
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 2 -&gt; /dev/pts/0
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 3 -&gt; 'socket:[279160]'
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 4 -&gt; 'socket:[279819]'
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 5 -&gt; 'anon_inode:[io_uring]'
lrwx------ 1 vagrant vagrant 64 Dec 26 02:53 6 -&gt; 'anon_inode:[io_uring]'</code></pre>
            <p>… four workers will be spawned in total – two for each of the program threads. This is reflected by the owner thread ID present in the worker’s name (<code>iou-wrk-&lt;tid&gt;</code>).</p><p>So you might think - “It makes sense! Each thread has their own dedicated pool of I/O workers, which service all the <code>io_uring</code> instances operated by that thread.”</p><p>And you would be right<sup>1</sup>. If we follow the code – <code>task_struct</code> has an instance of <code>io_uring_task</code>, aka <code>io_uring</code> context for the task<sup>2</sup>. Inside the context, we have a reference to the <code>io_uring</code> work queue (<code>struct io_wq</code>), which is actually an array of work queue entries (<code>struct io_wqe</code>). More on why that is an array soon.</p><p>Moving down to the work queue entry, we arrive at the work queue accounting table (<code>struct io_wqe_acct [2]</code>), with one record for each type of work – bounded and unbounded. This is where <code>io_uring</code> keeps track of the worker pool limit (<code>max_workers</code>) the number of existing workers (<code>nr_workers</code>).</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5czT5432CY7bwHC2jznios/dc8672e1c4ed4d215e5b4d659988806b/image4-3.png" />
            
            </figure><p>The perhaps not-so-obvious consequence of this arrangement is that setting just the <code>RLIMIT_NPROC</code> limit, without touching <code>IORING_REGISTER_IOWQ_MAX_WORKERS</code>, can backfire for multi-threaded programs.</p><p>See, when the maximum number of workers for an io_uring instance is not configured, <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io-wq.c#L1162">it defaults to <code>RLIMIT_NPROC</code></a>. This means that <code>io_uring</code> will try to scale the unbounded worker pool to <code>RLIMIT_NPROC</code> for each thread that operates on an <code>io_uring</code> instance.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/42pJE30GsrNspCHfDzq5xa/08dadfaa1120aaca648d8cb2ebbb1fc1/io_uring_workers_multi_threaded.png" />
            
            </figure><p>A multi-threaded process, by definition, creates threads. Now recall that the process management in the kernel tracks the number of tasks <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=21d1c5e386bc751f1953b371d72cd5b7d9c9e270">per UID within the user namespace</a>. Each spawned thread depletes the quota set by <code>RLIMIT_NPROC</code>. As a consequence, <code>io_uring</code> will never be able to fully scale up the worker pool, and will burn the CPU trying to do so.</p>
            <pre><code>$ unshare -U prlimit --nproc=4 ./udp-read --async --threads 2 --rings 1 &amp;
[1] 26249
vagrant@bullseye:/blog/io-uring-worker-pool$ pstree -pt $!
udp-read(26249)─┬─{iou-wrk-26251}(26252)
                ├─{iou-wrk-26251}(26253)
                ├─{udp-read}(26250)
                └─{udp-read}(26251)
$ sudo bpftrace --btf -e 'kretprobe:create_io_thread { @[retval] = count(); } interval:s:1 { print(@); clear(@); } END { clear(@); }' -c '/usr/bin/sleep 3' | cat -s
Attaching 3 probes...
@[-11]: 517270
@[-11]: 509508
@[-11]: 461403

$ mpstat 1 3
Linux 5.15.9-cloudflare-2021.12.8 (bullseye)    01/04/22        _x86_64_        (4 CPU)
                                   ???
02:23:23     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
02:23:24     all    0.00    0.00   50.13    0.00    0.00    0.00    0.00    0.00    0.00   49.87
02:23:25     all    0.00    0.00   50.25    0.00    0.00    0.00    0.00    0.00    0.00   49.75
02:23:26     all    0.00    0.00   49.87    0.00    0.00    0.50    0.00    0.00    0.00   49.62
Average:     all    0.00    0.00   50.08    0.00    0.00    0.17    0.00    0.00    0.00   49.75
$</code></pre>
            
    <div>
      <h2>NUMA, NUMA, yay <a href="https://en.wikipedia.org/wiki/Numa_Numa_(video)">?</a></h2>
      <a href="#numa-numa-yay">
        
      </a>
    </div>
    <p>Lastly, there’s the case of NUMA systems with more than one memory node. <code>io_uring</code> documentation clearly says that <code>IORING_REGISTER_IOWQ_MAX_WORKERS</code> configures the maximum number of workers per NUMA node.</p><p>That is why, as we have seen, <a href="https://elixir.bootlin.com/linux/v5.15.16/source/fs/io-wq.c#L125"><code>io_wq.wqes</code></a> is an array. It contains one entry, struct <code>io_wqe</code>, for each NUMA node. If your servers are NUMA systems like <a href="/the-epyc-journey-continues-to-milan-in-cloudflares-11th-generation-edge-server/">Cloudflare</a>, that is something to take into account.</p><p>Luckily, we don’t need a NUMA machine to experiment. <a href="https://www.qemu.org/">QEMU</a> happily emulates NUMA architectures. If you are hardcore enough, you can configure the NUMA layout with the right combination of <a href="https://www.qemu.org/docs/master/system/qemu-manpage.html"><code>-smp</code> and <code>-numa</code> options</a>.</p><p>But why bother when the <a href="https://github.com/vagrant-libvirt/vagrant-libvirt"><code>libvirt</code> provider</a> for Vagrant makes it so simple to configure a 2 node / 4 CPU layout:</p>
            <pre><code>    libvirt.numa_nodes = [
      {:cpus =&gt; "0-1", :memory =&gt; "2048"},
      {:cpus =&gt; "2-3", :memory =&gt; "2048"}
    ]</code></pre>
            <p>Let’s confirm how io_uring behaves on a NUMA system.Here’s our NUMA layout with two vCPUs per node ready for experimentation:</p>
            <pre><code>$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1
node 0 size: 1980 MB
node 0 free: 1802 MB
node 1 cpus: 2 3
node 1 size: 1950 MB
node 1 free: 1751 MB
node distances:
node   0   1
  0:  10  20
  1:  20  10</code></pre>
            <p>If we once again run our test workload and ask it to create a single <code>io_uring</code> with a maximum of two workers per NUMA node, then:</p>
            <pre><code>$ ./udp-read --async --threads 1 --rings 1 --workers 2 &amp;
[1] 693
$ pstree -pt $!
udp-read(693)─┬─{iou-wrk-693}(696)
              └─{iou-wrk-693}(697)</code></pre>
            <p>… we get just two workers on a machine with two NUMA nodes. Not the outcome we were hoping for.</p><p>Why are we not reaching the expected pool size of <code>&lt;max workers&gt; × &lt;# NUMA nodes&gt;</code> = 2 × 2 = 4 workers? And is it possible to make it happen?</p><p>Reading the code reveals that – yes, it is possible. However, for the per-node worker pool to be scaled up for a given NUMA node, we have to submit requests, that is, call <code>io_uring_enter()</code>, from a CPU that belongs to that node. In other words, the process scheduler and thread CPU affinity have a say in how many I/O workers will be created.</p><p>We can demonstrate the effect that jumping between CPUs and NUMA nodes has on the worker pool by operating two instances of <code>io_uring</code>. We already know that having more than one io_uring instance per thread does not impact the worker pool limit.</p><p>This time, however, we are going to ask the workload to pin itself to a particular CPU before submitting requests with the <code>--cpu</code> option – first it will run on CPU 0 to enter the first ring, then on CPU 2 to enter the second ring.</p>
            <pre><code>$ strace -e sched_setaffinity,io_uring_enter ./udp-read --async --threads 1 --rings 2 --cpu 0 --cpu 2 --workers 2 &amp; sleep 0.1 &amp;&amp; echo
[1] 6949
sched_setaffinity(0, 128, [0])          = 0
io_uring_enter(4, 4096, 0, 0, NULL, 128) = 4096
sched_setaffinity(0, 128, [2])          = 0
io_uring_enter(5, 4096, 0, 0, NULL, 128) = 4096
io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 128
$ pstree -pt 6949
strace(6949)───udp-read(6953)─┬─{iou-wrk-6953}(6954)
                              ├─{iou-wrk-6953}(6955)
                              ├─{iou-wrk-6953}(6956)
                              └─{iou-wrk-6953}(6957)
$</code></pre>
            <p>Voilà. We have reached the said limit of <code>&lt;max workers&gt; x &lt;# NUMA nodes&gt;</code>.</p>
    <div>
      <h2>Outro</h2>
      <a href="#outro">
        
      </a>
    </div>
    <p>That is all for the very first installment of the Missing Manuals. <code>io_uring</code> has more secrets that deserve a write-up, like request ordering or handling of interrupted syscalls, so Missing Manuals might return soon.</p><p>In the meantime, please tell us what topic would you nominate to have a Missing Manual written?</p><p>Oh, and did I mention that if you enjoy putting cutting edge Linux APIs to use, <a href="https://www.cloudflare.com/careers/jobs/">we are hiring</a>? Now also remotely ?.</p><p>_____</p><p><sup>1</sup>And it probably does not make the users of runtimes that implement a hybrid threading model, like Golang, too happy.</p><p><sup>2</sup>To the Linux kernel, processes and threads are just kinds of tasks, which either share or don’t share some resources.</p> ]]></content:encoded>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <guid isPermaLink="false">6aZLxCaZdX6PbblPyyepdf</guid>
            <dc:creator>Jakub Sitnicki</dc:creator>
        </item>
        <item>
            <title><![CDATA[Conntrack turns a blind eye to dropped SYNs]]></title>
            <link>https://blog.cloudflare.com/conntrack-turns-a-blind-eye-to-dropped-syns/</link>
            <pubDate>Thu, 04 Mar 2021 12:00:00 GMT</pubDate>
            <description><![CDATA[ We have been dealing with conntrack, the connection tracking layer in the Linux kernel, for years. And yet, despite the collected know-how, questions about its inner workings occasionally come up. When they do, it is hard to resist the temptation to go digging for answers. ]]></description>
            <content:encoded><![CDATA[ 
    <div>
      <h2>Intro</h2>
      <a href="#intro">
        
      </a>
    </div>
    <p>We have been working with conntrack, the connection tracking layer in the Linux kernel, for years. And yet, despite the collected know-how, questions about its inner workings occasionally come up. When they do, it is hard to resist the temptation to go digging for answers.</p><p>One such question popped up while writing <a href="/conntrack-tales-one-thousand-and-one-flows/">the previous blog post on conntrack</a>:</p><blockquote><p>“Why are there no entries in the conntrack table for SYN packets dropped by the firewall?”</p></blockquote><p>Ready for a deep dive into the network stack? Let’s find out.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2hrMaSaIFXFsXevfMqgsdh/becd0e20191174d5c30970e7e6c00a1e/tqbla_C4bF9esDdiQKx-12wXVI3xKv6IDUklAgB0zu6G4KiC3ziZeCJkSgUWechnpaCnFBL6XY_glt1HNaXDqURrRm6ttta7ciHiG8vidp7x6Th0eQUqXQF4Ure.jpeg" />
            
            </figure><p><a href="https://pixabay.com/photos/female-diver-sea-scuba-diving-4829158/"><i>Image</i></a> <i>by</i> <a href="https://pixabay.com/users/chulmin1700-15022416/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=4829158"><i>chulmin park</i></a> <i>from</i> <a href="https://pixabay.com/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=4829158"><i>Pixabay</i></a></p><p>We already know from <a href="/conntrack-tales-one-thousand-and-one-flows/">last time</a> that conntrack is in charge of tracking incoming and outgoing network traffic. By running conntrack -L we can inspect existing network flows, or as conntrack calls them, connections.</p><p>So if we spin up a <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2021-03-conntrack-syn-drop/Vagrantfile">toy VM</a>, <i>connect</i> to it over SSH, and inspect the contents of the conntrack table, we will see…</p>
            <pre><code>$ vagrant init fedora/33-cloud-base
$ vagrant up
…
$ vagrant ssh
Last login: Sun Jan 31 15:08:02 2021 from 192.168.122.1
[vagrant@ct-vm ~]$ sudo conntrack -L
conntrack v1.4.5 (conntrack-tools): 0 flow entries have been shown.</code></pre>
            <p>… nothing!</p><p>Even though the conntrack kernel module is loaded:</p>
            <pre><code>[vagrant@ct-vm ~]$ lsmod | grep '^nf_conntrack\b'
nf_conntrack          163840  1 nf_conntrack_netlink</code></pre>
            <p>Hold on a minute. Why is the SSH connection to the VM not listed in conntrack entries? SSH is working. With each keystroke we are sending packets to the VM. But conntrack doesn’t register it.</p><p>Isn’t conntrack an integral part of the network stack that sees every packet passing through it? ?</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2NeBs0DqiYv9no3mYlBp7f/092bced2f034366e69afb52ad2c101a4/image1-5.png" />
            
            </figure><p>Based on an <a href="https://commons.wikimedia.org/wiki/File:Netfilter-packet-flow.svg"><i>image</i></a> <i>by</i> <a href="https://commons.wikimedia.org/wiki/User_talk:Jengelh"><i>Jan Engelhardt</i></a> <i>CC BY-SA 3.0</i></p><p>Clearly everything we learned about conntrack last time is not the whole story.</p>
    <div>
      <h2>Calling into conntrack</h2>
      <a href="#calling-into-conntrack">
        
      </a>
    </div>
    <p>Our little experiment with SSH’ing into a VM begs the question — how does conntrack actually get notified about network packets passing through the stack?</p><p>We can walk <a href="https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/">the receive path step by step</a> and we won’t find any direct calls into the conntrack code in either the IPv4 or IPv6 stack. Conntrack does not interface with the network stack directly.</p><p>Instead, it relies on the Netfilter framework, and its set of hooks baked into in the stack:</p>
            <pre><code>int ip_rcv(struct sk_buff *skb, struct net_device *dev, …)
{
    …
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
               net, NULL, skb, dev, NULL,
               ip_rcv_finish);
}</code></pre>
            <p>Netfilter users, like conntrack, can register callbacks with it. Netfilter will then run all registered callbacks when its hook processes a network packet.</p><p>For the INET family, that is IPv4 and IPv6, there are five Netfilter hooks  to choose from:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3TAI6xvDHx8kWETIdRURIw/f69688dbf7790f766999fe904a5b3b2e/image5-1.png" />
            
            </figure><p>Based on <a href="https://thermalcircle.de/doku.php?id=blog:linux:nftables_packet_flow_netfilter_hooks_detail"><i>Nftables - Packet flow and Netfilter hooks in detail</i></a><i>,</i> <a href="https://thermalcircle.de/"><i>thermalcircle.de</i></a><i>,</i> <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en"><i>CC BY-SA 4.0</i></a></p><p>Which ones does conntrack use? We will get to that in a moment.</p><p>First, let’s focus on the trigger. What makes conntrack register its callbacks with Netfilter?</p><p>The SSH connection doesn’t show up in the conntrack table just because the module is loaded. We already saw that. This means that conntrack doesn’t register its callbacks with Netfilter at module load time.</p><p>Or at least, it doesn't do it by <i>default</i>. Since Linux v5.1 (May 2019) the conntrack module has the <a href="https://github.com/torvalds/linux/commit/ba3fbe663635ae7b33a2d972c5d2def036258e42">enable_hooks parameter</a>, which causes conntrack to register its callbacks on load:</p>
            <pre><code>[vagrant@ct-vm ~]$ modinfo nf_conntrack
…
parm:           enable_hooks:Always enable conntrack hooks (bool)</code></pre>
            <p>Going back to our toy VM, let’s try to reload the conntrack module with enable_hooks set:</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo rmmod nf_conntrack_netlink nf_conntrack
[vagrant@ct-vm ~]$ sudo modprobe nf_conntrack enable_hooks=1
[vagrant@ct-vm ~]$ sudo conntrack -L
tcp      6 431999 ESTABLISHED src=192.168.122.204 dst=192.168.122.1 sport=22 dport=34858 src=192.168.122.1 dst=192.168.122.204 sport=34858 dport=22 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
conntrack v1.4.5 (conntrack-tools): 1 flow entries have been shown.
[vagrant@ct-vm ~]$</code></pre>
            <p>Nice! The conntrack table now contains an entry for our SSH session.</p><p>The Netfilter hook notified conntrack about SSH session packets passing through the stack.</p><p>Now that we know how conntrack gets called, we can go back to our question — can we observe a TCP SYN packet dropped by the firewall with conntrack?</p>
    <div>
      <h2>Listing Netfilter hooks</h2>
      <a href="#listing-netfilter-hooks">
        
      </a>
    </div>
    <p>That is easy to check:</p><ol><li><p>Add a rule to drop anything coming to port tcp/2570<sup>2</sup></p></li></ol>
            <pre><code>[vagrant@ct-vm ~]$ sudo iptables -t filter -A INPUT -p tcp --dport 2570 -j DROP</code></pre>
            <ol><li><p>Connect to the VM on port tcp/2570 from the outside</p></li></ol>
            <pre><code>host $ nc -w 1 -z 192.168.122.204 2570</code></pre>
            <ol><li><p>List conntrack table entries</p></li></ol>
            <pre><code>[vagrant@ct-vm ~]$ sudo conntrack -L
tcp      6 431999 ESTABLISHED src=192.168.122.204 dst=192.168.122.1 sport=22 dport=34858 src=192.168.122.1 dst=192.168.122.204 sport=34858 dport=22 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
conntrack v1.4.5 (conntrack-tools): 1 flow entries have been shown.</code></pre>
            <p>No new entries. Conntrack didn’t record a new flow for the dropped SYN.</p><p>But did it process the SYN packet? To answer that we have to find out which callbacks conntrack registered with Netfilter.</p><p>Netfilter keeps track of callbacks registered for each hook in instances of <code>struct nf_hook_entries</code>. We can reach these objects through the Netfilter state (<code>struct netns_nf</code>), which lives inside network namespace (<code>struct net</code>).</p>
            <pre><code>struct netns_nf {
    …
    struct nf_hook_entries __rcu *hooks_ipv4[NF_INET_NUMHOOKS];
    struct nf_hook_entries __rcu *hooks_ipv6[NF_INET_NUMHOOKS];
    …
}</code></pre>
            <p><code>struct nf_hook_entries</code>, if you look at its <a href="https://elixir.bootlin.com/linux/v5.10.14/source/include/linux/netfilter.h#L101">definition</a>, is a bit of an exotic construct. A glance at how the object size is calculated during its <a href="https://elixir.bootlin.com/linux/v5.10.14/source/net/netfilter/core.c#L50">allocation</a> gives a hint about its memory layout:</p>
            <pre><code>    struct nf_hook_entries *e;
    size_t alloc = sizeof(*e) +
               sizeof(struct nf_hook_entry) * num +
               sizeof(struct nf_hook_ops *) * num +
               sizeof(struct nf_hook_entries_rcu_head);</code></pre>
            <p>It’s an element count, followed by two arrays glued together, and some <a href="https://en.wikipedia.org/wiki/Read-copy-update">RCU</a>-related state which we’re going to ignore. The two arrays have the same size, but hold different kinds of values.</p><p>We can walk the second array, holding pointers to struct nf_hook_ops, to discover the registered callbacks and their priority. Priority determines the invocation order.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5nKYeHIB6aTRVx36yUT3la/d7148917310c537a81ecfed5d639dfe2/image3-3.png" />
            
            </figure><p>With <a href="https://github.com/osandov/drgn">drgn</a>, a programmable C debugger tailored for the Linux kernel, we can locate the Netfilter state in kernel memory, and walk its contents relatively easily. Given we know what we are looking for.</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo drgn
drgn 0.0.8 (using Python 3.9.1, without libkdumpfile)
…
&gt;&gt;&gt; pre_routing_hook = prog['init_net'].nf.hooks_ipv4[0]
&gt;&gt;&gt; for i in range(0, pre_routing_hook.num_hook_entries):
...     pre_routing_hook.hooks[i].hook
...
(nf_hookfn *)ipv4_conntrack_defrag+0x0 = 0xffffffffc092c000
(nf_hookfn *)ipv4_conntrack_in+0x0 = 0xffffffffc093f290
&gt;&gt;&gt;</code></pre>
            <p>Neat! We have a way to access Netfilter state.</p><p>Let’s take it to the next level and list all registered callbacks for each Netfilter hook (using <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2021-03-conntrack-syn-drop/tools/list-nf-hooks">less than 100 lines of Python</a>):</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo /vagrant/tools/list-nf-hooks
? ipv4 PRE_ROUTING
       -400 → ipv4_conntrack_defrag     ☜ conntrack callback
       -300 → iptable_raw_hook
       -200 → ipv4_conntrack_in         ☜ conntrack callback
       -150 → iptable_mangle_hook
       -100 → nf_nat_ipv4_in

? ipv4 LOCAL_IN
       -150 → iptable_mangle_hook
          0 → iptable_filter_hook
         50 → iptable_security_hook
        100 → nf_nat_ipv4_fn
 2147483647 → ipv4_confirm
…</code></pre>
            <p>The output from our script shows that conntrack has two callbacks registered with the <code>PRE_ROUTING</code> hook - <code>ipv4_conntrack_defrag</code> and <code>ipv4_conntrack_in</code>. But are they being called?</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/iBNniB9sIszNQV2u34qd4/59c67b7c7e53749696ca82992296edf0/image4-3.png" />
            
            </figure><p>Based on <a href="https://thermalcircle.de/doku.php?id=blog:linux:nftables_packet_flow_netfilter_hooks_detail"><i>Netfilter PRE_ROUTING hook</i></a><i>,</i> <a href="https://thermalcircle.de/"><i>thermalcircle.de</i></a><i>,</i> <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en"><i>CC BY-SA 4.0</i></a></p>
    <div>
      <h2>Tracing conntrack callbacks</h2>
      <a href="#tracing-conntrack-callbacks">
        
      </a>
    </div>
    <p>We expect that when the Netfilter <code>PRE_ROUTING</code> hook processes a TCP SYN packet, it will invoke <code>ipv4_conntrack_defrag</code> and then <code>ipv4_conntrack_in</code> callbacks.</p><p>To confirm it we will put to use the tracing powers of <a href="https://ebpf.io/">BPF ?</a>. BPF programs can run on entry to functions. These kinds of programs are known as BPF kprobes. In our case we will attach BPF kprobes to conntrack callbacks.</p><p>Usually, when working with BPF, we would write the BPF program in C and use <code>clang -target bpf</code> to compile it. However, for tracing it will be much easier to use <a href="https://bpftrace.org/">bpftrace</a>. With bpftrace we can write <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2021-03-conntrack-syn-drop/tools/trace-conntrack-prerouting.bt">our BPF kprobe program</a> in a <a href="https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md">high-level language</a> inspired by <a href="https://en.wikipedia.org/wiki/AWK">AWK</a>:</p>
            <pre><code>kprobe:ipv4_conntrack_defrag,
kprobe:ipv4_conntrack_in
{
    $skb = (struct sk_buff *)arg1;
    $iph = (struct iphdr *)($skb-&gt;head + $skb-&gt;network_header);
    $th = (struct tcphdr *)($skb-&gt;head + $skb-&gt;transport_header);

    if ($iph-&gt;protocol == 6 /* IPPROTO_TCP */ &amp;&amp;
        $th-&gt;dest == 2570 /* htons(2570) */ &amp;&amp;
        $th-&gt;syn == 1) {
        time("%H:%M:%S ");
        printf("%s:%u &gt; %s:%u tcp syn %s\n",
               ntop($iph-&gt;saddr),
               (uint16)($th-&gt;source &lt;&lt; 8) | ($th-&gt;source &gt;&gt; 8),
               ntop($iph-&gt;daddr),
               (uint16)($th-&gt;dest &lt;&lt; 8) | ($th-&gt;dest &gt;&gt; 8),
               func);
    }
}</code></pre>
            <p>What does this program do? It is roughly an equivalent of a tcpdump filter:</p>
            <pre><code>dst port 2570 and tcp[tcpflags] &amp; tcp-syn != 0</code></pre>
            <p>But only for packets passing through conntrack <code>PRE_ROUTING</code> callbacks.</p><p>(If you haven’t used bpftrace, it comes with an excellent <a href="https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md">reference guide</a> and gives you the ability to explore kernel data types on the fly with <code>bpftrace -lv 'struct iphdr'</code>.)</p><p>Let’s run the tracing program while we connect to the VM from the outside (<code>nc -z 192.168.122.204 2570</code>):</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo bpftrace /vagrant/tools/trace-conntrack-prerouting.bt
Attaching 3 probes...
Tracing conntrack prerouting callbacks... Hit Ctrl-C to quit
13:22:56 192.168.122.1:33254 &gt; 192.168.122.204:2570 tcp syn ipv4_conntrack_defrag
13:22:56 192.168.122.1:33254 &gt; 192.168.122.204:2570 tcp syn ipv4_conntrack_in
^C

[vagrant@ct-vm ~]$</code></pre>
            <p>Conntrack callbacks have processed the TCP SYN packet destined to tcp/2570.</p><p>But if conntrack saw the packet, why is there no corresponding flow entry in the conntrack table?</p>
    <div>
      <h2>Going down the rabbit hole</h2>
      <a href="#going-down-the-rabbit-hole">
        
      </a>
    </div>
    <p>What actually happens inside the conntrack <code>PRE_ROUTING</code> callbacks?</p><p>To find out, we can trace the call chain that starts on entry to the conntrack callback. The <code>function_graph</code> tracer built into the <a href="https://lwn.net/Articles/365835/">Ftrace</a> framework is perfect for this task.</p><p>But because all incoming traffic goes through the <code>PRE_ROUTING</code> hook, including our SSH connection, our trace will be polluted with events from SSH traffic. To avoid that, let’s switch from SSH access to a serial console.</p><p>When using <a href="https://github.com/vagrant-libvirt/vagrant-libvirt">libvirt</a> as the Vagrant provider, you can connect to the serial console with <code>virsh</code>:</p>
            <pre><code>host $ virsh -c qemu:///session list
 Id   Name                State
-----------------------------------
 1    conntrack_default   running

host $ virsh -c qemu:///session console conntrack_default
Once connected to the console and logged into the VM, we can record the call chain using the trace-cmd wrapper for Ftrace:
[vagrant@ct-vm ~]$ sudo trace-cmd start -p function_graph -g ipv4_conntrack_defrag -g ipv4_conntrack_in
  plugin 'function_graph'
[vagrant@ct-vm ~]$ # … connect from the host with `nc -z 192.168.122.204 2570` …
[vagrant@ct-vm ~]$ sudo trace-cmd stop
[vagrant@ct-vm ~]$ sudo cat /sys/kernel/debug/tracing/trace
# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 1)   1.219 us    |  finish_task_switch();
 1)   3.532 us    |  ipv4_conntrack_defrag [nf_defrag_ipv4]();
 1)               |  ipv4_conntrack_in [nf_conntrack]() {
 1)               |    nf_conntrack_in [nf_conntrack]() {
 1)   0.573 us    |      get_l4proto [nf_conntrack]();
 1)               |      nf_ct_get_tuple [nf_conntrack]() {
 1)   0.487 us    |        nf_ct_get_tuple_ports [nf_conntrack]();
 1)   1.564 us    |      }
 1)   0.820 us    |      hash_conntrack_raw [nf_conntrack]();
 1)   1.255 us    |      __nf_conntrack_find_get [nf_conntrack]();
 1)               |      init_conntrack.constprop.0 [nf_conntrack]() {  ❷
 1)   0.427 us    |        nf_ct_invert_tuple [nf_conntrack]();
 1)               |        __nf_conntrack_alloc [nf_conntrack]() {      ❶
                             … 
 1)   3.680 us    |        }
                           … 
 1) + 15.847 us   |      }
                         … 
 1) + 34.595 us   |    }
 1) + 35.742 us   |  }
 …
[vagrant@ct-vm ~]$</code></pre>
            <p>What catches our attention here is the allocation, <a href="https://elixir.bootlin.com/linux/v5.10.14/source/net/netfilter/nf_conntrack_core.c#L1471">__nf_conntrack_alloc()</a> (❶), inside <code>init_conntrack() (❷). __nf_conntrack_alloc()</code> creates a <a href="https://elixir.bootlin.com/linux/v5.10.14/source/include/net/netfilter/nf_conntrack.h#L58">struct nf_conn</a> object which represents a tracked connection.</p><p>This object is not created in vain. <a href="https://elixir.bootlin.com/linux/v5.10.14/source/net/netfilter/nf_conntrack_core.c#L1633">A glance</a> at <code>init_conntrack()</code> <a href="https://elixir.bootlin.com/linux/v5.10.14/source/net/netfilter/nf_conntrack_core.c#L1633">source</a> shows that it is pushed onto a list of unconfirmed connections<sup>3</sup>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4NAnZwLE3NnwyYdlSpnPls/7f87ba94ccade0bea86d95a22130e2cd/image6-1.png" />
            
            </figure><p>What does it mean that a connection is unconfirmed? As <a href="https://manpages.debian.org/buster/conntrack/conntrack.8.en.html">conntrack(8) man page</a> explains:</p>
            <pre><code>unconfirmed:
       This table shows new entries, that are not yet inserted into the
       conntrack table. These entries are attached to packets that  are
       traversing  the  stack, but did not reach the confirmation point
       at the postrouting hook.</code></pre>
            <p>Perhaps we have been looking for our flow in the wrong table? Does the unconfirmed table have a record for our dropped TCP SYN?</p>
    <div>
      <h2>Pulling the rabbit out of the hat</h2>
      <a href="#pulling-the-rabbit-out-of-the-hat">
        
      </a>
    </div>
    <p>I have bad news…</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo conntrack -L unconfirmed
conntrack v1.4.5 (conntrack-tools): 0 flow entries have been shown.
[vagrant@ct-vm ~]$</code></pre>
            <p>The flow is not present in the unconfirmed table. We have to dig deeper.</p><p>Let’s for a moment assume that a <code>struct nf_conn</code> object was added to the <code>unconfirmed</code> list. If the list is now empty, then the object must have been removed from the list before we inspected its contents.</p><p>Has an entry been removed from the <code>unconfirmed</code> table? What function removes entries from the <code>unconfirmed</code> table?</p><p>It turns out that <code>nf_ct_add_to_unconfirmed_list()</code> which <code>init_conntrack()</code> invokes, has its opposite defined just right beneath it - <code>nf_ct_del_from_dying_or_unconfirmed_list()</code>.</p><p>It is worth a shot to check if this function is being called, and if so, from where. For that we can again use a BPF tracing program, attached to function entry. However, this time our program will record a kernel stack trace:</p>
            <pre><code>kprobe:nf_ct_del_from_dying_or_unconfirmed_list { @[kstack()] = count(); exit(); }</code></pre>
            <p>With <code>bpftrace</code> running our one-liner, we connect to the VM from the host with <code>nc</code> as before:</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo bpftrace -e 'kprobe:nf_ct_del_from_dying_or_unconfirmed_list { @[kstack()] = count(); exit(); }'
Attaching 1 probe...

@[
    nf_ct_del_from_dying_or_unconfirmed_list+1 ❹
    destroy_conntrack+78
    nf_conntrack_destroy+26
    skb_release_head_state+78
    kfree_skb+50 ❸
    nf_hook_slow+143 ❷
    ip_local_deliver+152 ❶
    ip_sublist_rcv_finish+87
    ip_sublist_rcv+387
    ip_list_rcv+293
    __netif_receive_skb_list_core+658
    netif_receive_skb_list_internal+444
    napi_complete_done+111
    …
]: 1

[vagrant@ct-vm ~]$</code></pre>
            <p>Bingo. The conntrack delete function was called, and the captured stack trace shows that on local delivery path (❶), where <code>LOCAL_IN</code> Netfilter hook runs (❷), the packet is destroyed (❸). Conntrack must be getting called when <a href="https://elixir.bootlin.com/linux/v5.10.14/source/include/linux/skbuff.h#L713">sk_buff</a> (the packet and its metadata) is destroyed. This causes conntrack to remove the unconfirmed flow entry (❹).</p><p>It makes sense. After all we have a <code>DROP</code> rule in the <code>filter/INPUT</code> chain. And that <code>iptables -j DROP</code> rule has a significant side effect. It cleans up an entry in the conntrack <code>unconfirmed</code> table!</p><p>This explains why we can’t observe the flow in the <code>unconfirmed</code> table. It lives for only a very short period of time.</p><p>Not convinced? You don’t have to take my word for it. I will prove it with a dirty trick!</p>
    <div>
      <h2>Making the rabbit disappear, or actually appear</h2>
      <a href="#making-the-rabbit-disappear-or-actually-appear">
        
      </a>
    </div>
    <p>If you recall the output from <code>list-nf-hooks</code> that we’ve seen earlier, there is another conntrack callback there - <code>ipv4_confirm</code>, which I have ignored:</p>
            <pre><code>[vagrant@ct-vm ~]$ sudo /vagrant/tools/list-nf-hooks
…
? ipv4 LOCAL_IN
       -150 → iptable_mangle_hook
          0 → iptable_filter_hook
         50 → iptable_security_hook
        100 → nf_nat_ipv4_fn
 2147483647 → ipv4_confirm              ☜ another conntrack callback
… </code></pre>
            <p><code>ipv4_confirm</code> is “the confirmation point” mentioned in the <a href="https://manpages.debian.org/buster/conntrack/conntrack.8.en.html">conntrack(8) man page</a>. When a flow gets confirmed, it is moved from the <code>unconfirmed</code> table to the main <code>conntrack</code> table.</p><p>The callback is registered with a “weird” priority – 2,147,483,647. It’s the maximum positive value of a 32-bit signed integer can hold, and at the same time, the lowest possible priority a callback can have.</p><p>This ensures that the <code>ipv4_confirm</code> callback runs last. We want the flows to graduate from the <code>unconfirmed</code> table to the main <code>conntrack</code> table only once we know the corresponding packet has made it through the firewall.</p><p>Luckily for us, it is possible to have more than one callback registered with the same priority. In such cases, the order of registration matters. We can put that to use. Just for educational purposes.</p><p>Good old <code>iptables</code> won’t be of much help here. Its Netfilter callbacks have hard-coded priorities which we can’t change. But <code>nftables</code>, the <code>iptables</code> <a href="https://developers.redhat.com/blog/2016/10/28/what-comes-after-iptables-its-successor-of-course-nftables/">successor</a>, is much more flexible in this regard. With <code>nftables</code> we can create a rule chain with arbitrary priority.</p><p>So this time, let’s use nftables to install a filter rule to drop traffic to port tcp/2570. The trick, though, is to register our chain before conntrack registers itself. This way our filter will run <i>last</i>.</p><p>First, delete the tcp/2570 drop rule in iptables and unregister conntrack.</p>
            <pre><code>vm # iptables -t filter -F
vm # rmmod nf_conntrack_netlink nf_conntrack</code></pre>
            <p>Then add tcp/2570 drop rule in <code>nftables</code>, with lowest possible priority.</p>
            <pre><code>vm # nft add table ip my_table
vm # nft add chain ip my_table my_input { type filter hook input priority 2147483647 \; }
vm # nft add rule ip my_table my_input tcp dport 2570 counter drop
vm # nft -a list ruleset
table ip my_table { # handle 1
        chain my_input { # handle 1
                type filter hook input priority 2147483647; policy accept;
                tcp dport 2570 counter packets 0 bytes 0 drop # handle 4
        }
}</code></pre>
            <p>Finally, re-register conntrack hooks.</p>
            <pre><code>vm # modprobe nf_conntrack enable_hooks=1</code></pre>
            <p>The registered callbacks for the <code>LOCAL_IN</code> hook now look like this:</p>
            <pre><code>vm # /vagrant/tools/list-nf-hooks
…
? ipv4 LOCAL_IN
       -150 → iptable_mangle_hook
          0 → iptable_filter_hook
         50 → iptable_security_hook
        100 → nf_nat_ipv4_fn
 2147483647 → ipv4_confirm, nft_do_chain_ipv4
…</code></pre>
            <p>What happens if we connect to port tcp/2570 now?</p>
            <pre><code>vm # conntrack -L
tcp      6 115 SYN_SENT src=192.168.122.1 dst=192.168.122.204 sport=54868 dport=2570 [UNREPLIED] src=192.168.122.204 dst=192.168.122.1 sport=2570 dport=54868 mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
conntrack v1.4.5 (conntrack-tools): 1 flow entries have been shown.</code></pre>
            <p>We have fooled conntrack ?</p><p>Conntrack promoted the flow from the <code>unconfirmed</code> to the main <code>conntrack</code> table despite the fact that the firewall dropped the packet. We can observe it.</p>
    <div>
      <h2>Outro</h2>
      <a href="#outro">
        
      </a>
    </div>
    <p>Conntrack processes every received packet<sup>4</sup> and creates a flow for it. A flow entry is always created even if the packet is dropped shortly after. The flow might never be promoted to the main conntrack table and can be short lived.</p><p>However, this blog post is not really about conntrack. Its internals have been covered by <a href="https://www.usenix.org/publications/login/june-2006-volume-31-number-3/netfilters-connection-tracking-system">magazines</a>, <a href="https://www.semanticscholar.org/paper/Netfilter-Connection-Tracking-and-NAT-Boye/3f3c09dbc2a13c4840bb4a148753bb528493b607">papers</a>, <a href="https://books.google.pl/books?id=RpsQAwAAQBAJ&amp;lpg=PP1&amp;pg=PA253#v=onepage&amp;q&amp;f=false">books</a>, and on other blogs long before. We probably could have learned elsewhere all that has been shown here.</p><p>For us, conntrack was really just an excuse to demonstrate various ways to discover the inner workings of the Linux network stack. As good as any other.</p><p>Today we have powerful introspection tools like <a href="https://github.com/osandov/drgn">drgn</a>, <a href="https://bpftrace.org/">bpftrace</a>, or <a href="https://lwn.net/Articles/365835/">Ftrace</a>, and a <a href="https://elixir.bootlin.com/">cross referencer</a> to plow through the source code, at our fingertips. They help us look under the hood of a live operating system and gradually deepen our understanding of its workings.</p><p>I have to warn you, though. Once you start digging into the kernel, it is hard to stop…</p><p>...........</p><p><sup>1</sup>Actually since <a href="https://kernelnewbies.org/Linux_5.10#Networking">Linux v5.10</a> (Dec 2020) there is an additional Netfilter hook for the INET family named <code>NF_INET_INGRESS</code>. The new hook type allows users to attach nftables chains to the Traffic Control ingress hook.</p><p><sup>2</sup>Why did I pick this port number? Because 2570 = 0x0a0a. As we will see later, this saves us the trouble of converting between the network byte order and the host byte order.</p><p><sup>3</sup>To be precise, there are multiple lists of unconfirmed connections. One per each CPU. This is a common pattern in the kernel. Whenever we want to prevent CPUs from contending for access to a shared state, we give each CPU a private instance of the state.</p><p><sup>4</sup>Unless we explicitly exclude it from being tracked with <code>iptables -j NOTRACK</code>.</p> ]]></content:encoded>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Network]]></category>
            <category><![CDATA[Kernel]]></category>
            <guid isPermaLink="false">2F04ZqBN3X4bKtLaLCptMm</guid>
            <dc:creator>Jakub Sitnicki</dc:creator>
        </item>
        <item>
            <title><![CDATA[Speeding up Linux disk encryption]]></title>
            <link>https://blog.cloudflare.com/speeding-up-linux-disk-encryption/</link>
            <pubDate>Wed, 25 Mar 2020 12:00:00 GMT</pubDate>
            <description><![CDATA[ Encrypting data at rest is vital for Cloudflare with more than 200 data centres across the world. In this post, we will investigate the performance of disk encryption on Linux and explain how we made it at least two times faster for ourselves and our customers! ]]></description>
            <content:encoded><![CDATA[ <p>Data encryption at rest is a must-have for any modern Internet company. Many companies, however, don't encrypt their disks, because they fear the potential performance penalty caused by encryption overhead.</p><p>Encrypting data at rest is vital for Cloudflare with <a href="https://www.cloudflare.com/network/">more than 200 data centres across the world</a>. In this post, we will investigate the performance of disk encryption on Linux and explain how we made it at least two times faster for ourselves and our customers!</p>
    <div>
      <h3>Encrypting data at rest</h3>
      <a href="#encrypting-data-at-rest">
        
      </a>
    </div>
    <p>When it comes to encrypting data at rest there are several ways it can be implemented on a modern operating system (OS). Available techniques are tightly coupled with a <a href="https://en.wikibooks.org/wiki/The_Linux_Kernel/Storage">typical OS storage stack</a>. A simplified version of the storage stack and encryption solutions can be found on the diagram below:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1IM596LYKyiKGoM8dQv0a2/459b3d6be1edf9328914470ed9eee925/storage-stack.png" />
            
            </figure><p>On the top of the stack are applications, which read and write data in files (or streams). The file system in the OS kernel keeps track of which blocks of the underlying block device belong to which files and translates these file reads and writes into block reads and writes, however the hardware specifics of the underlying storage device is abstracted away from the filesystem. Finally, the block subsystem actually passes the block reads and writes to the underlying hardware using appropriate device drivers.</p><p>The concept of the storage stack is actually similar to the <a href="https://www.cloudflare.com/learning/ddos/glossary/open-systems-interconnection-model-osi/">well-known network OSI model</a>, where each layer has a more high-level view of the information and the implementation details of the lower layers are abstracted away from the upper layers. And, similar to the OSI model, one can apply encryption at different layers (think about <a href="https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/">TLS</a> vs <a href="https://en.wikipedia.org/wiki/IPsec">IPsec</a> or <a href="https://www.cloudflare.com/learning/access-management/what-is-a-vpn/">a VPN</a>).</p><p>For data at rest we can apply encryption either at the block layers (either in hardware or in software) or at the file level (either directly in applications or in the filesystem).</p>
    <div>
      <h4>Block vs file encryption</h4>
      <a href="#block-vs-file-encryption">
        
      </a>
    </div>
    <p>Generally, the higher in the stack we apply encryption, the more flexibility we have. With application level encryption the application maintainers can apply any encryption code they please to any particular data they need. The downside of this approach is they actually have to implement it themselves and encryption in general is not very developer-friendly: one has to know the ins and outs of a specific cryptographic algorithm, properly generate keys, nonces, IVs etc. Additionally, application level encryption does not leverage OS-level caching and <a href="https://en.wikipedia.org/wiki/Page_cache">Linux page cache</a> in particular: each time the application needs to use the data, it has to either decrypt it again, wasting CPU cycles, or implement its own decrypted “cache”, which introduces more complexity to the code.</p><p>File system level encryption makes data encryption transparent to applications, because the file system itself encrypts the data before passing it to the block subsystem, so files are encrypted regardless if the application has crypto support or not. Also, file systems can be configured to encrypt only a particular directory or have different keys for different files. This flexibility, however, comes at a cost of a more complex configuration. File system encryption is also considered less secure than block device encryption as only the contents of the files are encrypted. Files also have associated metadata, like file size, the number of files, the directory tree layout etc., which are still visible to a potential adversary.</p><p>Encryption down at the block layer (often referred to as <a href="https://en.wikipedia.org/wiki/Disk_encryption">disk encryption</a> or full disk encryption) also makes data encryption transparent to applications and even whole file systems. Unlike file system level encryption it encrypts all data on the disk including file metadata and even free space. It is less flexible though - one can only encrypt the whole disk with a single key, so there is no per-directory, per-file or per-user configuration. From the crypto perspective, not all cryptographic algorithms can be used as the block layer doesn't have a high-level overview of the data anymore, so it needs to process each block independently. Most <a href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Common_modes">common algorithms require some sort of block chaining</a> to be secure, so are not applicable to disk encryption. Instead, <a href="https://en.wikipedia.org/wiki/Disk_encryption_theory#Block_cipher-based_modes">special modes were developed</a> just for this specific use-case.</p><p>So which layer to choose? As always, it depends... Application and file system level encryption are usually the preferred choice for client systems because of the flexibility. For example, each user on a multi-user desktop may want to encrypt their home directory with a key they own and leave some shared directories unencrypted. On the contrary, on server systems, managed by SaaS/PaaS/IaaS companies (including Cloudflare) the preferred choice is configuration simplicity and security - with full disk encryption enabled any data from any application is automatically encrypted with no exceptions or overrides. We believe that all data needs to be protected without sorting it into "important" vs "not important" buckets, so the selective flexibility the upper layers provide is not needed.</p>
    <div>
      <h4>Hardware vs software disk encryption</h4>
      <a href="#hardware-vs-software-disk-encryption">
        
      </a>
    </div>
    <p>When encrypting data at the block layer it is possible to do it directly in the storage hardware, if the hardware <a href="https://en.wikipedia.org/wiki/Hardware-based_full_disk_encryption">supports it</a>. Doing so usually gives better read/write performance and consumes less resources from the host. However, since most hardware firmware is proprietary, it does not receive as much attention and review from the security community. In the past this led to <a href="https://www.us-cert.gov/ncas/current-activity/2018/11/06/Self-Encrypting-Solid-State-Drive-Vulnerabilities">flaws in some implementations of hardware disk encryption</a>, which render the whole security model useless. Microsoft, for example, <a href="https://support.microsoft.com/en-us/help/4516071/windows-10-update-kb4516071">started to prefer software-based disk encryption</a> since then.</p><p>We didn't want to put our data and our customers' data to the risk of using potentially insecure solutions and we <a href="/helping-to-build-cloudflare-part-4/">strongly believe in open-source</a>. That's why we rely only on software disk encryption in the Linux kernel, which is open and has been audited by many security professionals across the world.</p>
    <div>
      <h3>Linux disk encryption performance</h3>
      <a href="#linux-disk-encryption-performance">
        
      </a>
    </div>
    <p>We aim not only to save bandwidth costs for our customers, but to deliver content to Internet users as fast as possible.</p><p>At one point we noticed that our disks were not as fast as we would like them to be. Some profiling as well as a quick A/B test pointed to Linux disk encryption. Because not encrypting the data (even if it is supposed-to-be a public Internet cache) is not a sustainable option, we decided to take a closer look into Linux disk encryption performance.</p>
    <div>
      <h4>Device mapper and dm-crypt</h4>
      <a href="#device-mapper-and-dm-crypt">
        
      </a>
    </div>
    <p>Linux implements transparent disk encryption via a <a href="https://en.wikipedia.org/wiki/Dm-crypt">dm-crypt module</a> and <code>dm-crypt</code> itself is part of <a href="https://en.wikipedia.org/wiki/Device_mapper">device mapper</a> kernel framework. In a nutshell, the device mapper allows pre/post-process IO requests as they travel between the file system and the underlying block device.</p><p><code>dm-crypt</code> in particular encrypts "write" IO requests before sending them further down the stack to the actual block device and decrypts "read" IO requests before sending them up to the file system driver. Simple and easy! Or is it?</p>
    <div>
      <h4>Benchmarking setup</h4>
      <a href="#benchmarking-setup">
        
      </a>
    </div>
    <p>For the record, the numbers in this post were obtained by running specified commands on an idle <a href="/a-tour-inside-cloudflares-g9-servers/">Cloudflare G9 server</a> out of production. However, the setup should be easily reproducible on any modern x86 laptop.</p><p>Generally, benchmarking anything around a storage stack is hard because of the noise introduced by the storage hardware itself. Not all disks are created equal, so for the purpose of this post we will use the fastest disks available out there - that is no disks.</p><p>Instead Linux has an option to emulate a disk directly in <a href="https://en.wikipedia.org/wiki/Random-access_memory">RAM</a>. Since RAM is much faster than any persistent storage, it should introduce little bias in our results.</p><p>The following command creates a 4GB ramdisk:</p>
            <pre><code>$ sudo modprobe brd rd_nr=1 rd_size=4194304
$ ls /dev/ram0</code></pre>
            <p>Now we can set up a <code>dm-crypt</code> instance on top of it thus enabling encryption for the disk. First, we need to generate the disk encryption key, "format" the disk and specify a password to unlock the newly generated key.</p>
            <pre><code>$ fallocate -l 2M crypthdr.img
$ sudo cryptsetup luksFormat /dev/ram0 --header crypthdr.img

WARNING!
========
This will overwrite data on crypthdr.img irrevocably.

Are you sure? (Type uppercase yes): YES
Enter passphrase:
Verify passphrase:</code></pre>
            <p>Those who are familiar with <code>LUKS/dm-crypt</code> might have noticed we used a <a href="http://man7.org/linux/man-pages/man8/cryptsetup.8.html">LUKS detached header</a> here. Normally, LUKS stores the password-encrypted disk encryption key on the same disk as the data, but since we want to compare read/write performance between encrypted and unencrypted devices, we might accidentally overwrite the encrypted key during our benchmarking later. Keeping the encrypted key in a separate file avoids this problem for the purposes of this post.</p><p>Now, we can actually "unlock" the encrypted device for our testing:</p>
            <pre><code>$ sudo cryptsetup open --header crypthdr.img /dev/ram0 encrypted-ram0
Enter passphrase for /dev/ram0:
$ ls /dev/mapper/encrypted-ram0
/dev/mapper/encrypted-ram0</code></pre>
            <p>At this point we can now compare the performance of encrypted vs unencrypted ramdisk: if we read/write data to <code>/dev/ram0</code>, it will be stored in <a href="https://en.wikipedia.org/wiki/Plaintext">plaintext</a>. Likewise, if we read/write data to <code>/dev/mapper/encrypted-ram0</code>, it will be decrypted/encrypted on the way by <code>dm-crypt</code> and stored in <a href="https://en.wikipedia.org/wiki/Ciphertext">ciphertext</a>.</p><p>It's worth noting that we're not creating any file system on top of our block devices to avoid biasing results with a file system overhead.</p>
    <div>
      <h4>Measuring throughput</h4>
      <a href="#measuring-throughput">
        
      </a>
    </div>
    <p>When it comes to storage testing/benchmarking <a href="https://fio.readthedocs.io/en/latest/fio_doc.html">Flexible I/O tester</a> is the usual go-to solution. Let's simulate simple sequential read/write load with 4K block size on the ramdisk without encryption:</p>
            <pre><code>$ sudo fio --filename=/dev/ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=plain
plain: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1
fio-2.16
Starting 1 process
...
Run status group 0 (all jobs):
   READ: io=21013MB, aggrb=1126.5MB/s, minb=1126.5MB/s, maxb=1126.5MB/s, mint=18655msec, maxt=18655msec
  WRITE: io=21023MB, aggrb=1126.1MB/s, minb=1126.1MB/s, maxb=1126.1MB/s, mint=18655msec, maxt=18655msec

Disk stats (read/write):
  ram0: ios=0/0, merge=0/0, ticks=0/0, in_queue=0, util=0.00%</code></pre>
            <p>The above command will run for a long time, so we just stop it after a while. As we can see from the stats, we're able to read and write roughly with the same throughput around <code>1126 MB/s</code>. Let's repeat the test with the encrypted ramdisk:</p>
            <pre><code>$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=crypt
crypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1
fio-2.16
Starting 1 process
...
Run status group 0 (all jobs):
   READ: io=1693.7MB, aggrb=150874KB/s, minb=150874KB/s, maxb=150874KB/s, mint=11491msec, maxt=11491msec
  WRITE: io=1696.4MB, aggrb=151170KB/s, minb=151170KB/s, maxb=151170KB/s, mint=11491msec, maxt=11491msec</code></pre>
            <p>Whoa, that's a drop! We only get <code>~147 MB/s</code> now, which is more than 7 times slower! And this is on a totally idle machine!</p>
    <div>
      <h4>Maybe, crypto is just slow</h4>
      <a href="#maybe-crypto-is-just-slow">
        
      </a>
    </div>
    <p>The first thing we considered is to ensure we use the fastest crypto. <code>cryptsetup</code> allows us to benchmark all the available crypto implementations on the system to select the best one:</p>
            <pre><code>$ sudo cryptsetup benchmark
# Tests are approximate using memory only (no storage IO).
PBKDF2-sha1      1340890 iterations per second for 256-bit key
PBKDF2-sha256    1539759 iterations per second for 256-bit key
PBKDF2-sha512    1205259 iterations per second for 256-bit key
PBKDF2-ripemd160  967321 iterations per second for 256-bit key
PBKDF2-whirlpool  720175 iterations per second for 256-bit key
#  Algorithm | Key |  Encryption |  Decryption
     aes-cbc   128b   969.7 MiB/s  3110.0 MiB/s
 serpent-cbc   128b           N/A           N/A
 twofish-cbc   128b           N/A           N/A
     aes-cbc   256b   756.1 MiB/s  2474.7 MiB/s
 serpent-cbc   256b           N/A           N/A
 twofish-cbc   256b           N/A           N/A
     aes-xts   256b  1823.1 MiB/s  1900.3 MiB/s
 serpent-xts   256b           N/A           N/A
 twofish-xts   256b           N/A           N/A
     aes-xts   512b  1724.4 MiB/s  1765.8 MiB/s
 serpent-xts   512b           N/A           N/A
 twofish-xts   512b           N/A           N/A</code></pre>
            <p>It seems <code>aes-xts</code> with a 256-bit data encryption key is the fastest here. But which one are we actually using for our encrypted ramdisk?</p>
            <pre><code>$ sudo dmsetup table /dev/mapper/encrypted-ram0
0 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0</code></pre>
            <p>We do use <code>aes-xts</code> with a 256-bit data encryption key (count all the zeroes conveniently masked by <code>dmsetup</code> tool - if you want to see the actual bytes, add the <code>--showkeys</code> option to the above command). The numbers do not add up however: <code>cryptsetup benchmark</code> tells us above not to rely on the results, as "Tests are approximate using memory only (no storage IO)", but that is exactly how we've set up our experiment using the ramdisk. In a somewhat worse case (assuming we're reading all the data and then encrypting/decrypting it sequentially with no parallelism) doing <a href="https://en.wikipedia.org/wiki/Back-of-the-envelope_calculation">back-of-the-envelope calculation</a> we should be getting around <code>(1126 * 1823) / (1126 + 1823) =~696 MB/s</code>, which is still quite far from the actual <code>147 * 2 = 294 MB/s</code> (total for reads and writes).</p>
    <div>
      <h4>dm-crypt performance flags</h4>
      <a href="#dm-crypt-performance-flags">
        
      </a>
    </div>
    <p>While reading the <a href="http://man7.org/linux/man-pages/man8/cryptsetup.8.html">cryptsetup man page</a> we noticed that it has two options prefixed with <code>--perf-</code>, which are probably related to performance tuning. The first one is <code>--perf-same_cpu_crypt</code> with a rather cryptic description:</p>
            <pre><code>Perform encryption using the same cpu that IO was submitted on.  The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs.  This option is only relevant for open action.</code></pre>
            <p>So we enable the option</p>
            <pre><code>$ sudo cryptsetup close encrypted-ram0
$ sudo cryptsetup open --header crypthdr.img --perf-same_cpu_crypt /dev/ram0 encrypted-ram0</code></pre>
            <p>Note: according to the <a href="http://man7.org/linux/man-pages/man8/cryptsetup.8.html">latest man page</a> there is also a <code>cryptsetup refresh</code> command, which can be used to enable these options live without having to "close" and "re-open" the encrypted device. Our <code>cryptsetup</code> however didn't support it yet.</p><p>Verifying if the option has been really enabled:</p>
            <pre><code>$ sudo dmsetup table encrypted-ram0
0 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 same_cpu_crypt</code></pre>
            <p>Yes, we can now see <code>same_cpu_crypt</code> in the output, which is what we wanted. Let's rerun the benchmark:</p>
            <pre><code>$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=crypt
crypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1
fio-2.16
Starting 1 process
...
Run status group 0 (all jobs):
   READ: io=1596.6MB, aggrb=139811KB/s, minb=139811KB/s, maxb=139811KB/s, mint=11693msec, maxt=11693msec
  WRITE: io=1600.9MB, aggrb=140192KB/s, minb=140192KB/s, maxb=140192KB/s, mint=11693msec, maxt=11693msec</code></pre>
            <p>Hmm, now it is <code>~136 MB/s</code> which is slightly worse than before, so no good. What about the second option <code>--perf-submit_from_crypt_cpus</code>:</p>
            <pre><code>Disable offloading writes to a separate thread after encryption.  There are some situations where offloading write bios from the encryption threads to a single thread degrades performance significantly.  The default is to offload write bios to the same thread.  This option is only relevant for open action.</code></pre>
            <p>Maybe, we are in the "some situation" here, so let's try it out:</p>
            <pre><code>$ sudo cryptsetup close encrypted-ram0
$ sudo cryptsetup open --header crypthdr.img --perf-submit_from_crypt_cpus /dev/ram0 encrypted-ram0
Enter passphrase for /dev/ram0:
$ sudo dmsetup table encrypted-ram0
0 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 submit_from_crypt_cpus</code></pre>
            <p>And now the benchmark:</p>
            <pre><code>$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=crypt
crypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1
fio-2.16
Starting 1 process
...
Run status group 0 (all jobs):
   READ: io=2066.6MB, aggrb=169835KB/s, minb=169835KB/s, maxb=169835KB/s, mint=12457msec, maxt=12457msec
  WRITE: io=2067.7MB, aggrb=169965KB/s, minb=169965KB/s, maxb=169965KB/s, mint=12457msec, maxt=12457msec</code></pre>
            <p><code>~166 MB/s</code>, which is a bit better, but still not good...</p>
    <div>
      <h4>Asking the community</h4>
      <a href="#asking-the-community">
        
      </a>
    </div>
    <p>Being desperate we decided to seek support from the Internet and <a href="https://www.spinics.net/lists/dm-crypt/msg07516.html">posted our findings to the <code>dm-crypt</code> mailing list</a>, but the response we got was not very encouraging:</p><blockquote><p>If the numbers disturb you, then this is from lack of understanding on your side. You are probably unaware that encryption is a heavy-weight operation...</p></blockquote><p>We decided to make a scientific research on this topic by typing "is encryption expensive" into Google Search and one of the top results, which actually contains meaningful measurements, is... <a href="/how-expensive-is-crypto-anyway/">our own post about cost of encryption</a>, but in the context of <a href="https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/">TLS</a>! This is a fascinating read on its own, but the gist is: modern crypto on modern hardware is very cheap even at Cloudflare scale (doing millions of encrypted HTTP requests per second). In fact, it is so cheap that Cloudflare was the first provider to offer <a href="https://www.cloudflare.com/application-services/products/ssl/">free SSL/TLS for everyone</a>.</p>
    <div>
      <h4>Digging into the source code</h4>
      <a href="#digging-into-the-source-code">
        
      </a>
    </div>
    <p>When trying to use the custom <code>dm-crypt</code> options described above we were curious why they exist in the first place and what is that "offloading" all about. Originally we expected <code>dm-crypt</code> to be a simple "proxy", which just encrypts/decrypts data as it flows through the stack. Turns out <code>dm-crypt</code> does more than just encrypting memory buffers and a (simplified) IO traverse path diagram is presented below:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4UenGQJyEiOzxwISPhhGR1/bdeae8ae7c9d331f4ca4d1cc75bbfb15/dm-crypt.png" />
            
            </figure><p>When the file system issues a write request, <code>dm-crypt</code> does not process it immediately - instead it puts it into a <a href="https://www.kernel.org/doc/html/v4.19/core-api/workqueue.html">workqueue</a> <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L3124">named "kcryptd"</a>. In a nutshell, a kernel workqueue just schedules some work (encryption in this case) to be performed at some later time, when it is more convenient. When "the time" comes, <code>dm-crypt</code> <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1940">sends the request</a> to <a href="https://www.kernel.org/doc/html/v4.19/crypto/index.html">Linux Crypto API</a> for actual encryption. However, modern Linux Crypto API <a href="https://www.kernel.org/doc/html/v4.19/crypto/api-skcipher.html#symmetric-key-cipher-api">is asynchronous</a> as well, so depending on which particular implementation your system will use, most likely it will not be processed immediately, but queued again for "later time". When Linux Crypto API will finally <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1980">do the encryption</a>, <code>dm-crypt</code> may try to <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1909-L1910">sort pending write requests by putting each request</a> into a <a href="https://en.wikipedia.org/wiki/Red%E2%80%93black_tree">red-black tree</a>. Then a <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1819">separate kernel thread</a> again at "some time later" actually takes all IO requests in the tree and <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1864">sends them down the stack</a>.</p><p>Now for read requests: this time we need to get the encrypted data first from the hardware, but <code>dm-crypt</code> does not just ask for the driver for the data, but queues the request into a different <a href="https://www.kernel.org/doc/html/v4.19/core-api/workqueue.html">workqueue</a> <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L3122">named "kcryptd_io"</a>. At some point later, when we actually have the encrypted data, we <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1742">schedule it for decryption</a> using the now familiar "kcryptd" workqueue. "kcryptd" <a href="https://github.com/torvalds/linux/blob/0d81a3f29c0afb18ba2b1275dcccf21e0dd4da38/drivers/md/dm-crypt.c#L1970">will send the request</a> to Linux Crypto API, which may decrypt the data asynchronously as well.</p><p>To be fair the request does not always traverse all these queues, but the important part here is that write requests may be queued up to 4 times in <code>dm-crypt</code> and read requests up to 3 times. At this point we were wondering if all this extra queueing can cause any performance issues. For example, there is a <a href="https://www.usenix.org/conference/srecon19asia/presentation/plenz">nice presentation from Google</a> about the relationship between queueing and tail latency. One key takeaway from the presentation is:</p><blockquote><p>A significant amount of tail latency is due to queueing effects</p></blockquote><p>So, why are all these queues there and can we remove them?</p>
    <div>
      <h4>Git archeology</h4>
      <a href="#git-archeology">
        
      </a>
    </div>
    <p>No-one writes more complex code just for fun, especially for the OS kernel. So all these queues must have been put there for a reason. Luckily, the Linux kernel source is managed by <a href="https://en.wikipedia.org/wiki/Git">git</a>, so we can try to retrace the changes and the decisions around them.</p><p>The "kcryptd" workqueue was in the source <a href="https://github.com/torvalds/linux/blob/1da177e4c3f41524e886b7f1b8a0c1fc7321cac2/drivers/md/dm-crypt.c">since the beginning of the available history</a> with the following comment:</p><blockquote><p>Needed because it would be very unwise to do decryption in an interrupt context, so bios returning from read requests get queued here.</p></blockquote><p>So it was for reads only, but even then - why do we care if it is interrupt context or not, if Linux Crypto API will likely use a dedicated thread/queue for encryption anyway? Well, back in 2005 Crypto API <a href="https://github.com/torvalds/linux/blob/1da177e4c3f41524e886b7f1b8a0c1fc7321cac2/Documentation/crypto/api-intro.txt">was not asynchronous</a>, so this made perfect sense.</p><p>In 2006 <code>dm-crypt</code> <a href="https://github.com/torvalds/linux/commit/23541d2d288cdb54f417ba1001dacc7f3ea10a97">started to use</a> the "kcryptd" workqueue not only for encryption, but for submitting IO requests:</p><blockquote><p>This patch is designed to help dm-crypt comply with the new constraints imposed by the following patch in -mm: md-dm-reduce-stack-usage-with-stacked-block-devices.patch</p></blockquote><p>It seems the goal here was not to add more concurrency, but rather reduce kernel stack usage, which makes sense again as the kernel has a common stack across all the code, so it is a quite limited resource. It is worth noting, however, that the <a href="https://github.com/torvalds/linux/commit/6538b8ea886e472f4431db8ca1d60478f838d14b">Linux kernel stack has been expanded</a> in 2014 for x86 platforms, so this might not be a problem anymore.</p><p>A <a href="https://github.com/torvalds/linux/commit/cabf08e4d3d1181d7c408edae97fb4d1c31518af">first version of "kcryptd_io" workqueue was added</a> in 2007 with the intent to avoid:</p><blockquote><p>starvation caused by many requests waiting for memory allocation...</p></blockquote><p>The request processing was bottlenecking on a single workqueue here, so the solution was to add another one. Makes sense.</p><p>We are definitely not the first ones experiencing performance degradation because of extensive queueing: in 2011 a change was introduced to <a href="https://github.com/torvalds/linux/commit/20c82538e4f5ede51bc2b4795bc6e5cae772796d">conditionally revert some of the queueing for read requests</a>:</p><blockquote><p>If there is enough memory, code can directly submit bio instead queuing this operation in a separate thread.</p></blockquote><p>Unfortunately, at that time Linux kernel commit messages were not as verbose as today, so there is no performance data available.</p><p>In 2015 <a href="https://github.com/torvalds/linux/commit/dc2676210c425ee8e5cb1bec5bc84d004ddf4179">dm-crypt started to sort writes</a> in a separate "dmcrypt_write" thread before sending them down the stack:</p><blockquote><p>On a multiprocessor machine, encryption requests finish in a different order than they were submitted. Consequently, write requests would be submitted in a different order and it could cause severe performance degradation.</p></blockquote><p>It does make sense as sequential disk access used to be much faster than the random one and <code>dm-crypt</code> was breaking the pattern. But this mostly applies to <a href="https://en.wikipedia.org/wiki/Hard_disk_drive">spinning disks</a>, which were still dominant in 2015. It may not be as important with modern fast <a href="https://en.wikipedia.org/wiki/Solid-state_drive">SSDs (including NVME SSDs)</a>.</p><p>Another part of the commit message is worth mentioning:</p><blockquote><p>...in particular it enables IO schedulers like CFQ to sort more effectively...</p></blockquote><p>It mentions the performance benefits for the <a href="https://www.kernel.org/doc/Documentation/block/cfq-iosched.txt">CFQ IO scheduler</a>, but Linux schedulers have improved since then to the point that <a href="https://github.com/torvalds/linux/commit/f382fb0bcef4c37dc049e9f6963e3baf204d815c">CFQ scheduler has been removed</a> from the kernel in 2018.</p><p>The same patchset <a href="https://github.com/torvalds/linux/commit/b3c5fd3052492f1b8d060799d4f18be5a5438">replaces the sorting list with a red-black tree</a>:</p><blockquote><p>In theory the sorting should be performed by the underlying disk scheduler, however, in practice the disk scheduler only accepts and sorts a finite number of requests. To allow the sorting of all requests, dm-crypt needs to implement its own sorting.</p><p>The overhead associated with rbtree-based sorting is considered negligible so it is not used conditionally.</p></blockquote><p>All that make sense, but it would be nice to have some backing data.</p><p>Interestingly, in the same patchset we see <a href="https://github.com/torvalds/linux/commit/0f5d8e6ee758f7023e4353cca75d785b2d4f6abe">the introduction of our familiar "submit_from_crypt_cpus" option</a>:</p><blockquote><p>There are some situations where offloading write bios from the encryption threads to a single thread degrades performance significantly</p></blockquote><p>Overall, we can see that every change was reasonable and needed, however things have changed since then:</p><ul><li><p>hardware became faster and smarter</p></li><li><p>Linux resource allocation was revisited</p></li><li><p>coupled Linux subsystems were rearchitected</p></li></ul><p>And many of the design choices above may not be applicable to modern Linux.</p>
    <div>
      <h3>The "clean-up"</h3>
      <a href="#the-clean-up">
        
      </a>
    </div>
    <p>Based on the research above we decided to try to remove all the extra queueing and asynchronous behaviour and revert <code>dm-crypt</code> to its original purpose: simply encrypt/decrypt IO requests as they pass through. But for the sake of stability and further benchmarking we ended up not removing the actual code, but rather adding yet another <code>dm-crypt</code> option, which bypasses all the queues/threads, if enabled. The flag allows us to switch between the current and new behaviour at runtime under full production load, so we can easily revert our changes should we see any side-effects. The resulting patch can be found on the <a href="https://github.com/cloudflare/linux/blob/12a61de6dd06408f4f3c27f8019beb66366e98e3/patches/0023-Add-DM_CRYPT_FORCE_INLINE-flag-to-dm-crypt-target.patch">Cloudflare GitHub Linux repository</a>.</p>
    <div>
      <h4>Synchronous Linux Crypto API</h4>
      <a href="#synchronous-linux-crypto-api">
        
      </a>
    </div>
    <p>From the diagram above we remember that not all queueing is implemented in <code>dm-crypt</code>. Modern Linux Crypto API may also be asynchronous and for the sake of this experiment we want to eliminate queues there as well. What does "may be" mean, though? The OS may contain different implementations of the same algorithm (for example, <a href="https://en.wikipedia.org/wiki/AES_instruction_set">hardware-accelerated AES-NI on x86 platforms</a> and generic C-code AES implementations). By default the system chooses the "best" one based on <a href="https://www.kernel.org/doc/html/v4.19/crypto/architecture.html#crypto-api-cipher-references-and-priority">the configured algorithm priority</a>. <code>dm-crypt</code> allows overriding this behaviour and <a href="https://gitlab.com/cryptsetup/cryptsetup/-/wikis/DMCrypt#mapping-table-for-crypt-target">request a particular cipher implementation</a> using the <code>capi:</code> prefix. However, there is one problem. Let us actually check the available AES-XTS (this is our disk encryption cipher, remember?) implementations on our system:</p>
            <pre><code>$ grep -A 11 'xts(aes)' /proc/crypto
name         : xts(aes)
driver       : xts(ecb(aes-generic))
module       : kernel
priority     : 100
refcnt       : 7
selftest     : passed
internal     : no
type         : skcipher
async        : no
blocksize    : 16
min keysize  : 32
max keysize  : 64
--
name         : __xts(aes)
driver       : cryptd(__xts-aes-aesni)
module       : cryptd
priority     : 451
refcnt       : 1
selftest     : passed
internal     : yes
type         : skcipher
async        : yes
blocksize    : 16
min keysize  : 32
max keysize  : 64
--
name         : xts(aes)
driver       : xts-aes-aesni
module       : aesni_intel
priority     : 401
refcnt       : 1
selftest     : passed
internal     : no
type         : skcipher
async        : yes
blocksize    : 16
min keysize  : 32
max keysize  : 64
--
name         : __xts(aes)
driver       : __xts-aes-aesni
module       : aesni_intel
priority     : 401
refcnt       : 7
selftest     : passed
internal     : yes
type         : skcipher
async        : no
blocksize    : 16
min keysize  : 32
max keysize  : 64</code></pre>
            <p>We want to explicitly select a synchronous cipher from the above list to avoid queueing effects in threads, but the only two supported are <code>xts(ecb(aes-generic))</code> (the generic C implementation) and <code>__xts-aes-aesni</code> (the <a href="https://en.wikipedia.org/wiki/AES_instruction_set">x86 hardware-accelerated implementation</a>). We definitely want the latter as it is much faster (we're aiming for performance here), but it is suspiciously marked as internal (see <code>internal: yes</code>). If we <a href="https://github.com/torvalds/linux/blob/fb33c6510d5595144d585aa194d377cf74d31911/include/linux/crypto.h#L91">check the source code</a>:</p><blockquote><p>Mark a cipher as a service implementation only usable by another cipher and never by a normal user of the kernel crypto API</p></blockquote><p>So this cipher is meant to be used only by other wrapper code in the Crypto API and not outside it. In practice this means, that the caller of the Crypto API needs to explicitly specify this flag, when requesting a particular cipher implementation, but <code>dm-crypt</code> does not do it, because by design it is not part of the Linux Crypto API, rather an "external" user. We already patch the <code>dm-crypt</code> module, so we could as well just add the relevant flag. However, there is another problem with <a href="https://en.wikipedia.org/wiki/AES_instruction_set">AES-NI</a> in particular: <a href="https://en.wikipedia.org/wiki/X87">x86 FPU</a>. "Floating point" you say? Why do we need floating point math to do symmetric encryption which should only be about bit shifts and XOR operations? We don't need the math, but AES-NI instructions use some of the CPU registers, which are dedicated to the FPU. Unfortunately the Linux kernel <a href="https://github.com/torvalds/linux/blob/fb33c6510d5595144d585aa194d377cf74d31911/arch/x86/kernel/fpu/core.c#L77">does not always preserve these registers in interrupt context</a> for performance reasons (saving/restoring FPU is expensive). But <code>dm-crypt</code> may execute code in interrupt context, so we risk corrupting some other process data and we go back to "it would be very unwise to do decryption in an interrupt context" statement in the original code.</p><p>Our solution to address the above was to create another somewhat <a href="https://github.com/cloudflare/linux/blob/master/patches/0024-Add-xtsproxy-Crypto-API-module.patch">"smart" Crypto API module</a>. This module is synchronous and does not roll its own crypto, but is just a "router" of encryption requests:</p><ul><li><p>if we can use the FPU (and thus AES-NI) in the current execution context, we just forward the encryption request to the faster, "internal" <code>__xts-aes-aesni</code> implementation (and we can use it here, because now we are part of the Crypto API)</p></li><li><p>otherwise, we just forward the encryption request to the slower, generic C-based <code>xts(ecb(aes-generic))</code> implementation</p></li></ul>
    <div>
      <h4>Using the whole lot</h4>
      <a href="#using-the-whole-lot">
        
      </a>
    </div>
    <p>Let's walk through the process of using it all together. The first step is to <a href="https://github.com/cloudflare/linux/blob/master/patches/">grab the patches</a> and recompile the kernel (or just compile <code>dm-crypt</code> and our <code>xtsproxy</code> modules).</p><p>Next, let's restart our IO workload in a separate terminal, so we can make sure we can reconfigure the kernel at runtime under load:</p>
            <pre><code>$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=crypt
crypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1
fio-2.16
Starting 1 process
...</code></pre>
            <p>In the main terminal make sure our new Crypto API module is loaded and available:</p>
            <pre><code>$ sudo modprobe xtsproxy
$ grep -A 11 'xtsproxy' /proc/crypto
driver       : xts-aes-xtsproxy
module       : xtsproxy
priority     : 0
refcnt       : 0
selftest     : passed
internal     : no
type         : skcipher
async        : no
blocksize    : 16
min keysize  : 32
max keysize  : 64
ivsize       : 16
chunksize    : 16</code></pre>
            <p>Reconfigure the encrypted disk to use our newly loaded module and enable our patched <code>dm-crypt</code> flag (we have to use low-level <code>dmsetup</code> tool as <code>cryptsetup</code> obviously is not aware of our modifications):</p>
            <pre><code>$ sudo dmsetup table encrypted-ram0 --showkeys | sed 's/aes-xts-plain64/capi:xts-aes-xtsproxy-plain64/' | sed 's/$/ 1 force_inline/' | sudo dmsetup reload encrypted-ram0</code></pre>
            <p>We just "loaded" the new configuration, but for it to take effect, we need to suspend/resume the encrypted device:</p>
            <pre><code>$ sudo dmsetup suspend encrypted-ram0 &amp;&amp; sudo dmsetup resume encrypted-ram0</code></pre>
            <p>And now observe the result. We may go back to the other terminal running the <code>fio</code> job and look at the output, but to make things nicer, here's a snapshot of the observed read/write throughput in <a href="https://grafana.com/">Grafana</a>:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/gU4JU2MqSRwllckZS17O0/eb5ce828cbd3e9a84bfa12411def3455/read-throughput-annotated.png" />
            
            </figure><p></p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6jxXWkIyojMU4AfFA3BVGm/223478d25385f5dbb18385074fa9d60e/write-throughput-annotated.png" />
            
            </figure><p>Wow, we have more than doubled the throughput! With the total throughput of <code>~640 MB/s</code> we're now much closer to the expected <code>~696 MB/s</code> from above. What about the IO latency? (The <code>await</code> statistic from the <a href="http://man7.org/linux/man-pages/man1/iostat.1.html">iostat reporting tool</a>):</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3gewP8Uml8nprMoLHFTeCR/5f559ee898a438446b8cbccf962290c9/await-annotated.png" />
            
            </figure><p>The latency has been cut in half as well!</p>
    <div>
      <h4>To production</h4>
      <a href="#to-production">
        
      </a>
    </div>
    <p>So far we have been using a synthetic setup with some parts of the full production stack missing, like file systems, real hardware and most importantly, production workload. To ensure we’re not optimising imaginary things, here is a snapshot of the production impact these changes bring to the caching part of our stack:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6efmOs555BLNsL8xsxw7YP/7c43e23c6c93013511e06fd191cab393/prod.png" />
            
            </figure><p>This graph represents a three-way comparison of the worst-case response times (99th percentile) for a <a href="/how-we-scaled-nginx-and-saved-the-world-54-years-every-day/">cache hit in one of our servers</a>. The green line is from a server with unencrypted disks, which we will use as baseline. The red line is from a server with encrypted disks with the default Linux disk encryption implementation and the blue line is from a server with encrypted disks and our optimisations enabled. As we can see the default Linux disk encryption implementation has a significant impact on our cache latency in worst case scenarios, whereas the patched implementation is indistinguishable from not using encryption at all. In other words the improved encryption implementation does not have any impact at all on our cache response speed, so we basically get it for free! That’s a win!</p>
    <div>
      <h3>We're just getting started</h3>
      <a href="#were-just-getting-started">
        
      </a>
    </div>
    <p>This post shows how an architecture review can double the performance of a system. Also we <a href="/how-expensive-is-crypto-anyway/">reconfirmed that modern cryptography is not expensive</a> and there is usually no excuse not to protect your data.</p><p>We are going to submit this work for inclusion in the main kernel source tree, but most likely not in its current form. Although the results look encouraging we have to remember that Linux is a highly portable operating system: it runs on powerful servers as well as small resource constrained IoT devices and on <a href="/arm-takes-wing/">many other CPU architectures</a> as well. The current version of the patches just optimises disk encryption for a particular workload on a particular architecture, but Linux needs a solution which runs smoothly everywhere.</p><p>That said, if you think your case is similar and you want to take advantage of the performance improvements now, you may <a href="https://github.com/cloudflare/linux/blob/master/patches/">grab the patches</a> and hopefully provide feedback. The runtime flag makes it easy to toggle the functionality on the fly and a simple A/B test may be performed to see if it benefits any particular case or setup. These patches have been running across our <a href="https://www.cloudflare.com/network/">wide network of more than 200 data centres</a> on five generations of hardware, so can be reasonably considered stable. Enjoy both performance and security from Cloudflare for all!</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2QGDkwHsSsWOJBpiZYunWH/81406719438a6797a82752855ee57cdd/perf-sec.png" />
            
            </figure>
    <div>
      <h3>Update (October 11, 2020)</h3>
      <a href="#update-october-11-2020">
        
      </a>
    </div>
    <p>The main patch from this blog (in a slightly updated form) has been <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/md/dm-crypt.c?id=39d42fa96ba1b7d2544db3f8ed5da8fb0d5cb877">merged</a> into mainline Linux kernel and is available since version 5.9 and onwards. The main difference is the mainline version exposes two flags instead of one, which provide the ability to bypass dm-crypt workqueues for reads and writes independently. For details, see <a href="https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/dm-crypt.html">the official dm-crypt documentation</a>.</p> ]]></content:encoded>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Performance]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Cryptography]]></category>
            <guid isPermaLink="false">2jUxkv277c7kroisM2311O</guid>
            <dc:creator>Ignat Korchagin</dc:creator>
        </item>
        <item>
            <title><![CDATA[A gentle introduction to Linux Kernel fuzzing]]></title>
            <link>https://blog.cloudflare.com/a-gentle-introduction-to-linux-kernel-fuzzing/</link>
            <pubDate>Wed, 10 Jul 2019 13:07:21 GMT</pubDate>
            <description><![CDATA[ For some time I’ve wanted to play with coverage-guided fuzzing. I decided to have a go at the Linux Kernel netlink machinery.  It's a good target: it's an obscure part of kernel, and it's relatively easy to automatically craft valid messages. ]]></description>
            <content:encoded><![CDATA[ <p>For some time I’ve wanted to play with coverage-guided <a href="https://en.wikipedia.org/wiki/Fuzzing">fuzzing</a>. Fuzzing is a powerful testing technique where an automated program feeds semi-random inputs to a tested program. The intention is to find such inputs that trigger bugs. Fuzzing is especially useful in finding memory corruption bugs in C or C++ programs.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/281ZRgfWvQ84ZxrUD29Bq2/f7f78fbced1487215ae7ce8e3801b55e/4152779709_d1ea8dd3b4_z.jpg" />
            
            </figure><p><a href="https://www.flickr.com/photos/patrick_s/4152779709">Image</a> by <a href="https://www.flickr.com/photos/patrick_s/">Patrick Shannon</a> CC BY 2.0</p><p>Normally it's recommended to pick a well known, but little explored, library that is heavy on parsing. Historically things like libjpeg, libpng and libyaml were perfect targets. Nowadays it's harder to find a good target - everything seems to have been fuzzed to death already. That's a good thing! I guess the software is getting better! Instead of choosing a userspace target I decided to have a go at the Linux Kernel netlink machinery.</p><p><a href="https://en.wikipedia.org/wiki/Netlink">Netlink is an internal Linux facility</a> used by tools like "ss", "ip", "netstat". It's used for low level networking tasks - configuring network interfaces, IP addresses, routing tables and such. It's a good target: it's an obscure part of kernel, and it's relatively easy to automatically craft valid messages. Most importantly, we can learn a lot about Linux internals in the process. Bugs in netlink aren't going to have security impact though - netlink sockets usually require privileged access anyway.</p><p>In this post we'll run <a href="http://lcamtuf.coredump.cx/afl/">AFL fuzzer</a>, driving our netlink shim program against a custom Linux kernel. All of this running inside KVM virtualization.</p><p>This blog post is a tutorial. With the easy to follow instructions, you should be able to quickly replicate the results. All you need is a machine running Linux and 20 minutes.</p>
    <div>
      <h2>Prior work</h2>
      <a href="#prior-work">
        
      </a>
    </div>
    <p>The technique we are going to use is formally called "coverage-guided fuzzing". There's a lot of prior literature:</p><ul><li><p><a href="https://blog.trailofbits.com/2017/02/16/the-smart-fuzzer-revolution/">The Smart Fuzzer Revolution</a> by Dan Guido, and <a href="https://lwn.net/Articles/677764/">LWN article</a> about it</p></li><li><p><a href="https://j00ru.vexillium.org/talks/blackhat-eu-effective-file-format-fuzzing-thoughts-techniques-and-results/">Effective file format fuzzing</a> by Mateusz “j00ru” Jurczyk</p></li><li><p><a href="http://honggfuzz.com/">honggfuzz</a> by Robert Swiecki, is a modern, feature-rich coverage-guided fuzzer</p></li><li><p><a href="https://google.github.io/clusterfuzz/">ClusterFuzz</a></p></li><li><p><a href="https://github.com/google/fuzzer-test-suite">Fuzzer Test Suite</a></p></li></ul><p>Many people have fuzzed the Linux Kernel in the past. Most importantly:</p><ul><li><p><a href="https://github.com/google/syzkaller/blob/master/docs/syzbot.md">syzkaller (aka syzbot)</a> by Dmitry Vyukov, is a very powerful CI-style continuously running kernel fuzzer, which found hundreds of issues already. It's an awesome machine - it will even report the bugs automatically!</p></li><li><p><a href="https://github.com/kernelslacker/trinity">Trinity fuzzer</a></p></li></ul><p>We'll use <a href="http://lcamtuf.coredump.cx/afl/">the AFL</a>, everyone's favorite fuzzer. AFL was written by <a href="http://lcamtuf.coredump.cx">Michał Zalewski</a>. It's well known for its ease of use, speed and very good mutation logic. It's a perfect choice for people starting their journey into fuzzing!</p><p>If you want to read more about AFL, the documentation is in couple of files:</p><ul><li><p><a href="http://lcamtuf.coredump.cx/afl/historical_notes.txt">Historical notes</a></p></li><li><p><a href="http://lcamtuf.coredump.cx/afl/technical_details.txt">Technical whitepaper</a></p></li><li><p><a href="http://lcamtuf.coredump.cx/afl/README.txt">README</a></p></li></ul>
    <div>
      <h2>Coverage-guided fuzzing</h2>
      <a href="#coverage-guided-fuzzing">
        
      </a>
    </div>
    <p>Coverage-guided fuzzing works on the principle of a feedback loop:</p><ul><li><p>the fuzzer picks the most promising test case</p></li><li><p>the fuzzer mutates the test into a large number of new test cases</p></li><li><p>the target code runs the mutated test cases, and reports back code coverage</p></li><li><p>the fuzzer computes a score from the reported coverage, and uses it to prioritize the interesting mutated tests and remove the redundant ones</p></li></ul><p>For example, let's say the input test is "hello". Fuzzer may mutate it to a number of tests, for example: "hEllo" (bit flip), "hXello" (byte insertion), "hllo" (byte deletion). If any of these tests will yield an interesting code coverage, then it will be prioritized and used as a base for a next generation of tests.</p><p>Specifics on how mutations are done, and how to efficiently compare code coverage reports of thousands of program runs is the fuzzer secret sauce. Read on the <a href="http://lcamtuf.coredump.cx/afl/technical_details.txt">AFL's technical whitepaper</a> for nitty gritty details.</p><p>The code coverage reported back from the binary is very important. It allows fuzzer to order the test cases, and identify the most promising ones. Without the code coverage the fuzzer is blind.</p><p>Normally, when using AFL, we are required to instrument the target code so that coverage is reported in an AFL-compatible way. But we want to fuzz the kernel! We can't just recompile it with "afl-gcc"! Instead we'll use a trick. We'll prepare a binary that will trick AFL into thinking it was compiled with its tooling. This binary will report back the code coverage extracted from kernel.</p>
    <div>
      <h2>Kernel code coverage</h2>
      <a href="#kernel-code-coverage">
        
      </a>
    </div>
    <p>The kernel has at least two built-in coverage mechanisms - GCOV and KCOV:</p><ul><li><p><a href="https://www.kernel.org/doc/html/v4.15/dev-tools/gcov.html">Using gcov with the Linux kernel</a></p></li><li><p><a href="https://www.kernel.org/doc/html/latest/dev-tools/kcov.html">KCOV: code coverage for fuzzing</a></p></li></ul><p>KCOV was designed with fuzzing in mind, so we'll use this.</p><p>Using KCOV is pretty easy. We must compile the Linux kernel with the right setting. First, enable the KCOV kernel config option:</p>
            <pre><code>cd linux
./scripts/config \
    -e KCOV \
    -d KCOV_INSTRUMENT_ALL</code></pre>
            <p>KCOV is capable of recording code coverage from the whole kernel. It can be set with KCOV_INSTRUMENT_ALL option. This has disadvantages though - it would slow down the parts of the kernel we don't want to profile, and would introduce noise in our measurements (reduce "stability"). For starters, let's disable KCOV_INSTRUMENT_ALL and enable KCOV selectively on the code we actually want to profile. Today, we focus on netlink machinery, so let's enable KCOV on whole "net" directory tree:</p>
            <pre><code>find net -name Makefile | xargs -L1 -I {} bash -c 'echo "KCOV_INSTRUMENT := y" &gt;&gt; {}'</code></pre>
            <p>In a perfect world we would enable KCOV only for a couple of files we really are interested in. But netlink handling is peppered all over the network stack code, and we don't have time for fine tuning it today.</p><p>With KCOV in place, it's worth to add "kernel hacking" toggles that will increase the likelihood of reporting memory corruption bugs. See the <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2019-07-kernel-fuzzing/README.md">README</a> for the list of <a href="https://github.com/google/syzkaller/blob/master/docs/linux/kernel_configs.md">Syzkaller suggested options</a> - most importantly <a href="https://www.kernel.org/doc/html/latest/dev-tools/kasan.html">KASAN</a>.</p><p>With that set we can compile our KCOV and KASAN enabled kernel. Oh, one more thing. We are going to run the kernel in a kvm. We're going to use <a href="https://github.com/amluto/virtme">"virtme"</a>, so we need a couple of toggles:</p>
            <pre><code>./scripts/config \
    -e VIRTIO -e VIRTIO_PCI -e NET_9P -e NET_9P_VIRTIO -e 9P_FS \
    -e VIRTIO_NET -e VIRTIO_CONSOLE  -e DEVTMPFS ...</code></pre>
            <p>(see the <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2019-07-kernel-fuzzing/README.md">README</a> for full list)</p>
    <div>
      <h2>How to use KCOV</h2>
      <a href="#how-to-use-kcov">
        
      </a>
    </div>
    <p>KCOV is super easy to use. First, note the code coverage is recorded in a per-process data structure. This means you have to enable and disable KCOV within a userspace process, and it's impossible to record coverage for non-task things, like interrupt handling. This is totally fine for our needs.</p><p>KCOV reports data into a ring buffer. Setting it up is pretty simple, <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2019-07-kernel-fuzzing/src/kcov.c">see our code</a>. Then you can enable and disable it with a trivial ioctl:</p>
            <pre><code>ioctl(kcov_fd, KCOV_ENABLE, KCOV_TRACE_PC);
/* profiled code */
ioctl(kcov_fd, KCOV_DISABLE, 0);</code></pre>
            <p>After this sequence the ring buffer contains the list of %rip values of all the basic blocks of the KCOV-enabled kernel code. To read the buffer just run:</p>
            <pre><code>n = __atomic_load_n(&amp;kcov_ring[0], __ATOMIC_RELAXED);
for (i = 0; i &lt; n; i++) {
    printf("0x%lx\n", kcov_ring[i + 1]);
}</code></pre>
            <p>With tools like <code>addr2line</code> it's possible to resolve the %rip to a specific line of code. We won't need it though - the raw %rip values are sufficient for us.</p>
    <div>
      <h2>Feeding KCOV into AFL</h2>
      <a href="#feeding-kcov-into-afl">
        
      </a>
    </div>
    <p>The next step in our journey is to learn how to trick AFL. Remember, AFL needs a specially-crafted executable, but we want to feed in the kernel code coverage. First we need to understand how AFL works.</p><p>AFL sets up an array of 64K 8-bit numbers. This memory region is called "shared_mem" or "trace_bits" and is shared with the traced program. Every byte in the array can be thought of as a hit counter for a particular (branch_src, branch_dst) pair in the instrumented code.</p><p>It's important to notice that AFL prefers random branch labels, rather than reusing the %rip value to identify the basic blocks. This is to increase entropy - we want our hit counters in the array to be uniformly distributed. The algorithm AFL uses is:</p>
            <pre><code>cur_location = &lt;COMPILE_TIME_RANDOM&gt;;
shared_mem[cur_location ^ prev_location]++; 
prev_location = cur_location &gt;&gt; 1;</code></pre>
            <p>In our case with KCOV we don't have compile-time-random values for each branch. Instead we'll use a hash function to generate a uniform 16 bit number from %rip recorded by KCOV. This is how to feed a KCOV report into the AFL "shared_mem" array:</p>
            <pre><code>n = __atomic_load_n(&amp;kcov_ring[0], __ATOMIC_RELAXED);
uint16_t prev_location = 0;
for (i = 0; i &lt; n; i++) {
        uint16_t cur_location = hash_function(kcov_ring[i + 1]);
        shared_mem[cur_location ^ prev_location]++;
        prev_location = cur_location &gt;&gt; 1;
}</code></pre>
            
    <div>
      <h2>Reading test data from AFL</h2>
      <a href="#reading-test-data-from-afl">
        
      </a>
    </div>
    <p>Finally, we need to actually write the test code hammering the kernel netlink interface! First we need to read input data from AFL. By default AFL sends a test case to stdin:</p>
            <pre><code>/* read AFL test data */
char buf[512*1024];
int buf_len = read(0, buf, sizeof(buf));</code></pre>
            
    <div>
      <h2>Fuzzing netlink</h2>
      <a href="#fuzzing-netlink">
        
      </a>
    </div>
    <p>Then we need to send this buffer into a netlink socket. But we know nothing about how netlink works! Okay, let's use the first 5 bytes of input as the netlink protocol and group id fields. This will allow the AFL to figure out and guess the correct values of these fields. The code testing netlink (simplified):</p>
            <pre><code>netlink_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_NONBLOCK, buf[0]);

struct sockaddr_nl sa = {
        .nl_family = AF_NETLINK,
        .nl_groups = (buf[1] &lt;&lt;24) | (buf[2]&lt;&lt;16) | (buf[3]&lt;&lt;8) | buf[4],
};

bind(netlink_fd, (struct sockaddr *) &amp;sa, sizeof(sa));

struct iovec iov = { &amp;buf[5], buf_len - 5 };
struct sockaddr_nl sax = {
      .nl_family = AF_NETLINK,
};

struct msghdr msg = { &amp;sax, sizeof(sax), &amp;iov, 1, NULL, 0, 0 };
r = sendmsg(netlink_fd, &amp;msg, 0);
if (r != -1) {
      /* sendmsg succeeded! great I guess... */
}</code></pre>
            <p>That's basically it! For speed, we will wrap this in a short loop that mimics <a href="https://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html">the AFL "fork server" logic</a>. I'll skip the explanation here, <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2019-07-kernel-fuzzing/src/forksrv.c">see our code for details</a>. The resulting code of our AFL-to-KCOV shim looks like:</p>
            <pre><code>forksrv_welcome();
while(1) {
    forksrv_cycle();
    test_data = afl_read_input();
    kcov_enable();
    /* netlink magic */
    kcov_disable();
    /* fill in shared_map with tuples recorded by kcov */
    if (new_crash_in_dmesg) {
         forksrv_status(1);
    } else {
         forksrv_status(0);
    }
}</code></pre>
            <p><a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2019-07-kernel-fuzzing/src/fuzznetlink.c">See full source code</a>.</p>
    <div>
      <h2>How to run the custom kernel</h2>
      <a href="#how-to-run-the-custom-kernel">
        
      </a>
    </div>
    <p>We're missing one important piece - how to actually run the custom kernel we've built. There are three options:</p><p><b>"native"</b>: You can totally boot the built kernel on your server and fuzz it natively. This is the fastest technique, but pretty problematic. If the fuzzing succeeds in finding a bug you will crash the machine, potentially losing the test data. Cutting the branches we sit on should be avoided.</p><p><b>"uml"</b>: We could configure the kernel to run as <a href="http://user-mode-linux.sourceforge.net/">User Mode Linux</a>. Running a UML kernel requires no privileges. The kernel just runs a user space process. UML is pretty cool, but sadly, it doesn't support KASAN, therefore the chances of finding a memory corruption bug are reduced. Finally, UML is a pretty magic special environment - bugs found in UML may not be relevant on real environments. Interestingly, UML is used by <a href="https://source.android.com/devices/architecture/kernel/network_tests">Android network_tests framework</a>.</p><p><b>"kvm"</b>: we can use kvm to run our custom kernel in a virtualized environment. This is what we'll do.</p><p>One of the simplest ways to run a custom kernel in a KVM environment is to use <a href="https://github.com/amluto/virtme">"virtme" scripts</a>. With them we can avoid having to create a dedicated disk image or partition, and just share the host file system. This is how we can run our code:</p>
            <pre><code>virtme-run \
    --kimg bzImage \
    --rw --pwd --memory 512M \
    --script-sh "&lt;what to run inside kvm&gt;" </code></pre>
            <p>But hold on. We forgot about preparing input corpus data for our fuzzer!</p>
    <div>
      <h2>Building the input corpus</h2>
      <a href="#building-the-input-corpus">
        
      </a>
    </div>
    <p>Every fuzzer takes a carefully crafted test cases as input, to bootstrap the first mutations. The test cases should be short, and cover as large part of code as possible. Sadly - I know nothing about netlink. How about we don't prepare the input corpus...</p><p>Instead we can ask AFL to "figure out" what inputs make sense. This is what <a href="https://lcamtuf.blogspot.com/2014/11/pulling-jpegs-out-of-thin-air.html">Michał did back in 2014 with JPEGs</a> and it worked for him. With this in mind, here is our input corpus:</p>
            <pre><code>mkdir inp
echo "hello world" &gt; inp/01.txt</code></pre>
            <p>Instructions, how to compile and run the whole thing are in <a href="https://github.com/cloudflare/cloudflare-blog/blob/master/2019-07-kernel-fuzzing">README.md</a> on our github. It boils down to:</p>
            <pre><code>virtme-run \
    --kimg bzImage \
    --rw --pwd --memory 512M \
    --script-sh "./afl-fuzz -i inp -o out -- fuzznetlink" </code></pre>
            <p>With this running you will see the familiar AFL status screen:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3Ms0btT4aEecNGhSwflYYX/c519bc18d15a5a33ae486456cdbde6e3/Screenshot-from-2019-07-10-11-44-50.png" />
            
            </figure>
    <div>
      <h2>Further notes</h2>
      <a href="#further-notes">
        
      </a>
    </div>
    <p>That's it. Now you have a custom hardened kernel, running a basic coverage-guided fuzzer. All inside KVM.</p><p>Was it worth the effort? Even with this basic fuzzer, and no input corpus, after a day or two the fuzzer found an interesting code path: <a href="https://lore.kernel.org/netdev/CAJPywTJWQ9ACrp0naDn0gikU4P5-xGcGrZ6ZOKUeeC3S-k9+MA@mail.gmail.com/T/#u">NEIGH: BUG, double timer add, state is 8</a>. With a more specialized fuzzer, some work on improving the "stability" metric and a decent input corpus, we could expect even better results.</p><p>If you want to learn more about what netlink sockets actually do, see a blog post by my colleague Jakub Sitnicki <a href="http://codecave.cc/multipath-routing-in-linux-part-1.html">Multipath Routing in Linux - part 1</a>. Then there is a good chapter about it in <a href="https://books.google.pl/books?redir_esc=y&amp;hl=pl&amp;id=96V4AgAAQBAJ&amp;q=netlink#v=snippet&amp;q=netlink&amp;f=false">Linux Kernel Networking book by Rami Rosen</a>.</p><p>In this blog post we haven't mentioned:</p><ul><li><p>details of AFL shared_memory setup</p></li><li><p>implementation of AFL persistent mode</p></li><li><p>how to create a network namespace to isolate the effects of weird netlink commands, and improve the "stability" AFL score</p></li><li><p>technique on how to read dmesg (/dev/kmsg) to find kernel crashes</p></li><li><p>idea to run AFL outside of KVM, for speed and stability - currently the tests aren't stable after a crash is found</p></li></ul><p>But we achieved our goal - we set up a basic, yet still useful fuzzer against a kernel. Most importantly: the same machinery can be reused to fuzz other parts of Linux subsystems - from file systems to bpf verifier.</p><p>I also learned a hard lesson: tuning fuzzers is a full time job. Proper fuzzing is definitely not as simple as starting it up and idly waiting for crashes. There is always something to improve, tune, and re-implement. A quote at the beginning of the mentioned presentation by Mateusz Jurczyk resonated with me:</p><blockquote><p>"Fuzzing is easy to learn but hard to master."</p></blockquote><p>Happy bug hunting!</p> ]]></content:encoded>
            <category><![CDATA[Linux]]></category>
            <category><![CDATA[Kernel]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <guid isPermaLink="false">5t8I5tspUrkYUD9SY5kNP3</guid>
            <dc:creator>Marek Majkowski</dc:creator>
        </item>
    </channel>
</rss>