<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>nu1lptr - rllvm</title>
    <subtitle>Learner &#x2F;&#x2F; Cybersecurity Ph.D.@UMich CSE &#x2F;&#x2F; Photographer</subtitle>
    <link rel="self" type="application/atom+xml" href="https://shengtuo.me/tags/rllvm/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://shengtuo.me"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-21T18:00:00-07:00</updated>
    <id>https://shengtuo.me/tags/rllvm/atom.xml</id>
    <entry xml:lang="en">
        <title>rllvm: From Capture to Query</title>
        <published>2026-09-21T18:00:00-07:00</published>
        <updated>2026-09-21T18:00:00-07:00</updated>
        
        <author>
          <name>
            Shengtuo Hu
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shengtuo.me/blog/from-capture-to-query/"/>
        <id>https://shengtuo.me/blog/from-capture-to-query/</id>
        
        <content type="html" xml:base="https://shengtuo.me/blog/from-capture-to-query/">&lt;h2 id=&quot;tl-dr&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#tl-dr&quot; aria-label=&quot;Anchor link for: tl-dr&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
TL;DR&lt;&#x2F;h2&gt;
&lt;p&gt;The first two posts in this series were about producing artifacts: extracting
&lt;a href=&quot;https:&#x2F;&#x2F;shengtuo.me&#x2F;blog&#x2F;whole-program-bitcode&#x2F;&quot;&gt;whole-program bitcode&lt;&#x2F;a&gt;, then keeping the
modules and their provenance in a
&lt;a href=&quot;https:&#x2F;&#x2F;shengtuo.me&#x2F;blog&#x2F;beyond-single-bitcode-file&#x2F;&quot;&gt;catalog&lt;&#x2F;a&gt; instead of merging them away.
&lt;code&gt;rllvm-query&lt;&#x2F;code&gt; adds the step those were building toward. It reads a catalog and
answers questions in source terms: who defines this, who calls it, what does it
call, what path reaches it.&lt;&#x2F;p&gt;
&lt;p&gt;This post puts that step to work on a program written in two languages. The
example is CVE-2026-11941, a use-after-free in Cloudflare&#x27;s quiche that is made
entirely of safe Rust and still hands C a pointer to freed memory. The queries
locate it, scan the rest of the library for the same shape, and then run out of
road in a place worth being precise about.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-blind-spot-in-mixed-language-programs&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-blind-spot-in-mixed-language-programs&quot; aria-label=&quot;Anchor link for: a-blind-spot-in-mixed-language-programs&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
A blind spot in mixed-language programs&lt;&#x2F;h2&gt;
&lt;p&gt;Programs written in a single language are becoming the exception. A Rust crate
pulls in a &lt;code&gt;-sys&lt;&#x2F;code&gt; dependency and half of its real work happens in C. A C
codebase adopts Rust one module at a time.&lt;&#x2F;p&gt;
&lt;p&gt;The boundary between the two is the riskiest part of such a program. Ownership
stops being tracked there, error conventions stop matching, and who frees what
has to be written in a comment because no type carries it across. It is also the
one place that ordinary tooling does not look, for an understandable reason:
every ecosystem&#x27;s index is scoped to its own build. &lt;code&gt;rust-analyzer&lt;&#x2F;code&gt; stops at the
&lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt; declaration, because Cargo compiled nothing past it. &lt;code&gt;clangd&lt;&#x2F;code&gt; never
heard of the Rust caller. &lt;code&gt;cargo audit&lt;&#x2F;code&gt; reasons over the dependency graph, which
suits crates and says nothing about the BoringSSL that a build script compiled
along the way. &lt;code&gt;unsafe&lt;&#x2F;code&gt; marks where Rust stops trusting itself, and stays silent
about the other side.&lt;&#x2F;p&gt;
&lt;p&gt;So the questions that matter most at the boundary are the ones nobody can ask.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;getting-both-languages-into-one-module&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#getting-both-languages-into-one-module&quot; aria-label=&quot;Anchor link for: getting-both-languages-into-one-module&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Getting both languages into one module&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;rustc&lt;&#x2F;code&gt; and &lt;code&gt;clang&lt;&#x2F;code&gt; are both LLVM front ends. Once a call across the boundary
becomes an LLVM &lt;code&gt;call&lt;&#x2F;code&gt; instruction, the language has been erased: there is a
caller, a callee, and a source location for each. The boundary belongs to how we
build software rather than to the compiled program, so the whole job reduces to
capturing both halves at once. That used to take one environment variable. Now
it takes two.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;export &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CC&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-cc &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CXX&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-cxx &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;RUSTC_WRAPPER&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-rustc
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;tree&#x2F;main&#x2F;examples&#x2F;ffi&quot;&gt;&lt;code&gt;examples&#x2F;ffi&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;
directory holds the smallest version of this I could write: two programs whose
call graphs cross the boundary, one led from each language, because the gap in
the tooling runs both ways.&lt;&#x2F;p&gt;
&lt;figure class=&quot;ffi-fig ffi-crossings&quot; aria-label=&quot;Two small programs whose call graphs cross the FFI boundary, one led from Rust and one led from C&quot;&gt;
&lt;div class=&quot;ffi-group&quot;&gt;
&lt;span class=&quot;ffi-kicker&quot;&gt;Rust-led&lt;&#x2F;span&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span class=&quot;ffi-lang is-rust&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;main::main&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-arrow&quot;&gt;&amp;#9656;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-lang is-c&quot;&gt;C&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;c_double&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-arrow&quot;&gt;&amp;#9656;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-lang is-rust&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;rust_add&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-where&quot;&gt;main.rs:12&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;span class=&quot;ffi-lang is-rust&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;main::main&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-arrow&quot;&gt;&amp;#9656;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-lang is-cxx&quot;&gt;C++&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;cxx_triple&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-where&quot;&gt;main.rs:13&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-group&quot;&gt;
&lt;span class=&quot;ffi-kicker&quot;&gt;C-led&lt;&#x2F;span&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span class=&quot;ffi-lang is-c&quot;&gt;C&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;main&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-arrow&quot;&gt;&amp;#9656;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-lang is-rust&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;rust_scale&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-arrow&quot;&gt;&amp;#9656;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-lang is-c&quot;&gt;C&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-fn&quot;&gt;c_offset&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-where&quot;&gt;c_main.c:8&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;div&gt;
&lt;figcaption&gt;One module holds all three languages, and each crossing is reported at the line that makes the call. Direction matters more than it looks: a Rust staticlib drags in a prebuilt &lt;b&gt;std&lt;&#x2F;b&gt; that never went through the wrapper, so whichever language leads decides how much of the program you can see.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;a-real-bug-where-the-languages-meet&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-real-bug-where-the-languages-meet&quot; aria-label=&quot;Anchor link for: a-real-bug-where-the-languages-meet&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
A real bug where the languages meet&lt;&#x2F;h2&gt;
&lt;p&gt;quiche is Cloudflare&#x27;s QUIC and HTTP&#x2F;3 library: a Cargo workspace that also
builds BoringSSL through a build script and exposes a C API behind a feature
flag. In June it published
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cloudflare&#x2F;quiche&#x2F;security&#x2F;advisories&#x2F;GHSA-mh64-ph39-mrc9&quot;&gt;GHSA-mh64-ph39-mrc9&lt;&#x2F;a&gt;,
CVE-2026-11941, a use-after-free in two of its FFI functions, fixed in 0.29.2.&lt;&#x2F;p&gt;
&lt;p&gt;The entire defect, from &lt;code&gt;quiche&#x2F;src&#x2F;ffi.rs&lt;&#x2F;code&gt; at 0.29.1:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;impl&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;&amp;#39;a&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; Iterator &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;for &lt;&#x2F;span&gt;&lt;span&gt;ConnectionIdIter&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;&amp;#39;a&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;type &lt;&#x2F;span&gt;&lt;span&gt;Item &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;ConnectionId&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;&amp;#39;a&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;fn &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a2ba43;&quot;&gt;next&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;mut &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;) -&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e7e7e7;&quot;&gt;Option&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;Self::&lt;&#x2F;span&gt;&lt;span&gt;Item&amp;gt; {
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; v &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;.cids.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;.index)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;?&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;.index &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;+= 1&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e7e7e7;&quot;&gt;Some&lt;&#x2F;span&gt;&lt;span&gt;(v.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;clone&lt;&#x2F;span&gt;&lt;span&gt;())            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;&#x2F;&#x2F; an owned ConnectionId
&lt;&#x2F;span&gt;&lt;span&gt;    }
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;#[&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;no_mangle&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;pub &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;extern &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;C&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;fn &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a2ba43;&quot;&gt;quiche_connection_id_iter_next&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;iter&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;mut&lt;&#x2F;span&gt;&lt;span&gt; ConnectionIdIter,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;out&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;mut *const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;u8&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;out_len&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;mut&lt;&#x2F;span&gt;&lt;span&gt; size_t,
&lt;&#x2F;span&gt;&lt;span&gt;) -&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;bool &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e7e7e7;&quot;&gt;Some&lt;&#x2F;span&gt;&lt;span&gt;(conn_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; iter.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;next&lt;&#x2F;span&gt;&lt;span&gt;() {   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;&#x2F;&#x2F; owned; dropped below
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3cbe3;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; id &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; conn_id.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;as_ref&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;out &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; id.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;as_ptr&lt;&#x2F;span&gt;&lt;span&gt;();                &lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;&#x2F;&#x2F; escapes to C
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;out_len &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; id.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;len&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    }                                      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;&#x2F;&#x2F; conn_id freed here
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;false
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There is no &lt;code&gt;unsafe&lt;&#x2F;code&gt; block. The borrow checker is satisfied and it is right to
be: &lt;code&gt;conn_id&lt;&#x2F;code&gt; is an owned value, dropped correctly at the end of its scope.
Every guarantee Rust makes holds.&lt;&#x2F;p&gt;
&lt;p&gt;What goes wrong is that a raw pointer into that value crossed an &lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;
signature on the way out, and C reads it after the drop. Rust&#x27;s ownership model
ends at the signature, and both halves of this program are correct on their own.&lt;&#x2F;p&gt;
&lt;figure class=&quot;ffi-fig&quot; aria-label=&quot;A timeline showing the ConnectionId being cloned, its pointer escaping to C, the value being dropped, and C reading the freed memory&quot;&gt;
&lt;div class=&quot;ffi-life&quot;&gt;
&lt;div class=&quot;ffi-step is-alive&quot;&gt;&lt;span class=&quot;ffi-side&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-what&quot;&gt;clone a &lt;b&gt;ConnectionId&lt;&#x2F;b&gt; out of the iterator&lt;span class=&quot;ffi-at&quot;&gt;ffi.rs:1157&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-bar&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-step is-alive&quot;&gt;&lt;span class=&quot;ffi-side&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-what&quot;&gt;*out = id.as_ptr(), the pointer escapes&lt;span class=&quot;ffi-at&quot;&gt;ffi.rs:1158&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-bar&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-step is-dead&quot;&gt;&lt;span class=&quot;ffi-side&quot;&gt;Rust&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-what&quot;&gt;drop_glue::&amp;lt;ConnectionId&amp;gt;, the buffer is freed&lt;span class=&quot;ffi-at&quot;&gt;ffi.rs:1162&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-bar&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-return&quot;&gt;return to C, and out still holds the pointer&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-step is-dead is-fault&quot;&gt;&lt;span class=&quot;ffi-side&quot;&gt;C&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-what&quot;&gt;fprintf(&quot;%02x&quot;, cid[i])&lt;span class=&quot;ffi-at&quot;&gt;cid_logger.c:21&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-bar&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;figcaption&gt;The solid bar is the value&#x27;s lifetime; the dashed continuation is the pointer outliving it. Only the join between the two halves is wrong, and the join is what neither language&#x27;s tooling reads.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;asking-rllvm-query-about-it&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#asking-rllvm-query-about-it&quot; aria-label=&quot;Anchor link for: asking-rllvm-query-about-it&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Asking rllvm-query about it&lt;&#x2F;h2&gt;
&lt;p&gt;What follows runs against quiche 0.29.1 with the FFI feature on, plus a small C
program that uses the iterator the way the advisory describes.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span&gt; clone https:&#x2F;&#x2F;github.com&#x2F;cloudflare&#x2F;quiche &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;amp;&amp;amp; cd&lt;&#x2F;span&gt;&lt;span&gt; quiche
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span&gt; checkout 0.29.1
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;export &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;RUSTC_WRAPPER&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-rustc &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CC&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-cc
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span&gt; build&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; -p&lt;&#x2F;span&gt;&lt;span&gt; quiche&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --features&lt;&#x2F;span&gt;&lt;span&gt; ffi
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-cc&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; -g -Iquiche&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;include cid_logger.c \
&lt;&#x2F;span&gt;&lt;span&gt;    target&#x2F;debug&#x2F;libquiche.a&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; -o&lt;&#x2F;span&gt;&lt;span&gt; cid_logger
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-get-bc&lt;&#x2F;span&gt;&lt;span&gt; cid_logger&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --output-dir&lt;&#x2F;span&gt;&lt;span&gt; cat
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The binary now records both halves. Every answer below is JSON; I have reduced
each one to the fields under discussion, and shortened Rust crate paths to fit.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CAT&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;cat&#x2F;catalog.json
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;FN&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;quiche_connection_id_iter_next
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The definition is Rust, and the caller is C, each reported at its own line:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --catalog &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CAT&lt;&#x2F;span&gt;&lt;span&gt; defs &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;FN
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   quiche&#x2F;src&#x2F;ffi.rs:1154
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --catalog &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CAT&lt;&#x2F;span&gt;&lt;span&gt; callers &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;FN
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   log_source_ids   at cid_logger.c:19
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;callees&lt;&#x2F;code&gt; on the FFI function is the bug in three lines. Rust symbols come back
demangled, so a cross-language answer reads as one answer:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --catalog &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CAT&lt;&#x2F;span&gt;&lt;span&gt; callees &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;FN
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   ffi.rs:1157  &amp;lt;ConnectionIdIter as Iterator&amp;gt;::next
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   ffi.rs:1158  &amp;lt;ConnectionId as AsRef&amp;lt;[u8]&amp;gt;&amp;gt;::as_ref
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   ffi.rs:1162  core::ptr::drop_glue::&amp;lt;ConnectionId&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Clone, take a pointer, free. Run the same query against 0.29.2 and the
&lt;code&gt;drop_glue&lt;&#x2F;code&gt; line is gone, because the fix stopped owning anything:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   ffi.rs:1146  &amp;lt;Vec&amp;lt;ConnectionId&amp;gt; as Deref&amp;gt;::deref
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   ffi.rs:1146  &amp;lt;[ConnectionId]&amp;gt;::get::&amp;lt;usize&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;#   ffi.rs:1147  &amp;lt;ConnectionId as AsRef&amp;lt;[u8]&amp;gt;&amp;gt;::as_ref
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;reach&lt;&#x2F;code&gt; walks from the C entry point to the Rust function, and records the
crossing as its own kind of step:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span&gt;[
&lt;&#x2F;span&gt;&lt;span&gt;  { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;call&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;symbol&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;main&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;},
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;block_index&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;instruction_index&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;1 &lt;&#x2F;span&gt;&lt;span&gt;},
&lt;&#x2F;span&gt;&lt;span&gt;  { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;call&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;symbol&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;log_source_ids&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;},
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;block_index&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;instruction_index&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;1 &lt;&#x2F;span&gt;&lt;span&gt;},
&lt;&#x2F;span&gt;&lt;span&gt;  { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;binding&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;symbol&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;quiche_connection_id_iter_next&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;declared_in&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [ &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;e98486…&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;candidates&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [ { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: { &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;module_id&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;00a3d4…&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;} } ],
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;unique&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Two ordinary calls inside C, then a &lt;code&gt;binding&lt;&#x2F;code&gt;: a C declaration that resolved to
one candidate definition in a Rust module. The answer distinguishes a call it
observed from a link it resolved, and says which one carries the path.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;scanning-the-whole-ffi-surface&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#scanning-the-whole-ffi-surface&quot; aria-label=&quot;Anchor link for: scanning-the-whole-ffi-surface&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Scanning the whole FFI surface&lt;&#x2F;h2&gt;
&lt;p&gt;Everything above starts from knowing the answer. The more useful question is
whether the bug has a shape you can search for, stated without reference to the
CVE:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;An &lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt; entry point hands C a raw pointer into a value, and then drops
that value before returning.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;In call-graph terms that is two conditions on one function&#x27;s callees: something
that takes a pointer into &lt;code&gt;T&lt;&#x2F;code&gt;, and &lt;code&gt;drop_glue::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;. Both already appear in the
output, so the scan is a loop.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;nm&lt;&#x2F;span&gt;&lt;span&gt; target&#x2F;debug&#x2F;libquiche.a \
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;awk &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;$2==&amp;quot;T&amp;quot; &amp;amp;&amp;amp; $3 ~ &#x2F;^_quiche_&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;sort&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; -u &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; surface.txt
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;wc&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; -l &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt; surface.txt        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;# 169 entry points
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; fn &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;in &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span&gt; surface.txt)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;; do
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --catalog &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CAT&lt;&#x2F;span&gt;&lt;span&gt; callees &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;fn&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#dbbb3d;&quot;&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;done
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;# keep any whose callees hold both drop_glue::&amp;lt;T&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;# and something that takes a pointer into T
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The leading underscore is Mach-O&#x27;s prefix on C symbols; an ELF build wants
&lt;code&gt;&#x2F;^quiche_&#x2F;&lt;&#x2F;code&gt; instead. That platform dependency, and the fact that this step
reaches for &lt;code&gt;nm&lt;&#x2F;code&gt; at all, is a gap: the catalog already knows which functions a
Rust module exported to C, so &lt;code&gt;rllvm-query&lt;&#x2F;code&gt; should be able to list them itself
(&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;issues&#x2F;243&quot;&gt;#243&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;169 entry points reduce to 7 candidates, and both functions the advisory names
are among them:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Candidate&lt;&#x2F;th&gt;&lt;th&gt;Reading it&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_connection_id_iter_next&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CVE-2026-11941&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_conn_retired_scid_next&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CVE-2026-11941&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_conn_source_id&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;safe: the dropped &lt;code&gt;ConnectionId&lt;&#x2F;code&gt; is the borrowed variant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_conn_destination_id&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;safe: the same&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_accept&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;filter noise: the match was &lt;code&gt;Option::as_ref&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_conn_new_with_tls&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;filter noise: the same&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quiche_h3_take_last_priority_update&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;safe &lt;em&gt;if&lt;&#x2F;em&gt; the C callback behaves&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;One caveat on how much this proves. I wrote the filter after reading the
advisory. The shape is general, since handing C a pointer into something you
then drop is a whole class of FFI bug. This run still does not establish that I
would have picked it blind. What it does establish is that the class fits in two
predicates over ordinary output, that it ranks both real defects into seven of a
hundred and sixty-nine, and that the signal disappears when the bug is fixed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;does-it-affect-my-build&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#does-it-affect-my-build&quot; aria-label=&quot;Anchor link for: does-it-affect-my-build&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Does it affect my build?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;cargo audit&lt;&#x2F;code&gt; gives one answer for any quiche below 0.29.2: vulnerable. The
advisory is more careful, and names the thing that actually decides it.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only applications using those FFI functions are affected. The FFI API is
disabled by default by a build-time feature flag.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;That is a whole-program question, with four different true answers depending on
how the program was built.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Build&lt;&#x2F;th&gt;&lt;th&gt;Definition present&lt;&#x2F;th&gt;&lt;th&gt;Callers&lt;&#x2F;th&gt;&lt;th&gt;What is true&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;default features&lt;&#x2F;td&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;n&#x2F;a&lt;&#x2F;td&gt;&lt;td&gt;the code was never compiled&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--features ffi&lt;&#x2F;code&gt;, quiche&#x27;s own &lt;code&gt;client.c&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;linked in, never called&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--features ffi&lt;&#x2F;code&gt;, a C app that iterates CIDs&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;reachable&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0.29.2, same C app&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;fixed&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The second row is worth sitting with. &lt;code&gt;nm&lt;&#x2F;code&gt; confirms
&lt;code&gt;_quiche_connection_id_iter_next&lt;&#x2F;code&gt; is in that binary as a defined symbol, and
&lt;code&gt;callers&lt;&#x2F;code&gt; still returns zero. Present and reachable are different facts, and
only one of them describes your risk.&lt;&#x2F;p&gt;
&lt;p&gt;For a caller that does exist, &lt;code&gt;callees&lt;&#x2F;code&gt; on it gives the order of operations, so
you can see what happens after the crossing:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;&lt;span&gt;blk 0 #7   line 14   quiche_conn_source_ids
&lt;&#x2F;span&gt;&lt;span&gt;blk 1 #1   line 19   quiche_connection_id_iter_next   &amp;lt;- the crossing
&lt;&#x2F;span&gt;&lt;span&gt;blk 4 #6   line 21   fprintf                          &amp;lt;- after it
&lt;&#x2F;span&gt;&lt;span&gt;blk 6 #1   line 23   fprintf                          &amp;lt;- after it
&lt;&#x2F;span&gt;&lt;span&gt;blk 7 #1   line 26   quiche_connection_id_iter_free
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Anything sequenced after the crossing is a candidate consumer of a pointer that
Rust has already freed. The two &lt;code&gt;fprintf&lt;&#x2F;code&gt; calls are a three-line reading window.&lt;&#x2F;p&gt;
&lt;figure class=&quot;ffi-fig ffi-funnel&quot; aria-label=&quot;Each step narrows the search: 169 FFI entry points, 7 candidates, 1 caller, 3 lines of C to read&quot;&gt;
&lt;div class=&quot;ffi-rung&quot;&gt;&lt;span class=&quot;ffi-count&quot;&gt;169&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-track&quot;&gt;&lt;span class=&quot;ffi-fill&quot; style=&quot;--w:100%&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-label&quot;&gt;&lt;b&gt;FFI entry points&lt;&#x2F;b&gt;, listed by nm&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-rung&quot;&gt;&lt;span class=&quot;ffi-count&quot;&gt;7&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-track&quot;&gt;&lt;span class=&quot;ffi-fill&quot; style=&quot;--w:34%&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-label&quot;&gt;&lt;b&gt;candidates&lt;&#x2F;b&gt;: callees take a pointer into T and drop T&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-rung&quot;&gt;&lt;span class=&quot;ffi-count&quot;&gt;1&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-track&quot;&gt;&lt;span class=&quot;ffi-fill&quot; style=&quot;--w:16%&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-label&quot;&gt;&lt;b&gt;caller&lt;&#x2F;b&gt;, with a file and a line&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;ffi-rung&quot;&gt;&lt;span class=&quot;ffi-count&quot;&gt;3&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-track&quot;&gt;&lt;span class=&quot;ffi-fill&quot; style=&quot;--w:7%&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;ffi-label&quot;&gt;&lt;b&gt;lines of C&lt;&#x2F;b&gt; left to read&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;figcaption&gt;The bars are illustrative rather than to scale. Every step is mechanical except the last one, which is the point: the tool decides where to look and a person decides what is there.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;where-the-answers-stop&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#where-the-answers-stop&quot; aria-label=&quot;Anchor link for: where-the-answers-stop&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Where the answers stop&lt;&#x2F;h2&gt;
&lt;p&gt;Five of those seven candidates are safe, for two different kinds of reason.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ConnectionId&lt;&#x2F;code&gt; is a Cow-like type: &lt;code&gt;enum { Vec(Vec&amp;lt;u8&amp;gt;), Ref(&amp;amp;&#x27;a [u8]) }&lt;&#x2F;code&gt;.
&lt;code&gt;quiche_conn_source_id&lt;&#x2F;code&gt; drops one, but it drops the &lt;em&gt;borrowed&lt;&#x2F;em&gt; variant, so the
drop frees nothing and the pointer stays valid. The call graph sees
&lt;code&gt;drop_glue::&amp;lt;ConnectionId&amp;gt;&lt;&#x2F;code&gt; either way and cannot separate the two.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;quiche_accept&lt;&#x2F;code&gt; and &lt;code&gt;quiche_conn_new_with_tls&lt;&#x2F;code&gt; are the cheaper kind of wrong.
Both call &lt;code&gt;Option::as_ref&lt;&#x2F;code&gt;, a borrow helper that hands out no raw pointer at
all, and my filter matched on the method name. Neither function gives C a
pointer into anything it later drops, so the pattern never applied to them.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;quiche_h3_take_last_priority_update&lt;&#x2F;code&gt; is more interesting. It takes a pointer
into an owned &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;&#x2F;code&gt;, passes it to a callback supplied by C, and frees the
buffer when the callback returns. Whether that is safe depends on what the C
callback does with the pointer, and the contract lives in a header comment that
nothing enforces. I called it safe above by reading the Rust and assuming the C
behaves. That was an assumption rather than an analysis.&lt;&#x2F;p&gt;
&lt;p&gt;The Cow case and the callback case ask the same question: does this pointer
outlive the value behind it? &lt;code&gt;rllvm-query&lt;&#x2F;code&gt; does not perform data-flow analysis,
so it cannot answer either.
The same limit shows up at indirect calls, where &lt;code&gt;indirect-targets&lt;&#x2F;code&gt; reports a
target set when LLVM recorded one and reports the callback above as unresolved.
Tracking a value through a store, which is how the C side would retain that
pointer, is outside what a call graph describes at all.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-comes-next&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-comes-next&quot; aria-label=&quot;Anchor link for: what-comes-next&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What comes next&lt;&#x2F;h2&gt;
&lt;p&gt;Two things, in the order I care about them.&lt;&#x2F;p&gt;
&lt;p&gt;The queries in this post form a short procedure: is the function here, does
anything call it, what happens after the crossing. Two of those steps can end
the investigation early. That belongs in tooling rather than in a blog post.
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;issues&#x2F;188&quot;&gt;Issue #188&lt;&#x2F;a&gt; is a Claude Code
plugin bundling the MCP server with two skills: how to capture bitcode for the
build in front of you, and how to read an answer without over-claiming it. An
assistant reading source approximates a call graph by matching names, and across
an FFI boundary it cannot even approximate. The early exits matter most, because
&quot;the version range matches, therefore you are affected&quot; is the failure an agent
would otherwise reproduce at scale.&lt;&#x2F;p&gt;
&lt;p&gt;The second is the data flow this post kept needing. I would rather borrow that
than build it. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;secure-software-engineering&#x2F;phasar&quot;&gt;PhASAR&lt;&#x2F;a&gt;
and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SVF-tools&#x2F;SVF&quot;&gt;SVF&lt;&#x2F;a&gt; have solved pointer and value
analysis for years, and have always taken whole-program C and C++ bitcode.
Getting that file out of a real build was the awkward step they were waiting on.
A module with Rust in it too is new input for tools that already work. Running
them behind the same query contract
(&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;issues&#x2F;149&quot;&gt;#149&lt;&#x2F;a&gt;,
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;issues&#x2F;242&quot;&gt;#242&lt;&#x2F;a&gt;) is what would separate a
borrowed &lt;code&gt;ConnectionId&lt;&#x2F;code&gt; from an owned one, and say whether a pointer escapes
past a drop. The output should stay evidence: sources, sinks, and the path. A
tool that reports &quot;vulnerable&quot; without showing its work has only moved the
guessing somewhere else.&lt;&#x2F;p&gt;
&lt;p&gt;Indirect calls remain the hard problem they have always been. I do not expect
that to change soon, and the honest move is to keep saying so in the answer.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;rllvm&lt;&#x2F;code&gt; is Apache-2.0 and available on
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&quot;&gt;GitHub&lt;&#x2F;a&gt;, including the
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;tree&#x2F;main&#x2F;examples&#x2F;ffi&quot;&gt;FFI example&lt;&#x2F;a&gt; and
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;tree&#x2F;main&#x2F;examples&#x2F;external&#x2F;quiche&quot;&gt;notes on quiche&lt;&#x2F;a&gt;
used here. Measurements were taken on arm64 macOS against quiche 0.29.1 and
0.29.2, built with rustc 1.98.0 and read with LLVM 23.1.1.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Beyond a Single Bitcode File</title>
        <published>2026-09-13T18:00:00-07:00</published>
        <updated>2026-09-13T18:00:00-07:00</updated>
        
        <author>
          <name>
            Shengtuo Hu
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shengtuo.me/blog/beyond-single-bitcode-file/"/>
        <id>https://shengtuo.me/blog/beyond-single-bitcode-file/</id>
        
        <content type="html" xml:base="https://shengtuo.me/blog/beyond-single-bitcode-file/">&lt;h2 id=&quot;tl-dr&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#tl-dr&quot; aria-label=&quot;Anchor link for: tl-dr&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
TL;DR&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;rllvm&lt;&#x2F;code&gt; used to produce one answer: a merged whole-program &lt;code&gt;.bc&lt;&#x2F;code&gt; file. Version
0.5 can instead inventory the modules first, keep their identities and known
provenance in a catalog, select the ones an analysis needs, and only then copy,
archive, or merge them. It can acquire those modules from a wrapped build or
regenerate selected C and C++ compilations from &lt;code&gt;compile_commands.json&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The original wrapper workflow still works. It now sits inside a larger model:
&lt;strong&gt;acquire, catalog, select, materialize&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-bc-file-was-only-the-first-output&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-bc-file-was-only-the-first-output&quot; aria-label=&quot;Anchor link for: the-bc-file-was-only-the-first-output&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
The &lt;code&gt;.bc&lt;&#x2F;code&gt; file was only the first output&lt;&#x2F;h2&gt;
&lt;p&gt;In &lt;a href=&quot;https:&#x2F;&#x2F;shengtuo.me&#x2F;blog&#x2F;whole-program-bitcode&#x2F;&quot;&gt;my previous post&lt;&#x2F;a&gt;, I described &lt;code&gt;rllvm&lt;&#x2F;code&gt; as the
third tool in the &lt;code&gt;wllvm&lt;&#x2F;code&gt; and &lt;code&gt;gllvm&lt;&#x2F;code&gt; lineage: replace the compiler, build
normally, then recover one LLVM module from the linked program. I also wrote that
there was little urgency to switch if either older tool already worked for you.&lt;&#x2F;p&gt;
&lt;p&gt;That remains fair for the narrow job of producing one &lt;code&gt;.bc&lt;&#x2F;code&gt; file. It no longer
describes the whole project.&lt;&#x2F;p&gt;
&lt;p&gt;A merged module is useful, but merging is a poor first step. It collapses module
boundaries before a consumer can decide what belongs in an analysis. It also
hides useful distinctions: two compilations of the same source can use different
flags, a missing module is different from an unsupported one, and a module found
in an executable says something different from one regenerated from a compilation
database.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;rllvm&lt;&#x2F;code&gt; 0.5 makes the unmerged modules addressable. The whole-program file becomes
one possible output rather than the only interface.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;modules-before-merging&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#modules-before-merging&quot; aria-label=&quot;Anchor link for: modules-before-merging&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Modules before merging&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;rllvm-info --json&lt;&#x2F;code&gt; now inventories bitcode, objects, executables, regular
archives, and existing catalogs without merging anything:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-info&lt;&#x2F;span&gt;&lt;span&gt; app&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --json &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; catalog.json
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Each catalog entry has an opaque module ID and a content hash. Where available,
it also records source associations, target and data layout, compilation
configuration, debug-information presence, and diagnostics. A module can be
&lt;code&gt;available&lt;&#x2F;code&gt;, &lt;code&gt;missing&lt;&#x2F;code&gt;, &lt;code&gt;unsupported&lt;&#x2F;code&gt;, or &lt;code&gt;failed&lt;&#x2F;code&gt;; failure does not erase the
other evidence.&lt;&#x2F;p&gt;
&lt;p&gt;The catalog is deliberately careful about what it does not know. Finding a
source filename in debug information does not prove which compiler produced it.
Finding several modules in an archive does not prove that they form a complete
program. Those fields stay empty, and whole-program completeness stays unknown.&lt;&#x2F;p&gt;
&lt;figure class=&quot;substrate-fig substrate-acquire&quot; aria-label=&quot;Two acquisition paths feed a module catalog: a wrapped build provides link membership, while a compilation database provides compilation configurations from the current source tree&quot;&gt;
&lt;div class=&quot;substrate-sources&quot;&gt;
&lt;div class=&quot;substrate-card&quot;&gt;
&lt;span class=&quot;substrate-kicker&quot;&gt;live build&lt;&#x2F;span&gt;
&lt;strong&gt;object · archive · binary&lt;&#x2F;strong&gt;
&lt;small&gt;which modules reached the link&lt;&#x2F;small&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;substrate-card&quot;&gt;
&lt;span class=&quot;substrate-kicker&quot;&gt;recorded build&lt;&#x2F;span&gt;
&lt;strong&gt;compile_commands.json&lt;&#x2F;strong&gt;
&lt;small&gt;how each source was compiled&lt;&#x2F;small&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;substrate-down&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;&#x2F;span&gt;&lt;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;substrate-catalog&quot;&gt;
&lt;span class=&quot;substrate-kicker&quot;&gt;shared representation&lt;&#x2F;span&gt;
&lt;strong&gt;module catalog&lt;&#x2F;strong&gt;
&lt;small&gt;identity · integrity · provenance · uncertainty&lt;&#x2F;small&gt;
&lt;&#x2F;div&gt;
&lt;figcaption&gt;Both paths produce modules and the same catalog format, but they provide different evidence.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;two-kinds-of-build-evidence&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#two-kinds-of-build-evidence&quot; aria-label=&quot;Anchor link for: two-kinds-of-build-evidence&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Two kinds of build evidence&lt;&#x2F;h2&gt;
&lt;p&gt;The wrapper observes a real build. A path recorded in the finished executable is
evidence that the corresponding object participated in that link. This answers
the whole-program question, but the old path section does not record a complete
compiler identity, environment, or source snapshot.&lt;&#x2F;p&gt;
&lt;p&gt;A compilation database answers a different question. It preserves one command
per translation unit, so &lt;code&gt;rllvm-compdb&lt;&#x2F;code&gt; can list those entries and regenerate only
the current-tree modules an analysis asks for:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-compdb&lt;&#x2F;span&gt;&lt;span&gt; list build&#x2F; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; compilations.json
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-compdb&lt;&#x2F;span&gt;&lt;span&gt; generate build&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --source&lt;&#x2F;span&gt;&lt;span&gt; src&#x2F;parser.c \
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt;  --extra-arg&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;-O0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --output-dir&lt;&#x2F;span&gt;&lt;span&gt; analysis&#x2F;parser
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The importer keeps duplicate compilations distinct, records the original and
effective arguments, and puts failures beside successful modules. It does not
pretend to reconstruct the historical build. Generated headers must exist now,
the original environment is unknown, and a compile command says nothing about
which executable eventually used its object.&lt;&#x2F;p&gt;
&lt;p&gt;This distinction matters for analysis. Use wrapper capture when linked membership
is the fact you need. Use the compilation database when you want selected,
repeatable analysis compilations from the current tree.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;select-then-materialize&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#select-then-materialize&quot; aria-label=&quot;Anchor link for: select-then-materialize&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Select, then materialize&lt;&#x2F;h2&gt;
&lt;p&gt;Catalogs can be filtered by module ID, source, or known configuration. Alternatives
within one selector are combined; different selector types intersect. An unmatched
selector is an error rather than an empty success.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-get-bc&lt;&#x2F;span&gt;&lt;span&gt; app&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --source&lt;&#x2F;span&gt;&lt;span&gt; src&#x2F;parser.c&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; --output-dir&lt;&#x2F;span&gt;&lt;span&gt; analysis&#x2F;selected
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-get-bc&lt;&#x2F;span&gt;&lt;span&gt; analysis&#x2F;selected&#x2F;catalog.json&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffffff;&quot;&gt; -o&lt;&#x2F;span&gt;&lt;span&gt; parser.bc
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The first command copies separate, hash-checked modules into a new directory and
writes the catalog last. Its module paths are relative, so the directory can move
as a unit. The second command chooses to merge that collection. Archive and partial
merge strategies remain available when one large link is the wrong shape.&lt;&#x2F;p&gt;
&lt;figure class=&quot;substrate-fig substrate-materialize&quot; aria-label=&quot;A module catalog is filtered before three possible outputs: a relocatable collection, a bitcode archive, or merged whole-program bitcode&quot;&gt;
&lt;div class=&quot;substrate-modules&quot; aria-label=&quot;catalog modules&quot;&gt;
&lt;span&gt;&lt;b&gt;01&lt;&#x2F;b&gt; parse.c &lt;i&gt;debug&lt;&#x2F;i&gt;&lt;&#x2F;span&gt;
&lt;span&gt;&lt;b&gt;02&lt;&#x2F;b&gt; parse.c &lt;i&gt;release&lt;&#x2F;i&gt;&lt;&#x2F;span&gt;
&lt;span&gt;&lt;b&gt;03&lt;&#x2F;b&gt; lexer.c &lt;i&gt;release&lt;&#x2F;i&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;substrate-dim&quot;&gt;&lt;b&gt;04&lt;&#x2F;b&gt; plugin.c &lt;i&gt;missing&lt;&#x2F;i&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;substrate-filter&quot;&gt;
&lt;span class=&quot;substrate-kicker&quot;&gt;selection&lt;&#x2F;span&gt;
&lt;strong&gt;source + configuration&lt;&#x2F;strong&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;substrate-outputs&quot;&gt;
&lt;span&gt;relocatable&lt;br&gt;&lt;b&gt;collection&lt;&#x2F;b&gt;&lt;&#x2F;span&gt;
&lt;span&gt;bitcode&lt;br&gt;&lt;b&gt;archive&lt;&#x2F;b&gt;&lt;&#x2F;span&gt;
&lt;span&gt;merged&lt;br&gt;&lt;b&gt;.bc&lt;&#x2F;b&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;div&gt;
&lt;figcaption&gt;Selection preserves the useful boundary. Materialization can remain separate or merge only the chosen modules.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;real-builds-are-mostly-edge-cases&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#real-builds-are-mostly-edge-cases&quot; aria-label=&quot;Anchor link for: real-builds-are-mostly-edge-cases&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Real builds are mostly edge cases&lt;&#x2F;h2&gt;
&lt;p&gt;The high-level model works only if acquisition survives actual compiler command
lines. Recent work therefore looks less glamorous and matters just as much.&lt;&#x2F;p&gt;
&lt;p&gt;Response files are expanded for classification while the real compiler still
receives the original arguments. Nested files, quoting, escapes, byte-order marks,
and relative paths follow Clang&#x27;s rules. Commands generated by &lt;code&gt;rllvm&lt;&#x2F;code&gt; switch to
response files automatically when the operating system&#x27;s argument limit would be
too small.&lt;&#x2F;p&gt;
&lt;p&gt;LTO needs separate handling because its “object” may already be bitcode. The
default marker mode covers full and ThinLTO on ELF and Mach-O; save-temps mode can
capture a full-LTO linker&#x27;s optimized module. Rust needs crate-aware markers and
archive updates. Cross-target builds need architecture and sysroot choices to
survive secondary compilations.&lt;&#x2F;p&gt;
&lt;p&gt;eBPF was the pleasant counterexample. An eBPF object built through &lt;code&gt;rllvm-cc&lt;&#x2F;code&gt;
already carried the ordinary &lt;code&gt;.rllvm_bc&lt;&#x2F;code&gt; section. &lt;code&gt;libbpf&lt;&#x2F;code&gt; ignored that section
when loading the program and preserved it when linking objects. Supporting the
target required an integration test and documentation, not a new production
backend. A mechanism that composes with a tool it was not written for is a useful
kind of validation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;measuring-the-tax&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#measuring-the-tax&quot; aria-label=&quot;Anchor link for: measuring-the-tax&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Measuring the tax&lt;&#x2F;h2&gt;
&lt;p&gt;Compiler wrappers are not free. C and C++ normally compile twice: once for the
native object and once for analysis bitcode. &lt;code&gt;rllvm&lt;&#x2F;code&gt; now has a reproducible
workflow harness that validates native behavior and extracted IR before accepting
a timing sample.&lt;&#x2F;p&gt;
&lt;p&gt;The first recorded baseline used an Apple M4, LLVM 22.1.8, eight build jobs, and
three repetitions. Clean build-only time relative to native compilation was:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Workload&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Cache off&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Primed cache&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;nghttp2 C&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.84×&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.61×&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;nghttp2 C++&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.83×&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.20×&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Quiche&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.59×&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.50×&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The cache avoids repeated C&#x2F;C++ bitcode compilation, but still preprocesses and
hashes current inputs before declaring a hit. It does not cache Rust compilation.
The measurements also had an uncontrolled filesystem cache and normal desktop
activity, so they are evidence for these workloads rather than a universal ratio.
They compare &lt;code&gt;rllvm&lt;&#x2F;code&gt; with native builds, not with &lt;code&gt;gllvm&lt;&#x2F;code&gt; or &lt;code&gt;wllvm&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-lineage-now-has-a-fork&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-lineage-now-has-a-fork&quot; aria-label=&quot;Anchor link for: the-lineage-now-has-a-fork&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
The lineage now has a fork&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;wllvm&lt;&#x2F;code&gt;, &lt;code&gt;gllvm&lt;&#x2F;code&gt;, and &lt;code&gt;rllvm&lt;&#x2F;code&gt; still share the same good trick: record sidecar
bitcode paths in object files and let the linker accumulate them. The older tools
center their interface on extracting a merged module or archive. &lt;code&gt;rllvm&lt;&#x2F;code&gt; now keeps
that workflow while adding another acquisition path and a representation between
capture and merge.&lt;&#x2F;p&gt;
&lt;p&gt;That does not make it a strict superset. &lt;code&gt;gllvm&lt;&#x2F;code&gt; supports Fortran and thin
archives; &lt;code&gt;wllvm&lt;&#x2F;code&gt; can drive GCC through dragonegg. But for Clang-based program
analysis, &lt;code&gt;rllvm&lt;&#x2F;code&gt; now exposes a much larger surface: Rust, WebAssembly and eBPF,
real LTO extraction, relocatable paths, validated caching, module catalogs,
selection, and compilation-database imports.&lt;&#x2F;p&gt;
&lt;p&gt;The meaningful difference is no longer implementation language or wrapper speed.
It is where the tool stops. The older workflow stops when it produces bitcode.
&lt;code&gt;rllvm&lt;&#x2F;code&gt; is starting to preserve the structure that the next tool will need.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-comes-next&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-comes-next&quot; aria-label=&quot;Anchor link for: what-comes-next&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What comes next&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;shengtuo.me&#x2F;blog&#x2F;whole-program-bitcode&#x2F;#what-comes-next&quot;&gt;My previous post&lt;&#x2F;a&gt; ended with a
query interface for coding assistants: definitions, callers, callees,
reachability, possible indirect targets, and an inspectable path back to source.
The roadmap now gives that idea a clearer shape.&lt;&#x2F;p&gt;
&lt;p&gt;The first step is to make program facts easy to ask for. Scripts and coding
assistants should be able to query a catalog in source-level terms and receive
the locations and paths that support each answer. Missing modules, unresolved
calls, and unavailable mappings should remain visible rather than turning into
false certainty.&lt;&#x2F;p&gt;
&lt;p&gt;The second is to make the ask, edit, rebuild, ask loop practical. Unchanged work
should be reused without letting an answer from an older build appear current.
From there, simple reachability can grow into deeper security evidence when a
question needs it, and program facts can be compared across revisions or build
configurations. The useful result is still evidence a person can inspect, not an
automatic verdict.&lt;&#x2F;p&gt;
&lt;p&gt;The final direction is broader reach: following calls across Rust and C&#x2F;C++
boundaries, and carrying a verified analysis snapshot to another machine after
the original build tree is gone.&lt;&#x2F;p&gt;
&lt;p&gt;The progression is from artifacts, to facts, to evidence. Extraction was the
prerequisite. The catalog is the substrate that keeps every later answer tied to
what was actually analyzed and what remains unknown.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;rllvm&lt;&#x2F;code&gt; is Apache-2.0 and available on
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&quot;&gt;GitHub&lt;&#x2F;a&gt;. The repository includes the
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;blob&#x2F;main&#x2F;docs&#x2F;CATALOG.md&quot;&gt;catalog schema&lt;&#x2F;a&gt; and
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&#x2F;tree&#x2F;main&#x2F;benchmarks&#x2F;baselines&#x2F;2026-09-12-apple-m4&quot;&gt;benchmark evidence&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Two Ways to Extract Whole-Program Bitcode</title>
        <published>2026-09-07T18:00:00-07:00</published>
        <updated>2026-09-07T18:00:00-07:00</updated>
        
        <author>
          <name>
            Shengtuo Hu
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://shengtuo.me/blog/whole-program-bitcode/"/>
        <id>https://shengtuo.me/blog/whole-program-bitcode/</id>
        
        <content type="html" xml:base="https://shengtuo.me/blog/whole-program-bitcode/">&lt;p&gt;A compiler sees one source file at a time. That is the whole design: hand it a file,
get back an object file, repeat, and let the linker staple the results together. It
works so well that we forget it is a constraint.&lt;&#x2F;p&gt;
&lt;p&gt;It becomes a constraint the moment you want to analyze a program instead of build it.
Checking a property that spans function boundaries, running a custom compiler pass
over everything at once, auditing every call site of a risky API — each of these needs
the whole program in one piece, in a form built for analysis rather than for
execution. LLVM bitcode is that form: what the compiler works in after it has
understood your code but before it has turned it into machine instructions.&lt;&#x2F;p&gt;
&lt;p&gt;Getting bitcode for one file is easy. Getting it for a whole program, out of a real
build, is not. The difficulty is not that the information is missing but that the
build throws it away. After &lt;code&gt;make&lt;&#x2F;code&gt; finishes, nothing on disk records which source
files went into which binary, or how each one was compiled. The linker knew, briefly,
and did not write it down.&lt;&#x2F;p&gt;
&lt;p&gt;I have built two tools that recover it. They share a name, a goal, and almost no code,
because they come at the problem from opposite ends.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rllvm-pose-as-the-compiler&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#rllvm-pose-as-the-compiler&quot; aria-label=&quot;Anchor link for: rllvm-pose-as-the-compiler&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
rllvm: pose as the compiler&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;shengtuo.me&#x2F;rllvm&#x2F;&quot;&gt;rllvm&lt;&#x2F;a&gt; is a set of compiler wrappers written in Rust. You
point your build at them and build normally:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#dbbb3d;&quot;&gt;export &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CC&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-cc &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;CXX&lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;rllvm-cxx
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;.&#x2F;configure &lt;&#x2F;span&gt;&lt;span style=&quot;color:#db7c6d;&quot;&gt;&amp;amp;&amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;make
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;rllvm-get-bc&lt;&#x2F;span&gt;&lt;span&gt; .&#x2F;myprogram      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#707180;&quot;&gt;# produces myprogram.bc
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-it-works&quot; aria-label=&quot;Anchor link for: how-it-works&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
How it works&lt;&#x2F;h3&gt;
&lt;p&gt;Underneath, each source file compiles twice: once to the object file your build
expects, and once to bitcode written off to the side. The second copy then has to be
findable again, and that is the interesting part. Every object file carries a small
extra section — a labelled region of bytes that object files can hold alongside code
and data — recording the path to that file&#x27;s bitcode. The linker has no idea what that
section means, so it does the one thing linkers do with sections they do not
recognize: it concatenates them. The finished binary ends up carrying a complete list
of the bitcode files that make it up, assembled by a tool that does not know it is
helping.&lt;&#x2F;p&gt;
&lt;figure class=&quot;bc-fig bc-ledger&quot; aria-label=&quot;Five source files each record one bitcode path, and the linker concatenates them into a single section naming the whole program&quot;&gt;
&lt;ol&gt;
&lt;li style=&quot;--i:0&quot;&gt;&lt;span class=&quot;src&quot;&gt;parse.c&lt;&#x2F;span&gt;&lt;span class=&quot;tie&quot; aria-hidden=&quot;true&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;bc&quot;&gt;parse.bc&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;li style=&quot;--i:1&quot;&gt;&lt;span class=&quot;src&quot;&gt;lexer.c&lt;&#x2F;span&gt;&lt;span class=&quot;tie&quot; aria-hidden=&quot;true&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;bc&quot;&gt;lexer.bc&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;li style=&quot;--i:2&quot;&gt;&lt;span class=&quot;src&quot;&gt;codegen.c&lt;&#x2F;span&gt;&lt;span class=&quot;tie&quot; aria-hidden=&quot;true&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;bc&quot;&gt;codegen.bc&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;li style=&quot;--i:3&quot;&gt;&lt;span class=&quot;src&quot;&gt;util.c&lt;&#x2F;span&gt;&lt;span class=&quot;tie&quot; aria-hidden=&quot;true&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;bc&quot;&gt;util.bc&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;li style=&quot;--i:4&quot;&gt;&lt;span class=&quot;src&quot;&gt;main.c&lt;&#x2F;span&gt;&lt;span class=&quot;tie&quot; aria-hidden=&quot;true&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;bc&quot;&gt;main.bc&lt;&#x2F;span&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;figcaption&gt;Each object file carries one line. The linker concatenates them into &lt;b&gt;__RLLVM,__rllvm_bc&lt;&#x2F;b&gt;, so the finished binary holds the whole list, and &lt;b&gt;rllvm-get-bc&lt;&#x2F;b&gt; reads it back out as &lt;b&gt;prog.bc&lt;&#x2F;b&gt;.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;I still think that is a good trick. It borrows the linker as an accumulator, and it
asks nothing of your project except permission to be its compiler — a knob every build
system already has.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;where-the-idea-came-from&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#where-the-idea-came-from&quot; aria-label=&quot;Anchor link for: where-the-idea-came-from&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Where the idea came from&lt;&#x2F;h3&gt;
&lt;p&gt;The trick is not mine. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;travitch&#x2F;whole-program-llvm&quot;&gt;wllvm&lt;&#x2F;a&gt;
established it years ago with Python wrappers that build each file normally, build it
again as bitcode, and record where that bitcode landed in a section called &lt;code&gt;.llvm_bc&lt;&#x2F;code&gt;;
a companion tool, &lt;code&gt;extract-bc&lt;&#x2F;code&gt;, reads the section back out of the finished binary and
links the pieces together. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SRI-CSL&#x2F;gllvm&quot;&gt;gllvm&lt;&#x2F;a&gt; is the same
design rewritten in Go, and it runs faster for an unglamorous reason: it generates
bitcode in parallel where the Python wrappers fork and wait. The two differ mostly in
reach, as wllvm can drive gcc through the dragonegg plugin while gllvm is clang-only
and its README rules out combining it with link-time optimization.&lt;&#x2F;p&gt;
&lt;p&gt;rllvm is the third entry in that lineage, and the naming says so: &lt;code&gt;w&lt;&#x2F;code&gt; for
whole-program, &lt;code&gt;g&lt;&#x2F;code&gt; for Go, &lt;code&gt;r&lt;&#x2F;code&gt; for Rust. If gllvm or wllvm already work for you, there
is no urgency to switch.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-rllvm-adds&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-rllvm-adds&quot; aria-label=&quot;Anchor link for: what-rllvm-adds&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What rllvm adds&lt;&#x2F;h3&gt;
&lt;p&gt;What rllvm adds, above all, is records that survive being moved. The original design
writes down where each bitcode file landed as an absolute path, which holds up exactly
as long as nothing moves. Rename the directory, run the build inside a container and
read the results outside it, or reuse a cached object file that another machine
compiled, and every recorded path names a directory that does not exist. What makes
this particular failure unpleasant is that extraction does not stop; it simply
resolves fewer files than it should and hands you a smaller program than you asked
for. rllvm can record those paths relative to a root you name at extraction time
instead. Because the section is plain text, both forms can appear in the same binary
and objects built before the change keep working.&lt;&#x2F;p&gt;
&lt;p&gt;The rest of what rllvm adds is reach. Turning on link-time optimization removes the
object file that the original mechanism needs to write into, so rllvm records the path
another way and hides the difference behind the same interface. Its rustc wrapper
copes with cargo naming outputs its own way, so a cargo build gives back bitcode with
dependency crates included. It produces whole-program bitcode for a linked WebAssembly
module rather than only for the pieces that went into it. For projects too large to
link in one pass it can stage the merge directory by directory, or produce an archive
instead. And &lt;code&gt;rllvm-info&lt;&#x2F;code&gt; reports what a module actually contains, which is worth more
than it sounds when the characteristic failure is an empty result.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-it-costs&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-it-costs&quot; aria-label=&quot;Anchor link for: what-it-costs&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What it costs&lt;&#x2F;h3&gt;
&lt;p&gt;The weaknesses follow from the same trick as the strengths. The section holds a path,
not the bitcode, so the binary is not self-contained: &lt;code&gt;make clean&lt;&#x2F;code&gt; breaks it, moving
the directory breaks it, and copying the binary to another machine breaks it. Storing
the bitcode itself would fix this, and I have a working prototype, but bitcode is
comparable to or larger than the object code it would ride along with, and I have not
been willing to pay that in binary size. Compiling every file twice is a real tax on a
large C++ project. And because a wrapper has to understand the compiler&#x27;s command line
well enough to know what is being built, one misread flag is enough to make bitcode
stop appearing.&lt;&#x2F;p&gt;
&lt;p&gt;Linkers supply the rest of the difficulty, and each lesson arrived the hard way.
Nothing in the program references that extra section, so a linker that discards
unreferenced data throws it away unless it is explicitly marked to keep. The
WebAssembly linker deletes sections with certain LLVM-standard names while faithfully
keeping every other custom section, which is exactly why rllvm&#x27;s sections are not
named after LLVM&#x27;s.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rules-rllvm-ask-the-build-system&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#rules-rllvm-ask-the-build-system&quot; aria-label=&quot;Anchor link for: rules-rllvm-ask-the-build-system&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
rules_rllvm: ask the build system&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;shengtuo.me&#x2F;rules_rllvm&#x2F;&quot;&gt;rules_rllvm&lt;&#x2F;a&gt; answers the same question for Bazel
builds, and it does not use rllvm to do it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-i-stopped-wrapping-rllvm&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-i-stopped-wrapping-rllvm&quot; aria-label=&quot;Anchor link for: why-i-stopped-wrapping-rllvm&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Why I stopped wrapping rllvm&lt;&#x2F;h3&gt;
&lt;p&gt;I did not start from scratch. I started by wrapping rllvm inside Bazel rules — keep
the wrappers, keep the sections, let Bazel drive them — and spent a while on it before
concluding it could not be made to work properly.&lt;&#x2F;p&gt;
&lt;p&gt;The obstacles were not details. Bazel keeps only the files an action declares in
advance as its outputs, so bitcode written off to the side gets discarded when the
action finishes and never comes back at all when the build runs on another machine.
The absolute paths recorded in those sections are wrong on any machine but the one
that compiled them, which quietly breaks a shared cache. Extraction afterwards is a
separate pass Bazel knows nothing about, so none of it is incremental or cached. And
depending on the rllvm binary tied the whole thing to one processor architecture,
which meant it would not run on my own laptop. Working around all of that meant asking
Bazel to stop doing the things people use Bazel for.&lt;&#x2F;p&gt;
&lt;p&gt;So I stopped wrapping and started again on a better premise. Bazel already knows the
dependency graph, and it already knows the exact command it used to compile every
file, because it built that command itself. A wrapper under Bazel spends all its
ingenuity recovering information that was never lost.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-it-works-1&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-it-works-1&quot; aria-label=&quot;Anchor link for: how-it-works-1&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
How it works&lt;&#x2F;h3&gt;
&lt;p&gt;These rules ask instead. They use a Bazel &lt;em&gt;aspect&lt;&#x2F;em&gt; — a piece of code that walks over
the targets you already have and attaches extra work to them, without you editing a
single build file. For each source file it finds, the aspect asks the toolchain for
the compile command Bazel was going to run anyway, changes where the output goes, and
declares the result as a proper build output. There is no compiler wrapper, nothing
hidden inside object files, and no absolute path anywhere in the results.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#31333d;color:#ffffffc4;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a3cbe3;&quot;&gt;bazel&lt;&#x2F;span&gt;&lt;span&gt; build &#x2F;&#x2F;:app_bc
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;what-living-in-the-graph-buys&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-living-in-the-graph-buys&quot; aria-label=&quot;Anchor link for: what-living-in-the-graph-buys&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What living in the graph buys&lt;&#x2F;h3&gt;
&lt;p&gt;The first thing this buys is that extraction costs nothing until somebody asks for it.
The bitcode files are not part of what a target normally produces; they sit in a
separate group you request by name, and Bazel runs work only when something needs its
output. An ordinary build runs none of it. That is what makes it reasonable to leave
these rules switched on in a repository other people share, because the people who
never wanted bitcode never pay for it.&lt;&#x2F;p&gt;
&lt;p&gt;The second is that every point in the dependency graph becomes an extraction point.
The bitcode travels up the graph node by node, which makes a library just as
addressable as a finished program. &quot;Give me the bitcode for this one library, not
linked into anything&quot; is a request the wrapper approach cannot express at all, because
reading those sections starts at the final binary.&lt;&#x2F;p&gt;
&lt;p&gt;Handling shared dependencies is where this earns its keep: each point in the graph
collects only its own source files, never its dependencies&#x27;. Folding a dependency&#x27;s
bitcode into everything that uses it would include a shared library twice, and merging
then fails on the duplicate definitions. Instead the bitcode travels in a set that
treats the same file as the same file, so when two libraries both depend on a third,
the shared one contributes exactly one copy.&lt;&#x2F;p&gt;
&lt;figure class=&quot;bc-fig bc-graph&quot; aria-label=&quot;A diamond dependency graph in which two libraries both depend on a third, which contributes one copy of its bitcode&quot;&gt;
&lt;svg viewBox=&quot;0 0 420 300&quot; role=&quot;img&quot;&gt;
&lt;g class=&quot;edges&quot;&gt;
&lt;line x1=&quot;172&quot; y1=&quot;66&quot; x2=&quot;112&quot; y2=&quot;128&quot;&#x2F;&gt;
&lt;line x1=&quot;248&quot; y1=&quot;66&quot; x2=&quot;308&quot; y2=&quot;128&quot;&#x2F;&gt;
&lt;line x1=&quot;112&quot; y1=&quot;172&quot; x2=&quot;172&quot; y2=&quot;234&quot;&#x2F;&gt;
&lt;line x1=&quot;308&quot; y1=&quot;172&quot; x2=&quot;248&quot; y2=&quot;234&quot;&#x2F;&gt;
&lt;&#x2F;g&gt;
&lt;g class=&quot;node&quot; style=&quot;--i:0&quot;&gt;
&lt;rect x=&quot;155&quot; y=&quot;22&quot; width=&quot;110&quot; height=&quot;44&quot; rx=&quot;8&quot;&#x2F;&gt;
&lt;text class=&quot;target&quot; x=&quot;210&quot; y=&quot;41&quot;&gt;&#x2F;&#x2F;:diamond&lt;&#x2F;text&gt;
&lt;text class=&quot;bc&quot; x=&quot;210&quot; y=&quot;57&quot;&gt;diamond.bc&lt;&#x2F;text&gt;
&lt;&#x2F;g&gt;
&lt;g class=&quot;node&quot; style=&quot;--i:1&quot;&gt;
&lt;rect x=&quot;37&quot; y=&quot;128&quot; width=&quot;110&quot; height=&quot;44&quot; rx=&quot;8&quot;&#x2F;&gt;
&lt;text class=&quot;target&quot; x=&quot;92&quot; y=&quot;147&quot;&gt;&#x2F;&#x2F;:lib_a&lt;&#x2F;text&gt;
&lt;text class=&quot;bc&quot; x=&quot;92&quot; y=&quot;163&quot;&gt;lib_a.bc&lt;&#x2F;text&gt;
&lt;&#x2F;g&gt;
&lt;g class=&quot;node&quot; style=&quot;--i:1&quot;&gt;
&lt;rect x=&quot;273&quot; y=&quot;128&quot; width=&quot;110&quot; height=&quot;44&quot; rx=&quot;8&quot;&#x2F;&gt;
&lt;text class=&quot;target&quot; x=&quot;328&quot; y=&quot;147&quot;&gt;&#x2F;&#x2F;:lib_b&lt;&#x2F;text&gt;
&lt;text class=&quot;bc&quot; x=&quot;328&quot; y=&quot;163&quot;&gt;lib_b.bc&lt;&#x2F;text&gt;
&lt;&#x2F;g&gt;
&lt;g class=&quot;node shared&quot; style=&quot;--i:2&quot;&gt;
&lt;rect x=&quot;155&quot; y=&quot;234&quot; width=&quot;110&quot; height=&quot;44&quot; rx=&quot;8&quot;&#x2F;&gt;
&lt;text class=&quot;target&quot; x=&quot;210&quot; y=&quot;253&quot;&gt;&#x2F;&#x2F;:lib_c&lt;&#x2F;text&gt;
&lt;text class=&quot;bc&quot; x=&quot;210&quot; y=&quot;269&quot;&gt;lib_c.bc&lt;&#x2F;text&gt;
&lt;&#x2F;g&gt;
&lt;&#x2F;svg&gt;
&lt;figcaption&gt;Both paths down from &lt;b&gt;&#x2F;&#x2F;:diamond&lt;&#x2F;b&gt; arrive at &lt;b&gt;&#x2F;&#x2F;:lib_c&lt;&#x2F;b&gt;. Because the bitcode travels in a set keyed on file identity, &lt;b&gt;lib_c.bc&lt;&#x2F;b&gt; is built once and merged once. The edges are dashed because they are declared during analysis and run nothing until an output group asks for them.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;The third is that the bitcode always matches the real compile. The aspect never
interprets a command line; it asks the toolchain for the command Bazel had already
built and changes only where the output goes. A wrapper has to understand flags it did
not write, and anything it misreads becomes bitcode that quietly differs from the
object that actually shipped. Here there is nothing to misread. For the same reason,
the work behaves like every other step in the build: scheduled, parallelized, cached,
and distributed across machines, with no special case for a shared cache because there
are no absolute paths to go stale.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-it-costs-1&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-it-costs-1&quot; aria-label=&quot;Anchor link for: what-it-costs-1&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What it costs&lt;&#x2F;h3&gt;
&lt;p&gt;The cost of all this is that it works with Bazel and nothing else, and with a recent
Bazel at that, since older-style workspace configuration is no longer supported. What
makes the approach clean is exactly what makes it narrow.&lt;&#x2F;p&gt;
&lt;p&gt;The smaller costs are worth naming too. A rule cannot mark itself as &quot;do not build me
by default,&quot; so bitcode targets get swept up by a build-everything command unless
whoever writes them opts out. Running a one-off extraction without editing build files
works, but the incantation is clumsy and easy to get subtly wrong. Because the aspect
reuses the toolchain&#x27;s own machinery, it is coupled to the internals of the LLVM and
Rust rule sets, and an upstream release can require changes here; I took that deal
deliberately, since deriving those commands independently would drift out of sync
silently, which is worse. It is also early software: not yet in Bazel&#x27;s central
package registry, so depending on it takes an extra line. Objective-C works only on
macOS and needs an extra flag, and Swift does not work yet because the bitcode it
emits is newer than what the toolchain&#x27;s LLVM tools will read.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;which-one-to-use&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#which-one-to-use&quot; aria-label=&quot;Anchor link for: which-one-to-use&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
Which one to use&lt;&#x2F;h2&gt;
&lt;p&gt;If you build with Bazel, use rules_rllvm. If you build with anything else, use rllvm.
That is the honest short answer, and the second half covers most existing C and C++
software.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;rllvm&lt;&#x2F;th&gt;&lt;th&gt;rules_rllvm&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Build system&lt;&#x2F;td&gt;&lt;td&gt;Any&lt;&#x2F;td&gt;&lt;td&gt;Bazel only&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;How you adopt it&lt;&#x2F;td&gt;&lt;td&gt;Set &lt;code&gt;CC&lt;&#x2F;code&gt; and build&lt;&#x2F;td&gt;&lt;td&gt;Point an aspect at existing targets&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Extract from&lt;&#x2F;td&gt;&lt;td&gt;The finished binary&lt;&#x2F;td&gt;&lt;td&gt;Any library or binary in the graph&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Cost when unused&lt;&#x2F;td&gt;&lt;td&gt;Every file compiles twice&lt;&#x2F;td&gt;&lt;td&gt;Nothing runs&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Result&lt;&#x2F;td&gt;&lt;td&gt;Points at bitcode by path&lt;&#x2F;td&gt;&lt;td&gt;Ordinary build outputs&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Portability&lt;&#x2F;td&gt;&lt;td&gt;The entire point&lt;&#x2F;td&gt;&lt;td&gt;Not a goal&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The more useful question is what each tool assumes about your situation. Reach for
rllvm when you do not control the build — third-party source, a vendored tree,
reproducing somebody else&#x27;s build — or when you need one method that behaves the same
across many projects that share no build system. Reach for rules_rllvm when you are
already using Bazel, and especially when you want bitcode for individual libraries,
when your builds are cached or spread across machines, or when you cannot afford to
double the cost of a build people run all day.&lt;&#x2F;p&gt;
&lt;p&gt;Neither is a strict upgrade on the other. rllvm buys portability with a second compile
and a result that points at files instead of containing them. rules_rllvm buys
precision and costs nothing when idle, by giving up every build system but one.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-building-both-taught-me&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-building-both-taught-me&quot; aria-label=&quot;Anchor link for: what-building-both-taught-me&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What building both taught me&lt;&#x2F;h2&gt;
&lt;p&gt;Where you get into the build decides everything else. Both tools answer the same
question, and nearly every difference between them follows from one choice about where
to intervene. rllvm intervenes at the compiler, which every build system can be told
to swap out, so it works everywhere and understands nothing about the project it sits
inside. rules_rllvm intervenes at the build system, which understands everything and
exists in exactly one place. Portability, cost, robustness, and what you can even
extract were not four decisions. They were one, and I did not see that clearly until I
had built the second tool.&lt;&#x2F;p&gt;
&lt;p&gt;Recovering what somebody already knows is a losing game when you have the option of
asking. rllvm&#x27;s central trick reconstructs, after the fact, something the build never
bothered to record. That is satisfying to make work, and it is also labor that exists
only because nobody wrote the answer down. Under Bazel the answer is written down, and
reconstructing it anyway means maintaining a careful parser for command lines that the
build system generated itself. This does not make reconstruction bad. With make or
autotools there is nobody to ask, and reconstruction is the only approach that works
at all. It makes noticing which situation you are in the actual skill, because the
answer is a property of the environment rather than of the problem.&lt;&#x2F;p&gt;
&lt;p&gt;Trying to get both sets of benefits got me neither, which is what the pivot taught me.
Wrapping rllvm inside Bazel rules looked like reuse: one mechanism, one codebase, a
thin adapter. What it produced was the fragility of reconstruction plus a constant
fight with the guarantees that make Bazel worth using. The two designs are not layers.
They are alternatives, and the honest move was to stop making one wear the other as a
costume.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-comes-next&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-comes-next&quot; aria-label=&quot;Anchor link for: what-comes-next&quot;&gt;&lt;i class=&quot;icon&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt;
What comes next&lt;&#x2F;h2&gt;
&lt;p&gt;Extraction is a means, not an end. A &lt;code&gt;.bc&lt;&#x2F;code&gt; file matters only because something else
reads it, and plenty of things already do: symbolic execution engines like KLEE, and
static analysis frameworks like SVF and Phasar, all take whole-program bitcode as
their input. Getting that file out of a real build has always been the awkward step,
which is why tools like these exist at all.&lt;&#x2F;p&gt;
&lt;p&gt;The direction I find most interesting now is handing those facts to a coding
assistant. An assistant reading your source approximates the call graph by matching
names and patterns. It cannot know what actually reaches what once everything is
linked, and indirect calls make the guess worse. Bitcode knows. A small query
interface over a whole-program module — who calls this function, whether anything can
reach that one, what an indirect call might actually target — turns a guess into a
lookup. The answers have to come back as file and line numbers rather than as compiler
IR, because that is where the work happens.&lt;&#x2F;p&gt;
&lt;p&gt;For security review the useful output is evidence, not a verdict. Asking whether
attacker-controlled input can reach a risky function is a reachability question, and
the honest answer is the path itself, so a person can read those particular functions
and decide. Static analysis over-approximates. A tool that reports &quot;vulnerable&quot;
without showing its work has only moved the guessing somewhere else.&lt;&#x2F;p&gt;
&lt;p&gt;None of this is built yet, and two things would decide whether it works. The bitcode
has to be compiled with debug information and little optimization, or there is nothing
to map back to source. Indirect calls remain the hard problem they have always been.
But extraction that works per library and costs nothing when idle is a good substrate
for a loop that asks a question, changes a file, and asks again.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Both projects are Apache-2.0 and take issues and patches:
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rllvm&quot;&gt;rllvm&lt;&#x2F;a&gt; ·
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;h1994st&#x2F;rules_rllvm&quot;&gt;rules_rllvm&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
