<feed xmlns="http://www.w3.org/2005/Atom"> <id>https://tech.dreamleaves.org/</id> <title>Red Leaves</title> <subtitle>My whole life was unlimited red code.</subtitle> <updated>2026-05-10T01:38:30+02:00</updated> <author> <name>Arnaud 'red' Rouyer</name> <uri>https://tech.dreamleaves.org/</uri> </author> <link rel="self" type="application/atom+xml" href="https://tech.dreamleaves.org/feed.xml"/> <link rel="alternate" type="text/html" hreflang="en" href="https://tech.dreamleaves.org/"/> <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator> <rights> © 2026 Arnaud 'red' Rouyer </rights> <icon>/assets/img/favicons/favicon.ico</icon> <logo>/assets/img/favicons/favicon-96x96.png</logo> <entry> <title>RomHacking with Ghidra, Part 1: Loading SNES ROMs</title> <link href="https://tech.dreamleaves.org/posts/romhacking-with-ghidra-part-1-loading-snes-roms/" rel="alternate" type="text/html" title="RomHacking with Ghidra, Part 1: Loading SNES ROMs" /> <published>2026-05-08T00:00:00+02:00</published> <updated>2026-05-08T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/romhacking-with-ghidra-part-1-loading-snes-roms/</id> <content type="html"><![CDATA[ <p>Adding to the myriad of (unfinished) projects I’ve still got on the backburner, I’ve recently started looking into <a href="/tags/romhacking/">RomHacking</a> in order to translate the dialogs of a fighting game. Since my <a href="/posts/exploring-spira-ps3-binary-with-ghidra/">previous reverse-engineering endeavors in Ghidra</a> were a very pleasant experience, I decided it would be great to keep using that tool for this next task.</p> <p>I loaded the ROM in Ghidra, started it in the <gh repo="SourMesen/Mesen2">Mesen2</gh> emulator, and started to delimit the first string I had found in the debugger. Alas, it was not in its expected place: I could find the exact bytes I was looking for, but they were in a completely different place.</p> <p>Treachery! Ghidra had perfectly mapped the <wiki page="Executable and Linkable Format">ELF</wiki> sections the previous time, why wasn’t it doing the same here? What’s so <em>special</em> about an SNES ROM?</p> <h1 id="straight-from-the-shoulders-of-giants">Straight from the shoulders of giants</h1> <p>Fresh from last time’s mistake, I decided to start fresh from what was already there: an archived <gh repo="achan1989/ghidra-snes-loader">ghidra-snes-loader</gh> repository exists. Let’s see…</p> <blockquote> <p>Works with Ghidra v9.1 or later only.</p> </blockquote> <p>So claimed the documentation. Unfortunately, “or later” apparently meant “but before your version”. Still, let’s check the code of that <code class="language-console highlighter-rouge"><span class="go">SnesLoader.java</span></code>. Clearly that’s extending an abstract class, setting some boilerplate configurations through getters, and then the actual loading logic:</p> <div class="language-java highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">class</span> <span class="nc">SnesLoader</span> <span class="kd">extends</span> <span class="nc">AbstractProgramLoader</span> <span class="o">{</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="nc">Collection</span><span class="o">&amp;lt;</span><span class="nc">LoadSpec</span><span class="o">&amp;gt;</span> <span class="nf">findSupportedLoadSpecs</span><span class="o">(</span><span class="nc">ByteProvider</span> <span class="n">provider</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
    <span class="c1">// (...)</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">protected</span> <span class="nc">List</span><span class="o">&amp;lt;</span><span class="nc">Program</span><span class="o">&amp;gt;</span> <span class="nf">loadProgram</span><span class="o">(</span><span class="nc">ByteProvider</span> <span class="n">provider</span><span class="o">,</span> <span class="nc">String</span> <span class="n">programName</span><span class="o">,</span>
      <span class="nc">DomainFolder</span> <span class="n">programFolder</span><span class="o">,</span> <span class="nc">LoadSpec</span> <span class="n">loadSpec</span><span class="o">,</span> <span class="nc">List</span><span class="o">&amp;lt;</span><span class="nc">Option</span><span class="o">&amp;gt;</span> <span class="n">options</span><span class="o">,</span> <span class="nc">MessageLog</span> <span class="n">log</span><span class="o">,</span>
      <span class="nc">Object</span> <span class="n">consumer</span><span class="o">,</span> <span class="nc">TaskMonitor</span> <span class="n">monitor</span><span class="o">)</span>
      <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">CancelledException</span> <span class="o">{</span>
    <span class="c1">// (...)</span>
  <span class="o">}</span>
<span class="o">}</span>
</pre></div></div> <p>That all looks very easy. So that <code class="language-console highlighter-rouge"><span class="go">findSupportedLoadSpecs</span></code> obviously sets the language/architecture, and <code class="language-console highlighter-rouge"><span class="go">loadProgram</span></code> will set up the ROM file<sup id="fnref:program"><a href="#fn:program" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. Pretty basic. What’s next? Ghidra invokes <code class="language-console highlighter-rouge"><span class="go">loadProgram</span></code>, passes it the ROM content in <code class="language-console highlighter-rouge"><span class="go">ByteProvider provider</span></code>, and then it all goes in the memory map, that’s easy, and (as we’ll see later, dear reader!) the Ghidra extension model is actually pretty pleasant to work with.</p> <p>Supporting the <code class="language-console highlighter-rouge"><span class="go">65816</span></code> processor was pretty easy too, I just had to copy over the <code class="language-console highlighter-rouge"><span class="go">data/languages</span></code> folder from the (archived) &amp;lt;gh repo=achan1989/ghidra-65816&amp;gt;ghidra-65816&amp;lt;/gh&amp;gt; repository, fix a <gh repo="achan1989/ghidra-65816/issues/9">minor bug</gh>, add some specialization to the spec, so it would be tied to my extension, and call it a day.</p> <p>This feels easy because it…sorta is. If all you want your loader to do is “recognize that file type, assign it that language/arch”, you can stop reading here.</p> <p>But if you keep reading, the <gh repo="achan1989/ghidra-snes-loader/blob/08123562ccca70d8394e10e33dd4886bc5a8c2cc/SnesLoader/src/main/java/snesloader/SnesLoader.java#L112">second line of the load method</gh> invokes <code class="language-console highlighter-rouge"><span class="go">detectRomKind(provider)</span></code>. I knew SNES ROMs had different sizes, but different types… I guess that’s not about RPGs and platformers?</p> <h1 id="banks-and-rommers">Banks and rommers</h1> <p>I’m no stranger to <a href="https://www.romhacking.net">romhacking</a>, ROMs and ISOs, but I never thought too much about it. This changed with the 3DS and PSVita scenes, where translations and mods would be distributed using the <gh repo="LumaTeam/Luma3DS">LayeredFS</gh> and <gh repo="dots-tb/rePatch-reDux0">rePatch</gh> methods respectively. Those were no mere <code class="language-console highlighter-rouge"><span class="go">.ips</span></code> or <code class="language-console highlighter-rouge"><span class="go">.xdelta</span></code> patches, but an imitation of the game’s filesystem, only containing the modified files, with the system’s <code class="language-console highlighter-rouge"><span class="go">open(3)</span></code> syscall being hooked with a conditional loader:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="kt">int</span> <span class="nf">open</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">path</span><span class="p">,</span> <span class="kt">int</span> <span class="n">oflag</span><span class="p">,</span> <span class="p">...</span> <span class="p">)</span> <span class="p">{</span>
  <span class="kt">char</span> <span class="o">*</span><span class="n">patchPath</span> <span class="o">=</span> <span class="n">replace</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s">"/game_path/"</span><span class="p">,</span> <span class="s">"/patch_path"</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">exists</span><span class="p">(</span><span class="n">patchPath</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">_open</span><span class="p">(</span><span class="n">patchPath</span><span class="p">,</span> <span class="p">...)</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">_open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="p">...)</span>
<span class="p">}</span>
</pre></div></div> <p>A very smart piece of technology, which makes me wonder why it wasn’t invented before the 3DS era. Could it be because a <strong>ROM</strong>, being <strong>R</strong>ead-<strong>O</strong>nly <u><strong>M</strong>emory</u> has no concept of a file system?</p> <p>See, ROMs have always looked like the exact representation of a game’s cart, and nobody running <wiki>ZSNES</wiki> or <wiki>Snes9x</wiki> would dare challenge that idea, since all the data was there, and there was nothing missing, right? This was all a sweet lie perpetuated by emulator developers because the approach of emulators (at the time) was to use shortcuts in emulation to work in spite of the low specs of emulation machines.</p> <p>The <em>statu quo</em> was questioned when <wiki page="Near (programmer)">byuu/Near</wiki> first published the <wiki page="higan (emulator)">bsnes/higan</wiki> emulator: it required powerful (at the time) machine specs to run, but was the first to entirely emulate the <wiki page="Super Nintendo Entertainment System">SNES</wiki> down to each of its circuits, be they inside the console, or inside the game cart. Because yes: there are different chips inside the carts, and the ROM was only one of them!</p> <p>This new model completely reset the preservation efforts for the console: GoodSNES<sup id="fnref:goodsnes"><a href="#fn:goodsnes" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> only included the ROMs and there was no way to know which chips came with it. The emulators were just guessing with heuristics, or maintaining their internal database, which was the only way an emulator would know that you could save your game. Thanks to <wiki page="Near (programmer)">byuu/Near</wiki>’s preservation efforts, all game carts were identified by boards, chips, ROMs,… and mappings. But those “chips” didn’t explain why the bytes in my debugger didn’t line up with the bytes in my ROM, nor with the bytes in Ghidra!</p> <p>See, one thing we didn’t touch on is that the SNES is a 16-bit console, which means it performs calculations over 16 bits, 2 bytes, in the hexadecimal range <code class="language-console highlighter-rouge"><span class="go">0x0000-0xFFFF</span></code> (so, <code class="language-console highlighter-rouge"><span class="go">0-65,535</span></code> in decimal). If it could only address <code class="language-console highlighter-rouge"><span class="go">64KB</span></code><sup id="fnref:65C816"><a href="#fn:65C816" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>, how could it access data over a <code class="language-console highlighter-rouge"><span class="go">1MB</span></code><sup id="fnref:sotop"><a href="#fn:sotop" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>? The same way everyone else did: <wiki>Bank switching</wiki>. Depending on a single 8-bit <wiki page="Processor register">register</wiki> (<code class="language-console highlighter-rouge"><span class="go">0x00-0xFF</span></code>, so <code class="language-console highlighter-rouge"><span class="go">0-255</span></code>), memory accesses would now be directed to the chosen “page” of the ROM. You’re in page 4 and want to read something in page 6? Easy: just bank-switch!</p> <p>I like that “page” analogy. It’s just a shame that the real model is closer to the Necronomicon than to a neatly numbered book.</p> <h1 id="o-yell-take-the-high-rom-and-ill-take-the-low-rom">O ye’ll take the high ROM and I’ll take the low ROM</h1> <p>The full system is a bit complicated<sup id="fnref:fullsnes"><a href="#fn:fullsnes" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>, but in short, the SNES and the game cart will form a single unit, and share the same memory layout. As shown in the following image (courtesy of <a href="https://snes.nesdev.org/wiki/Memory_map">SNesDev Wiki</a>), some areas will ALWAYS be dedicated to the SNES I/O and RAM:</p> <p><img src="/assets/images/2026-05-08/Snes_overall_map.png" alt="" class="right" width="438px" /></p> <ul> <li>In bank <code class="language-console highlighter-rouge"><span class="gp">$</span>00</code>, from <code class="language-console highlighter-rouge"><span class="go">0x0000</span></code> to <code class="language-console highlighter-rouge"><span class="go">0x1FFF</span></code> are the first <code class="language-console highlighter-rouge"><span class="go">8KB</span></code> of RAM.</li> <li>In bank <code class="language-console highlighter-rouge"><span class="gp">$</span>00</code>, from <code class="language-console highlighter-rouge"><span class="go">0x2000</span></code> to <code class="language-console highlighter-rouge"><span class="go">0x5FFF</span></code> is the full SNES I/O in multiple registers.</li> <li>These two mappings are also accessible from <code class="language-console highlighter-rouge"><span class="gp">$</span>01</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>3F</code> and from <code class="language-console highlighter-rouge"><span class="gp">$</span>80</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>BF</code>. It is not a separate mapping but a mirror that functions just the same, like a shortcut.</li> <li>In banks <code class="language-console highlighter-rouge"><span class="gp">$</span>7E</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>7F</code>, from <code class="language-console highlighter-rouge"><span class="go">0x0000</span></code> to <code class="language-console highlighter-rouge"><span class="go">0xFFFF</span></code>, the whole SNES WRAM can be accessed.</li> </ul> <p>And then, there is the rest, which the cart is <strong>free to dedicate to ROM mappings as it pleases</strong>. In practice, there are two (and a half) <strong>canonical</strong> mapping types:</p> <ul> <li><strong>LoROM</strong> will split the ROM into chunks of 32KB (so <code class="language-console highlighter-rouge"><span class="go">0x8000</span></code> bytes), and map them to banks <code class="language-console highlighter-rouge"><span class="gp">$</span>80</code><sup id="fnref:bankdollars"><a href="#fn:bankdollars" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> (128) to <code class="language-console highlighter-rouge"><span class="gp">$</span>FF</code> (255). Since the chunks are half the size of a bank, they will be mapped from <code class="language-console highlighter-rouge"><span class="go">0x8000</span></code> to <code class="language-console highlighter-rouge"><span class="go">0xFFFF</span></code>.</li> <li><strong>HiROM</strong> will split the ROM into chunks of 64KB (so <code class="language-console highlighter-rouge"><span class="go">0x10000</span></code> bytes<sup id="fnref:hexten"><a href="#fn:hexten" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>). Since the lower parts of banks <code class="language-console highlighter-rouge"><span class="gp">$</span>80</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>BF</code> already contain mirror mappings of the SNES RAM and I/O, these ROM chunks will instead be mapped in banks <code class="language-console highlighter-rouge"><span class="gp">$</span>C0</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>FF</code>.</li> <li><strong>ExHiROM</strong> is an extension of <strong>HiROM</strong> and will map its first 64 banks the same way, then map the next ones from <code class="language-console highlighter-rouge"><span class="gp">$</span>40</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>7D</code> (and not <code class="language-console highlighter-rouge"><span class="gp">$</span>7F</code> since it’s used by the RAM!), and can ALSO map two final banks in <code class="language-console highlighter-rouge"><span class="gp">$</span>3E</code> and <code class="language-console highlighter-rouge"><span class="gp">$</span>3F</code>, for a total of 128 banks.</li> </ul> <p>This mapping was a bit of a pain to implement, but nothing that a good loop cannot solve, although I must admit that for the mapping of the last two banks of an <code class="language-console highlighter-rouge"><span class="go">ExHiROM</span></code>, I decided this was “a problem for someone skilled enough to be working on the games that would require them”. Don’t thank me, I love providing my peers with pull request opportunities.</p> <p>So, now that we know how to map, how do we know whether our ROM is <code class="language-console highlighter-rouge"><span class="go">LoROM</span></code> or <code class="language-console highlighter-rouge"><span class="go">HiROM</span></code>?</p> <p>The safest method is to calculate the ROM’s SHA256 digest and compare it to <a href="https://github.com/libretro/higan/blob/master/higan/System/Super%20Famicom/boards.bml">bsnes/higan’s Super Famicom.bml</a>, a COMPLETE database of all identified SNES carts. If we get a result, we can identify the board model, then use the <a href="https://problemkaputt.de/fullsnes.htm#snescartridgepcbs">nocash specifications</a> to identify the ROM type. Needless to say, it won’t work on RomHacks, since they are not included in the <code class="language-console highlighter-rouge"><span class="go">Super Famicom.bml</span></code> database.</p> <p>The old-school but proven method is the heuristic emulators usually rely on, based on detecting the ROM’s title label:</p> <ul> <li>In <strong>LoROM</strong>, this is ROM offset <code class="language-console highlighter-rouge"><span class="go">0x7FC0</span></code>.</li> <li>In <strong>HiROM</strong>, this is ROM offset <code class="language-console highlighter-rouge"><span class="go">0xFFC0</span></code>.</li> <li>In <strong>ExHiROM</strong>, this is ROM offset <code class="language-console highlighter-rouge"><span class="go">0x40FFC0</span></code>.</li> </ul> <p>So in the same file, we read 21 bytes at each of those addresses, and depending on which is the most plausible title, we know the ROM type. Those numbers may seem arbitrary, but it’s because the SNES expects the ROM header contents (which include the title label) in bank <code class="language-console highlighter-rouge"><span class="gp">$</span>00</code> at <code class="language-console highlighter-rouge"><span class="go">0xFFC0</span></code>.</p> <p>Wait, I never said this area was mapped, no?</p> <h1 id="mirror-mirror-on-the-rom-whos-the-fairest-of-them-all">Mirror, mirror, on the ROM, who’s the fairest of them all?</h1> <p>By this point, I had already seen the <a href="https://snes.nesdev.org/wiki/CPU_vectors">CPU vectors</a> defined in the language specs, and I knew of the <gh repo="achan1989/ghidra-65816/blob/b70cb21d5297ebae8dee54e5d140bacd612d670c/data/languages/65816.pspec#L17">VEC_RESET_EMULATION</gh> vector, which is where the console looks for the ROM entry point, but I had never noticed its value: <code class="language-console highlighter-rouge"><span class="gp">$</span>00:FFFC</code>.</p> <p>And what the hell, even using Ghidra’s “Go to”, I could never access this address: it just DID NOT exist in my listing, as if there was nothing there, and obviously there was nothing, because we’ve already discussed where the ROM chunks are mapped. So how is the SNES expecting to find ROM code here?</p> <p>Well, do you remember how I told you the bank <code class="language-console highlighter-rouge"><span class="gp">$</span>00</code>’s I/O and RAM mappings were also accessible from <code class="language-console highlighter-rouge"><span class="gp">$</span>01</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>3F</code> and from <code class="language-console highlighter-rouge"><span class="gp">$</span>80</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>BF</code>?</p> <div class="content-img" style="width: 80%"> <img src="/assets/images/2026-05-08/snes-all-mirrors.jpg" title=" When you stare into the [SNesDev documentation](https://snes.nesdev.org/wiki/Memory_map) for too long, it stares back at you. " /> <div class="img-alt"> <p>When you stare into the <a href="https://snes.nesdev.org/wiki/Memory_map">SNesDev documentation</a> for too long, it stares back at you.</p> </div> </div> <p>Yes, the area from <code class="language-console highlighter-rouge"><span class="gp">$</span>00:8000</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>00:FFFF</code> is a complete mirror of one of the canonical mappings of the ROM. And the best part? It’s not always the same.</p> <ul> <li>In <strong>LoROM</strong>, it mirrors <code class="language-console highlighter-rouge"><span class="gp">$</span>80:8000</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>80:FFFF</code>.</li> <li>In <strong>HiROM</strong>, it mirrors <code class="language-console highlighter-rouge"><span class="gp">$</span>C0:8000</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>C0:FFFF</code>.</li> <li>In <strong>ExHiROM</strong>, it mirrors <code class="language-console highlighter-rouge"><span class="gp">$</span>40:8000</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>40:FFFF</code>.</li> </ul> <p>While this is enough pain on its own, it’s at least <em>kind of</em> standard. Would you believe me if I told you there were EVEN MORE mirror mappings, and that all of them are dependent on the physical board in the cart?</p> <h1 id="smashing-the-banks-for-fun-and-profits">Smashing the banks for fun and profits</h1> <p>At this point of my research, the first thing that came to my mind was just a question: “How did the emulators of old manage to work that well?”. The truth is that most of them were KINDA duct-taped, and using heuristics to deduce the mirror mappings, and TECHNICALLY, you can just see the memory layout as eight zones, with two taken by the SNES I/O and RAM, and mirror the canonical ROM banks everywhere you can.</p> <p>And then, the emulator has to add more duct-tape because “actually, <a href="https://tcrf.net/Mega_Man_X#Copy_Protection">this game</a> does NOT have ROM mapping here, and uses that behavior<sup id="fnref:behavior"><a href="#fn:behavior" class="footnote" rel="footnote" role="doc-noteref">8</a></sup> to check that it’s running on real hardware”.</p> <p>Discovering it made me imagine an emulator-proof board and ROM: a ROM can have its entry point at an unexpected offset, but as long as the board properly maps it to <code class="language-console highlighter-rouge"><span class="gp">$</span>00:FFFC</code>, a console will run the game, and an emulator will fail, until a specific board mapping rule is added for the emulator.</p> <p>But really, don’t I have enough work mapping the mirrors I already know about?</p> <h1 id="99-bottles-of-chips-on-the-wall">99 Bottles of Chips on the wall</h1> <p>So in practice, I now have <gh repo="joshleaves/ghidra-snes">a loader</gh> that currently only exposes the canonical mappings directly, and leaves the weirder board-specific mirrors to future work (and user-defined mappings). Which is honestly fine for a first version: by that point, the goal wasn’t to perfectly emulate every possible SNES board, but to finally have Ghidra display the same addresses as the debugger (though if the code relies on mirror mappings, they aren’t visible yet).</p> <p>Finally, for that one ROM I was working on, once I reimported it, suddenly, the bytes lined up. Time to hack.</p> <h1 id="credits-and-acknowledgements">Credits and Acknowledgements</h1> <ul> <li>This article and <gh repo="joshleaves/ghidra-snes">ghidra-snes</gh> are dedicated to Near (formerly known as byuu), whose contributions to SNES documentation, emulation, and preservation have had a lasting impact on the community.</li> <li>Thanks to achan1989 for <gh repo="achan1989/ghidra-65816">ghidra-65816</gh>, the original 65816 processor implementation in Ghidra.</li> <li>Thanks to the <a href="https://snes.nesdev.org/wiki/SNESdev_Wiki">SNesDev Wiki</a> and the <a href="https://discord.gg/yXNEV6p">very helpful members of the Discord</a></li> <li>Thanks to nocash for the <a href="https://problemkaputt.de/fullsnes.htm">fullsnes documentation</a>.</li> <li>Thanks to the NSA guy watching over me if they participated in <gh repo="NationalSecurityAgency/ghidra/">Ghidra</gh>.</li> </ul> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:program"> <p>In Ghidra parlance, a “program” is a file you’re analyzing. <a href="#fnref:program" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:goodsnes"> <p>The <code class="language-console highlighter-rouge"><span class="go">GoodTools</span></code> are tools to properly rename ROMs according to their hash, and the set of renamed files it produces are called <code class="language-console highlighter-rouge"><span class="go">GoodSets</span></code>, with the one related to the SNES being <code class="language-console highlighter-rouge"><span class="go">GoodSNES</span></code>. <a href="#fnref:goodsnes" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:65C816"> <p>Of course, it’s a bit more complicated: the 65C816 processor uses 16-bit registers, but a 24-bit address space split into banks. Most instructions operate within the currently selected bank, which keeps addresses smaller and operations faster. <a href="#fnref:65C816" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:sotop"> <p>The two largest games released for the console, <wiki>Tales of Phantasia</wiki> and <wiki page="Star Ocean (video game)">Star Ocean</wiki>, both used <code class="language-console highlighter-rouge"><span class="go">6MB</span></code> ROMs. <a href="#fnref:sotop" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:fullsnes"> <p>But you can read the <a href="https://problemkaputt.de/fullsnes.htm">full specifications by nocash</a> if you’re curious! <a href="#fnref:fullsnes" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:bankdollars"> <p>In SNES development lingo, bank numbers are usually prefixed with a dollar sign, like <code class="language-console highlighter-rouge"><span class="gp">$</span>00</code> to <code class="language-console highlighter-rouge"><span class="gp">$</span>FF</code>. <a href="#fnref:bankdollars" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:hexten"> <p>Yes, in hex, the double of <code class="language-console highlighter-rouge"><span class="go">0x08</span></code> is <code class="language-console highlighter-rouge"><span class="go">0x10</span></code>! <a href="#fnref:hexten" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:behavior"> <p>Technically, you can read/write to non-mapped areas of the memory layout to trigger console-only behavior. <a href="#fnref:behavior" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>The S is for Super, not Standard</summary> </entry> <entry> <title>Exploring Spira's PS3 binary with Ghidra</title> <link href="https://tech.dreamleaves.org/posts/exploring-spira-ps3-binary-with-ghidra/" rel="alternate" type="text/html" title="Exploring Spira&amp;apos;s PS3 binary with Ghidra" /> <published>2026-05-03T00:00:00+02:00</published> <updated>2026-05-03T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/exploring-spira-ps3-binary-with-ghidra/</id> <content type="html"><![CDATA[ <p>I <a href="https://blog.dreamleaves.org/posts/final-fantasy-x/">finished Final Fantasy X more than one month ago</a>, and I still haven’t written about how almost half of my quest didn’t happen in front of my <wiki>PlayStation 3</wiki>, but in <wiki>Ghidra</wiki>, trying to cheat some of the game’s most difficult trophies…</p> <h1 id="cheating-is-cooler-when-you-learn-stuff">Cheating is cooler when you learn stuff</h1> <p>It’s okay: <wiki>Final Fantasy X</wiki> isn’t really a HARD game, and neither is <wiki page="Final Fantasy X/X-2 HD Remaster">the remaster that came out in 2013</wiki>. Some parts of it are obviously going to take a while, but according to <a href="https://psnprofiles.com/guide/358-final-fantasy-x-hd-trophy-guide">PSNProfiles Trophy Guide</a>, it’s rather tame: 110 hours, and a 5 (out of 10) difficulty. So what’s the hard part? An optional super-boss? A gauntlet of fights without healing items? A <a href="https://gamefaqs.gamespot.com/ps2/197344-final-fantasy-x/faqs/16203">low-level run</a>? Worse than that: a chocobo race.</p> <p>A <wiki>Chocobo</wiki> is a bipedal animal in the Final Fantasy franchise, kind of a bird that can be used like a horse. In this episode, they are used both as a means of transportation, and racing animals. They are also tied to three trophies:</p> <ul> <li><a href="https://psnprofiles.com/trophy/2358-final-fantasy-x-hd/10-chocobo-license">Chocobo License (Bronze)</a>: <em>Pass all chocobo training</em><br /> Actually, that one isn’t too hard, let’s not think about it.</li> <li><a href="https://psnprofiles.com/trophy/2358-final-fantasy-x-hd/18-chocobo-rider">Chocobo Rider (Bronze)</a>: <em>Win a race with a catcher chocobo with a total time of 0:0:0</em><br /> Okay, just from the description, this one reads like bad news.</li> <li><a href="https://psnprofiles.com/trophy/2358-final-fantasy-x-hd/23-chocobo-master">Chocobo Master (Silver)</a>: <em>Get 5 treasure chests during the Chocobo Race at Remiem Temple and win the race</em> <br /> This one reads easy but of course, there’s a reason it’s <em>Master</em>.</li> </ul> <p>The first one already sounds CRAZY: you must finish a race with a total time of <code class="language-console highlighter-rouge"><span class="go">0:0:0</span></code>. While it sounds impossible, it can be achieved based on your final time in this race: the race itself takes around 30 seconds, and you can catch balloons that will each subtract 3 seconds from your final time. You can also be hit by hazards that will ADD 3 seconds to your final time. And of course, balloons and hazards are completely random (but will also affect your opponent!).</p> <p>The second one requires you to follow a VERY precise path on a race track, collect treasures along the way, and just avoid touching the “walls” of the tracks to avoid losing some seconds there. It’s a test of skills, but not one riddled with randomness.</p> <p>As I was making my way through the game, I got <code class="language-console highlighter-rouge"><span class="go">Chocobo License</span></code> easily, and then tried to get <code class="language-console highlighter-rouge"><span class="go">Chocobo Rider</span></code>. Once, twice,… maybe twenty times. Screw that!</p> <p>It’s very easy to <gh repo="bucanero/apollo-ps3">unlock trophies on the PS3</gh>, but I just thought of something else: why not look for a cheat that would set my time to zero, give me a thousand balloons, or make each balloon subtract a thousand seconds?</p> <h1 id="the-state-of-cheating-on-the-ps3">The state of cheating on the PS3</h1> <p>While there is <gh repo="bucanero/ArtemisPS3">a very good cheat engine available on the PS3</gh>, there is no proper (and multi-platform) solution to find new cheats<sup id="fnref:cheatscan"><a href="#fn:cheatscan" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. And even though I could find a cheat, it worked only on the US release of the game: I was playing on the Japanese.</p> <p>But maybe I could analyze it?</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre>Time Chocobo Training
0
Hiei-YYH
0 00769D60 2C090014
0 00769D64 41820008
0 00769D68 48000018
0 00769D6C 3F800C00
0 00769D70 639C00B0
0 00769D74 7C1C2800
0 00769D78 40820008
0 00769D7C 3BE00000
0 00769D80 9BE30000
0 00769D84 4E800020
0 00339A9C 484302C5
</pre></div></div> <p>First off, that seems like a lot of instructions, which is strange when you’re mostly used to <wiki>Action Replay</wiki>-style cheats: usually, after you’ve found where the value you’re interested in is stored in memory, you hook the cheat engine to it, and overwrite it with your value, and prevent further writes to it.</p> <p>I also remember how my PS3 has crashed A LOT when using cheats, because I was using those similar “always-on” cheats, while “once” cheats, would always work. Maybe the PS3 isn’t powerful enough to run “always-on” memory hooks, but accepts writing arbitrary data straight into the game’s code to alter behavior?</p> <p>So I asked an LLM for a first pass at what this code was doing. The answer came in the form of ASM, and spat out the following pseudo-C code, with registers:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">hook</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">r9</span> <span class="o">==</span> <span class="mh">0x14</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">r28</span> <span class="o">=</span> <span class="mh">0x0C0000B0</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">r5</span> <span class="o">==</span> <span class="n">r28</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">r31</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="o">*</span><span class="p">(</span><span class="kt">uint8_t</span><span class="o">*</span><span class="p">)</span><span class="n">r3</span> <span class="o">=</span> <span class="n">r31</span><span class="p">;</span>
  <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <p>I’m not sure I get it all yet<sup id="fnref:not-knowing"><a href="#fn:not-knowing" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. Those <code class="language-console highlighter-rouge"><span class="go">rX</span></code> thingies are registers and there are at least 31 of them (what? where’s my <code class="language-console highlighter-rouge"><span class="go">RAX</span></code>?), but since I’m here already, I might as well dig into the game’s binary.</p> <h1 id="spira-on-my-laptop">Spira on my laptop</h1> <p>Two minutes later, I’ve got a 9.4MB file called <code class="language-console highlighter-rouge"><span class="go">ffx_phyreApp.self</span></code> on my laptop. I’ve spent enough time around to understand it’s a “signed ELF” or, well, a <code class="language-console highlighter-rouge"><span class="go">SELF</span></code>. The first part is easy to circumvent, thanks to <wiki>RPCS3</wiki>’s “Decrypt PS3 Binaries” to turn it into a <code class="language-console highlighter-rouge"><span class="go">ffx_phyreApp.elf</span></code> file. And guess what? The <wiki page="Executable and Linkable Format">ELF</wiki> format is so standard, that any Unix worth its salt can understand it:</p> <div class="language-shell highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>file ffx_phyreApp.elf
ffx_phyreApp.elf: ELF 64-bit MSB executable, 64-bit PowerPC or cisco 7500, Unspecified or Power ELF V1 ABI, version 1, statically linked, stripped

<span class="nv">$ </span>objdump <span class="nt">--section-headers</span> ffx_phyreApp.elf
ffx_phyreApp.elf:       file format elf64-powerpc

Sections:
Idx Name          Size     VMA              Type
  0               00000000 0000000000000000
  1               0000002c 0000000000010200 TEXT
  2               007298e0 0000000000010230 TEXT
  3               00000024 0000000000739b10 TEXT
  4               000028e0 0000000000739b34 TEXT
  5               00000188 000000000073c414 DATA
  6               0000051c 000000000073c59c DATA
  7               00000004 000000000073cab8 DATA
  8               00000004 000000000073cabc DATA
  9               00000004 000000000073cac0 DATA
 10               00000420 000000000073cac4 DATA
 11               00000004 000000000073cee4 DATA
 12               00079650 000000000073cef0 DATA
 13               000a4550 00000000007b6580 DATA
 14               00000028 000000000085aad0 DATA
 15               00000040 000000000085aaf8 DATA
 16               000003a4 0000000000860000 DATA
 17               00000304 00000000008603a4 DATA
 18               00000004 00000000008606a8 DATA
 19               00003a34 00000000008606b0 DATA
 20               0000051c 00000000008640e4 DATA
 21               0001d9b8 0000000000864600 DATA
 22               00003868 0000000000881fb8 DATA
 23               00000008 0000000000885820 DATA
 24               000001f0 0000000000885828 BSS
 25               0007f73c 0000000000885a80 DATA
 26               01d26d60 0000000000905200 BSS
 27               00003206 0000000000000000
 28               000004e0 0000000000000000
 29               0000012c 0000000000000000
</pre></div></div> <p>Oh yeah, it works. My school taught me to use <code class="language-console highlighter-rouge"><span class="go">objdump</span></code> and <code class="language-console highlighter-rouge"><span class="go">nm</span></code>, and it was my first glimpse into the beauty of the <wiki page="UNIX System V">System V ABI</wiki>, so let’s see what else our good old, reliable toolbox can tell…</p> <div class="language-shell highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>nm ffx_phyreApp.elf
ffx_phyreApp.elf: no symbols
</pre></div></div> <p><img src="/assets/images/2026-05-03/ghidra_import.png" alt="" class="right" width="360px" /></p> <p>Oh, fuck me.</p> <p>Time to bring out the big guns and open <strong>Ghidra</strong>.</p> <p>I love this tool and its approach: smart enough to detect straight away what I could have chosen myself. Remember our <code class="language-console highlighter-rouge"><span class="go">file</span></code> output? All there: <code class="language-console highlighter-rouge"><span class="go">PowerPC</span></code>, <code class="language-console highlighter-rouge"><span class="go">64-bit</span></code>, and <code class="language-console highlighter-rouge"><span class="go">MSB Executable</span></code><sup id="fnref:msb"><a href="#fn:msb" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> so <code class="language-console highlighter-rouge"><span class="go">Big Endian</span></code>, which maps to <code class="language-console highlighter-rouge"><span class="go">PowerPC:BE:64:default:default</span></code>.</p> <p>A few seconds later, the file is imported and I’m faced with a wall of hex. I run “Analyze” on the code a few times, and try to look up stuff that could be my code, like instances of <code class="language-console highlighter-rouge"><span class="go">3</span></code> (remember? seconds to add or subtract to my time). Of course, no luck anywhere.</p> <p>I need to start planning a better approach, because there’s no way I’m reading 10 Megs of assembly.</p> <h1 id="pedal-to-the-metal">Pedal to the metal</h1> <p>Since I’ve acquired a <wiki page="PlayStation Vita">PS Vita</wiki> in Japan<sup id="fnref:psvita"><a href="#fn:psvita" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>, in 2017, I’ve been hanging around <a href="https://www.reddit.com/r/vitahacks/">r/vitahacks</a>. Sometimes, I would read the source of stuff published there (mainly that one time someone posted a “trophy_unlocker.vpk” app). The name is a mouthful, but I remember it from the community VitaSDK project: <gh repo="vitasdk/vita-headers/blob/master/db/360/SceNpTrophy.yml#L25">sceNpTrophyUnlockTrophy</gh>.</p> <p>Since both consoles lived around the same time, chances are they both use the same approach. What is the PSL1GHT project telling me? Oh, <gh repo="an0nym0u5/PSL1GHT/blob/master/ppu/sprx/todo/FNIDS/libnptrophy#L11">it's got an FNID definition</gh>!</p> <p>A “function <code class="language-console highlighter-rouge"><span class="go">NID</span></code>” (sometimes called <code class="language-console highlighter-rouge"><span class="go">FNID</span></code>) is the hashed name of a function. In our case, <code class="language-console highlighter-rouge"><span class="go">0x8CEEDD21</span></code> is the NID for <code class="language-console highlighter-rouge"><span class="go">sceNpTrophyUnlockTrophy</span></code>. It seems like Sony was one of the few to do that kind of stuff, and it’s got to do with dynamic linking, size reduction, and a bit of <wiki>Security through obscurity</wiki>. All we need to know is that I cannot look up <code class="language-console highlighter-rouge"><span class="go">sceNpTrophyUnlockTrophy</span></code>, but I can look up <code class="language-console highlighter-rouge"><span class="go">0x8CEEDD21</span></code>. One result, perfect.</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre>    XREF[1]:     0073cd40(*)  
0073c858 11 97 b5 2c     undefined4   // sceNpTrophyRegisterContext
0073c85c 1C 25 47 0D     undefined4   // sceNpTrophyCreateHandle
0073c860 37 01 36 FE     undefined4   // sceNpTrophyGetRequiredDiskSpace
0073c864 37 41 EC C7     undefined4   // sceNpTrophyDestroyContext
0073c868 39 56 77 81     undefined4   // sceNpTrophyInit
0073c86c 49 D1 82 17     undefined4   // sceNpTrophyGetGameInfo
0073c870 62 3C D2 DC     undefined4   // sceNpTrophyDestroyHandle
0073c874 8C EE DD 21     undefined4   // sceNpTrophyUnlockTrophy
0073c878 A7 FA BF 4D     undefined4   // sceNpTrophyTerm
0073c87c B3 AC 34 78     undefined4   // sceNpTrophyGetTrophyUnlockState
0073c880 E3 BF 9A 28     undefined4   // sceNpTrophyCreateContext
</pre></div></div> <p>Can you believe that? I just hit the mother lode! Now let’s check on that XREF<sup id="fnref:xref"><a href="#fn:xref" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> at <code class="language-console highlighter-rouge"><span class="go">0x0073cd40</span></code>.</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>0073cd38 00 00 00 00     addr
0073cd3c 00 73 c5 00     addr
0073cd40 00 73 c8 58     addr
0073cd44 00 86 44 28     addr
0073cd48 00 00 00 00     addr
</pre></div></div> <p>Okay, pretty useless. What’s around at <code class="language-console highlighter-rouge"><span class="go">0x0073c500</span></code>? Another set of NIDs?</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre>0073c500 73              ??         73h    s
0073c501 63              ??         63h    c
0073c502 65              ??         65h    e
0073c503 4e              ??         4Eh    N
0073c504 70              ??         70h    p
0073c505 54              ??         54h    T
0073c506 72              ??         72h    r
0073c507 6f              ??         6Fh    o
0073c508 70              ??         70h    p
0073c509 68              ??         68h    h
0073c50a 79              ??         79h    y
0073c50b 00              ??         00h
</pre></div></div> <p>Oh. Oh. Oh. That’s a very pretty <wiki page="Null-terminated string">C String there</wiki>. Let’s go back to <code class="language-console highlighter-rouge"><span class="go">0x0073cd3c</span></code> and follow the other path to <code class="language-console highlighter-rouge"><span class="go">0x00864428</span></code>.</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre>
00864428 00 73 ad 34     addr      XREF[2]:     0073ad3C(R), 0073cd44(*)  
0086442c 00 73 ad 54     addr      XREF[1]:     0073ad5c(R)
00864430 00 73 ad 74     addr      XREF[1]:     0073ad7c(R)
00864434 00 73 ad 94     addr      XREF[1]:     0073ad9c(R)
00864438 00 73 ad b4     addr      XREF[1]:     0073adbc(R)  
0086443c 00 73 ad d4     addr      XREF[1]:     0073addc(R)
00864440 00 73 ad f4     addr      XREF[1]:     0073adfc(R)
00864444 00 73 ae 14     addr      XREF[1]:     0073ae1c(R)
00864448 00 73 ae 34     addr      XREF[1]:     0073ae3c(R)  
0086444c 00 73 ae 54     addr      XREF[1]:     0073ae5c(R)
00864450 00 73 ae 74     addr      XREF[1]:     0073ae7c(R)
</pre></div></div> <p>Now that is..interesting? They all point to something at <code class="language-console highlighter-rouge"><span class="go">0x0073Axx4</span></code> and are XREF at <code class="language-console highlighter-rouge"><span class="go">0x0073AxxC</span></code>?</p> <p>Let’s check that, plus along the way, Ghidra (and ChatGPT) have been nice enough to help me to spot functions boundaries in assembly.</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre>0073adf4 39 80 00 00     li         r12,0x0
0073adf8 65 8c 00 86     oris       r12,r12,0x86
0073adfc 81 8c 44 40     lwz        r12,offset sceNpTrophy_func_address_table[6](r)
0073ae00 f8 41 00 28     std        r2,local_res28(r1)
0073ae04 80 0c 00 00     lwz        r0,0x0(r12)=&amp;gt;LAB_0073adf4
0073ae08 80 4c 00 04     lwz        r2,0x4(r12)=&amp;gt;LAB_0073adf8
0073ae0c 7c 09 03 a6     mtspr      CTR,r0
0073ae10 4e 80 04 20     bctr

0073ae14 39 80 00 00     li         r12,0x0
0073ae18 65 8c 00 86     oris       r12,r12,0x86
0073ae1c 81 8c 44 44     lwz        r12,offset sceNpTrophy_func_address_table[7](r)
0073ae20 f8 41 00 28     std        r2=&amp;gt;TOC_BASE,local_res28(r1)
0073ae24 80 0c 00 00     lwz        r0,0x0(r12)=&amp;gt;LAB_0073ae14
0073ae28 80 4c 00 04     lwz        r2,0x4(r12)=&amp;gt;LAB_0073ae18
0073ae2c 7c 09 03 a6     mtspr      CTR,r0
0073ae30 4e 80 04 20     bctr
</pre></div></div> <p>I check once, twice, three times, yell “WTF” a few times, think of a meme like “this code duplication could have been a function argument”, but I’m looking at assembly, and furthermore, it’s game console assembly, so maybe there’s a logic here that eludes me.</p> <p>I ask my LLM coworker for its opinion: these are import <code class="language-console highlighter-rouge"><span class="go">stub</span></code> / <code class="language-console highlighter-rouge"><span class="go">trampoline</span></code> functions. They are inserted at <wiki page="Linker (computing)">link time</wiki> to provide a jump from the game code to the real function implementation. I’m still not entirely sure why this is required: I’ve heard a ton about linking, but zilch about stubs.</p> <p>Until I remember something else: on the <wiki page="PlayStation Portable">PSP</wiki><sup id="fnref:psp"><a href="#fn:psp" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>, the homebrew plugins are often distributed as <code class="language-console highlighter-rouge"><span class="go">.prx</span></code> files, and the PS3 homebrew scene often provides <code class="language-console highlighter-rouge"><span class="go">.sprx</span></code> files. Maybe this isn’t just a homebrew thing, but a console thing, the local equivalents of <code class="language-console highlighter-rouge"><span class="go">.dll</span></code> and <code class="language-console highlighter-rouge"><span class="go">.so</span></code> for dynamically loaded code. Wouldn’t that explain why I’ve only found eleven NIDs related to trophies, and not <gh repo="an0nym0u5/PSL1GHT/blob/master/ppu/sprx/todo/FNIDS/libnptrophy">the full seventeen referenced in the SDK</gh>?</p> <p>I dig a bit further and <a href="https://www.psdevwiki.com/ps3/Trophy#Firmware_related_files">find the answer</a>: there are indeed a few <code class="language-console highlighter-rouge"><span class="go">np_trophy_*.sprx</span></code> files.</p> <p>So that explains everything: the binary contains a list of imported NIDs, and a function table. In the static file, those table entries point back to tiny trampoline functions (yes, it’s a loop, I know, it makes me crazy too). At runtime, the loader resolves the requested NIDs from the relevant SPRX modules and replaces those table entries with pointers to the real function descriptors. The stubs then load the real address from there and jump to it.</p> <div class="content-img" style="width: 75%"> <img src="/assets/images/2026-05-03/Oh_Yeah,_It's_All_Coming_Together.jpg" title=" Me, when I finally understand what the fuck that mess was. " /> <div class="img-alt"> <p>Me, when I finally understand what the fuck that mess was.</p> </div> </div> <p>Okay, but seriously, even if I found a trail for trophy unlocking, I’m not really in the mood to annotate all the NIDs one by one, and my limited knowledge of the PS3 might lead me to some very questionable interpretations.</p> <p>Time to pull more guns.</p> <h1 id="from-the-shoulders-of-giants">From the shoulders of giants</h1> <p>I had actually stumbled on the <gh repo="clienthax/Ps3GhidraScripts">Ps3GhidraScripts</gh> repository earlier, but a part of me felt too lazy to even look at it. Which, in hindsight, is a good thing: it saved me from skipping all the discoveries above, and it saved you from missing out on my brilliant prose.</p> <p>That said.</p> <blockquote> <p>It will then parse the information sections and define imports/exports and name the ones it can from the nids file, and then set the TOC.</p> </blockquote> <p>Rinse, repeat. I removed the file from the project, and reimported it. At this point, I realized Ghidra actually had a more fitting language spec for the PS3: <code class="language-console highlighter-rouge"><span class="go">PowerPC:BE:64:A2ALT</span></code>, meaning <code class="language-console highlighter-rouge"><span class="go">64-bit PowerPC</span></code>, <code class="language-console highlighter-rouge"><span class="go">big-endian</span></code>, with AltiVec support. There is actually an EVEN MORE fitting one, but I won’t understand that it is needed<sup id="fnref:arch"><a href="#fn:arch" class="footnote" rel="footnote" role="doc-noteref">7</a></sup> before reversing at least a dozen <code class="language-console highlighter-rouge"><span class="go">struct</span></code>s. But that will be a problem for future me, let’s run those scripts and see the results.</p> <p><img src="/assets/images/2026-05-03/ghidra_stubs.png" alt="The stubs are back!" class="right" width="240px" /></p> <p>True to its documentation, the script set up the function stubs for the trophies, and for other dynamically-linked modules like <code class="language-console highlighter-rouge"><span class="go">sceNpManager</span></code> and <code class="language-console highlighter-rouge"><span class="go">sceNpTus</span></code>. Completely useless to me. The syscalls are a bit funnier to look at, since most of them got the prototype you would expect if you spent enough time coding in Unix C.</p> <p>Oh well, who cares? We’re hunting trophies now!</p> <p>There’s only one function calling <code class="language-console highlighter-rouge"><span class="go">sceNpTrophyUnlockTrophy</span></code>, so let’s follow it and call it <code class="language-console highlighter-rouge"><span class="go">UnlockTrophyWrapper</span></code>. Thanks to the Ghidra disassembly pseudo-C, we can extract a few interesting informations:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="n">s32</span> <span class="n">FFXTrophyPS3Backend</span><span class="o">::</span><span class="n">UnlockWrapper</span><span class="p">(</span><span class="n">FFXTrophyPS3Backend</span> <span class="o">*</span><span class="n">t</span><span class="p">,</span> <span class="n">s32</span> <span class="n">trophy_id</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">s32</span> <span class="n">result</span><span class="p">;</span>
  <span class="n">sysTrophyId</span> <span class="n">platinum_id</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
  
  <span class="n">sys_lwmutex_lock</span><span class="p">(</span><span class="n">trophy_mutex</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

  <span class="n">result</span> <span class="o">=</span> <span class="n">sceNpTrophyUnlockTrophy</span><span class="p">(</span><span class="n">t</span><span class="o">-&amp;gt;</span><span class="n">context</span><span class="p">,</span> <span class="n">t</span><span class="o">-&amp;gt;</span><span class="n">handle</span><span class="p">,</span> <span class="n">trophy_id</span><span class="p">,</span><span class="o">&amp;amp;</span><span class="n">platinum_id</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">result</span> <span class="o">&amp;lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">error_printf</span><span class="p">(</span><span class="n">s_Error</span><span class="o">:</span><span class="n">_sceNpTrophyUnlockTrophy</span><span class="p">()</span><span class="n">_00755a98</span><span class="p">,</span><span class="n">result</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">sys_lwmutex_unlock</span><span class="p">(</span><span class="n">trophy_mutex</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <ul> <li>While <code class="language-console highlighter-rouge"><span class="go">sceNpTrophyUnlockTrophy</span></code> needs a <code class="language-console highlighter-rouge"><span class="go">sysTrophyContext</span></code>, <code class="language-console highlighter-rouge"><span class="go">sysTrophyHandle</span></code>, and a trophy ID, this function takes a single structure, and a trophy ID. So we can know the game passes around a struct of its own (let’s call it <code class="language-console highlighter-rouge"><span class="go">FFXTrophyPS3Backend</span></code>). This also confirms that we’ve moved out of “OS code” and are in “game code”.</li> <li>We’ve found the <code class="language-console highlighter-rouge"><span class="go">fprintf</span></code> equivalent!</li> <li>There are mutexes surrounding the trophy unlock, so the game is handling it and not relying on the system? Funny.</li> <li>Omitted from the sample, the <code class="language-console highlighter-rouge"><span class="go">FFXTrophyPS3Backend</span></code> struct contains a bit map of the already-unlocked trophies? Can’t the system handle that on its own?</li> </ul> <p>That was very interesting. Now let’s see who’s calling that, I’m sure I’ll find a call passing the trophy ID I’m looking for down the chain!</p> <p>Or not:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">ReceiverThread</span><span class="p">(</span><span class="n">FFXTrophyPS3Backend</span> <span class="o">*</span><span class="n">t</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">while</span> <span class="p">(</span><span class="nb">true</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">FFXTrophyEvent</span> <span class="n">event</span><span class="p">;</span>

    <span class="kt">int</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">sys_event_queue_receive</span><span class="p">(</span><span class="n">t</span><span class="o">-&amp;gt;</span><span class="n">eventQueue</span><span class="p">,</span> <span class="o">&amp;amp;</span><span class="n">event</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">sys_ppu_thread_exit</span><span class="p">(</span><span class="n">ret</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">switch</span> <span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="n">command</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">case</span> <span class="n">UNLOCK_TROPHY</span><span class="p">:</span>
        <span class="n">UnlockWrapper</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">event</span><span class="p">.</span><span class="n">trophy_id</span><span class="p">);</span>
        <span class="k">break</span><span class="p">;</span>

      <span class="k">case</span> <span class="n">EXIT</span><span class="p">:</span>
        <span class="n">sys_ppu_thread_exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="k">break</span><span class="p">;</span>

      <span class="nl">default:</span>
        <span class="n">error_printf</span><span class="p">(</span><span class="s">"Unknown event command"</span><span class="p">);</span>
        <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></div></div> <p>While I do want to cry (at carefully synchronized intervals, one eye at a time, to be thread-safe), this gives me two new leads:</p> <ul> <li>There’s something somewhere sending events to this thread, telling it to unlock trophies.</li> <li>There’s a setup somewhere that sets up this thread loop.</li> </ul> <p>Following the setup isn’t really complicated, except that it spans a lot of functions, since it has to set up the <code class="language-console highlighter-rouge"><span class="go">sysTrophyContext</span></code> and the <code class="language-console highlighter-rouge"><span class="go">sysTrophyHandle</span></code>, plus handle all the related error cases. After around eight functions of setup, I get to the function that spawns the trophy initialization (and would you believe it? It’s another thread!).</p> <p>That gives me two new insights:</p> <ul> <li>The struct <code class="language-console highlighter-rouge"><span class="go">FFXTrophyPS3Backend</span></code> contains a boolean that indicates whether the trophy setup thread was started, let’s call it <code class="language-console highlighter-rouge"><span class="go">flagTrophyThreadRegistered</span></code>.</li> <li>The struct also contains a boolean that, if set to false, will just not launch the trophy setup thread<sup id="fnref:trophies"><a href="#fn:trophies" class="footnote" rel="footnote" role="doc-noteref">8</a></sup>.</li> </ul> <p>Wait, what.</p> <p>I check Wikipedia: the <wiki page="Achievement (video games)">PS3 got trophies in 2008</wiki>, and <wiki page="Final Fantasy X/X-2 HD Remaster">that remaster came out in 2013</wiki>. The Switch version, the only achievement-less platform, came out in 2019.</p> <p>Could this mean that, from the start, the remaster wasn’t a PlayStation-centric port, but was built to support multiple achievement backends? Since the Windows/Steam version came out in 2016, and the Xbox One version in 2019, that feels like both overengineering and careful planning.</p> <p>Anyway, going further up that path gets me in game engine setup territory, even finding out some localization backend stuff. I don’t need many languages to find out that I suck at chocobo riding, I need my trophies!</p> <h1 id="finding-our-first-trophies">Finding our first trophies</h1> <p>Actually, a bit around that time, I had advanced in the game, and while I was running some stuff from my laptop<sup id="fnref:laptop"><a href="#fn:laptop" class="footnote" rel="footnote" role="doc-noteref">9</a></sup>, I managed to legitimately obtain <code class="language-console highlighter-rouge"><span class="go">Chocobo Rider</span></code>. But things were too much fun to stop now, plus maybe I could leverage all the things I found at some other point.</p> <p><img src="/assets/images/2026-05-03/ps3_sys_events.png" alt="Ports and queues" class="right" width="300px" /></p> <p>So, let’s get back: we know there’s an event queue somewhere, as <code class="language-console highlighter-rouge"><span class="go">sys_event_queue_receive</span></code> told us. Next step is easy, just look up <code class="language-console highlighter-rouge"><span class="go">sys_event_queue_send</span></code>, right? Well, as you can see from the screenshot, not exactly, and that’s where I’m glad that <gh repo="clienthax/Ps3GhidraScripts/blob/master/ghidra_scripts/DefinePS3Syscalls.java">a script had already mapped the syscalls for me</gh>, so I could easily find <code class="language-console highlighter-rouge"><span class="go">sys_event_port_send</span></code>.</p> <p>While it is smart, in hindsight, to send events from multiple sources on a <code class="language-console highlighter-rouge"><span class="go">port</span></code> to receive them from a <code class="language-console highlighter-rouge"><span class="go">queue</span></code>, that’s the kind of API design that makes you understand why <wiki>Mark Cerny</wiki> spent the PS4 generation trying to win back the developers.</p> <p>But still, there are only seven uses of <code class="language-console highlighter-rouge"><span class="go">sys_event_port_send</span></code>, and right at the second one:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">s32</span> <span class="nf">LAB_001af1e0</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="n">param_1</span><span class="p">,</span> <span class="kt">long</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">s32</span> <span class="n">result</span><span class="p">;</span>
  
  <span class="n">result</span> <span class="o">=</span> <span class="n">sys_event_port_send</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x2c</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">param_2</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">result</span> <span class="o">&amp;lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">error_printf</span><span class="p">(</span><span class="n">s_Error_</span><span class="o">:</span><span class="n">_sys_event_port_send</span><span class="p">()</span><span class="n">_fa_00755884</span><span class="p">,(</span><span class="kt">long</span><span class="p">)</span><span class="n">result</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <p>This looks very unassuming, and it would be easy to dismiss it, but there’s just something about it that screams at me: “You got it bro!”, and it’s because of assembly and structs.</p> <p>See, while I’ve written some assembly<sup id="fnref:asm"><a href="#fn:asm" class="footnote" rel="footnote" role="doc-noteref">10</a></sup> for fun, I’ve never done more than small functions, and never used anything beyond basic types. So I had always wondered: how does assembly deal with <code class="language-console highlighter-rouge"><span class="go">struct</span></code> members?</p> <p>This reverse-engineering endeavor is where I got the answer: it doesn’t. It just passes the struct’s base address, plus the member’s offset<sup id="fnref:struct"><a href="#fn:struct" class="footnote" rel="footnote" role="doc-noteref">11</a></sup>. And that <code class="language-console highlighter-rouge"><span class="go">+ 0x2c</span></code>? I’ve seen it before, when I first defined the <code class="language-console highlighter-rouge"><span class="go">FFXTrophyPS3Backend</span></code> struct (which got split with <code class="language-console highlighter-rouge"><span class="go">FFXTrophyContext</span></code> as its base)!</p> <p>Of course, it could be another structure with a similar layout, but the five last candidates all use very different offsets (as for the first candidate, it’s a trophy context teardown function). So let’s assume this one is our <code class="language-console highlighter-rouge"><span class="go">FFXTrophyPS3Backend::PostEvent</span></code> function, and move up the two XREFs. For funsies, let’s take the second one, since it’s the one I went with, and my first foray into the game code.</p> <p>The prototype looks promising: <code class="language-console highlighter-rouge"><span class="go">param_1</span></code> is obviously our <code class="language-console highlighter-rouge"><span class="go">FFXTrophyContext</span></code>, and <code class="language-console highlighter-rouge"><span class="go">param_2</span></code> is an ID, then there’s a loop. A loop that only runs five times. The game got 34 trophies, so that cannot be here, but let’s keep digging what happens during those five iterations: first, <code class="language-console highlighter-rouge"><span class="go">param_2</span></code> is compared to a global <code class="language-console highlighter-rouge"><span class="go">g_0075291c + loop</span></code> variable, and if equal, a trophy event is sent with <code class="language-console highlighter-rouge"><span class="go">g_0075291c + loop + 0x4</span></code> as the trophy ID. That’s crazy, what the hell is in <code class="language-console highlighter-rouge"><span class="go">g_0075291c</span></code>?</p> <div class="language-text highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>0075291c 00 00 01 14
00752920 00 00 00 1f
00752924 00 00 01 58
00752928 00 00 00 1e
0075292c 00 00 00 82
00752930 00 00 00 0a
00752934 00 00 00 b0
00752938 00 00 00 20
0075293c 00 00 00 83
00752940 00 00 00 0b
</pre></div></div> <p>So, first value is the check value, and the second would be the trophy being unlocked. Let’s see, if I convert these to decimal, I get: 31, 30, 10, 32, 11. And assuming the Platinum trophy is zero, those would map to trophies…</p> <ul> <li>31: <strong>Overcoming the Nemesis</strong>: <em>Defeat Nemesis</em></li> <li>30: <strong>Perseverance</strong>: <em>Defeat Penance</em></li> <li>10: <strong>Overcoming the Past</strong>: <em>Defeat Yunalesca</em></li> <li>32: <strong>The Eternal Calm</strong>: <em>Defeat Yu Yevon</em></li> <li>11: <strong>The Destination of Hatred</strong>: <em>Defeat Seymour Omnis</em></li> </ul> <p>I quickly make a new <code class="language-console highlighter-rouge"><span class="gp">struct FFXTrophyForBoss { s32 enemyId;</span><span class="w"> </span>sysTrophyId trophyId<span class="p">;</span> <span class="o">}</span><span class="p">;</span></code>, map it to the variable, and check the code. The disassembly is a bit wonky on indexes, but the result is telling:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="n">s32</span> <span class="n">ffxTrophyUnlock</span><span class="o">::</span><span class="n">DefeatEnemy</span><span class="p">(</span><span class="n">FFXTrophyContext</span> <span class="o">*</span><span class="n">gameObject</span><span class="p">,</span><span class="kt">int</span> <span class="n">enemyId</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&amp;lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">enemyId</span> <span class="o">==</span> <span class="n">g_ffxTrophyForBosses</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">enemyId</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">s32</span> <span class="n">trophyId</span> <span class="o">=</span> <span class="n">g_ffxTrophyForBosses</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">trophyId</span><span class="p">;</span>
      <span class="n">FFXTrophyPS3Backend</span><span class="o">::</span><span class="n">PostEvent</span><span class="p">(</span>
        <span class="o">&amp;amp;</span><span class="n">gameObject</span><span class="o">-&amp;gt;</span><span class="n">trophyObject</span><span class="p">,</span>
        <span class="n">trophyId</span><span class="p">);</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></div></div> <p>Even after a second and a third look at the trophy list, indeed, only those five are related to a “Defeat X” goal. Which means I have found an entry into the combat system. In retrospect, calling it <code class="language-console highlighter-rouge"><span class="go">enemyId</span></code> feels wrong, and I’m 99% sure this ID is closer to being an <code class="language-console highlighter-rouge"><span class="go">encounterID</span></code>.</p> <p>Okay, I don’t feel like going all the way up the combat system, so let’s go back a step and check the other routes that set those trophies!</p> <p>At this point, there are two separate functions acting as gates, one checking that a trophy backend is enabled, and the other checking whether the target trophy has been unlocked, which feels sub-optimal, since those are checked in a million places already. Give me back those cycles!</p> <p>After a few more misdirections, we get to a function being XREF eighteen times. Now THAT looks promising, since we only have twenty-eight more trophies to find!</p> <h1 id="more-functions-more-options">More functions, more options</h1> <p>Along the way, I’ve looked into the existing cheats for my version of the game to map more stuff. For instance, I now know where items are located. Could that be useful? Yes!</p> <h2 id="ffxcombateffectsteal">ffxCombatEffect::Steal</h2> <p>The first function is 157 instructions long, full of conditions, but at some point, even with Ghidra’s strange disassembly, it’s easy to understand:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="n">g_02059010</span> <span class="o">=</span> <span class="n">g_02059010</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">((</span><span class="o">*</span><span class="p">(</span><span class="n">param_2</span> <span class="o">+</span> <span class="mh">0xded</span><span class="p">)</span> <span class="o">==</span> <span class="sc">'\x06'</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="p">(</span><span class="mi">199</span> <span class="o">&amp;lt;</span> <span class="n">g_02059010</span><span class="p">))</span> <span class="p">{</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">FFX</span><span class="o">::</span><span class="n">InventoryItem</span><span class="o">::</span><span class="n">ModifyInventoryRow</span><span class="p">(</span>
  <span class="o">*</span><span class="p">(</span><span class="n">FFX</span><span class="o">::</span><span class="n">InventoryItem</span><span class="o">::</span><span class="n">InventoryRowItem</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_4</span> <span class="o">+</span> <span class="mi">10</span><span class="p">),</span>
  <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="kt">short</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_4</span> <span class="o">+</span> <span class="mi">8</span><span class="p">));</span>
</pre></div></div> <p>There are two trophies where the number 200 is relevant, but there’s only one that modifies your inventory: <strong>A Talent for Acquisition</strong>: <em>Steal successfully with Rikku 200 times</em>. So we can map that function, and we know where <code class="language-console highlighter-rouge"><span class="go">g_ffxTrophyUnlockRikkuStealCount</span></code> is located. We can also assume very easily that <code class="language-console highlighter-rouge"><span class="go">param_2</span></code> is a kind of <code class="language-console highlighter-rouge"><span class="go">struct PlayerCharacter</span></code> with <code class="language-console highlighter-rouge"><span class="go">0xded</span></code> being the character ID, which will become very interesting on the next function.</p> <h2 id="ffxcombateffectlancet">ffxCombatEffect::Lancet</h2> <p>Next up is 632 instructions, 28 local variables, and yet…</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((</span><span class="n">g_02058fcc</span> <span class="o">&amp;gt;&amp;gt;</span> <span class="mi">8</span> <span class="o">==</span> <span class="mh">0xff</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span>
    <span class="p">((</span><span class="n">g_02058fce</span> <span class="o">&amp;amp;</span> <span class="mh">0xf</span><span class="p">)</span> <span class="o">==</span> <span class="mh">0xf</span><span class="p">))</span> <span class="p">{</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x19</span><span class="p">);</span>
<span class="p">}</span>
</pre></div></div> <p>Found it: <strong>Learning!</strong>: <em>Learn to use all enemy abilities</em>. So if we check the code, we can see the first global check covers 8 bits, and the second covers 4 more, for a total of 12 flags, which matches the <a href="https://finalfantasy.fandom.com/wiki/Ronso_Rage#Ronso_Rages">12 Ronso Rages</a>. Perfect.</p> <p>This leads to an interesting bit of trophy design. By the end of Final Fantasy X, characters can learn nearly every ability, and very few things remain exclusive<sup id="fnref:summon"><a href="#fn:summon" class="footnote" rel="footnote" role="doc-noteref">12</a></sup>. You can use Steal with Kimahri, and Lancet with Rikku. Yet the stealing trophy specifically checks for Rikku, while the Lancet trophy only cares that all enemy abilities were learned, regardless of who triggered the final check.</p> <h2 id="ffxcombateffectbribe">ffxCombatEffect::Bribe</h2> <p>Let’s go: 1029 instructions, 54 local variables, and the answer is right in front of our eyes again:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">g_02059014</span> <span class="o">=</span> <span class="n">g_02059014</span> <span class="o">+</span> <span class="n">g_ffxEncounterBaseStruct</span><span class="p">.</span><span class="n">field11_0x118</span><span class="p">.</span><span class="n">_5288_4_</span><span class="p">;</span>
<span class="k">if</span> <span class="p">((</span><span class="n">DAT_0205e748</span> <span class="o">==</span> <span class="mh">0x302a</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="p">(</span><span class="mi">99999</span> <span class="o">&amp;lt;</span> <span class="n">g_02059014</span><span class="p">))</span> <span class="p">{</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x13</span><span class="p">);</span>
<span class="p">}</span>
</pre></div></div> <p>Since that trophy is <strong>Under the Table</strong>: <em>Spend 100,000 gil or more in bribes</em>, it makes sense to call this global <code class="language-console highlighter-rouge"><span class="go">g_ffxTrophyUnlockBribeTotal</span></code> and not bother about those strange structures we cannot make sense of. Who would have thought it would be this easy?</p> <h2 id="ffxtrophyunlockjechtspheres">ffxTrophyUnlock::JechtSpheres</h2> <p>By this point, I don’t even bother to read the rest of the code anymore</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">g_ffxTrophyUnlockJechtSpheresCount</span> <span class="o">==</span> <span class="sc">'\b'</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">g_ffxTrophyUnlockJechtSpheresIsObtained</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="p">((</span><span class="n">g_ffxTrophyUnlockJechtSpheresCount</span> <span class="o">==</span> <span class="sc">'\t'</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span>
        <span class="p">(</span><span class="n">g_ffxTrophyUnlockJechtSpheresIsObtained</span> <span class="o">==</span> <span class="nb">false</span><span class="p">))</span> <span class="p">{</span>
  <span class="n">g_ffxTrophyUnlockJechtSpheresIsObtained</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x14</span><span class="p">);</span>
<span class="p">}</span>
</pre></div></div> <p>Yes, the hardest part is to count in hexadecimal to map <code class="language-console highlighter-rouge"><span class="go">0x14</span></code> to the correct trophy: <strong>Messenger from the Past</strong>: <em>Obtain all Jecht Spheres</em>. At this point, I’ll just remember that <code class="language-console highlighter-rouge"><span class="go">'\t'</span></code> is 9 in the <wiki>ASCII</wiki> table and call it a day. What tedious work!</p> <h2 id="fun_00365344">FUN_00365344</h2> <p>However, some parts remain cryptic:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">g_ffxTrophyUnlockDamage9999NotObtained</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x12</span><span class="p">);</span>
  <span class="n">g_ffxTrophyUnlockDamage9999NotObtained</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">g_ffxTrophyUnlockDamage99999NotObtained</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x15</span><span class="p">);</span>
  <span class="n">g_ffxTrophyUnlockDamage99999NotObtained</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <p>Two more: <strong>Power Strike</strong>: <em>Do 9999 damage or more in a single attack</em> and <strong>Mega Strike</strong>: <em>Deal 99999 damage with one attack</em>. The logic is obvious, but the only XREFs to these globals are right here. So clearly, something else is setting those two flags to <code class="language-console highlighter-rouge"><span class="go">true</span></code>, but I haven’t mapped that part yet.</p> <h2 id="ffxtrophyunlockaeons">ffxTrophyUnlock::Aeons</h2> <p>At that point, I had made good progress in the game, but despite following a walkthrough, I had trouble obtaining the Magus Sisters Aeons, because I was trying to obtain them before Yojimbo. It took another guide to find out I had to obtain all other Aeons before.</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">uVar1</span> <span class="o">==</span> <span class="mh">0x11</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* Trophy 15: Delta Attack! (Obtain Magus Sisters) */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0xf</span><span class="p">);</span>
  <span class="cm">/* Trophy 26: Summon Master (Obtain all Aeons) */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x1a</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">uVar1</span> <span class="o">==</span> <span class="mh">0xd</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* Trophy 13: Feel the Pain (Obtain Anima) */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0xd</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">uVar1</span> <span class="o">==</span> <span class="mh">0xe</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* Trophy 14: It's All About the Money (Obtain Yojimbo) */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0xe</span><span class="p">);</span>
<span class="p">}</span>
</pre></div></div> <p>The code confirmed what my guide wasn’t telling: Unlocking the Magus Sisters implicitly meant all Aeons had been obtained.</p> <h2 id="ffxtrophyunlocktheatersphere">ffxTrophyUnlock::TheaterSphere</h2> <p>No idea what that <code class="language-console highlighter-rouge"><span class="go">g_FFXGlobalState</span></code> even is, but I looked at it a few times.</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((((</span><span class="n">g_FFXGlobalState</span> <span class="o">==</span> <span class="mh">0x9e</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="p">(</span><span class="n">g_ffxTrophyUnlockTheaterSpheresMovies</span> <span class="o">==</span> <span class="sc">'2'</span><span class="p">))</span> <span class="o">&amp;amp;&amp;amp;</span>
    <span class="p">(</span><span class="n">g_ffxTrophyUnlockTheaterSpheresMusic</span> <span class="o">==</span> <span class="sc">'G'</span><span class="p">))</span> <span class="o">&amp;amp;&amp;amp;</span>
    <span class="p">(</span><span class="n">g_ffxTrophyUnlockTheaterSpheresNotObtained</span> <span class="o">==</span> <span class="nb">true</span><span class="p">))</span> <span class="p">{</span>
  <span class="n">g_ffxTrophyUnlockTheaterSpheresNotObtained</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="cm">/* 16: Theater Enthusiast (Buy every sphere at the Luca Theater) */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x10</span><span class="p">);</span>
<span class="p">}</span>
</pre></div></div> <p>And once again, the <wiki>ASCII</wiki> table comes to the rescue: <code class="language-console highlighter-rouge"><span class="go">'2'</span></code> is 50, and <code class="language-console highlighter-rouge"><span class="go">'G'</span></code> is 71, matching the number of movie and music spheres listed in every guide.</p> <h2 id="ffxtrophyunlocketernalcalmmovie">ffxTrophyUnlock::EternalCalmMovie</h2> <p>An easy one:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">DAT_008f5a18</span> <span class="o">==</span> <span class="mh">0x3a</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* Trophy 33: A Journey's Catalyst (View "Eternal Calm") */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x21</span><span class="p">);</span>
  <span class="n">DAT_008f5a18</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <p>No idea what those globals are.</p> <h2 id="ffxtrophyunlockalbhedprimers">ffxTrophyUnlock::AlBhedPrimers</h2> <p>This one was actually the first one I mapped out and it was a LOT of fun:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="n">s32</span> <span class="n">ffxTrophyUnlock</span><span class="o">::</span><span class="n">AlBhedPrimers</span><span class="p">(</span><span class="kt">int</span> <span class="n">alBhedPrimerIndex</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">alBhedPrimerIndex</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">g_ffxAlBhedBitmap</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">alBhedPrimerIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">((</span><span class="n">alBhedPrimerIndex</span> <span class="o">&amp;lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="mh">0x19</span> <span class="o">&amp;lt;</span> <span class="n">alBhedPrimerIndex</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">alBhedPrimerIndex</span> <span class="o">==</span> <span class="mh">0xff</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">g_ffxAlBhedBitmap</span> <span class="o">=</span> <span class="mh">0x3ffffff</span><span class="p">;</span>
      <span class="n">alBhedPrimerIndex</span> <span class="o">=</span> <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x1c</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="p">{</span>
    <span class="n">g_ffxAlBhedBitmap</span> <span class="o">=</span> <span class="n">g_ffxAlBhedBitmap</span> <span class="o">|</span> <span class="mi">1</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="p">(</span><span class="n">alBhedPrimerIndex</span> <span class="o">&amp;amp;</span> <span class="mh">0x3fU</span><span class="p">);</span>
    <span class="cm">/* Trophy 01: Speaking in Tongues (Find 1 Al Bhed Primer) */</span>
    <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
    <span class="n">alBhedPrimerIndex</span> <span class="o">=</span> <span class="n">g_ffxAlBhedBitmap</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">g_ffxAlBhedBitmap</span> <span class="o">==</span> <span class="mh">0x3ffffff</span><span class="p">)</span> <span class="p">{</span>
      <span class="cm">/* Trophy 28: Master Linguist (Find all 26 Al Bhed Primers) */</span>
      <span class="n">alBhedPrimerIndex</span> <span class="o">=</span> <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x1c</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">alBhedPrimerIndex</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <p>There are 26 “Al Bhed Primer” items in the game, which maps perfectly to a 26-bit bitmap.</p> <p>Everything in this function is crystal clear:</p> <ul> <li>There is a “reset” path.</li> <li>The normal path is triggered only if the index is below 26 (indexes start at 0): the bitmap is updated, the <strong>Speaking in Tongues</strong> trophy is always triggered, and then the bitmap is checked against a full 26-bit mask for the <strong>Master Linguist</strong> trophy.</li> <li>There is a debug path that gives out <strong>Master Linguist</strong>, but not the <strong>Speaking in Tongues</strong>, which could lead to a very funny outcome of getting <strong>Master Linguist</strong> before <strong>Speaking in Tongues</strong>, which should be impossible.</li> </ul> <p>This is one of the rare places where the game logic is both simple and perfectly aligned with the data model, which isn’t the case everywhere…</p> <h2 id="ffxtrophyunlockspheregrid">ffxTrophyUnlock::SphereGrid</h2> <p>While this function is short, it’s full of <code class="language-console highlighter-rouge"><span class="go">if</span></code> and other strange control flow I’d rather not touch, so let’s only look at the relevant parts:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">unaff_r28</span> <span class="o">&amp;lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* Trophy 23: Sphere Master (Complete a Sphere Grid for one character) */</span>
  <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x17</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="cm">/* Trophy 29: Perfect Sphere Master (Complete the Sphere Grids for all main characters) */</span>
<span class="n">sVar9</span> <span class="o">=</span> <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="mh">0x1d</span><span class="p">);</span>
</pre></div></div> <p>That was easy to map, and easier to discard.</p> <h2 id="fun_003103f8">FUN_003103f8</h2> <p>Strangely, this one is one level higher in the call chain, and calls <code class="language-console highlighter-rouge"><span class="go">ffxTrophyTryPostById</span></code> while all others above used <code class="language-console highlighter-rouge"><span class="go">ffxTrophyTryPostIfGateOpen</span></code>. The end result is the same, since both paths will call <code class="language-console highlighter-rouge"><span class="go">ffxTrophyGateCheck</span></code>, but it’s still surprising that the code would be structured that way:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">DAT_008e55cc</span> <span class="o">!=</span> <span class="n">DAT_008e55c8</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">DAT_008e55cc</span> <span class="o">=</span> <span class="n">DAT_008e55c8</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">DAT_008e55c8</span> <span class="o">==</span> <span class="mh">0x933</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">sVar7</span> <span class="o">=</span> <span class="n">ffxTrophyGateCheck</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">sVar7</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="cm">/* Trophy 06: Heartstrings (View the "Underwater Date" scene) */</span>
      <span class="n">ffxTrophyTryPostById</span><span class="p">(</span><span class="mi">6</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">DAT_008e55c8</span> <span class="o">==</span> <span class="mh">0x43d</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">sVar7</span> <span class="o">=</span> <span class="n">ffxTrophyGateCheck</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">sVar7</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="cm">/* Trophy 05: All Together (All party members come together) */</span>
      <span class="n">ffxTrophyTryPostById</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">((</span><span class="n">DAT_008e55c8</span> <span class="o">==</span> <span class="mh">0xa4</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="p">(</span><span class="n">sVar7</span> <span class="o">=</span> <span class="n">ffxTrophyGateCheck</span><span class="p">(),</span> <span class="n">sVar7</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">))</span> <span class="p">{</span>
    <span class="cm">/* Trophy 03: The Right Thing (Clear the Besaid Cloister of Trials) */</span>
    <span class="n">ffxTrophyTryPostById</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="k">else</span> <span class="nf">if</span> <span class="p">((</span><span class="n">DAT_02055e99</span> <span class="o">==</span> <span class="mh">0x7f</span><span class="p">)</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="p">(</span><span class="n">g_obtainedAllCelestialWeaponsTrophy</span> <span class="o">==</span> <span class="mi">0</span><span class="p">))</span> <span class="p">{</span>
  <span class="n">g_obtainedAllCelestialWeaponsTrophy</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="n">sVar7</span> <span class="o">=</span> <span class="n">ffxTrophyGateCheck</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">sVar7</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/* Trophy 27: Weapon Master (Obtain all Celestial Weapons) */</span>
    <span class="n">ffxTrophyTryPostById</span><span class="p">(</span><span class="mh">0x1b</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></div></div> <p>The first three are story-related beats, while the last one is clearly a bitmask check against the set of obtained Celestial Weapons.</p> <h1 id="dude-wheres-my-chocobo">Dude, where’s my Chocobo?</h1> <p>Out of 33 trophies, I have now mapped out how to obtain 25 of them, and still no chocobo in sight. Let’s recap the eight remaining trophies:</p> <ul> <li>02 <strong>Teamwork!</strong>: <em>Win a blitzball match</em></li> <li>07 <strong>Show Off!</strong>: <em>Win a blitzball tournament</em></li> <li>08 <strong>Striker</strong>: <em>Learn the Jecht shot</em></li> <li>09 <strong>Chocobo License</strong>: <em>Pass all chocobo training</em></li> <li>10 <strong>Lightning Dancer</strong> <em>Dodge 200 lightning strikes and obtain the reward</em></li> <li>17 <strong>Chocobo Rider</strong>: <em>Win a race with a catcher chocobo with a total time of 0:0:0</em></li> <li>22 <strong>Chocobo Master</strong>: <em>Get 5 treasure chests during the Chocobo Race at Remiem Temple and win the race</em></li> <li>24 <strong>Blitzball Master</strong>: <em>Unlock all slot reels</em></li> </ul> <p>None of those seems to match any of the previous trophy categories, and we have three new categories: blitzball minigame, chocobo minigames, and that 200 lightning strikes nightmare. And only one call to <code class="language-console highlighter-rouge"><span class="go">ffxTrophyTryPostIfGateOpen</span></code> still unmapped:</p> <div class="language-c highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="n">s32</span> <span class="nf">ffxTrophyRemapAndTryPost</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">param_1</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">param_2</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">param_3</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">param_4</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">s32</span> <span class="n">trophyId</span><span class="p">;</span>
  <span class="n">trophyId</span> <span class="o">=</span> <span class="n">FUN_00337f30</span><span class="p">(</span><span class="n">param_1</span><span class="p">,</span> <span class="n">param_2</span><span class="p">,</span> <span class="n">param_3</span><span class="p">,</span> <span class="n">param_4</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">trophyId</span> <span class="o">!=</span> <span class="mh">0x16</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">trophyId</span> <span class="o">==</span> <span class="mh">0x17</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">trophyId</span> <span class="o">=</span> <span class="mh">0x16</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">trophyId</span> <span class="o">==</span> <span class="mh">0x19</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">trophyId</span> <span class="o">=</span> <span class="mh">0x18</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">ffxTrophyTryPostIfGateOpen</span><span class="p">(</span><span class="n">trophyId</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">trophyId</span><span class="p">;</span>
<span class="p">}</span>
</pre></div></div> <p>That is… a very strange bit of code. If the computed trophy ID is <code class="language-console highlighter-rouge"><span class="go">22</span></code>, it does nothing. If it is <code class="language-console highlighter-rouge"><span class="go">23</span></code>, it remaps it to <code class="language-console highlighter-rouge"><span class="go">22</span></code>. If it is <code class="language-console highlighter-rouge"><span class="go">25</span></code>, it remaps it to <code class="language-console highlighter-rouge"><span class="go">24</span></code>. Every other value goes through unchanged, and the final result is passed to <code class="language-console highlighter-rouge"><span class="go">ffxTrophyTryPostIfGateOpen</span></code>.</p> <p>By this point, I’m hoping I can just go up the chain, straight to my minigames, and my hopes are crushed: I end up in a TOC pointer, with a single XREF, and no way to find out who’s calling it without mapping more of the code. And looking at the pseudo-code sends a chill down my spine: I may be looking at a virtual machine.</p> <p>In spite of everything, I give it one more try and decide to go for even bigger guns: other humans! I find a <gh repo="nyterage/FFXRando">repository dedicated to a FFX game randomizer</gh>, and even <a href="https://discord.com/invite/vEu5wkjXGv">a Discord dedicated to reverse-engineering the full game</a>, who nicely give me access to their work and findings on the Steam and Switch versions of the game.</p> <p>Unfortunately, my worst fears are confirmed: there is a scripting language in there, and a script engine to run it.</p> <h1 id="learning-to-give-up">Learning to give up</h1> <p>By this point, I’m starting to realize it’s the second day in a row I’m going to bed at 6 AM. I can recognize the <wiki page="Function prologue and epilogue">prologue and epilogue of a function</wiki> at a glance. I even had a weird dream about assembly, where I made assumptions about code that I then confirmed when I woke up. And I haven’t actually played the game in almost a week.</p> <p>I’m at a crossroads.</p> <p>Going through the game’s code has been a ton of fun. I’ve learned how <wiki page="Virtual method table">C++ vtables</wiki> actually work with structs, assembly, and inheritance. I’ve even discovered the game uses <wiki page="Linked list">intrusive linked lists</wiki>, just like the Linux kernel.</p> <p>On the other hand, we’re talking about a 9.4 MB binary: that’s 2,352,212 instructions. Even ignoring the <code class="language-console highlighter-rouge"><span class="go">.bss</span></code> segment and system stubs, it’s still a massive undertaking. And at this point, I’m not even sure why I’m doing it anymore. Or rather: is cheating my way to one last trophy really worth all that much work?</p> <p>As I ponder the question, I notice the rules to the Discord I just joined:</p> <blockquote> <ol> <li>Don’t stress yourself about contributing to modding/reverse engineering. <br /> If your current situation doesn’t allow it it’s no big deal. Always put your well being first.</li> </ol> </blockquote> <p>I’m not stressing myself, but I know a part of me could keep going on and on over this binary, down to its last byte, until I burn myself out.</p> <p>Which would have been stupid, since I got the trophy the regular way less than two days later. Turns out, the answer is in the classics: winning is knowing when not to play.</p> <h1 id="thanks">Thanks</h1> <p>Huge thanks to:</p> <ul> <li> <p>The <gh repo="nationalsecurityagency/ghidra">Ghidra</gh> team for making reverse-engineering actually fun</p> </li> <li> <p>The <gh repo="RPCS3/rpcs3">RPCS3</gh> team for making this whole mess possible</p> </li> <li> <p>The <gh repo="clienthax/Ps3GhidraScripts">Ps3GhidraScripts</gh> contributors for saving me hours of setting up NIDs</p> </li> <li> <p>The FFX reverse-engineering Discord, <a href="https://discord.com/invite/vEu5wkjXGv">Cid’s Salvage Ship</a>, for giving me access to their knowledge, and telling me to touch grass</p> </li> <li> <p>My step-brother for gifting me his PS3 on a whim twelve years ago</p> </li> <li> <p>My sister for dealing with my live commentary over WhatsApp for three weeks</p> </li> </ul> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:cheatscan"> <p>A problem I am <gh repo="joshleaves/cheatscan">actively trying to solve</gh>. <a href="#fnref:cheatscan" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:not-knowing"> <p>And isn’t that actually for the better, since it means I’m gonna learn stuff? <a href="#fnref:not-knowing" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:msb"> <p><code class="language-console highlighter-rouge"><span class="go">Most Significant Byte</span></code>, meaning <wiki page="Endianness">Big Endian</wiki>. <a href="#fnref:msb" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:psvita"> <p>Obviously, I bought the <a href="https://www.psdevwiki.com/vita/File:Pch1100ab03_front_ss.png">Cosmic Red</a> version. <a href="#fnref:psvita" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:xref"> <p>In Ghidra, an XREF is a reference from another part of the binary. <a href="#fnref:xref" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:psp"> <p>Actually my first console ever, which I acquired in 2008 in its <wiki page="Crisis Core: Final Fantasy VII">Crisis Core</wiki> limited edition. <a href="#fnref:psp" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:arch"> <p>And I will only discover it exists while writing this article: the processor is 64-bit, but the PS3 uses 32-bit addressing. <a href="#fnref:arch" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:trophies"> <p>Therefore sparing me one of the sources of my ADHD. <a href="#fnref:trophies" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:laptop"> <p>The reason it’s a laptop is because it’s ALWAYS on my lap, even when I’m in front of my TV playing console games. <a href="#fnref:laptop" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:asm"> <p>My greatest hit was writing a recursive <code class="language-console highlighter-rouge"><span class="go">strlen(3)</span></code>, and it took me until this year to learn how this was a stupid idea. <a href="#fnref:asm" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:struct"> <p>Struct size arithmetics being another nice C subject, but, like everything C and beautiful: deterministic! <a href="#fnref:struct" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:summon"> <p>Except “Summon” which is exclusive to Yuna. <a href="#fnref:summon" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>Because you cannot hate chocobos enough</summary> </entry> <entry> <title>macOS Document Icons: a composition system exists (good luck finding it!)</title> <link href="https://tech.dreamleaves.org/posts/macos-document-icons-composition-exists/" rel="alternate" type="text/html" title="macOS Document Icons: a composition system exists (good luck finding it!)" /> <published>2026-04-16T00:00:00+02:00</published> <updated>2026-04-16T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/macos-document-icons-composition-exists/</id> <content type="html"><![CDATA[ <p>Still building stuff for <a href="https://spark-club.fr/">Spark-Club</a>, and after way too many rewrites of my FFI<sup id="fnref:ffi"><a href="#fn:ffi" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, the <a href="/posts/making-a-raspberry-pi-image-builder/">Spark-Flasher</a> desktop app was coming together, with every new commit bringing either an important feature, or some small UX polish.</p> <p><img src="/assets/images/2026-04-16/spark-flasher-logo.png" alt="&amp;quot;i can haz graphix dezign&amp;quot;" class="right" width="80px" /></p> <p>While I did dabble in image manipulation<sup id="fnref:psp"><a href="#fn:psp" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, and had a long <wiki page="Adobe Illustrator">Illustrator</wiki> phase, I am not a designer, not even close. So when I designed the app icon, I just took the <strong>Spark-Club</strong> logo, slapped a <a href="https://www.flaticon.com/free-icon/flash_252590">free “flash” icon from Flaticon</a> over it, and I called it a day.</p> <p>A few commits and features later, as I was learning how to associate the <code class="language-console highlighter-rouge"><span class="go">.spark-pi</span></code> bundles to my application, I noticed they had no icon<sup id="fnref:defaulticon"><a href="#fn:defaulticon" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>. Right, obviously, I needed an icon (or as Apple calls them: “Document Icons”) for the app, and an icon for the documents. If I wanted my <code class="language-console highlighter-rouge"><span class="go">.spark-pi</span></code> files to stand out, I needed to give them identity: this time, I would slap a <a href="https://www.flaticon.com/free-icon/raspberry_680943">free “raspberry” icon from Flaticon</a> on top of the logo. Oh well, how hard can that be?</p> <h1 id="the-best-kept-secret-of-macos-app-development">The best-kept secret of macOS app development</h1> <p>Things you can hear in love stories and macOS app development: “How do you turn the page?”</p> <p>Sure, I can stack layers, but while browsing my <code class="language-console highlighter-rouge"><span class="go">Downloads</span></code> folder to see how other icons worked, I noticed it: they all had the page curl. Yes, you KNOW which one. Every document got it, and it’s almost ALWAYS the same, as if there were a single tool generating them.</p> <p>But, no, whichever tool I was looking at, <a href="https://github.com/SAP/macOS-icon-generator">even in open-source</a>, and even <a href="https://developer.apple.com/icon-composer/">the official one</a>, I could find this insidious page curl anywhere! No matter what keywords I used, I would only get results about “Here is how you add <em>the icon you already made like the wonderful person that you are</em> into your app”.</p> <p><img src="/assets/images/2026-04-16/the-two-curls.png" alt="The elusive two curls" class="right" width="130px" /></p> <p>The closest suggestion was to make an icon off Apple’s <code class="language-console highlighter-rouge"><span class="go">GenericDocumentIcon.icns</span></code><sup id="fnref:genericdocumenticon"><a href="#fn:genericdocumenticon" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>, which…was actually something I could do, but even then, this one had a rounded curl, while most of the icons on my machine used a square curl? The mystery deepened: there were two curls: one was for “real things”, and the other was “generic”, and I didn’t want that for my app!</p> <p>I was clearly missing something, and the best next step was to gaze at the situation from atop the shoulders of giants. In this case: <gh repo="transmission/transmission/blob/main/macosx/Info.plist">Transmission</gh>. The project was mature, open-source, perfectly integrated, and they had a beautiful document icon, so there should have been something in there, helping me, no? No. Their document icon was <a href="https://github.com/transmission/transmission/blob/main/macosx/Images/TransmissionDocument.icns">a perfect <code class="language-console highlighter-rouge"><span class="go">.icns</span></code> file</a> with raster graphics, and trying to edit my half-assed logo over it was beyond my capabilities.</p> <p>I was getting nowhere that wasn’t in a frustrated state.</p> <h1 id="going-to-the-source">Going to the source</h1> <p>Something great about Apple<sup id="fnref:consistency"><a href="#fn:consistency" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>, is the rule that “an app made for Apple must work just like any other app on Apple”. That’s how you do proper UX, that’s how you don’t end up with a crazy designer trying to sell you their “framework on design” to justify a huge bill. So clearly, somewhere in their <a href="https://developer.apple.com/design/human-interface-guidelines">design guidelines</a>, there must have been a guide about that. Luckily for me, <a href="https://developer.apple.com/design/human-interface-guidelines/icons">there was</a>.</p> <p><img src="/assets/images/2026-04-16/compositing.png" alt="" /></p> <blockquote> <p>To create a custom document icon, you can supply any combination of background fill, center image, and text. The system layers, positions, and masks these elements as needed and composites them onto the familiar folded-corner icon shape. <br /> (…) <br /> macOS composites the elements you supply to produce your custom document icon. <br /> — <a href="https://developer.apple.com/design/human-interface-guidelines/icons#Platform-considerations">Platform considerations - macOS: Document icons</a></p> </blockquote> <p>The example was mind-blowing. So that’s how it was working! I scrolled down: a link to the <a href="https://developer.apple.com/design/resources/#macos-apps">Apple Design Resources</a> promised “a template you can use to create a custom background fill and center image for a document icon”. I clicked, opened it both in <em>Figma</em> and <em>Sketch</em>, and looked up “icon” then “document” in both. Nothing relevant. What the hell!?</p> <p>I went back to the guidelines and kept reading. It seemed like each of the two elements needed to be in the app as a <code class="language-console highlighter-rouge"><span class="go">.iconset</span></code>. Fine, but that was still not telling me anything about the API in use there!</p> <p>I kept scrolling until I saw the document icon for the <code class="language-console highlighter-rouge"><span class="go">.xcodeproj</span></code> file type. Yes, I know you, I’m seeing you every day, each time I’m opening you in XCo… Wait a minute.</p> <h1 id="dragging-the-answer-out-of-the-corpse">Dragging the answer out of the corpse</h1> <p>So far, every time I had been opening a FLOSS project’s GitHub<sup id="fnref:github"><a href="#fn:github" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>, I was always going straight for the <code class="language-console highlighter-rouge"><span class="go">Info.plist</span></code> file, hoping to find out what was different, and not finding anything. But this time, I knew an app that was doing it, no more prodding in the dark.</p> <p>I went into XCode’s <code class="language-console highlighter-rouge"><span class="go">.app</span></code> bundle, and looked at <code class="language-console highlighter-rouge"><span class="go">Info.plist</span></code>:</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span><span class="nb">cd</span> /Applications/Xcode.app/Contents/
<span class="nv">$ </span>plutil <span class="nt">-p</span> Info.plist | <span class="nb">grep </span>xcodeproj <span class="nt">-A5</span> <span class="nt">-B15</span>
      <span class="o">}</span>
    <span class="o">}</span>
    39 <span class="o">=&amp;gt;</span> <span class="o">{</span>
      <span class="s2">"UTTypeConformsTo"</span> <span class="o">=&amp;gt;</span> <span class="o">[</span>
        0 <span class="o">=&amp;gt;</span> <span class="s2">"public.composite-content"</span>
        1 <span class="o">=&amp;gt;</span> <span class="s2">"com.apple.package"</span>
      <span class="o">]</span>
      <span class="s2">"UTTypeDescription"</span> <span class="o">=&amp;gt;</span> <span class="s2">"Xcode Project"</span>
      <span class="s2">"UTTypeIcons"</span> <span class="o">=&amp;gt;</span> <span class="o">{</span>
        <span class="s2">"UTTypeIconBackgroundName"</span> <span class="o">=&amp;gt;</span> <span class="s2">"xcode-project-fill"</span>
      <span class="o">}</span>
      <span class="s2">"UTTypeIdentifier"</span> <span class="o">=&amp;gt;</span> <span class="s2">"com.apple.xcode.project"</span>
      <span class="s2">"UTTypeReferenceURL"</span> <span class="o">=&amp;gt;</span> <span class="s2">"http://developer.apple.com/tools/xcode/"</span>
      <span class="s2">"UTTypeTagSpecification"</span> <span class="o">=&amp;gt;</span> <span class="o">{</span>
        <span class="s2">"public.filename-extension"</span> <span class="o">=&amp;gt;</span> <span class="o">[</span>
          0 <span class="o">=&amp;gt;</span> <span class="s2">"xcodeproj"</span>
          1 <span class="o">=&amp;gt;</span> <span class="s2">"xcode"</span>
          2 <span class="o">=&amp;gt;</span> <span class="s2">"pbproj"</span>
        <span class="o">]</span>
      <span class="o">}</span>
    <span class="o">}</span>
</pre></div></div> <p>Once again, the answer was right in my face. Obviously, it was the <code class="language-console highlighter-rouge"><span class="go">UTTypeIconBackgroundName</span></code> property. I opened a search engine tab, clicked on it. Only two pages, no official API from Apple.</p> <p>And the first one was a <a href="https://stackoverflow.com/questions/69389736/how-to-create-custom-document-icon-in-ios-14-apps">StackOverflow question</a>. I read it with shock: the question described the full API:</p> <div class="language-xml highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="nt">&amp;lt;key&amp;gt;</span>UTTypeIcons<span class="nt">&amp;lt;/key&amp;gt;</span>
<span class="nt">&amp;lt;dict&amp;gt;</span>
    <span class="nt">&amp;lt;key&amp;gt;</span>UTTypeIconBackgroundName<span class="nt">&amp;lt;/key&amp;gt;</span>
    <span class="nt">&amp;lt;string&amp;gt;</span>Icon Fill<span class="nt">&amp;lt;/string&amp;gt;</span>
    <span class="nt">&amp;lt;key&amp;gt;</span>UTTypeIconBadgeName<span class="nt">&amp;lt;/key&amp;gt;</span>
    <span class="nt">&amp;lt;string&amp;gt;</span>Icon Image<span class="nt">&amp;lt;/string&amp;gt;</span>
    <span class="nt">&amp;lt;key&amp;gt;</span>UTTypeIconText<span class="nt">&amp;lt;/key&amp;gt;</span>
    <span class="nt">&amp;lt;string&amp;gt;</span>Dapka<span class="nt">&amp;lt;/string&amp;gt;</span>
<span class="nt">&amp;lt;/dict&amp;gt;</span>
</pre></div></div> <p>To my horror, the most upvoted answer was a complete dismissal of this API, instead telling the user to use the <code class="language-console highlighter-rouge"><span class="go">CFBundleTypeIconFile</span></code> configuration key to provide a full icon… I looked at the other links: one <a href="https://github.com/pock/pock/blob/main/Pock%20copy-Info.plist">open-source app</a></p> <h1 id="wrapping">Wrapping</h1> <p>At that point, I had everything I needed: a barely documented API, a couple of scattered examples, and a system that clearly worked. Just not in any way that was discoverable. I wired up my “badge” (attention: it must be a <code class="language-console highlighter-rouge"><span class="go">.iconset</span></code>, using a <code class="language-console highlighter-rouge"><span class="go">.imageset</span></code> won’t work!), killed the Finder, waited for it to restart, and the Spark-Raspberry logo showed up, with a glorious page curl.</p> <p>What still puzzles me isn’t about <em>how it works</em>, but about <em>how to find it</em>. Looking up <code class="language-console highlighter-rouge"><span class="go">CFBundleTypeIconFile</span></code> on Github gives thousands of results, while the <code class="language-console highlighter-rouge"><span class="go">UTTypeIcon*</span></code> keys barely give more than five hundred each, and while Apple’s own guidelines clearly describe a compositing system, the actual keys (<code class="language-console highlighter-rouge"><span class="go">UTTypeIconBackgroundName</span></code>, etc.) are barely documented and you have to read their app’s <code class="language-console highlighter-rouge"><span class="go">Info.plist</span></code> to get clues, while the internet as a whole will just point you toward “make your own icon and use <code class="language-console highlighter-rouge"><span class="go">CFBundleTypeIconFile</span></code>!”.</p> <p>In the end, macOS <em>does</em> have a proper document icon composition system, just not one you’re supposed to find easily (or not one anyone bothered to explain properly?), that almost sounds like it’s a private Apple API.</p> <p>Either way, if you were also wondering where that page curl came from:</p> <p>Now you too can turn the page.</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:ffi"> <p>A story for another post, promise. <a href="#fnref:ffi" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:psp"> <p>In this place, we worship <wiki>Paint Shop Pro</wiki>! <a href="#fnref:psp" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:defaulticon"> <p>Restarting my Finder would have fixed the association, and they would have gotten the default icon, which is something I admit I did not think of at the time. <a href="#fnref:defaulticon" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:genericdocumenticon"> <p>Located in ` /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns`. <a href="#fnref:genericdocumenticon" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:consistency"> <p>Midwits would call it annoying. <a href="#fnref:consistency" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:github"> <p>For brevity’s sake, I did not describe ALL the repositories I went to search into… <a href="#fnref:github" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>How is everyone doing that page curl in their icons anyway?</summary> </entry> <entry> <title>How to write to /dev/rdiskX without root: a look at macOS authopen</title> <link href="https://tech.dreamleaves.org/posts/how-to-write-to-dev-rdiskx-without-root-a-look-at-macos-authopen/" rel="alternate" type="text/html" title="How to write to /dev/rdiskX without root: a look at macOS authopen" /> <published>2026-04-15T00:00:00+02:00</published> <updated>2026-04-15T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/how-to-write-to-dev-rdiskx-without-root-a-look-at-macos-authopen/</id> <content type="html"><![CDATA[ <p>Hot off the heels of <a href="/posts/making-a-raspberry-pi-image-builder/">writing a C API to manipulate Raspbian images</a>, I started writing the Swift app, which led to more API rewrites, until finally, I had a final image file and a <code class="language-console highlighter-rouge"><span class="go">/dev/rdiskX</span></code> device as the destination. All that was left was a simple copy from one to the other.</p> <p>And obviously, no sane operating system would let a user arbitrarily write onto a raw disk device, right?</p> <h1 id="privilege-escalations-on-macos">Privilege escalations on MacOS</h1> <p>If you look this up online, you would get a lot of very nice security writeups, and a looming sense of despair, as you don’t have a <wiki page="Zero-day vulnerability">0-day</wiki> handy to give your (perfectly legitimate!) app the same capabilities.</p> <p>The truth is that beyond compromising your app, it’s generally not possible to securely elevate an application’s privileges once it’s already running. If you look at the <a href="https://developer.apple.com/documentation/security/authorizationexecutewithprivileges">countless</a> <a href="https://developer.apple.com/documentation/servicemanagement/smjobbless(_:_:_:_:)">deprecated</a> <a href="https://developer.apple.com/documentation/servicemanagement/smappservice">solutions</a> Apple gave to developers, you’ll see it’s all the same principle: “We’ll allow you to launch a helper with privileged access and communicate with it”.</p> <p>Which is actually quite a sensible, if somewhat cumbersome, approach: I was writing a Swift GUI app, the last thing I wanted was to spin up a separate privileged helper (and end up back in Rust).</p> <p>Thankfully, there exists a tool for that.</p> <h1 id="the-wisdom-hiding-in-plain-sight">The wisdom hiding in plain sight</h1> <p>Among all the links you’ll find when searching for “macOS privilege escalation” (welcome to the watchlist!), one stands out, even if it doesn’t look like it. Unassuming at first, it turned out to be one of the smartest pieces of developer documentation I’ve read this year: Apple’s very own <a href="https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/AccessControl.html">Elevating Privileges Safely - Secure Coding Guide</a>.</p> <p>Compared to the rest of Apple’s developer documentation, this page feels very raw, almost empty, there’s no code at all, and yet everything it says is something I’ve repeated countless times to any junior developer asking for advice:</p> <blockquote> <p>Any program can come under attack, and probably will. <br /> (…) <br /> The principle of least privilege states: <br /> “Every program and every user of the system should operate using the least set of privileges necessary to complete the job.” <br /> —Saltzer, J.H. AND Schroeder, M.D., “The Protection of Information in Computer Systems,” Proceedings of the IEEE, vol. 63, no. 9, Sept 1975. <br /> — <a href="https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/AccessControl.html">The Hostile Environment and the Principle of Least Privilege</a></p> </blockquote> <p>While every line feels like a repeat of stuff that was hammered in my brain in school<sup id="fnref:school"><a href="#fn:school" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, down to the simplest of “Never trust user input parameters”, it feels good, especially in this emerging era of <em>vibe-coded slop</em> to still say those things out loud.</p> <p>That said, the document was long, light on examples, and I wasn’t convinced it would actually help in my case. I didn’t feel like experimenting with <code class="language-console highlighter-rouge"><span class="go">seteuid</span></code>, so I dismissed it, perhaps too quickly, and went back to the real source of truth: <gh repo="raspberrypi/rpi-imager">rpi-imager</gh>.</p> <h1 id="upon-the-shoulders-of-people-who-solved-the-problem-before-me">Upon the shoulders of people who solved the problem before me</h1> <p>Searching for “<gh repo="search?q=repo%3Araspberrypi%2Frpi-imager%20privilege&amp;amp;type=code">privileges</gh>” in the code didn’t immediately help, but it was nudging me in the right direction: <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/main.cpp#L771">both Windows and Linux required elevated privileges</gh>, while macOS had a <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/platformquirks_macos.mm#L338">"sensible permissions model that operates as expected"</gh>. Well, I know iOS does, and I always yell at apps asking me access to my contact list, but macOS too, really?</p> <p>I was nowhere close to an answer, so I continued through the MacOS parts of the code, marveling at all the tricks it used, like writing the result image straight to the device. Meanwhile, my Swift tool was writing the image to a temporary file and then copying it to <code class="language-console highlighter-rouge"><span class="go">/dev/rdiskX</span></code>.</p> <p>And then, I found <code class="language-console highlighter-rouge"><span class="go">FileError MacOSFileOperations::OpenDevice(const std::string&amp;amp; path)</span></code>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="n">FileError</span> <span class="n">MacOSFileOperations</span><span class="o">::</span><span class="n">OpenDevice</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;amp;</span> <span class="n">path</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="s">"Opening macOS device: "</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="n">path</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
  
  <span class="kt">bool</span> <span class="n">isBlockDevice</span> <span class="o">=</span> <span class="n">IsBlockDevicePath</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>
  
  <span class="c1">// For raw device access on macOS, we need to use the authorization mechanism</span>
  <span class="c1">// Similar to how MacFile::authOpen() works</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">isBlockDevice</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="s">"Device path detected, using macOS authorization..."</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    
    <span class="c1">// Create a MacFile instance to handle authorization</span>
    <span class="n">MacFile</span> <span class="n">macfile</span><span class="p">;</span>
    <span class="n">QByteArray</span> <span class="n">devicePath</span> <span class="o">=</span> <span class="n">QByteArray</span><span class="o">::</span><span class="n">fromStdString</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>
    
    <span class="k">auto</span> <span class="n">authResult</span> <span class="o">=</span> <span class="n">macfile</span><span class="p">.</span><span class="n">authOpen</span><span class="p">(</span><span class="n">devicePath</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">authResult</span> <span class="o">==</span> <span class="n">MacFile</span><span class="o">::</span><span class="n">authOpenCancelled</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="s">"Authorization cancelled by user"</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
      <span class="k">return</span> <span class="n">FileError</span><span class="o">::</span><span class="n">kOpenError</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">authResult</span> <span class="o">==</span> <span class="n">MacFile</span><span class="o">::</span><span class="n">authOpenError</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="s">"Authorization failed"</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
      <span class="k">return</span> <span class="n">FileError</span><span class="o">::</span><span class="n">kOpenError</span><span class="p">;</span>
    <span class="p">}</span>
</pre></div></div> <p>Of course. I had flashed my own microSD cards a dozen times and every time, this MacOS authentification window would appear and ask me for my password because “Raspberry Pi Imager needs to access the disk to write the image.”. That was it, that was the gold mine: a <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/macfile.cpp#L27">112-line function</gh>, all centered around something I had already heard of: <code class="language-console highlighter-rouge"><span class="gp">const char *cmd = "/usr/libexec/authopen";</span></code>.</p> <p>And just like that, the answer was right there in the Apple documentation:</p> <blockquote> <p>If you do need to run code with elevated privileges, there are several approaches you can take: <br /> You can use the authopen command to read, create, or update a file (see authopen). <br /> — <a href="https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/AccessControl.html">Running with Elevated Privileges</a></p> </blockquote> <h1 id="whats-an-authopen-anyway">What’s an authopen anyway?</h1> <p>As the online documentation was sparse, I turned to the other documentation source: <wiki page="man page">man(1)</wiki>.</p> <p>Running <code class="language-console highlighter-rouge"><span class="go">man authopen</span></code> proved to be informative enough:</p> <blockquote> <p><strong>authopen</strong> provides authorization-based file opening services. In its simplest form, <strong>authopen</strong> verifies that it is allowed to open ~filename~ (using an appropriate <em>sys.openfile.*</em> authorization right) and then writes the file to stdout. (…) <br /> <strong>authopen</strong> is designed to be used both from the command line and programmatically. The <strong>-stdoutpipe</strong> flag allows a parent process to receive an open file descriptor pointing to the file in question. <br /> — <a href="https://www.unix.com/man_page/osx/1/authopen/">man authopen(1)</a></p> </blockquote> <p>That was exactly what I needed: a temporary authorization to write to <code class="language-console highlighter-rouge"><span class="go">/dev/rdiskX</span></code>!</p> <p>And then something dawned upon me: no matter how much I searched online, it seemed no one had ever documented how <code class="language-console highlighter-rouge"><span class="go">authopen</span></code> worked. Sure, I was looking at this code, and I even found a few other implementations in Java or Rust, but otherwise, no one seemed to have documented how it actually worked.</p> <p>Well then. Time to reverse-engineer some C++.</p> <h1 id="the-missing-authopen-documentation">The missing authopen documentation</h1> <h2 id="all-eating-functions-start-with-a-fork">All eating functions start with a fork()</h2> <p>The <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/macfile.cpp#L59-L65">first useful part of the code</gh> is a standard <code class="language-console highlighter-rouge"><span class="go">fork()</span></code> setup:</p> <div class="language-cpp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">// Prepare command to run</span>
<span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">cmd</span> <span class="o">=</span> <span class="s">"/usr/libexec/authopen"</span><span class="p">;</span>
<span class="n">QByteArray</span> <span class="n">mode</span> <span class="o">=</span> <span class="n">QByteArray</span><span class="o">::</span><span class="n">number</span><span class="p">(</span><span class="n">O_RDWR</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">pipe</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
<span class="kt">int</span> <span class="n">stdinpipe</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
<span class="c1">// Set up a two-directional socket</span>
<span class="o">::</span><span class="n">socketpair</span><span class="p">(</span><span class="n">AF_UNIX</span><span class="p">,</span> <span class="n">SOCK_STREAM</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">pipe</span><span class="p">);</span>
<span class="c1">// set up a pipe for stdin (unidirectional)</span>
<span class="o">::</span><span class="n">pipe</span><span class="p">(</span><span class="n">stdinpipe</span><span class="p">);</span>
<span class="c1">// Let's fork</span>
<span class="n">pid_t</span> <span class="n">pid</span> <span class="o">=</span> <span class="o">::</span><span class="n">fork</span><span class="p">();</span>
</pre></div></div> <p>While they feel like a lost art in a world of GUI, <wiki page="Pipeline (Unix)">Unix Pipes</wiki> still power a huge part of systems, and their simplicity makes them usable in a ton of situations. In my case, having already written a shell<sup id="fnref:shell"><a href="#fn:shell" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> in C, there was no dark magic here, yet.</p> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>Pipes and sockets</strong> <p>Please note that in this case, we are creating a <strong>pipe</strong> for <strong>STDIN</strong> and a bi-directional <strong>socket</strong> for communication. There is a small difference in treatment between the two, and it will influence what happens later.</p> </div> <h2 id="getting-to-authopen">Getting to authopen</h2> <p>As the <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/macfile.cpp#L68">original code comment</gh> says: we are in “child” territory. In Unix, a <a href="https://www.unix.com/man-page/linux/2/fork/">fork(2)</a> separates the process in two branches, with the return value indicating in which you are, a 0 indicating the child, and any other result, the parent.</p> <div class="language-cpp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">pid</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">{</span>
  <span class="c1">// child</span>
  <span class="c1">// Close the end of the socket we won't use</span>
  <span class="o">::</span><span class="n">close</span><span class="p">(</span><span class="n">pipe</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
  <span class="c1">// Close the writing side of the pipe</span>
  <span class="o">::</span><span class="n">close</span><span class="p">(</span><span class="n">stdinpipe</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
  <span class="c1">// Duplicate our side of the socket to capture STDOUT</span>
  <span class="o">::</span><span class="n">dup2</span><span class="p">(</span><span class="n">pipe</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">STDOUT_FILENO</span><span class="p">);</span>
  <span class="c1">// Duplicate the reading side of the pipe to write to STDIN</span>
  <span class="o">::</span><span class="n">dup2</span><span class="p">(</span><span class="n">stdinpipe</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">STDIN_FILENO</span><span class="p">);</span>
  <span class="c1">// Execute authopen</span>
  <span class="o">::</span><span class="n">execl</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">cmd</span><span class="p">,</span> <span class="s">"-stdoutpipe"</span><span class="p">,</span> <span class="s">"-extauth"</span><span class="p">,</span> <span class="s">"-o"</span><span class="p">,</span> <span class="n">mode</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">filename</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="nb">NULL</span><span class="p">);</span>
  <span class="c1">// In case we couldn't run authopen, return an error</span>
  <span class="o">::</span><span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</pre></div></div> <p>So, we close the parts of the pipes we won’t need, and reuse the remaining ones as <code class="language-console highlighter-rouge"><span class="go">authopen</span></code>’s <code class="language-console highlighter-rouge"><span class="go">STDIN</span></code> and <code class="language-console highlighter-rouge"><span class="go">STDOUT</span></code> to communicate with the parent. At this point, it’s just plumbing.</p> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>Exit minus one</strong> <p>While it may seem strange to exit with a status of <strong>-1</strong> here, it is actually normal: on successful execution, <a href="https://www.unix.com/man_page/linux/3/execl/">execl(3)</a> (and the whole <strong>exec</strong> family) replaces the current process. Therefore, this line is only reached if launching <strong>authopen</strong> failed.</p> </div> <h2 id="local-news-parent-closes-door-on-child">Local news: parent closes door on child</h2> <p>On the <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/macfile.cpp#L76">parent's side</gh>, nothing too unusual happens at first:</p> <div class="language-cpp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">// pid != 0, so we're still in the parent</span>
<span class="k">else</span>
<span class="p">{</span>
  <span class="c1">// Close the end of the socket we won't use</span>
  <span class="o">::</span><span class="n">close</span><span class="p">(</span><span class="n">pipe</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
  <span class="c1">// Close the reading side of the pipe</span>
  <span class="o">::</span><span class="n">close</span><span class="p">(</span><span class="n">stdinpipe</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
  <span class="c1">// Write on the pipe</span>
  <span class="o">::</span><span class="n">write</span><span class="p">(</span><span class="n">stdinpipe</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">externalForm</span><span class="p">.</span><span class="n">bytes</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">externalForm</span><span class="p">.</span><span class="n">bytes</span><span class="p">));</span>
  <span class="c1">// Close the pipe</span>
  <span class="o">::</span><span class="n">close</span><span class="p">(</span><span class="n">stdinpipe</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
</pre></div></div> <p>Writing and closing the <code class="language-console highlighter-rouge"><span class="go">STDIN</span></code> pipe isn’t something you’d typically do when using <code class="language-console highlighter-rouge"><span class="go">authopen</span></code> on its own, but in this case, since the <code class="language-console highlighter-rouge"><span class="go">-extauth</span></code> argument is passed, it’s required, as per the manual:</p> <blockquote> <p><strong>-extauth</strong> specifies that <strong>authopen</strong> should read one AuthorizationExternalForm structure from <strong>stdin</strong>, convert it to an AuthorizationRef, and attempt to use it to authorize the open(2) operation. The authorization should refer to the <strong>sys.openfile</strong> right corresponding to the requested operation. <br /> — <a href="https://www.unix.com/man_page/osx/1/authopen/">man authopen(1)</a></p> </blockquote> <p>In other words, while you can use <code class="language-console highlighter-rouge"><span class="go">authopen</span></code> to simply open a file, you can also <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/macfile.cpp#L40">pass additional options, like a custom prompt</gh>, for a better user experience.</p> <p>As for closing the pipe, it signals <wiki page="End-of-file">EOF</wiki>, letting <code class="language-console highlighter-rouge"><span class="go">authopen</span></code> know that input is complete, and that it can now kick things into gear!</p> <h2 id="local-news-parent-steals-file-descriptor-from-child">Local news: parent steals file descriptor from child</h2> <p>The <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/mac/macfile.cpp#L83">following part</gh> is where I actually began to stumble. And I still had no idea: how could a <wiki page="File descriptor">file descriptor</wiki> be transferred from one process to another? So far, every time I had used one, it was an integer, and I knew its value was scoped to the process, and I couldn’t make sense of the model, so I looked up the first term that seemed promising: <code class="language-console highlighter-rouge"><span class="go">struct iovec</span></code>.</p> <blockquote> <p>iovec - data storage structure for I/O using uio — <a href="https://www.unix.com/man-page/opensolaris/9s/iovec/">man iovec(9s)</a></p> </blockquote> <p>Like a good episode of <wiki page="Lost (TV series)">LOST</wiki>, this actually provides me with a clean answer, and invites me to more questions. Unlike even the best episodes of <wiki page="Lost (TV series)">LOST</wiki>, these are actually questions I believe I can find answers to!</p> <p>While a lot of these terms sound intimidating, looking them up one by one will orient us towards <a href="https://www.unix.com/man-page/linux/3/cmsg_space/">very helpful documentation</a>, and even a very informative <a href="https://blog.cloudflare.com/know-your-scm_rights/#meet-scm_rights">Cloudflare blog post</a><sup id="fnref:cf"><a href="#fn:cf" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="c1">// Set up an iovec buffer, big enough for one integer</span>
<span class="k">const</span> <span class="kt">size_t</span> <span class="n">bufSize</span> <span class="o">=</span> <span class="n">CMSG_SPACE</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
<span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="n">bufSize</span><span class="p">];</span>
<span class="k">struct</span> <span class="nc">iovec</span> <span class="n">io_vec</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
<span class="n">io_vec</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">iov_base</span> <span class="o">=</span> <span class="n">buf</span><span class="p">;</span>
<span class="n">io_vec</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">iov_len</span> <span class="o">=</span> <span class="n">bufSize</span><span class="p">;</span>
<span class="c1">// Set up a control message buffer, big enough for one integer</span>
<span class="k">const</span> <span class="kt">size_t</span> <span class="n">cmsgSize</span> <span class="o">=</span> <span class="n">CMSG_SPACE</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
<span class="kt">char</span> <span class="n">cmsg</span><span class="p">[</span><span class="n">cmsgSize</span><span class="p">];</span>

<span class="c1">// Set up a header structure, referencing our two previous buffer</span>
<span class="k">struct</span> <span class="nc">msghdr</span> <span class="n">msg</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
<span class="n">msg</span><span class="p">.</span><span class="n">msg_iov</span> <span class="o">=</span> <span class="n">io_vec</span><span class="p">;</span>
<span class="n">msg</span><span class="p">.</span><span class="n">msg_iovlen</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">msg</span><span class="p">.</span><span class="n">msg_control</span> <span class="o">=</span> <span class="n">cmsg</span><span class="p">;</span>
<span class="n">msg</span><span class="p">.</span><span class="n">msg_controllen</span> <span class="o">=</span> <span class="n">cmsgSize</span><span class="p">;</span>

<span class="c1">// Read from the socket until we get something</span>
<span class="kt">ssize_t</span> <span class="n">size</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
    <span class="n">size</span> <span class="o">=</span> <span class="n">recvmsg</span><span class="p">(</span><span class="n">pipe</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="o">&amp;amp;</span><span class="n">msg</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">size</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="n">errno</span> <span class="o">==</span> <span class="n">EINTR</span><span class="p">);</span>

<span class="c1">// If we get some data</span>
<span class="k">if</span> <span class="p">(</span><span class="n">size</span> <span class="o">&amp;gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Extract the first control message header</span>
  <span class="k">struct</span> <span class="nc">cmsghdr</span> <span class="o">*</span><span class="n">chdr</span> <span class="o">=</span> <span class="n">CMSG_FIRSTHDR</span><span class="p">(</span><span class="o">&amp;amp;</span><span class="n">msg</span><span class="p">);</span>
  <span class="c1">// Check that we received SCM_RIGHTS</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">chdr</span> <span class="o">&amp;amp;&amp;amp;</span> <span class="n">chdr</span><span class="o">-&amp;gt;</span><span class="n">cmsg_type</span> <span class="o">==</span> <span class="n">SCM_RIGHTS</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">qDebug</span><span class="p">()</span> <span class="o">&amp;lt;&amp;lt;</span> <span class="s">"SCMRIGHTS"</span><span class="p">;</span>
    <span class="c1">// Extract the FD from the data part</span>
    <span class="n">fd</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span> <span class="p">(</span><span class="kt">int</span><span class="o">*</span><span class="p">)</span> <span class="p">(</span><span class="n">CMSG_DATA</span><span class="p">(</span><span class="n">chdr</span><span class="p">))</span> <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></div></div> <p>Okay, that was strange!</p> <p>The Unix socket API includes a lesser-known feature called <a href="https://www.unix.com/man-page/linux/2/recvmsg/">control messages</a>: a way to attach metadata to a message, alongside the regular byte stream. One such metadata type is <code class="language-console highlighter-rouge"><span class="go">SCM_RIGHTS</span></code>, which allows a process to transfer an open file descriptor to another process.</p> <p>In our case, this is the “secret sauce” behind <code class="language-console highlighter-rouge"><span class="go">authopen</span></code>: instead of returning a file path or raw data, it sends back a fully opened (and privileged) file descriptor through that mechanism.</p> <p>And just like a good <wiki page="Lost (TV series)">LOST</wiki> episode, my initial assumptions got trumped, as the <code class="language-console highlighter-rouge"><span class="go">iovec</span></code> structure wasn’t the solution to the mystery.</p> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>Reading for zero</strong> <p>While you may be accustomed to the form <code class="language-console highlighter-rouge"><span class="go">read(stream, buf, size)</span></code>, in that case, the third argument to <code class="language-console highlighter-rouge"><span class="go">recvmsg</span></code> is <code class="language-console highlighter-rouge"><span class="go">flags</span></code>, with the <code class="language-console highlighter-rouge"><span class="go">msghdr</span></code> describing the buffers used to receive both data and metadata.</p> </div> <h1 id="wrapping-up">Wrapping up</h1> <p>What started as a simple question (“how to write to <code class="language-console highlighter-rouge"><span class="go">/dev/rdiskX</span></code>?”) turned into a small journey through macOS internals, Unix IPC, and a surprisingly elegant piece of engineering.</p> <p>Instead of elevating the entire process, <code class="language-console highlighter-rouge"><span class="go">authopen</span></code> takes a much more surgical approach: it opens the file with the right privileges, and hands you back a file descriptor. No daemon, no persistent helper, no global privilege escalation. Just a capability, passed from one process to another.</p> <p>Along the way, I also got a reminder of how much power still lies in the old Unix primitives. Pipes, sockets, <code class="language-console highlighter-rouge"><span class="go">fork</span></code>, <code class="language-console highlighter-rouge"><span class="go">exec</span></code>, and a few lesser-known features like control messages are enough to build surprisingly robust systems… if you know where to look.</p> <p>And, perhaps more importantly, that sometimes the best documentation isn’t documentation at all, but the code of someone who already solved the problem.</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:school"> <p>My school is famous for piping <code class="language-console highlighter-rouge"><span class="go">/dev/urandom</span></code> into student binaries to test how much they clean up user input. <a href="#fnref:school" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:shell"> <p>A very humbling experience… <a href="#fnref:shell" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:cf"> <p>Another reason <a href="/posts/cloudflare-gateway-drug/">I love their service</a>! <a href="#fnref:cf" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>You don't need to sudo into your .app</summary> </entry> <entry> <title>Making a Raspberry Pi image builder</title> <link href="https://tech.dreamleaves.org/posts/making-a-raspberry-pi-image-builder/" rel="alternate" type="text/html" title="Making a Raspberry Pi image builder" /> <published>2026-04-11T00:00:00+02:00</published> <updated>2026-04-11T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/making-a-raspberry-pi-image-builder/</id> <content type="html"><![CDATA[ <p>My work with <a href="https://spark-club.fr/">Spark-Club</a> has led me to implement an IoT fleet. While the process to build a common system on each device came with its own set of complications, the most complicated was to write a guide to ensure anyone with a Mac could set up IoTs. After encountering one too many PEBKAC<sup id="fnref:pebkac"><a href="#fn:pebkac" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> issues, I decided that having a single-step process was the new priority.</p> <h1 id="the-old-way-rpi-imager">The old way: rpi-imager</h1> <p>I cannot overstate how much of <gh repo="raspberrypi/rpi-imager">rpi-imager</gh> is a work of art, passion, and dedication. When I set up the first Pi for <a href="https://spark-club.fr/">Spark-Club</a>, it just felt so easy, my Wi-Fi info was already there, I could SSH into the Pi without relying on Ethernet, I could take out the microSD card and restart the process to try new things,…</p> <p>And, it’s great, really. I’m pretty sure anyone who gets their hand on a Pi would be delighted about how it just works, the GUI wizard is easy, image download, light customization,… It’s perfect, too perfect actually, because it’s only perfect for humans.</p> <p>While the app can be started in CLI mode to control it from a script it’s unfortunately very limited, and the best you can do is attach a <code class="language-console highlighter-rouge"><span class="go">firstrun.sh</span></code> file to the Linux image that will include some basic setup that will run on the device’s first boot.</p> <h1 id="our-requirements">Our requirements</h1> <p>When I finalized the setup required to run our Pis, we had the following files:</p> <ul> <li><code class="language-console highlighter-rouge"><span class="go">firstrun.sh</span></code> sets up the hostname, current user, wi-fi passwords, <code class="language-console highlighter-rouge"><span class="go">.authorized_keys</span></code>, sound devices, services, and copies files to the user <code class="language-console highlighter-rouge"><span class="gp">$</span>HOME</code> directory.</li> <li><code class="language-console highlighter-rouge"><span class="go">setup.sh</span></code> is ran on the user’s first boot, and sets up user-level configuration we don’t have access to during the first run process.</li> <li><code class="language-console highlighter-rouge"><span class="go">{UUID}.spark-club</span></code> is the ENV file the device uses to uniquely identify itself.</li> <li>And some more secret ingredients…</li> </ul> <p>Since <gh repo="raspberrypi/rpi-imager">rpi-imager</gh> would only copy the first one to the image, there would be a multi-step process:</p> <ul> <li>Get the configuration files for the device being set up</li> <li>Flash the microSD with the Raspbian image, customized with <code class="language-console highlighter-rouge"><span class="go">firstrun.sh</span></code>.</li> <li>Re-insert the microSD in the mac, and copy the rest of files on the device.</li> </ul> <p>With too any steps, it was VERY EASY to for things to break, so I quickly looked into alternatives.</p> <h1 id="the-alternatives-how-much-heredoc-do-you-want">The alternatives: How much HEREDOC do you want?</h1> <p>The first thing I looked into was the <code class="language-console highlighter-rouge"><span class="gp">--cloudinit-userdata &amp;lt;cloudinit-userdata&amp;gt;</span></code> flag.</p> <p>Touting itself as “The standard for customising cloud instances”, <a href="https://cloud-init.io/">cloud-init</a> could have solved my problem. As a Rails user, I’m not intimidated by YAML files, but writing huge files using YAML, <a href="https://docs.cloud-init.io/en/latest/reference/examples.html#writing-out-arbitrary-files">while possible</a> felt very counter-intuitive to my usage: I wanted to add multiple files at need, and here I was, writing their content and packing them all as strings in a dictionary.</p> <p>The solution could have been there, but not for my needs.</p> <h1 id="the-alternatives-are-you-a-sysadmin-enough-dude">The alternatives: “are you a sysadmin enough dude?”</h1> <p>Therefore, how else do you customize a Raspbian image?</p> <p>Easily: you <gh repo="raspberrypi/rpi-image-gen">build your own</gh>.</p> <p>I won’t even go there: there’s a point where “customizing your image” turns into “Spark-Club becomes the first sports company with its own Linux distribution”, and it’s a line I’m not crossing. But if you’ve got the team for it, if your business relies on owning the tech, and requires the distro tailored to your needs, it’s the perfect answer!</p> <h1 id="in-between-an-image-with-arbitrary-files">In-between: an image with arbitrary files</h1> <p>A Raspberry Pi image is actually a MBR<sup id="fnref:mbr"><a href="#fn:mbr" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> defining two partitions: a 512MB FAT32 boot partition, and a 5-and-some-gigs ext4 Linux partition. While there are ways to <gh repo="yuoo655/ext4_rs">play with an ext4 partition without mounting it</gh>, since my previous process involved mounting the microSD’s FAT32 boot partition to copy setup files, I thought playing with the boot partition would be the best (and easiest!) solution.</p> <p>And it was!</p> <h1 id="enter-rpi-imgpatcher">Enter rpi-imgpatcher</h1> <p>Using <gh repo="rust-disk-partition-management/mbrman">mbrman</gh> to parse the MBR records and identify the FAT32 partition location on the drive, then <gh repo="rafalh/rust-fatfs">a rust implementation of fatfs</gh> to manipulate the FAT32 partition filesystem, a quick Rust script managed to write a “Hello World” inside my image, which felt really great!</p> <p>The next step was devising an API and turning it into a proper library, which led to some design decisions.</p> <p>For instance, <gh repo="rafalh/rust-fatfs">fatfs</gh> writes straight to the FAT buffer it’s working on, which means a careless approach would modify an image in place. If you want to generate multiple images, keeping the original untouched is a must<sup id="fnref:fiber"><a href="#fn:fiber" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p> <p><strong>First design decision:</strong> we never modify the original image in place, the FAT partition is always extracted so it can be worked on, and then saved to a new file, keeping the original MBR record and ext4 partition intact (they are copied straight from the original image).</p> <p><strong>Second design decision:</strong> how to interact with the FAT filesystem. The simplest idea was an obvious <code class="language-console highlighter-rouge"><span class="go">add_file(fat_path: String, local_file: PathBuf)</span></code>, but I quickly realized I also needed to write arbitrary bytes, and even append to existing files, would be good, since it’s (almost!) what <gh repo="raspberrypi/rpi-imager">rpi-imager</gh> does when editing <code class="language-console highlighter-rouge"><span class="go">cmdline.txt</span></code>.</p> <p>After a few hours of work, I had a small but useful API I was very proud of:</p> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">impl</span> <span class="n">RpiImage</span> <span class="p">{</span>
  <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">image_path</span><span class="p">:</span> <span class="k">impl</span> <span class="nb">AsRef</span><span class="o">&amp;lt;</span><span class="n">Path</span><span class="o">&amp;gt;</span><span class="p">)</span> <span class="k">-&amp;gt;</span> <span class="nb">Result</span><span class="o">&amp;lt;</span><span class="k">Self</span><span class="p">,</span> <span class="n">Error</span><span class="o">&amp;gt;</span>
  <span class="k">pub</span> <span class="k">fn</span> <span class="nf">read_file</span><span class="p">(</span><span class="o">&amp;amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">fat_path</span><span class="p">:</span> <span class="o">&amp;amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&amp;gt;</span> <span class="nb">Result</span><span class="o">&amp;lt;</span><span class="nb">Vec</span><span class="o">&amp;lt;</span><span class="nb">u8</span><span class="o">&amp;gt;</span><span class="p">,</span> <span class="n">Error</span><span class="o">&amp;gt;</span>
  <span class="k">pub</span> <span class="k">fn</span> <span class="nf">write_file</span><span class="p">(</span><span class="o">&amp;amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">fat_path</span><span class="p">:</span> <span class="o">&amp;amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">file</span><span class="p">:</span> <span class="k">impl</span> <span class="nb">AsRef</span><span class="o">&amp;lt;</span><span class="n">Path</span><span class="o">&amp;gt;</span><span class="p">)</span> <span class="k">-&amp;gt;</span> <span class="nb">Result</span><span class="o">&amp;lt;</span><span class="nb">u64</span><span class="p">,</span> <span class="n">Error</span><span class="o">&amp;gt;</span>
  <span class="k">pub</span> <span class="k">fn</span> <span class="nf">write_bytes</span><span class="p">(</span><span class="o">&amp;amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">fat_path</span><span class="p">:</span> <span class="o">&amp;amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">bytes</span><span class="p">:</span> <span class="o">&amp;amp;</span><span class="p">[</span><span class="nb">u8</span><span class="p">])</span> <span class="k">-&amp;gt;</span> <span class="nb">Result</span><span class="o">&amp;lt;</span><span class="nb">u64</span><span class="p">,</span> <span class="n">Error</span><span class="o">&amp;gt;</span>
  <span class="k">pub</span> <span class="k">fn</span> <span class="nf">append_bytes</span><span class="p">(</span><span class="o">&amp;amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">fat_path</span><span class="p">:</span> <span class="o">&amp;amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">bytes</span><span class="p">:</span> <span class="o">&amp;amp;</span><span class="p">[</span><span class="nb">u8</span><span class="p">])</span> <span class="k">-&amp;gt;</span> <span class="nb">Result</span><span class="o">&amp;lt;</span><span class="nb">u64</span><span class="p">,</span> <span class="n">Error</span><span class="o">&amp;gt;</span>
  <span class="k">pub</span> <span class="k">fn</span> <span class="nf">save_to_file</span><span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="n">out_file</span><span class="p">:</span> <span class="k">impl</span> <span class="nb">AsRef</span><span class="o">&amp;lt;</span><span class="n">Path</span><span class="o">&amp;gt;</span><span class="p">)</span> <span class="k">-&amp;gt;</span> <span class="nb">Result</span><span class="o">&amp;lt;</span><span class="p">(),</span> <span class="n">Error</span><span class="o">&amp;gt;</span>
<span class="p">}</span>
</pre></div></div> <p>Simple enough to use in a script, but flexible enough to cover most use cases I could think of.</p> <p>I felt so good, that I decided against going to sleep (I mean, it was Sunday already morning, there was no point anymore), and kept adding features.</p> <h1 id="ffi-foreign-function-interface">FFI: Foreign Function Interface</h1> <p>A huge selling point of <gh repo="raspberrypi/rpi-imager">rpi-imager</gh>, that can’t be overstated , is that it runs on all OSes, with a level of abstraction that’s close to artistry. Whatever your desktop OS is, it WORKS.</p> <p>In my case, this wasn’t a concern: the whole team uses MacBooks. I did consider using a Rust GUI library for a while, but I felt like going the extra mile and learning some more. Plus I had already <gh repo="cheatscan/blob/master/src/ffi.rs">written an FFI implementation</gh> (for a WASM library) a few days ago, so things felt fresh, and it’s always fun to use <wiki page="nm (Unix)">nm</wiki> in CLI to look at a library’s contents.</p> <p>Just as much as I love C for the beauty of its design, I love the <wiki>Foreign function interface</wiki>. Just imagine: by using a common calling method, you can compile your Rust code into a library, and then that library <gh repo="ffi/ffi">can be called by a script language</gh>,…</p> <p>It does, however, come with caveats: everyone must adhere to C is the common denominator, which can sometimes be a hurdle. While <code class="language-console highlighter-rouge"><span class="go">::new()</span></code> was easy to write (we just return a pointer to an opaque struct, could be worse!), <code class="language-console highlighter-rouge"><span class="go">::write_file</span></code> came with some complications: how to return a value OR an error? Rust’s <code class="language-console highlighter-rouge"><span class="gp">Result&amp;lt;Ok, Err&amp;gt;</span></code> makes this trivial, but in C…?</p> <p>I’m not gonna blow your brains here, the answer is as old as C: one goes in the return value, the other in an outparam, a function argument that can be modified and checked by the caller. By convention, the return value is the <code class="language-console highlighter-rouge"><span class="go">Ok()</span></code> value and the outparam is the <code class="language-console highlighter-rouge"><span class="go">Err()</span></code> value, if there is one.</p> <p>A simple macro made quick work of it:</p> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nd">macro_rules!</span> <span class="n">return_out</span> <span class="p">{</span>
  <span class="p">(</span><span class="nv">$out:expr</span><span class="p">,</span> <span class="nv">$err:expr</span><span class="p">)</span> <span class="k">=&amp;gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="o">!</span><span class="nv">$out</span><span class="nf">.is_null</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="nv">$out</span> <span class="o">=</span> <span class="nv">$err</span> <span class="p">};</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</pre></div></div> <p>A “writing” function would return the number of bytes written, or -1 in the case of an error, and the error value would be the out param. Perfect!</p> <p>Which got me to my first real defeat: <code class="language-console highlighter-rouge"><span class="go">::read()</span></code>.</p> <p>While C is beautiful, it’s also a harsh mistress, and won’t let you casually move string buffers around. The cleanest way to get a String from rust to C is to know its size, allocate a buffer, and give it to Rust to write into.</p> <p>Unfortunately, the <gh repo="rafalh/rust-fatfs">fatfs</gh> library doesn’t implement an easy <code class="language-console highlighter-rouge"><span class="go">::size()</span></code> function on a file, which means my C API has no way to know the file size before reading it. The only alternative would be to have Rust allocate a buffer, lose ownership of it, and give it to C, which is the best way to leak memory (and nobody wants that).</p> <p>A similar issue came when implementing the C API for <code class="language-console highlighter-rouge"><span class="go">::read_bytes()</span></code>. While the argument is just an array of bytes, C has no native idea of arrays<sup id="fnref:c_char"><a href="#fn:c_char" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>. Therefore, from C, you must pass both a pointer to the array of bytes AND its length. Nothing too hard, but always a surprise.</p> <p>In the end, with a good C API, I could <gh repo="joshleaves/rpi-imgpatcher/actions/workflows/test_ffi.yml">write a full implementation test in C</gh>, and the library was ready to integrate into anything, even a Swift app, which was the initial goal!</p> <h1 id="everyone-wants-their-thingfile">Everyone wants their Thingfile</h1> <p>I wasn’t ready to sleep yet. While I was coding, one idea kept coming back: “Why not have a DSL for the tool?”. I mean, sure, you could use the library from any language, but really…would you want to?</p> <p>That’s why I needed a binary, and not something with <a href="/posts/trimming-down-a-rust-binary-in-half/">a bloated args parsing library</a>. Since <gh repo="docker">docker</gh> got the <gh repo="docker/getting-started/blob/master/Dockerfile">Dockerfile</gh>, having a similar DSL felt like a no-brainer:</p> <div class="language-Dockerfile highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">FROM</span><span class="s"> 2025-12-04-raspios-trixie-arm64.img</span>

EXEC cat firstrun.sh | sed "s|spark-base|spark-$NAME|g" &amp;gt; firstrun-$NAME.sh

<span class="k">ADD</span><span class="s"> FILE firstrun.sh firstrun-$NAME.sh</span>
(...more instructions)

APPEND FILE cmdline.txt "systemd.run=/boot/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target"

SAVE trixie-$NAME.img
</pre></div></div> <p>I love writing parsers for DSLs, especially in Rust. Every time I write a small VM for an <gh repo="joshleaves/advent-rs">AdventOfCode exercise</gh>, Rust’s <code class="language-console highlighter-rouge"><span class="go">match</span></code> operator makes it a delight to do, and the implementation of traits trivialized <gh repo="joshleaves/rpi-imgpatcher/blob/master/src/patcher/mod.rs">turning error enums into helpful error messages</gh>.</p> <p>So it comes as no surprise that my problem wasn’t with the code, but with the image itself: Raspbian follows instructions from <code class="language-console highlighter-rouge"><span class="go">cmdline.txt</span></code> to boot up. When you set up a <code class="language-console highlighter-rouge"><span class="go">firstrun.sh</span></code> in rpi-imager, <gh repo="raspberrypi/rpi-imager/blob/994108d39e3817184d80178f1df8d2889e7e7b6e/src/downloadthread.cpp#L2335">it appends a configuration telling it to run it on first boot</gh>.</p> <p>As the name implies, it’s a single line.</p> <p>Which comes back to bite you the moment you realize the original file ends with a newline, preventing any meaningful <code class="language-console highlighter-rouge"><span class="go">APPEND</span></code> operation on it.</p> <p>Maybe because of the lack of sleep, I decided to add an <code class="language-console highlighter-rouge"><span class="go">APPEND CMDLINE</span></code> instruction to the DSL, and treat this file as a special case. Though saner people may prefer overwriting the whole file with a clean one with <code class="language-console highlighter-rouge"><span class="go">ADD FILE</span></code>.</p> <p>And because there’s no such thing as “enough work”, I also added <code class="language-console highlighter-rouge"><span class="gp">$</span>VARIABLE</code> substitution using environment variables, turning what started as a script into a proper build pipeline.</p> <h1 id="one-last-thing">One last thing</h1> <p>Anytime you download a Raspbian image from <a href="https://downloads.raspberrypi.com/os_list_imagingutility_v3.json">the Raspberry Pi image server</a>, it comes as a <code class="language-console highlighter-rouge"><span class="go">.xz</span></code> file, a <wiki page="XZ Utils">compression format derived from LZMA</wiki>. There is <gh repo="hasenbanck/lzma-rust2">a Rust library that can handle that</gh>, of course, but the question was: how much changes would I have to implement if I wanted to support reading from (or even writing to!) a <code class="language-console highlighter-rouge"><span class="go">.xz</span></code> archive?</p> <p>The answer was…not much.</p> <p>My original implementation was based on <a href="https://doc.rust-lang.org/std/fs/struct.File.html">File</a> structs, and passing data from input to output using <a href="https://doc.rust-lang.org/std/io/fn.copy.html">std::io::copy</a>. Do you notice the arguments to that function? Those are generic traits: <a href="https://doc.rust-lang.org/std/io/trait.Read.html">std::io::Read</a> and <a href="https://doc.rust-lang.org/std/io/trait.Write.html">std::io::Write</a>.</p> <p>As long as I could provide a type implementing those traits, everything became plug-and-play<sup id="fnref:plugandplay"><a href="#fn:plugandplay" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>, and could even be extended to any archive type exposing a <code class="language-console highlighter-rouge"><span class="go">Read</span></code> stream.</p> <p>That said, my enthusiasm had limits: reading from a <code class="language-console highlighter-rouge"><span class="go">.img.xz</span></code> slowed things down by a factor of 10: from ~6 seconds to nearly a minute. And my patience ran out before even trying to save an <code class="language-console highlighter-rouge"><span class="go">.img.xz</span></code> archive.</p> <p>Nonetheless, discovering how Rust traits can improve internal API compatibility was yet another moment of appreciation for the language’s design, just like when I first discovered <a href="https://ruby-doc.org/3.4.1/Enumerable.html#module-Enumerable-label-Usage">how Ruby’s Enumerable works</a>.</p> <h1 id="coming-next">Coming next</h1> <p>Despite a few rough edges, the API already feels very close to production-ready. The real test now is integrating it into my Swift desktop application.</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:pebkac"> <p>Problem Exists Between Keyboard And Chair <a href="#fnref:pebkac" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:mbr"> <p>A <wiki>Master Boot Record</wiki> is the boot sector of a hard drive, defining its partitions. <a href="#fnref:mbr" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:fiber"> <p>It feels shameful to admit, but I don’t have fiber at home, and my download speed is limited to 2MB/s. <a href="#fnref:fiber" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:c_char"> <p>While C strings are “char arrays”, and by convention end on <code class="language-console highlighter-rouge"><span class="go">\0</span></code>, and while it’s often a convention to indicate the end of a pointer array with a <code class="language-console highlighter-rouge"><span class="go">NULL</span></code>, it’s not a convention you can rely on for arbitrary data! <a href="#fnref:c_char" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:plugandplay"> <p>I’m not gonna sugar-coat it, type-checker’s ability to guide you through any change you make is a huge reason to love Rust. <a href="#fnref:plugandplay" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>Because nothing beats automation</summary> </entry> <entry> <title>Cloudflare's Gateway Drug</title> <link href="https://tech.dreamleaves.org/posts/cloudflare-gateway-drug/" rel="alternate" type="text/html" title="Cloudflare&amp;apos;s Gateway Drug" /> <published>2025-06-06T00:00:00+02:00</published> <updated>2025-06-06T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/cloudflare-gateway-drug/</id> <content type="html"><![CDATA[ <p>When you’re not yet huge enough to run your own datacenter, or rent a 2U server colocation, hosting web applications can be a pain, and as DHH eloquently wrote it: <a href="https://world.hey.com/dhh/merchants-of-complexity-4851301b">the merchants of complexity</a> made sure all developers found themselves faced with overpriced technologies they wouldn’t actually need.</p> <p>But it turns out there’s a much simpler (and often free!) path to getting projects online, if you know where to look.</p> <h1 id="the-myth-of-the-million-visitors">The myth of the million visitors</h1> <p>Three years ago, I was writing the API for a chat app where the higher-ups suddenly decided we required a Kubernetes cluster to handle the influx of users that would be coming our way on launch day. It took three months and a team of two freelancers. In all fairness, those freelancers built exactly what was asked of them: our very own K8S cluster, with production/staging support, CD on git deploy, and even auto-rent of short-burst Amazon EC2 instances to get them for a lower price. Seriously, what could go wrong?</p> <p>The infrastructure was bulletproof. No traffic ever bothered to shoot at us.</p> <h1 id="before">Before</h1> <p>As mainly a web developer, each project always comes with a pain: where am I gonna host it? I’m not a viral developer most of the time, my projects could actually be run and served off my own laptop<sup id="fnref:badnetwork"><a href="#fn:badnetwork" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. What then?</p> <ul> <li><strong>Spin up a t3.nano on AWS?</strong> No, I don’t want to set up a server every time I want to put something online?</li> <li><strong>One giant server for everything<sup id="fnref:oneserver"><a href="#fn:oneserver" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>?</strong> That’s too much work, too expensive upfront, and often, too many app environments to handle together. And on <strong>AWS</strong>, do I really want to live in fear of the EGRESS FEES!?</li> <li><strong>So-called “serverless” <sup id="fnref:serverless"><a href="#fn:serverless" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> platforms like <a href="https://www.heroku.com/">Heroku</a> or <a href="https://fly.io/">Fly</a>?</strong> They are actually nice, but some of them (looking at you <a href="https://vercel.com/">Vercel</a>!) have a tendency to produce bills in the six-figure ranges<sup id="fnref:horrorstories"><a href="#fn:horrorstories" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>.</li> <li><strong><a href="https://pages.github.com/">Github Pages</a>?</strong> It’s free, comes with <a href="https://github.com/features/actions">GitHub actions</a> for deployments, and…there’s messing with <code class="language-console highlighter-rouge"><span class="go">gh-pages</span></code>.</li> </ul> <p>So <strong>Heroku</strong> ended up as my default: cheap, easy, and if my project ever uses too much resources, they’ll gladly put it down. Kind of reassuring, actually.</p> <p>But then I stumbled onto <strong>Cloudflare</strong>, and everything changed.</p> <h1 id="hosting-my-own-blog">Hosting my own blog</h1> <p>In May of 2024, I got back into the habit of reading books<sup id="fnref:readingbooks"><a href="#fn:readingbooks" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>, and a bad one got me to write about them<sup id="fnref:verybadbook"><a href="#fn:verybadbook" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>. So I went for a static blog with <a href="https://jekyllrb.com/">Jekyll</a><sup id="fnref:whyjekyll"><a href="#fn:whyjekyll" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>, and my first instinct was to look at <strong>Github Pages</strong>, but again, <code class="language-console highlighter-rouge"><span class="go">gh-pages</span></code>: I didn’t want to deal with “artifacts” or any of those strange branch-switching shenanigans.</p> <p>At that point, I still thought <strong>Cloudflare</strong> was only about those annoying “Are you human?” pages on direct-download sites, CDN, or protecting sites from DDoS and all, which are not problems I’ve ever come into contact with. So I had no idea how I stumbled onto <strong>Cloudflare Pages</strong>. Surely the pricing for that kind of around-the-world service would be expensive, right?</p> <blockquote> <p>On both free and paid plans, requests to static assets are free and unlimited. A request is considered static when it does not invoke Functions. <br /> ― <a href="https://developers.cloudflare.com/pages/functions/pricing/">Cloudflare Pages Pricing</a></p> </blockquote> <p>In <strong>Ruby on Rails</strong> terms, “static assets” means stuff like the CSS and the JavaScript that the server would create once at build time, and serve as-is to visitors. But if I’m using <strong>Jekyll</strong>, then that would mean the HTML files generated are ALSO static assets, right? Is it saying <strong>Cloudflare</strong> would host my blog FOR FREE!?</p> <p>Yes. Yes, it is.</p> <h1 id="hosting-sites-for-other-people-and-feeling-good-about-it">Hosting sites for other people (and feeling good about it)</h1> <p>Chances are, if you’re reading this, you’re the most tech-aware person of your family or friend group, and you’re often asked: “Can you make me a website for my (small company/association/shop idea)?”. It’s okay, we’ve all been there, and I’m pretty sure we’ve all gone through the same thought process:</p> <blockquote> <p>Sure, I can make a website. Do they have a domain name or should I set it up? Do they have hosting? Should I set it up all by myself? I guess they won’t need something as powerful as a full Rails engine, or a machine with 4TB of RAM. Should I go for something static then? But where? How will I pay? How do I make sure the domain/website is under their name even if I’m doing all the legwork? And what if they want to update some text, should I add an admin panel, but then it would require auth,… Do I need to set up anything LetsEncrypt again? <br /> ― You, me, and everybody else, before going to <a href="https://wordpress.com/">Wordpress.com</a>.</p> </blockquote> <p>So when my aunt came to me for help, I was relieved: her association already had the domain and hosting. You know the kind: <em>shared</em> hosting, with its own FTP server. As I’m writing this, I’m trying to remember: did the admin panel indicate they were running PHP 4 or 5? Because I know it was neither 7 nor 8<sup id="fnref:php6"><a href="#fn:php6" class="footnote" rel="footnote" role="doc-noteref">8</a></sup> and it’s making me shiver.</p> <p>Thankfully, she just wanted me to rework an existing landing page, and it was static: no strange PHP scripts, no MySQL injections<sup id="fnref:injection"><a href="#fn:injection" class="footnote" rel="footnote" role="doc-noteref">9</a></sup> to sanitize,… Since static page means static assets, I put everything on <strong>GitHub</strong>, connected the repository to Cloudflare, changed the DNS records in their domain’s admin panel, and <em>voila!</em>, now if I (or a future developer) wanted to update the contents of their website, a simple <code class="language-console highlighter-rouge"><span class="go">git push</span></code> would suffice, and the site would automatically deploy, and with a working SSL certificate!</p> <p>The next person who needed my help for a “website” was a friend who wanted to host an image online, so a QR code would lead to it, as part of a birthday treasure hunt. I could have sent her to <a href="https://imgur.com/">Imgur</a>, or said no. But now, what was once the daunting task of hosting was so painless that I set it up while half-watching a TV show: I deployed the image under a subdomain of my main site, and it just worked.</p> <p>That’s when I realized how happy I was. Hosting had become simple: not a bill to stress over, or a sysadmin side quest. Now, I could just put a file online, instantly, for anyone, with zero drama about pricing, or the egress fees of an <strong>AWS S3</strong> bucket. And I had <strong>Cloudflare</strong> to thank for it.</p> <h1 id="code-everywhere">Code everywhere</h1> <p>A few months later, one of my brothers was the one to ask for my help. With crypto. Oh boy.</p> <p>There was no trading involved, no <em>degens</em> to cater to,… But my brother had found a few wallets doing extraordinary calls, and he didn’t trust copy bot services. So while he could manually check the wallets online, having code to automate the process would make his “work”<sup id="fnref:tradingnotjob"><a href="#fn:tradingnotjob" class="footnote" rel="footnote" role="doc-noteref">10</a></sup> easier.</p> <p>I hadn’t touched crypto in a while, and my latest crypto project<sup id="fnref:valentinecoin"><a href="#fn:valentinecoin" class="footnote" rel="footnote" role="doc-noteref">11</a></sup> was a lot of fun to code for. So I went along with it, looked up the documentation, wrote a JS script, and…now what? My brother did have a computer, an old Windows laptop from who-knows-when, and I wasn’t sure he could even run the script. Would you believe who came to the rescue again?</p> <p>For another project, I had been looking at whether <strong>Cloudflare</strong> supported CRON triggers. Well, if it could run mail jobs at 2am, surely it could also run my bot’s “check-balances-and-send-message” loops. But for what price, would that be free too?</p> <p><a href="https://developers.cloudflare.com/workers/platform/pricing/">Yes. Yes, it is.</a></p> <p>The craziness of this pricing can’t be overstated: for my brother’s crypto fun, I had a CRON job running and sending him regular updates. I was even using a <strong>D1 Database</strong> to store the balances (which was also <a href="https://developers.cloudflare.com/d1/platform/pricing/">free for that level of usage</a>), and best of all, I didn’t have to worry about any kind of intrusion, which I would have with a regular server.</p> <p>I don’t know how much my brother made, as I don’t really care about trading, but a few months later, he bought himself a MacBook Air, and later offered to buy me a PS5 Pro when I mentioned I was waiting for <a href="https://www.youtube.com/watch?v=wbLstJHlC4U">Death Stranding 2</a> coming out in June.</p> <p>Which is very nice from him, since, so far in this story, I still hadn’t paid a cent to <strong>Cloudflare</strong>.</p> <h1 id="finally-paying-for-the-product">Finally paying for the product</h1> <p>I finally paid for Cloudflare.</p> <p>I still had a professional project running on <strong>Heroku</strong>, both <strong>Rails</strong> server and <strong>PostgreSQL</strong> database, and I wanted to move some parts of the server API to faster NodeJS servers. Unfortunately, the provided database was VERY conservative with the number of allowed client connections. So <a href="https://blog.cloudflare.com/hyperdrive-making-regional-databases-feel-distributed/">Hyperdrive</a> felt like an awesome solution, both from a technological standpoint and as an answer to my needs. There was no free tier<sup id="fnref:hyperdrivefreetier"><a href="#fn:hyperdrivefreetier" class="footnote" rel="footnote" role="doc-noteref">12</a></sup>, but just subscribing to the “Workers Plan” enabled unlimited usage, plus it would even increase the (very generous) limits of all other products.</p> <p>After a lot of tomfoolery regarding client connection limits and worldwide latency<sup id="fnref:comingsoon"><a href="#fn:comingsoon" class="footnote" rel="footnote" role="doc-noteref">13</a></sup>, I ended up with a NodeJS API available worldwide, connecting to my database through <strong>Hyperdrive</strong>, with a performance measured in the hundreds of milliseconds, all in one night’s work, and a commitment to a monthly payment of only <strong>5 DOLLARS</strong> per month. Take that, Kubernetes!</p> <p>Honestly, it didn’t even feel strange to finally “give in” and punch in my credit card digits: after so much free utility, it was a no-brainer. For the price of a fancy coffee<sup id="fnref:starbucks"><a href="#fn:starbucks" class="footnote" rel="footnote" role="doc-noteref">14</a></sup>, I got performance and global reach my old infra couldn’t touch!</p> <h1 id="the-future">The future</h1> <p>Hey, if someone at <strong>Cloudflare</strong> is reading this, do you have any opening for a developer evangelist in France?</p> <p>When I look at the <strong>Node</strong> APIs I had started writing to decouple some parts of my <strong>Rails</strong> monolith, they are hosted on either <strong>Heroku</strong>, or <strong>Fly</strong>, and these products are great, but getting one too many “{application_name} ran out of memory and crashed” emails from them was the nail in the coffin. Sure, I’m not leaving <strong>Heroku</strong> anytime soon, because <a href="https://activeadmin.info/">ActiveAdmin</a> is still a wonderful backoffice solution and requires <strong>Rails</strong>, but for anything that only needs <strong>JavaScript</strong>, I can’t imagine going anywhere other than the <strong>Workers</strong>.</p> <p>Which is even funnier when I consider that I’ve barely touched all the available services: I set up <a href="https://developers.cloudflare.com/email-routing/">Email routing</a> because I don’t need a full-priced tool just to redirect domain emails to my Gmail, I’m still scratching the surface of <a href="https://developers.cloudflare.com/workflows/">Workflows</a> (integrating that into my CRONs), I don’t need <a href="https://developers.cloudflare.com/r2/">R2</a> yet, and I haven’t tried <a href="https://developers.cloudflare.com/queues/">Queues</a> but they look interesting. And there are <a href="https://developers.cloudflare.com/products/?product-group=Developer+platform">a bunch more I haven’t even looked at yet</a>!</p> <p>But hey, if you’d rather spend time on Twitter circlejerking about the size of the Kubernetes cluster serving five users, or complaining about the six-figure bill that crept up on you<sup id="fnref:sixfigures"><a href="#fn:sixfigures" class="footnote" rel="footnote" role="doc-noteref">15</a></sup>, you do you.</p> <p>Me? I’m building on Cloudflare.</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:badnetwork"> <p>I could, if I had something better than 2MB/s on my home line. <a href="#fnref:badnetwork" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:oneserver"> <p>Most notably, “indie” maker <a href="https://x.com/levelsio">@levelsio</a> does that. <a href="#fnref:oneserver" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:serverless"> <p>Kudos to whoever coined this term that’s so far away from reality! <a href="#fnref:serverless" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:horrorstories"> <p>If you don’t believe me, check out <a href="https://serverlesshorrors.com/">Serverless Horrors</a>! <a href="#fnref:horrorstories" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:readingbooks"> <p>The book to kickstart it was <a href="https://x.com/jakeadelstein">Jake Adelstein</a>’s <a href="https://blog.dreamleaves.org/posts/tokyo-vice-book/">Tokyo Vice</a>. <a href="#fnref:readingbooks" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:verybadbook"> <p>This book shall live in infamy: <a href="https://blog.dreamleaves.org/posts/aux-origines-de-castlevania-sotn/">Aux Origines de Castlevania Symphony of the Night</a>. <a href="#fnref:verybadbook" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:whyjekyll"> <p>Why choose Jekyll? I’m a ruby boy at heart, and I liked the theme I had seen. <a href="#fnref:whyjekyll" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:php6"> <p>Can you believe <a href="https://ma.ttias.be/php6-missing-version-number/">there never was a PHP 6 major version</a>? <a href="#fnref:php6" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:injection"> <p>A staple of all developers who ever touched PHP is learning about <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL injections</a>, sometimes the hard way. <a href="#fnref:injection" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:tradingnotjob"> <p>I don’t believe day-trading to be a real job, more like an addiction. <a href="#fnref:tradingnotjob" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:valentinecoin"> <p>For Valentine’s Day 2018, I launched <a href="https://www.thevalentinecoin.com/">The Valentine Coin</a> with a friend. <a href="#fnref:valentinecoin" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:hyperdrivefreetier"> <p>Quite funnily, <strong>Hyperdrive</strong> now has a free tier, but it’s too low for my requirements. <a href="#fnref:hyperdrivefreetier" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:comingsoon"> <p>Which will be covered in a further post. <a href="#fnref:comingsoon" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:starbucks"> <p>And to think that ten years ago, I was buying myself one of those “fancy coffees” every day, if not two per day… <a href="#fnref:starbucks" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:sixfigures"> <p>Don’t forget to tweet “VICTORY!” when the company discounts it to four in “a commercial gesture”. <a href="#fnref:sixfigures" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>Or how I learned to stop worrying and embrace the Edge.</summary> </entry> <entry> <title>Adding Giscus comments to a static blog</title> <link href="https://tech.dreamleaves.org/posts/adding-giscus-comments-to-a-static-blog/" rel="alternate" type="text/html" title="Adding Giscus comments to a static blog" /> <published>2024-10-31T00:00:00+01:00</published> <updated>2024-10-31T00:00:00+01:00</updated> <id>https://tech.dreamleaves.org/posts/adding-giscus-comments-to-a-static-blog/</id> <content type="html"><![CDATA[ <p>For my <a href="https://blog.dreamleaves.org/">personal blog</a>, which I started a few months ago already, I don’t really need comments. It’s more of a “keep-a-useful-calendar-of-medias-I-consume” list, I write it in French, and it’s not meant to open discussion, so I wasn’t looking at comments. At all.</p> <p>However, for this tech-related blog, <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/">posting the first article on Reddit</a> gave way to very interesting discussions and commentaries. So I figured: “Heh, why not?”.</p> <h1 id="the-state-of-internet-comments-in-2024">The state of Internet comments in 2024</h1> <p>My very first personal blog, back in 2004, was written in PHP and hosted on a Free server<sup id="fnref:free"><a href="#fn:free" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. The (anonymous) comments were saved in own my database, and when I went back to check on it twenty years later, the place was littered with spam, and what I’m pretty sure were various attempts at HTML injections.</p> <p>Clearly, nobody wants that. And nobody wants to spend time away from writing in order to play warden and build an user-authentification service just for a few comments<sup id="fnref:fewcomments"><a href="#fn:fewcomments" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. Plus, this blog’s HTML is only rendered once I deploy it, so there’ no way I can add comments anywhere, I guess.</p> <p>And then there is <a href="https://en.wikipedia.org/wiki/Disqus">Disqus</a>.</p> <p>Disqus looks nice. It’s a testament to its beauty in design that every solution tries to imitate Disqus today. I remember over time seeing a LOT of competitors<sup id="fnref:moot"><a href="#fn:moot" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>, but only Disqus survived the test of time, and grew old enough to drink some alcohol in France.</p> <p>However, Disqus is also a proprietary solution, I’m not entirely sure of where they stand on <a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">GDPR</a>, I’ve seen enough Disqus conversations get derailed by trolls,… This is not the right choice for a tech blog.</p> <h1 id="enter-giscus">Enter Giscus</h1> <p>On the paper, <a href="https://giscus.app/">Giscus</a> fills what I’m missing:</p> <ul> <li>Open-source.</li> <li>Commenting requires a GitHub account.</li> <li>Comments are hosted on GitHub.</li> </ul> <p>Therefore, only developers will comment. Maybe it’s gonna end up being a fate worse than trolls.</p> <h2 id="setting-up-giscus">Setting up Giscus</h2> <p>Setting up Giscus is actually a breeze and their homepage is a very nice setup wizard. My only issues were:</p> <ul> <li>Which repository is best suited for Giscus?</li> </ul> <p>Giscus comment sections are tied to a repository, which left me puzzled, as I wasn’t sure WHICH repository I should tie them to: <a href="https://github.com/joshleaves/redrust/">this blog’s repository</a>? Or another and completely unrelated one?</p> <p>The answer is…it’s up to you, but it must be public. So if your blog is in a private repository, you’ll need a specific repo for the comments. I chose to have everything together to have the full “static-page-blog-on-github” experience, but it’s nice to have a choice.</p> <ul> <li>What are those pathname/url/&amp;lt;title&amp;gt; options?</li> </ul> <p>Just pick one and forget about it. Having this option feels like too much hassle for what it is, and if you ever change an article’s title/URL, you’ll be screwed anyway. I’d have like an option to map the conversation to something like <code class="language-console highlighter-rouge"><span class="gp">&amp;lt;meta property="giscus:uuid" value=&amp;gt;</span></code>, and maybe this will be available in the future, BUT this would break the nice display of the discussions on Github.</p> <p>For my part, I started with <code class="language-console highlighter-rouge"><span class="go">pathname</span></code> but just having an URL as the title of the GitHub Discussion was irking me, so I went the <code class="language-console highlighter-rouge"><span class="go">og:title</span></code> route and customised it a little to add the date.</p> <ul> <li>Which theme should I use on my blog?</li> </ul> <p>I’m used to Dark Mode on Github, but this blog is very LIGHT. Starting with a dark theme was a wrong (and very ugly) move, and trying one of the “no-border” themes rendered the comment section almost invisible.</p> <p>The preview on the setup wizard isn’t very effective in guessing how it’s gonna fit on your blog, so apart from manually trying each theme, just decide on <code class="language-console highlighter-rouge"><span class="go">light_protanopia</span></code> or <code class="language-console highlighter-rouge"><span class="go">dark_protanopia</span></code> and roll on with it.</p> <h2 id="adding-giscus">Adding Giscus</h2> <p>You’ll get a nice <code class="language-console highlighter-rouge"><span class="gp">&amp;lt;script&amp;gt;</span></code> tag to copy-paste into your template. The whole process is <a href="https://github.com/joshleaves/redrust/commit/9f7e5257d1887207baaa70d71334851811ddb4dc">so simple, it’s almost ridiculous</a>.</p> <h1 id="and-then">And then?</h1> <p>An input will be available when your visitors reach the end of your posts, and they’ll be able to post comments.</p> <p>Whenever a comment is posted:</p> <ul> <li>you’ll get notification in GitHub (nice!).</li> <li>you’ll be able to follow (and moderate?) the discussion in your repository’s <a href="https://github.com/joshleaves/redrust/discussions">Discussions</a> tab.</li> </ul> <p>Happy commenting.</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:free"> <p>“Free” as in both <a href="https://en.wiktionary.org/wiki/free_of_charge">“free of charge”</a>, and <a href="https://en.wikipedia.org/wiki/Free_(ISP)">“Free the French ISP”</a>. <a href="#fnref:free" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:fewcomments"> <p>But I’m keeping hope I will become famous in the future and get tons of comments! <a href="#fnref:fewcomments" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:moot"> <p>I remember being quite fond of <a href="https://news.ycombinator.com/item?id=6818416">moot</a> back then, <a href="https://en.wikipedia.org/wiki/Christopher_Poole">just for its name</a>, but the product is dead as nails today. <a href="#fnref:moot" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>How easy it is to add Giscus to a blog?</summary> </entry> <entry> <title>Trimming down a rust binary in half</title> <link href="https://tech.dreamleaves.org/posts/trimming-down-a-rust-binary-in-half/" rel="alternate" type="text/html" title="Trimming down a rust binary in half" /> <published>2024-10-27T00:00:00+02:00</published> <updated>2024-10-27T00:00:00+02:00</updated> <id>https://tech.dreamleaves.org/posts/trimming-down-a-rust-binary-in-half/</id> <content type="html"><![CDATA[ <div class="alert alert-success alert-white rounded"> <div class="icon"><i class="fa fa-check"></i></div> <strong>I posted this on reddit!</strong> <p>The <a href="conversations that ensued">https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/</a> may interest you, but the best parts have been added to this post.</p> </div> <p>Lately, I’ve stumbled on a <a href="https://kobzol.github.io/rust/cargo/2024/01/23/making-rust-binaries-smaller-by-default.html">blog post about Rust binary sizes</a>. I haven’t done much compilation<sup id="fnref:compilation"><a href="#fn:compilation" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> since I last touched C in school, but I was intrigued by the subject and decided to look at how binary size reduction could impact my own Rust project: <a href="https://github.com/joshleaves/advent-rs">advent-rs</a>, a simple binary taking year/day/part/filename parameters and solving exercises from the <a href="https://adventofcode.com/">AdventOfCode</a> online contest.</p> <h2 id="starting-point">Starting point</h2> <p>There’s already some stuff I know and set up properly, so I already got the correct defaults for my <code class="language-console highlighter-rouge"><span class="go">Cargo.toml</span></code>.</p> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="p">[</span><span class="n">profile</span><span class="py">.release</span><span class="p">]</span>
<span class="n">opt</span><span class="o">-</span><span class="n">level</span> <span class="o">=</span> <span class="mi">3</span>
</pre></div></div> <p>Let’s look at the resulting file through <code class="language-console highlighter-rouge"><span class="go">size</span></code><sup id="fnref:size"><a href="#fn:size" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> and <code class="language-console highlighter-rouge"><span class="go">ls</span></code>:</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>size target/release/advent-rs
__TEXT	__DATA	__OBJC	others	dec	hex
1409024	16384	  0	      4295393280	4296818688	1001c4000

<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-lah</span>  target/release/advent-rs
 <span class="nt">-rwxr-xr-x</span>@ 1 red  staff   1.8M Oct 27 08:49 target/release/advent-rs
</pre></div></div> <p>So we are standing at 1.8M, for a binary that’s solving around 150 exercices. That doesn’t sound so bad, but again, I haven’t compiled code in a long time, so I have no idea where I stand. Let’s check the first binary I can think of.</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-lah</span> /bin/ls
<span class="nt">-rwxr-xr-x</span>  1 root  wheel   151K Sep  5 11:17 /bin/ls
</pre></div></div> <p>Well, I definitely need to do some trimming.</p> <h2 id="optimizing-compilation">Optimizing compilation</h2> <p>First things first, let’s check compilation.</p> <h3 id="start-with-stripping">Start with stripping</h3> <p>As I remember from my C days, and as the blog clearly states: not stripping symbols from a release is the root of all evil.</p> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>This is not an absolute!</strong> <p>As <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu0rcsh/">fredbrancz</a> rightly said, if you release a binary to the public at large and later require debugging it, missing the symbols will come back at you fast!</p> </div> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="p">[</span><span class="n">profile</span><span class="py">.release</span><span class="p">]</span>
<span class="n">opt</span><span class="o">-</span><span class="n">level</span> <span class="o">=</span>  <span class="mi">3</span>
<span class="n">strip</span> <span class="o">=</span> <span class="k">true</span>
</pre></div></div> <p>Build, check file.</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>/bin/ls <span class="nt">-lah</span>  target/release/advent-rs
<span class="nt">-rwxr-xr-x</span>@ 1 red  staff   1.5M Oct 27 09:03 target/release/advent-rs
</pre></div></div> <p>Already 300kb trimmed off!</p> <h3 id="halt-and-catch-fire">Halt and catch fire</h3> <p>A <strong>backtrace</strong> is a very useful to have when debugging, all interpreted languages come with them, and for a while, I wasn’t even surprised to see them pop up in Rust, in spite of C never printing one, only going as far as telling me “Segmentation fault” when my carefully-crafted binary would have the politeness to tell me on its own that I was doing something wrong.</p> <p>Well, Rust should not have them.</p> <p>Wait, let me explain myself here: backtraces are not a normal feature of a compiled language, since the backtraces you are used to actually come courtesy of the interpreter. So in a compiled language with no interpreter, where do they come from?</p> <p>Yes, from your binary.</p> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="p">[</span><span class="n">profile</span><span class="py">.release</span><span class="p">]</span>
<span class="n">opt</span><span class="o">-</span><span class="n">level</span> <span class="o">=</span>  <span class="mi">3</span>
<span class="n">strip</span> <span class="o">=</span> <span class="k">true</span>
<span class="n">panic</span> <span class="o">=</span> <span class="s">"abort"</span>
</pre></div></div> <p>Build, check file.</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>/bin/ls <span class="nt">-lah</span>  target/release/advent-rs
<span class="nt">-rwxr-xr-x</span>@ 1 red  staff   1.2M Oct 27 09:05 target/release/advent-rs
</pre></div></div> <p>Can you believe this shaved off another 300kb?</p> <h3 id="insane-stuff-i-dont-want">Insane stuff I don’t want</h3> <p>The <a href="https://doc.rust-lang.org/cargo/reference/profiles.html">Cargo documentation</a> gives us more options we can experiment with for fun. They don’t work for me but your mileage may vary.</p> <h4 id="optimize-for-size">Optimize for size</h4> <p>After <code class="language-console highlighter-rouge"><span class="go">3</span></code>, there are <code class="language-console highlighter-rouge"><span class="go">opt-level</span></code> values that target smaller binaries. I can try with <code class="language-console highlighter-rouge"><span class="go">s</span></code>or <code class="language-console highlighter-rouge"><span class="go">z</span></code> and they will respectively shave off 100 and 200kb, BUT the tradeoff here is execution speed, which is not a value I want to allow myself to play with<sup id="fnref:speed"><a href="#fn:speed" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p> <h4 id="dont-check-integer-overflow">Don’t check integer overflow</h4> <p><del>You can disable checking for integer overflow with <code class="language-console highlighter-rouge"><span class="go">overflow-checks = false</span></code> from your binary. Obviously, this is NOT recommended when you’re playing with user input, and in my case, it won’t even register any change.</del></p> <div class="alert alert-danger alert-white rounded"> <div class="icon"><i class="fa fa-times-circle"></i></div> <strong>Wrong!</strong> <p>As <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu17nls/">GamerCounter</a> pointed out, these checks are removed by default in release mode.</p> </div> <h4 id="link-time-optimization">Link-time optimization</h4> <p>It seems that other than compiling, you can also optimise linking<sup id="fnref:linking"><a href="#fn:linking" class="footnote" rel="footnote" role="doc-noteref">4</a></sup> with <code class="language-console highlighter-rouge"><span class="go">lto = true</span></code>. I don’t recommend it since it doubled my build time AND didn’t give me a good size reduction…</p> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>Attention!</strong> <p>I wasn’t clear enough on this, but as <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu1eg7j/">VorpalWay</a> and <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu1l7eg/">hubbamybubba</a> correctly pointed out, the benefits of this parameter depends on your project.</p> </div> <h1 id="cleaning-up-crates">Cleaning up crates</h1> <p>The other thing that will take up space is…code. More specifically, code you wouldn’t need. Let’s check my dependencies:</p> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="p">[</span><span class="n">dependencies</span><span class="p">]</span>
<span class="n">clap</span> <span class="o">=</span> <span class="p">{</span> <span class="n">version</span> <span class="o">=</span> <span class="s">"4.5.1"</span><span class="p">,</span> <span class="n">features</span> <span class="o">=</span> <span class="p">[</span><span class="s">"derive"</span><span class="p">]</span> <span class="p">}</span>
<span class="n">itertools</span> <span class="o">=</span> <span class="s">"0.12.1"</span>
<span class="n">md</span><span class="o">-</span><span class="mi">5</span> <span class="o">=</span> <span class="s">"0.10.6"</span>
<span class="n">mutants</span> <span class="o">=</span> <span class="s">"0.0.3"</span>

<span class="p">[</span><span class="n">dev</span><span class="o">-</span><span class="n">dependencies</span><span class="p">]</span>
<span class="n">assert_cmd</span> <span class="o">=</span> <span class="s">"2.0.13"</span>
<span class="n">predicates</span> <span class="o">=</span> <span class="s">"3.1.0"</span>
<span class="n">criterion</span> <span class="o">=</span> <span class="p">{</span> <span class="n">version</span> <span class="o">=</span> <span class="s">"0.5.1"</span><span class="p">,</span> <span class="n">features</span> <span class="o">=</span> <span class="p">[</span><span class="s">"html_reports"</span><span class="p">]</span> <span class="p">}</span>
</pre></div></div> <p>Nothing too barbaric here, <a href="https://github.com/clap-rs/clap">clap</a> is the <em>de-facto</em> crate to parse command-line arguments, <a href="https://docs.rs/itertools/latest/itertools/">itertools</a> is required because a ton of exercises require iterating over data in strange ways, I require <a href="https://docs.rs/md-5/latest/md5/">md-5</a> for four exercices, and <a href="https://github.com/sourcefrog/cargo-mutants">mutants</a> is allowed here only to <a href="https://mutants.rs/attrs.html">skip some tests when running mutations</a>.</p> <p>As for the development dependencies, I know I got nothing to fear from them.</p> <h1 id="removing-a-crate">Removing a crate</h1> <p>You may feel like the above dependencies are <strong>sane</strong>, that I need all of them, that there is no way my project could function without any of them,… But what if it could?</p> <h2 id="past-experience">Past experience</h2> <p>When I started writing all of these parsers I required for handling input data, I often used the <a href="https://docs.rs/regex/latest/regex/">regex crate</a>. While it was very useful, I knew from my benchmarks that using old-school “split-on-this-char” approach was faster<sup id="fnref:speed:1"><a href="#fn:speed" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>, so I slowly started phasing it out.</p> <p>As of <a href="https://github.com/joshleaves/advent-rs/commit/50cb0325dcaa039049106057a2c64e635b3e8955#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542L35">Year 2017: Day 15</a>, I removed the crate from my <code class="language-console highlighter-rouge"><span class="go">Cargo.toml</span></code>.</p> <h2 id="future-ideas">Future ideas</h2> <p>While I know that “rolling out your own crypto” is a bad idea, the crate <code class="language-console highlighter-rouge"><span class="go">md-5</span></code>can clearly be replaced. Plus the fact that its input and outputs are of different types mean one exercise could actually be way faster<sup id="fnref:fastermd5"><a href="#fn:fastermd5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> if I rolled out my own!</p> <h1 id="okay-now-wheres-the-bloat">Okay, now where’s the bloat?</h1> <p>I found out the appropriately-named tool <a href="https://github.com/RazrFalcon/cargo-bloat">cargo-bloat</a> on GitHub.</p> <p>Let’s install and try it, it’s just a command<sup id="fnref:cargo-bloat"><a href="#fn:cargo-bloat" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> after all.</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>cargo bloat <span class="nt">--release</span>
    Finished <span class="sb">`</span>release<span class="sb">`</span> profile <span class="o">[</span>optimized] target<span class="o">(</span>s<span class="o">)</span> <span class="k">in </span>0.01s
    Analyzing target/release/advent-rs

 File  .text     Size        Crate Name
 1.1%   1.6%  16.0KiB clap_builder clap_builder::parser::parser::Parser::get_matches_with
 1.1%   1.6%  15.8KiB    <span class="o">[</span>Unknown] __mh_execute_header
 0.7%   1.1%  10.5KiB clap_builder clap_builder::builder::command::Command::_build_self
 0.6%   0.9%   8.9KiB          std std::backtrace_rs::symbolize::gimli::resolve
 0.6%   0.9%   8.7KiB          std std::backtrace_rs::symbolize::gimli::Context::new
 0.5%   0.7%   7.1KiB          std gimli::read::dwarf::Unit&amp;lt;R&amp;gt;::new
 0.4%   0.7%   6.5KiB clap_builder &amp;lt;clap_builder::error::format::RichFormatter as clap_builder::error::format::ErrorFormatter&amp;gt;::format_error
 0.4%   0.6%   6.1KiB clap_builder clap_builder::parser::validator::Validator::validate
 0.4%   0.6%   5.8KiB    advent_rs core::slice::sort::stable::quicksort::quicksort
 0.4%   0.6%   5.6KiB          std addr2line::ResUnit&amp;lt;R&amp;gt;::find_function_or_location::
 0.4%   0.5%   5.2KiB          std addr2line::Lines::parse
 0.4%   0.5%   5.2KiB clap_builder &amp;lt;alloc::vec::Vec&amp;lt;T,A&amp;gt; as core::clone::Clone&amp;gt;::clone
 0.3%   0.5%   5.0KiB clap_builder clap_builder::output::help_template::HelpTemplate::write_all_args
 0.3%   0.5%   5.0KiB clap_builder clap_builder::output::usage::Usage::write_arg_usage
 0.3%   0.5%   4.9KiB    advent_rs advent_rs::year_2016::day_10::solve
 0.3%   0.4%   4.3KiB clap_builder clap_builder::parser::parser::Parser::react
 0.3%   0.4%   4.2KiB clap_builder clap_builder::output::usage::Usage::write_usage_no_title
 0.3%   0.4%   4.2KiB    advent_rs advent_rs::year_2017::day_21::Image::mutate
 0.3%   0.4%   4.1KiB          std addr2line::function::Function&amp;lt;R&amp;gt;::parse_children
 0.3%   0.4%   3.9KiB clap_builder clap_builder::output::help_template::HelpTemplate::write_templated_help
58.8%  87.9% 872.3KiB              And 1925 smaller methods. Use <span class="nt">-n</span> N to show more.
66.9% 100.0% 992.5KiB              .text section size, the file size is 1.4MiB
</pre></div></div> <div class="content-img" style="width: 50%"> <img src="/assets/images/2024-10-27/fortune-teller.jpeg" title=" Never going to that fortune-teller ever again " /> <div class="img-alt"> <p>Never going to that fortune-teller ever again</p> </div> </div> <p>Something must be wrong, let’s try another command.</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>cargo bloat <span class="nt">--release</span> <span class="nt">--crates</span>
    Finished <span class="sb">`</span>release<span class="sb">`</span> profile <span class="o">[</span>optimized] target<span class="o">(</span>s<span class="o">)</span> <span class="k">in </span>0.01s
    Analyzing target/release/advent-rs

 File  .text     Size Crate
25.9%  38.8% 384.8KiB std
22.3%  33.4% 331.1KiB advent_rs
15.8%  23.5% 233.6KiB clap_builder
 1.2%   1.7%  17.1KiB itertools
 1.1%   1.6%  15.9KiB <span class="o">[</span>Unknown]
 0.7%   1.0%  10.0KiB md5
 0.3%   0.5%   5.0KiB digest
 0.3%   0.5%   4.6KiB strsim
 0.2%   0.3%   3.3KiB clap_lex
 0.1%   0.2%   2.1KiB anstyle
 0.1%   0.2%   1.8KiB anstream
 0.0%   0.0%      16B colorchoice
66.9% 100.0% 992.5KiB .text section size, the file size is 1.4MiB
</pre></div></div> <p>First off, I’m surprised that md5 is only taking around 10Kb, so maybe I won’t need to replace it until I really want to shave a few milliseconds off that one exercise<sup id="fnref:fastermd5:1"><a href="#fn:fastermd5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>. But for the rest…</p> <h1 id="what-the-fuck-clap">What the fuck Clap?</h1> <p>It seems I’m <a href="https://github.com/clap-rs/clap/issues/1365">not the first person to complain about clap’s binary size</a>. I’m even surprised it can even get to this size because its only use is to parse an array of strings.</p> <p>I mean, seriously:</p> <div class="language-rust highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(version,</span> <span class="nd">about,</span> <span class="nd">long_about</span> <span class="nd">=</span> <span class="nd">None)]</span>
<span class="k">struct</span> <span class="n">Args</span> <span class="p">{</span>
  <span class="cd">/// The year of the exercise, from 2015 to today</span>
  <span class="nd">#[arg(short,</span> <span class="nd">long,</span> <span class="nd">value_parser</span> <span class="nd">=</span> <span class="nd">clap::value_parser</span><span class="err">!</span><span class="nd">(u16))]</span>
  <span class="n">year</span><span class="p">:</span>  <span class="nb">u16</span><span class="p">,</span>
  
  <span class="cd">/// The day of the exercise, from 1 to 25</span>
  <span class="nd">#[arg(short,</span> <span class="nd">long,</span> <span class="nd">value_parser</span> <span class="nd">=</span> <span class="nd">clap::value_parser</span><span class="err">!</span><span class="nd">(u8))]</span>
  <span class="n">day</span><span class="p">:</span>  <span class="nb">u8</span><span class="p">,</span>

  <span class="cd">/// The part of the exercise, 1 or 2</span>
  <span class="nd">#[arg(short,</span> <span class="nd">long,</span> <span class="nd">default_value_t</span> <span class="nd">=</span> <span class="mi">1</span><span class="nd">,</span> <span class="nd">value_parser</span> <span class="nd">=</span> <span class="nd">clap::value_parser</span><span class="err">!</span><span class="nd">(u8))]</span>
  <span class="n">part</span><span class="p">:</span>  <span class="nb">u8</span><span class="p">,</span>

  <span class="cd">/// File name</span>
  <span class="nd">#[arg(help</span> <span class="nd">=</span>  <span class="s">"Input file path (will read from STDIN if empty)"</span><span class="nd">,</span> <span class="nd">value_parser</span> <span class="nd">=</span> <span class="nd">clap::value_parser</span><span class="err">!</span><span class="nd">(PathBuf))]</span>
  <span class="n">input</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&amp;lt;</span><span class="n">PathBuf</span><span class="o">&amp;gt;</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">let</span>  <span class="n">args</span>  <span class="o">=</span> <span class="nn">Args</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
<span class="p">}</span>
</pre></div></div> <p>And I’m not even using ALL the features!</p> <div class="alert alert-info alert-white rounded"> <div class="icon"><i class="fa fa-info-circle"></i></div> <strong>By the way...</strong> <p>I wasn’t very clear on this, but as <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu1l7eg/">hubbamybubba</a> specified, you can select features from a crate import in your Cargo.toml.</p> </div> <h1 id="picking-a-sane-alternative-to-clap">Picking a sane alternative to Clap</h1> <p>Am I in the mood to look up for a new crate, and completely rewrite the input part of my code? Not really, but I’ve got a blog to write!</p> <p>Let’s do a quick Google, and thankfully, among the first results is a <a href="https://github.com/rosetta-rs/argparse-rosetta-rs">recap of various arg-parsing libraries</a>:</p> <ul> <li><strong>null</strong> Obviously, I’m not in the mood to parse them by name.</li> <li><strong><a href="https://github.com/google/argh">argh</a></strong> Looks very similar to Clap.</li> <li><strong><a href="https://github.com/pacak/bpaf">bpaf</a></strong> If I wanted a DSL, I’d be using Ruby.</li> </ul> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>That's not a DSL!</strong> <p>As <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu35rsm/">manpacket</a> corrected me, bpaf’s API is not a <a href="https://en.wikipedia.org/wiki/Domain-specific_language">DSL (Domain-Specific Language)</a>, but a <a href="https://en.wikipedia.org/wiki/Fluent_interface">Fluent Interface</a>.</p> </div> <ul> <li><strong><a href="https://docs.rs/gumdrop/latest/gumdrop/">gumdrop</a></strong> That looks like A LOT of code.</li> <li><strong><a href="https://github.com/blyxxyz/lexopt">lexop</a></strong> I kinda like this one’s approach!</li> <li><strong><a href="https://github.com/RazrFalcon/pico-args">pico-args</a></strong> No examples, that’s too simple for my taste.</li> <li><strong><a href="https://github.com/matklad/xflags">xflags</a></strong> Looks a bit too complicated.</li> </ul> <p>I can easily start with <strong>Argh</strong> then, it looks similar enough that I could maybe just drop it in and have it work.</p> <div class="alert alert-warning alert-white rounded"> <div class="icon"><i class="fa fa-warning"></i></div> <strong>Attention!</strong> <p>While many other libraries are just as small, I chose <strong>argh</strong> because it was simpler to adapt my code, but it comes with (depending on your setup) a huge caveat: <a href="https://www.reddit.com/r/rust/comments/1gdd0md/trimming_down_a_rust_binary_in_half/lu1cq23/">it’s got issues with invalid UTF-8</a>.</p> </div> <h1 id="the-beauty-of-rust-unit-tests">The beauty of Rust unit tests</h1> <p>As a developper, I’ve written a lot of untested code.</p> <p>With <a href="https://github.com/joshleaves/advent-rb">advent-rb</a>, I used unit tests for each exercice, using the examples as inputs and result values in <a href="https://github.com/rspec">RSpec tests</a>, which was very fun.</p> <p>With <a href="https://github.com/joshleaves/advent-rs">advent-rs</a>, and Rust in general, I discovered a new paradigm. While all the languages I ever used made it “easy” to forget to add another file with the correct name, asked me to remember a complicated syntax, made me think of ways to cheat on my code to access private members from another file,… Rust just made away with that: testing a function is done from the same file where it’s defined, and I don’t even have to care about visibility.</p> <div class="content-img" style="width: 75%"> <img src="/assets/images/2024-10-27/allthere-720x405.jpg" title=" It's all there! " /> <div class="img-alt"> <p>It’s all there!</p> </div> </div> <p>The only thing not handled out-of-the-box is testing a binary (well, that’s not really a unit test), but since everything else was so easy, it felt okay to take some time to write a test specifically for that in <code class="language-console highlighter-rouge"><span class="go">tests/cli.rs</span></code>.</p> <p>In that case, rewriting is very easy:</p> <ul> <li>remove <code class="language-console highlighter-rouge"><span class="go">clap</span></code> from dependencies.</li> <li>add <code class="language-console highlighter-rouge"><span class="go">argh</span></code> to dependencies.</li> <li>rewrite my <code class="language-console highlighter-rouge"><span class="gp">#</span><span class="o">[</span>derive<span class="o">()]</span></code></li> <li>some syntax differences</li> </ul> <p>Quite frankly, the <a href="https://github.com/joshleaves/advent-rs/commit/e821b287eb567627977087b49663717023d224ce">whole diff</a> is nothing short of hilarious given how simple it is.</p> <p>But the best part?</p> <p>I can just run <code class="language-console highlighter-rouge"><span class="go">cargo test</span></code> and my tests will tell me everything works the same!</p> <h1 id="cleaning-up">Cleaning up</h1> <p>Let’s check up on that bloat one more time, shall we?</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>cargo bloat <span class="nt">--release</span> <span class="nt">--crates</span>
    Finished <span class="sb">`</span>release<span class="sb">`</span> profile <span class="o">[</span>optimized] target<span class="o">(</span>s<span class="o">)</span> <span class="k">in </span>0.01s
    Analyzing target/release/advent-rs

 File  .text     Size Crate
32.8%  49.3% 360.3KiB std
30.3%  45.5% 332.6KiB advent_rs
 1.6%   2.4%  17.2KiB itertools
 1.0%   1.5%  11.2KiB <span class="o">[</span>Unknown]
 0.9%   1.4%  10.0KiB md5
 0.6%   0.8%   6.2KiB argh
 0.5%   0.7%   5.0KiB digest
66.5% 100.0% 730.3KiB .text section size, the file size is 1.1MiB
</pre></div></div> <p>As for the file size:</p> <div class="language-bash highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-lah</span>  target/release/advent-rs
<span class="nt">-rwxr-xr-x</span>@ 1 red  staff   898K Oct 27 11:02 target/release/advent-rs
</pre></div></div> <p>Woah, not bad, that shaved off 300kb more, and we reduced our total binary size by almost 1MB.</p> <h1 id="going-just-a-bit-further">Going just a bit further</h1> <p>Once again, I am standing on the shoulders of giants, and while I am quite happy with the trimming I did, I found way more experimentations I could perform on <a href="https://github.com/johnthagen/min-sized-rust">johnthagen’s min-sized-rust repository</a>.</p> <p>However, those solutions either require unstable features (we don’t do <code class="language-console highlighter-rouge"><span class="go">nightly</span></code> in this house), or stripping off Rust’s standard library, which is a whole other can of worms…</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:compilation"> <p>Blame NodeJS and Ruby! <a href="#fnref:compilation" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:size"> <p>As its <a href="https://linux.die.net/man/1/size">man page</a> will tell you, <code class="language-console highlighter-rouge"><span class="go">size</span></code> prints the “sections” in a binary, helping you visualise the separation between code and data for instance. It’s not very needed here, but it’s a useful command to know about. <a href="#fnref:size" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:speed"> <p>This project was also built to compete with <a href="https://github.com/joshleaves/advent-rb">advent-rb</a>, the same project written in Ruby, to marvel at the speed of execution I was missing from compiled languages. <a href="#fnref:speed" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a> <a href="#fnref:speed:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;<sup>2</sup></a></p> </li> <li id="fn:linking"> <p>Linking is the second part of building a project: in layman’s terms, it will bind all code parts together into a single executable. <a href="#fnref:linking" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> <li id="fn:fastermd5"> <p>Part two of <a href="https://adventofcode.com/2016/day/14">Year 2016: Day 14</a> requires for a string to be hashed on itself 2016 times. Since the input must be a string, and the output is a buffer of hexadecimal data, my code needs to re-translate that hex buffer to a string 2016 times… <a href="#fnref:fastermd5" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a> <a href="#fnref:fastermd5:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;<sup>2</sup></a></p> </li> <li id="fn:cargo-bloat"> <p>The <code class="language-console highlighter-rouge"><span class="go">cargo-bloat</span></code> command itself will actually bloat your code. I haven’t checked out how, but I’m 99% sure it’s keeping in some symbols in so it can return the name of the fonction taking space. In my case, it’s adding around 200kb. <a href="#fnref:cargo-bloat" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p> </li> </ol> </div> ]]></content> <author> <name>Arnaud 'red' Rouyer</name> </author> <summary>In which we learn to slim down Rust binaries.</summary> </entry> </feed>
