<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.osandov.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.osandov.com/" rel="alternate" type="text/html" /><updated>2025-04-29T02:35:19+00:00</updated><id>https://blog.osandov.com/feed.xml</id><title type="html">Omar Sandoval’s Blog</title><subtitle>Systems programming and computer magick</subtitle><author><name>Omar Sandoval</name></author><entry><title type="html">Calling Linux Kernel Functions From Userspace (!)</title><link href="https://blog.osandov.com/2024/09/17/calling-linux-kernel-functions-from-userspace.html" rel="alternate" type="text/html" title="Calling Linux Kernel Functions From Userspace (!)" /><published>2024-09-17T00:00:00+00:00</published><updated>2024-09-17T00:00:00+00:00</updated><id>https://blog.osandov.com/2024/09/17/calling-linux-kernel-functions-from-userspace</id><content type="html" xml:base="https://blog.osandov.com/2024/09/17/calling-linux-kernel-functions-from-userspace.html"><![CDATA[<p>I just landed a really exciting feature for <a href="https://github.com/osandov/drgn">drgn</a>: the ability to call arbitrary functions and write to memory in the Linux kernel. I think the technical details of the implementation are very interesting, and it’s probably the funniest thing I’ve ever done, so I wanted to write about how it works.</p>

<h2 id="background">Background</h2>

<p><a href="https://github.com/osandov/drgn">drgn</a> is a programmable debugger primarily targeted at the Linux kernel. It lets you read kernel debugging symbols and data structures from Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;&gt;</span> <span class="kn">from</span> <span class="nn">drgn.helpers.linux.list</span> <span class="kn">import</span> <span class="n">list_for_each_entry</span>
<span class="o">&gt;&gt;&gt;</span> <span class="k">for</span> <span class="n">mod</span> <span class="ow">in</span> <span class="n">list_for_each_entry</span><span class="p">(</span><span class="s">"struct module"</span><span class="p">,</span>
<span class="p">...</span>                                <span class="n">prog</span><span class="p">[</span><span class="s">"modules"</span><span class="p">].</span><span class="n">address_of_</span><span class="p">(),</span>
<span class="p">...</span>                                <span class="s">"list"</span><span class="p">):</span>
<span class="p">...</span>    <span class="k">if</span> <span class="n">mod</span><span class="p">.</span><span class="n">refcnt</span><span class="p">.</span><span class="n">counter</span> <span class="o">&gt;</span> <span class="mi">10</span><span class="p">:</span>
<span class="p">...</span>        <span class="k">print</span><span class="p">(</span><span class="n">mod</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
<span class="p">...</span>
<span class="p">(</span><span class="n">char</span> <span class="p">[</span><span class="mi">56</span><span class="p">])</span><span class="s">"snd"</span>
<span class="p">(</span><span class="n">char</span> <span class="p">[</span><span class="mi">56</span><span class="p">])</span><span class="s">"evdev"</span>
<span class="p">(</span><span class="n">char</span> <span class="p">[</span><span class="mi">56</span><span class="p">])</span><span class="s">"i915"</span>
</code></pre></div></div>

<p>Until now, drgn has been purely read-only: it can inspect anything in kernel memory, but it can’t change it or otherwise interfere. It uses safe kernel interfaces (namely, <a href="https://man7.org/linux/man-pages/man5/proc_kcore.5.html"><code class="language-plaintext highlighter-rouge">/proc/kcore</code></a>) that can’t crash the kernel (barring kernel bugs).</p>

<p>So, when one of my teammates asked me whether drgn could call arbitrary functions in the kernel, my initial impulse was to say “of course not”. But, after thinking about it for a few more moments, I had an idea.</p>

<h2 id="generating-kernel-module-source-code">Generating Kernel Module Source Code</h2>

<p>A crucial design decision in drgn is that it emphasizes programmatic interfaces for everything: not only can you print a variable or a type, but you can also use them through <a href="https://drgn.readthedocs.io/en/latest/api_reference.html">Python APIs</a>. These APIs enable many advanced use cases. In this case, they enabled my crazy idea: I could use drgn’s APIs to generate the source code for a loadable kernel module that would make the desired function call.</p>

<p>So, the first version of this feature translated this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">call_function</span><span class="p">(</span><span class="s">"_printk"</span><span class="p">,</span> <span class="s">"Hello, world %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">1234</span><span class="p">)</span>
</code></pre></div></div>

<p>into this source code:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;linux/module.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/uaccess.h&gt;</span><span class="cp">
</span>
<span class="k">static</span> <span class="kt">int</span> <span class="n">__init</span> <span class="nf">kmodify_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
        <span class="k">struct</span> <span class="p">{</span>
                <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">arg0</span><span class="p">[</span><span class="mi">17</span><span class="p">];</span>
                <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span>
        <span class="p">}</span> <span class="n">out</span> <span class="o">=</span> <span class="p">{</span>
                <span class="p">.</span><span class="n">arg0</span> <span class="o">=</span> <span class="p">{</span> <span class="mh">0x48</span><span class="p">,</span> <span class="mh">0x65</span><span class="p">,</span> <span class="mh">0x6c</span><span class="p">,</span> <span class="mh">0x6c</span><span class="p">,</span> <span class="mh">0x6f</span><span class="p">,</span> <span class="mh">0x2c</span><span class="p">,</span>
                          <span class="mh">0x20</span><span class="p">,</span> <span class="mh">0x77</span><span class="p">,</span> <span class="mh">0x6f</span><span class="p">,</span> <span class="mh">0x72</span><span class="p">,</span> <span class="mh">0x6c</span><span class="p">,</span> <span class="mh">0x64</span><span class="p">,</span>
                          <span class="mh">0x20</span><span class="p">,</span> <span class="mh">0x25</span><span class="p">,</span> <span class="mh">0x64</span><span class="p">,</span> <span class="mh">0x0a</span><span class="p">,</span> <span class="mh">0x00</span> <span class="p">},</span>
        <span class="p">};</span>

        <span class="n">out</span><span class="p">.</span><span class="n">ret</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">fmt</span><span class="p">,</span> <span class="p">...))</span><span class="mh">0xffffffffb34bafaaUL</span><span class="p">)(</span>
                <span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">out</span><span class="p">.</span><span class="n">arg0</span><span class="p">,</span>
                <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="mi">1234</span>
        <span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">copy_to_user</span><span class="p">((</span><span class="kt">void</span> <span class="n">__user</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x7f7ee60694a0UL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">out</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">out</span><span class="p">)))</span>
                <span class="k">return</span> <span class="o">-</span><span class="n">EFAULT</span><span class="p">;</span>

        <span class="k">return</span> <span class="o">-</span><span class="n">EINPROGRESS</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">module_init</span><span class="p">(</span><span class="n">kmodify_init</span><span class="p">);</span>
<span class="n">MODULE_LICENSE</span><span class="p">(</span><span class="s">"GPL"</span><span class="p">);</span>
</code></pre></div></div>

<p>Then, it invoked the kernel build system to build the module before loading it with <a href="https://man7.org/linux/man-pages/man2/finit_module.2.html"><code class="language-plaintext highlighter-rouge">finit_module(2)</code></a>.</p>

<p>There are a few important details here: the function is called via its address, casted to the proper type. This makes it possible to call static functions. The passed arguments are embedded directly into the source code. Additionally, the return value is copied back to drgn so that drgn can then return it to the user.</p>

<p>This can easily be extended to support writing to kernel memory by generating a call to <code class="language-plaintext highlighter-rouge">memcpy()</code>.</p>

<p>This approach was a great proof of concept, but it has a couple of major limitations:</p>

<ul>
  <li>It requires that the kernel module build system (<code class="language-plaintext highlighter-rouge">kernel-devel</code>) is installed. This is not the case in many environments. A suitable toolchain must also be installed. If the compiler used is not exactly the same as the one used to build the kernel, you get <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Makefile?h=v6.11#n1799">annoying warnings</a>. If the kernel was built with Clang/LLVM, then the module will fail to build with GCC/binutils.</li>
  <li>Compiling a kernel module is slow: about 3 seconds in my tests. It feels really clunky for a single function call to take that long.</li>
</ul>

<p>Unhappy with these limitations, I was ready to toss my script into <a href="https://github.com/osandov/drgn/tree/main/contrib"><code class="language-plaintext highlighter-rouge">drgn/contrib</code></a> as a gimmick and move on. But, I had a nagging feeling that there was a better way.</p>

<h2 id="what-is-a-kernel-module-really">What is a Kernel Module, Really?</h2>

<p>I was curious what it would take to craft a kernel module manually without the kernel build system. To do that, we need to understand the kernel module file format. A kernel module is a relocatable object file, similar to a <code class="language-plaintext highlighter-rouge">.o</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>file kmodify.ko
<span class="go">kmodify.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=21a6fa6683ab50d6a9231eb336c18766a99ba579, with debug_info, not stripped
</span></code></pre></div></div>

<p>Like any relocatable object file, it consists of sections containing code, data, and metadata. The file may reference symbols (functions, global variables) whose addresses are not known until the file is loaded. So, the file metadata includes a list of “relocations”: records indicating where the loader should store a resolved symbol address. In addition to the standard <code class="language-plaintext highlighter-rouge">.text</code>, <code class="language-plaintext highlighter-rouge">.data</code>, etc. sections, kernel modules have a couple of special sections. The <code class="language-plaintext highlighter-rouge">.modinfo</code> section comprises a list of key-value pairs with information about the module:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>readelf <span class="nt">-p</span> .modinfo kmodify.ko
<span class="go">
String dump of section '.modinfo':
  [     0]  license=GPL
  [     c]  depends=
  [    15]  retpoline=Y
  [    21]  name=kmodify
  [    2e]  vermagic=6.11.0-vmtest30.1default SMP preempt mod_unload
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.gnu.linkonce.this_module</code> section contains the <code class="language-plaintext highlighter-rouge">struct module</code> that will be used to track the module in the kernel. In the object file, it is zeroed out other than the <code class="language-plaintext highlighter-rouge">name</code> field.</p>

<p>Using some code from drgn’s unit tests for generating custom ELF files, I figured out the minimum requirements for a valid kernel module file:</p>

<ul>
  <li>It must have a symbol table and the corresponding string table (both of which may be empty).</li>
  <li>It must have a <code class="language-plaintext highlighter-rouge">.gnu.linkonce.this_module</code> section with <code class="language-plaintext highlighter-rouge">sh_size == sizeof(struct module)</code> and a non-empty <code class="language-plaintext highlighter-rouge">name</code>.</li>
  <li>It must have a <code class="language-plaintext highlighter-rouge">.modinfo</code> section containing a <code class="language-plaintext highlighter-rouge">vermagic</code> field that matches the kernel’s <code class="language-plaintext highlighter-rouge">vermagic</code>. (It’s a good idea to include some of the other fields.)</li>
</ul>

<p>In order to execute code when the module is loaded, there are a few more requirements:</p>

<ul>
  <li>It must have an <code class="language-plaintext highlighter-rouge">.init.text</code> section containing the executable code.</li>
  <li>It must have a function symbol named <code class="language-plaintext highlighter-rouge">init_module</code> referring to the code in the <code class="language-plaintext highlighter-rouge">.init.text</code> section.</li>
  <li>It must have a relocation that writes the address of the <code class="language-plaintext highlighter-rouge">init_module</code> symbol to the <code class="language-plaintext highlighter-rouge">init</code> function pointer in <code class="language-plaintext highlighter-rouge">.gnu.linkonce.this_module</code>.</li>
</ul>

<p>drgn is able to get all of the necessary information using the kernel debugging symbols, so I could now craft a kernel module to run whatever machine code I wanted without any additional dependencies.</p>

<h2 id="writing-a-tiny-compiler">Writing a Tiny Compiler</h2>

<p>Now I needed a way to translate my high-level <code class="language-plaintext highlighter-rouge">call_function()</code> Python function into machine code. In other words, I needed a compiler. So, I wrote one.</p>

<h3 id="front-end">Front End</h3>

<p>Like other compilers, this compiler has a “front end” that verifies the syntax and semantics of the input and transforms the input into an intermediate representation (IR). The input to this compiler is the <code class="language-plaintext highlighter-rouge">call_function()</code> call. First, it converts all of the arguments to <a href="https://drgn.readthedocs.io/en/latest/api_reference.html#drgn.Object"><code class="language-plaintext highlighter-rouge">drgn.Object</code></a>s. Then, it type checks the arguments just like a C compiler would. (In fact, I had to take a detour to implement a type checking helper function, <a href="https://drgn.readthedocs.io/en/latest/api_reference.html#drgn.implicit_convert"><code class="language-plaintext highlighter-rouge">drgn.implicit_convert()</code></a>.) Finally, it generates the IR, which for this compiler is a tree of Python objects. For the example from earlier:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">call_function</span><span class="p">(</span><span class="s">"_printk"</span><span class="p">,</span> <span class="s">"Hello, world %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">1234</span><span class="p">)</span>
</code></pre></div></div>

<p>the IR looks like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_Function</span><span class="p">(</span>
    <span class="n">body</span><span class="o">=</span><span class="p">[</span>
        <span class="n">_Call</span><span class="p">(</span>
            <span class="n">func</span><span class="o">=</span><span class="n">_Symbol</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"func"</span><span class="p">),</span>
            <span class="n">args</span><span class="o">=</span><span class="p">[</span>
                <span class="n">_Symbol</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">".data"</span><span class="p">,</span> <span class="n">section</span><span class="o">=</span><span class="bp">True</span><span class="p">),</span>
                <span class="n">_Integer</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">1234</span><span class="p">),</span>
            <span class="p">],</span>
        <span class="p">),</span>
        <span class="n">_StoreReturnValue</span><span class="p">(</span>
            <span class="n">size</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
            <span class="n">dst</span><span class="o">=</span><span class="n">_Symbol</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">".data"</span><span class="p">,</span> <span class="n">offset</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span> <span class="n">section</span><span class="o">=</span><span class="bp">True</span><span class="p">),</span>
        <span class="p">),</span>
        <span class="n">_Call</span><span class="p">(</span>
            <span class="n">func</span><span class="o">=</span><span class="n">_Symbol</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"copy_to_user_nofault"</span><span class="p">),</span>
            <span class="n">args</span><span class="o">=</span><span class="p">[</span>
                <span class="n">_Integer</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mh">0x7f7ee60694a0</span><span class="p">),</span>
                <span class="n">_Symbol</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">".data"</span><span class="p">,</span> <span class="n">section</span><span class="o">=</span><span class="bp">True</span><span class="p">),</span>
                <span class="n">_Integer</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">24</span><span class="p">),</span>
            <span class="p">],</span>
        <span class="p">),</span>
        <span class="n">_ReturnIfLastReturnValueNonZero</span><span class="p">(</span>
            <span class="n">value</span><span class="o">=</span><span class="n">_Integer</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">value</span><span class="o">=-</span><span class="n">errno</span><span class="p">.</span><span class="n">EFAULT</span><span class="p">),</span>
        <span class="p">),</span>
        <span class="n">_Return</span><span class="p">(</span>
            <span class="n">value</span><span class="o">=</span><span class="n">_Integer</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">value</span><span class="o">=-</span><span class="n">errno</span><span class="p">.</span><span class="n">EINPROGRESS</span><span class="p">),</span>
        <span class="p">),</span>
    <span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Notice the resemblance to the generated C source code from earlier. Also notice that references to functions and data are represented by symbols.</p>

<h3 id="back-end">Back End</h3>

<p>The compiler “back end” is responsible for generating the executable code for an IR. This can be a huge task. Luckily, our IR is very simple. In fact, the above example demonstrated the extent of its capabilities.</p>

<p>Our compiler contains code generation rules for each of these IR nodes. Function calls are the most complex since we need to implement the architecture’s calling convention (i.e., which registers and stack locations to use for arguments and return values). Referencing the <a href="https://gitlab.com/x86-psABIs/x86-64-ABI">x86-64 psABI specification</a> was a necessity. Symbol references also require us to generate relocations.</p>

<p>Since the whole point of this exercise was to avoid dependencies, we generate machine code directly instead of going through an assembler. Again, this is feasible because the needed operations are limited. I made heavy use of the <a href="https://wiki.osdev.org/X86-64_Instruction_Encoding">OSDev wiki</a> and <a href="https://man7.org/linux/man-pages/man1/objdump.1.html"><code class="language-plaintext highlighter-rouge">objdump(1)</code></a> to understand the details of x86-64 instruction encoding.</p>

<p>The final compilation step is wrapping up the generated code, data, relocations, symbols, and kernel module metadata into a file which can then be loaded.</p>

<h2 id="applications">Applications</h2>

<p>There are two main ways I envision this feature being used. The first use case is debugging. Calling internal functions can be very helpful for understanding the state of the system. This is especially useful during development, where much less caution is needed.</p>

<p>The second use case is mitigating bugs in production. If a critical system gets in a bad state, it may be possible to fix that state by making a function call or overwriting some memory. For example, lost wake-ups are a common class of bug where, due to a race condition, a thread that is waiting on a condition misses its signal to wake up and waits forever.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> A lost wake-up can be cleared by manually waking up the thread with <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/sched/core.c?h=v6.11#n4286"><code class="language-plaintext highlighter-rouge">wake_up_process()</code></a>. (This use case is complementary to <a href="https://docs.kernel.org/livepatch/livepatch.html">live patching</a>, which can prevent a bug from being hit in the future but usually doesn’t resolve the bug if it was already hit.)</p>

<p>A brief note on security: this feature of course requires root (specifically, <a href="https://man7.org/linux/man-pages/man7/capabilities.7.html"><code class="language-plaintext highlighter-rouge">CAP_SYS_MODULE</code></a>). It doesn’t allow a user to do anything they couldn’t do before, although it certainly makes it easier. The proper way to disallow this is to require module signatures with <a href="https://docs.kernel.org/admin-guide/module-signing.html"><code class="language-plaintext highlighter-rouge">CONFIG_MODULE_SIG_FORCE</code></a> or <a href="https://man7.org/linux/man-pages/man7/kernel_lockdown.7.html"><code class="language-plaintext highlighter-rouge">kernel_lockdown(7)</code></a>.</p>

<p>Regardless of security concerns, calling arbitrary functions and overwriting memory are very dangerous, so do it with care.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I’m really excited about this feature, and I had a blast implementing it. There’s still more work to do: supporting architectures other than x86-64; supporting the full calling convention (specifically, structure arguments and return values); making use of kprobes and ftrace to implement something akin to breakpoints; and integrating with module signing so that only very trusted users can use it.</p>

<p>This feature is available in the <a href="https://drgn.readthedocs.io/en/latest/helpers.html#kmodify"><code class="language-plaintext highlighter-rouge">drgn.helpers.experimental.kmodify</code></a> package in drgn’s main branch, and it will ship in drgn 0.0.28. Try it out!</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>I often <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6c0ca7ae292adea09b8bdd33a524bb9326c3e989">encountered</a> (and <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fcf38cdf332a81b20a59e3ebaea81f6b316bbe0c">created</a>) this class of bug when I worked on Linux’s multiqueue block layer. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Omar Sandoval</name></author><category term="kernel" /><category term="compilers" /><category term="debugging" /><summary type="html"><![CDATA[I just landed a really exciting feature for drgn: the ability to call arbitrary functions and write to memory in the Linux kernel. I think the technical details of the implementation are very interesting, and it’s probably the funniest thing I’ve ever done, so I wanted to write about how it works.]]></summary></entry><entry><title type="html">Making debuginfod Viable for the Linux Kernel</title><link href="https://blog.osandov.com/2024/07/25/making-debuginfod-viable-for-the-linux-kernel.html" rel="alternate" type="text/html" title="Making debuginfod Viable for the Linux Kernel" /><published>2024-07-25T00:00:00+00:00</published><updated>2024-07-25T00:00:00+00:00</updated><id>https://blog.osandov.com/2024/07/25/making-debuginfod-viable-for-the-linux-kernel</id><content type="html" xml:base="https://blog.osandov.com/2024/07/25/making-debuginfod-viable-for-the-linux-kernel.html"><![CDATA[<p><a href="https://sourceware.org/elfutils/Debuginfod.html">debuginfod</a> is a service providing debugging symbols, source code, and executables via an HTTP API. Most Linux distributions run a debuginfod server, and many debugging tools make use of it automatically. debuginfod removes the need to manually install debugging symbols, which is a massive usability improvement for debuggers.</p>

<p>However, when I attempted to use debuginfod in <a href="https://github.com/osandov/drgn">drgn</a>, a programmable debugger primarily targeted at the Linux kernel, I ran into some server-side performance issues. Specifically, getting debugging symbols for the kernel and loaded kernel modules took <em>over an hour</em>, and most of that time was spent waiting for the debuginfod server to respond to queries.</p>

<p>This turned out to be caused by design decisions in Linux package management, compression algorithms, and debuginfod that are completely reasonable in isolation but interacted pessimally. By learning about these components, I was able to come up with a solution that reduces the time to get the same debugging symbols to <em>2 minutes</em>.</p>

<h2 id="how-debuginfod-works">How debuginfod Works</h2>

<p>(Almost) every binary on Linux has a “build ID”: a byte string that uniquely identifies it. Here’s the build ID for a binary on my machine:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>readelf <span class="nt">--notes</span> /usr/bin/cat | <span class="nb">grep</span> <span class="s2">"Build ID"</span>
<span class="go">    Build ID: 4880bb013184e34a6a3ee3187e1d6282b6abcf2e
</span></code></pre></div></div>

<p>debuginfod queries are based on this build ID. For example, to get the debugging symbols for the above binary:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl https://debuginfod.fedoraproject.org/buildid/4880bb013184e34a6a3ee3187e1d6282b6abcf2e/debuginfo <span class="o">&gt;</span> cat.debug
</code></pre></div></div>

<p>On the server side, debuginfod periodically scans a set of directories looking for binaries and creates an index mapping build IDs to files. When it receives a query, it looks up the file with the given build ID and responds with its contents.</p>

<p>However, for Linux distributions, it’s not practical to have a copy of every binary lying around for debuginfod. What distros <em>do</em> already have is a copy of every <em>package</em> (e.g., RPM or deb) containing those binaries. So, when debuginfod scans for binaries, it also checks for packages. It temporarily extracts each package it finds in order to create another index mapping build IDs to packages. To respond to a query, debuginfod looks up the package containing the given build ID and extracts the desired file.</p>

<h2 id="linux-package-file-formats">Linux Package File Formats</h2>

<p>Software packages on Linux are generally glorified archive files with some extra metadata.</p>

<p>In particular, <a href="https://rpm-software-management.github.io/rpm/manual/format_v4.html">RPM</a> files (used by Fedora, Red Hat Enterprise Linux, SUSE, and others) consist of a few metadata sections plus a compressed <a href="https://en.wikipedia.org/wiki/Cpio">cpio</a> archive. The cpio archive contains the binaries and other files along with their metadata (name, owner, permissions, etc.). The cpio archive looks something like this internally:</p>

<table>
    <thead>
        <tr>
            <th colspan="3">File 1</th>
            <th colspan="3">File 2</th>
            <th>File 3</th>
            <th>...</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Metadata</td>
            <td>Name</td>
            <td>Data</td>
            <td>Metadata</td>
            <td>Name</td>
            <td>Data</td>
            <td>...</td>
            <td>...</td>
        </tr>
    </tbody>
</table>

<p>I.e., it is a flat list of file metadata, name, and data. Note that there’s no index of files. Furthermore, this whole thing is compressed as a <a href="https://en.wikipedia.org/wiki/Solid_compression">“solid archive”</a>. As a result, to get a specific file, you have to check every entry until you find it, which also means decompressing everything up to that point.</p>

<p><a href="https://en.wikipedia.org/wiki/Deb_(file_format)">deb</a> files (used by Debian, Ubuntu, and others) are similar, although they use different formats: a deb file is actually an <a href="https://en.wikipedia.org/wiki/Ar_(Unix)">ar</a> archive that contains, among other things, a compressed <a href="https://en.wikipedia.org/wiki/Tar_(computing)">tar</a> archive. That compressed tar archive contains the files and their metadata. The exact cpio, ar, and tar formats are different, but they have the same overall structure.</p>

<h2 id="the-problem-with-linux-kernel-packages">The Problem With Linux Kernel Packages</h2>

<p>As noted earlier, debuginfod responds to queries by extracting the desired file from a package, which requires decompressing and checking every archive entry until the desired entry is found. Most packages only contain a handful of files, so this isn’t too expensive. For example, the git-core-debuginfo package on Fedora is about 15 MB and contains 9 files. In contrast, the kernel-debuginfo package is almost 1 GB and contains over 4000 files, including the main kernel image (vmlinux) and numerous loadable kernel modules. Extracting a file from the end of the kernel-debuginfo package can therefore take a long time: over a minute in my tests.</p>

<p>debuginfod does have a couple of optimizations to mitigate this: it caches recently used files so that they don’t need to be extracted again, and it prefetches a few extra entries from the same package in case they’re needed, too. However, systems usually have tens of kernel modules loaded, which vary greatly by hardware and workload. If even a few of them are uncached and toward the end of the archive, user experience suffers greatly.</p>

<h2 id="random-access-reading-in-xz">Random-Access Reading in xz</h2>

<p>If debuginfod could skip to the desired file in an archive instead of needing to decompress everything before it, then this would be much faster. Most compression formats (e.g., gzip, bzip2, and Zstandard) don’t support this: in order to decompress a specific byte, you normally need to decompress every byte before it first. But, there is one widely-used compression format that supports random access: <a href="https://en.wikipedia.org/wiki/XZ_Utils">xz</a>. And, even better, the kernel-debuginfo package in the Fedora/RHEL ecosystem and the linux-image-dbg package in the Debian/Ubuntu world are already compressed with xz!</p>

<p>Surprisingly, liblzma, the reference implementation of xz, doesn’t have a convenient API for random-access reading. To use it, we actually need to understand the <a href="https://tukaani.org/xz/format.html">.xz file format</a>.</p>

<p>Random access in xz works by splitting the input stream into multiple, independently compressed blocks:</p>

<table>
    <thead>
        <tr>
            <th colspan="6">Stream</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Stream Header</td>
            <td>Block 1</td>
            <td>Block 2</td>
            <td>...</td>
            <td>Index</td>
            <td>Stream Footer</td>
        </tr>
    </tbody>
</table>

<p>The compressed xz stream ends with an index of block records:</p>

<table>
    <thead>
        <tr>
            <th colspan="7">Index</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Index Indicator</td>
            <td>Number of Records</td>
            <td>Record 1</td>
            <td>Record 2</td>
            <td>...</td>
            <td>Index Padding</td>
            <td>CRC32</td>
        </tr>
    </tbody>
</table>

<p>Each record stores the compressed and uncompressed sizes of one block:</p>

<table>
    <thead>
        <tr>
            <th colspan="2">Record</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Compressed Size</td>
            <td>Uncompressed Size</td>
        </tr>
    </tbody>
</table>

<p>The offset of a block in the compressed stream is the sum of the compressed size of every block before it, and likewise for the uncompressed offset. To read starting from a specific uncompressed offset, you must:</p>

<ol>
  <li>Use the index to find the block containing the uncompressed offset.</li>
  <li>Seek to the block’s compressed offset in the file.</li>
  <li>Calculate the difference between the target offset and the block’s uncompressed offset.</li>
  <li>Decompress and throw away that many bytes.</li>
  <li>Decompress and return the remaining bytes.</li>
</ol>

<p>Note that this isn’t true random access: you still have to decompress some unneeded data. But, if the block size is reasonable, you can avoid a lot of unnecessary decompression. On a recent Fedora kernel-debuginfo RPM, the uncompressed block size, and thus the worst case amount of unnecessary decompression, is 12 MB, which is a couple of orders of magnitude smaller than the entire 1 GB file.</p>

<p>It’s also important to note that xz compression defaults to one block, i.e., no random access. However, multi-threaded compression necessarily splits the input into blocks to compress independently, so anything compressed with multi-threaded xz gets random access for free.</p>

<p>I developed a <a href="https://lore.kernel.org/linux-debuggers/cover.1721773977.git.osandov@fb.com/">patch series</a> for debuginfod to make use of random-access reading from xz archives when possible. It will be available in elfutils 0.192.</p>

<h2 id="future-work">Future Work</h2>

<p>The biggest downside of this approach is that it depends on a packaging implementation detail. The Fedora and Debian developers chose multi-threaded xz for its performance, not because they wanted a format that supports random access. At the very least, I plan to contact the package maintainers and document this so that debuginfod is considered before changing compression types.</p>

<p>Specifically, Zstandard compression, which does not support random access, is now becoming the norm. There is an <a href="https://github.com/facebook/zstd/blob/dev/contrib/seekable_format/zstd_seekable_compression_format.md">experimental seekable format</a>, but it is implemented in a separate library and is not guaranteed to be stable. It is <a href="https://github.com/facebook/zstd/issues/395#issuecomment-2048888278">not currently a priority</a> for the Zstandard developers because they haven’t found a compelling enough use case. Perhaps the debuginfod use case would qualify, but I haven’t done any measurements of how much of an improvement it would be, and the xz status quo is acceptable.</p>

<p>There is also the possibility of using an indexed, non-solid archive format in package files. <a href="https://en.wikipedia.org/wiki/ZIP_(file_format)">ZIP files</a>, for example, compress each file separately and have a “central directory” listing all files. However, formats like this compress less efficiently, and a lot of tooling expects the current archive formats. A good middle ground might be to keep the current formats but align xz block boundaries to files or groups of files over a certain size threshold.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This was an interesting problem slightly outside of my usual domain, so it was very satisfying to come up with a solution. The debugging experience keeps getting better thanks to new tools and infrastructure like debuginfod, but there’s still a lot of exciting work to do.</p>]]></content><author><name>Omar Sandoval</name></author><category term="debugging" /><summary type="html"><![CDATA[debuginfod is a service providing debugging symbols, source code, and executables via an HTTP API. Most Linux distributions run a debuginfod server, and many debugging tools make use of it automatically. debuginfod removes the need to manually install debugging symbols, which is a massive usability improvement for debuggers.]]></summary></entry></feed>