iliana.fyiiliana’s blog2023-12-29T20:00:00+00:00https://iliana.fyi/atom.xmlHow I forked SteamOS for my living room PCiliana etaoin2023-12-29T20:00:00+00:002023-12-29T20:00:00+00:00https://iliana.fyi/blog/build-your-own-steamos-updates/<p>SteamOS 3 (“Holo”) is the Arch-based Linux distribution built for the Steam Deck, Valve Software’s portable PC gaming device. It’s a very interesting Linux distribution even when you only focus on how it updates itself: updates are performed atomically by downloading a new read-only root filesystem to an inactive partition, then rebooting into that partition. But consumers can also run <code>steamos-devmode</code> to unlock the root filesystem, put the pacman database in working order, and give them a working Linux distro with a normal package manager.</p>
<p>This A/B atomic updates system is pretty standard for OSes these days, but there’s a lot going on in SteamOS that makes them work even with heavy customization by the end-user. I wanted to explore that while still being able to make changes to the root filesystem images. <code>steamos-devmode</code> is the easy way out; I wanted to make a proper fork. Here’s how I did it.</p>
<ili-callout role="note">
<p>I use these instructions, but am providing them here to help publicly document some of what SteamOS does. I’m not responsible for you breaking your Steam Deck, and if you send me questions asking why these steps don’t work for you I won’t answer.</p>
</ili-callout>
<h2>Why?</h2>
<p>I don’t own a Steam Deck. A bunch of my friends do, but they know better than to let me have root access on a device they actually like using. What I do have is a recently-built living room PC that I wanted to play games on… and SteamOS seemed like a reasonable choice. It almost even worked perfectly out of the box, although I think that is primarily because I built a computer that looks vaguely similar to a really heavy, battery-less Steam Deck<sup><a href="#user-content-fn-fragile" id="user-content-fnref-fragile" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>.</p>
<p>The one thing that didn’t work was resume from suspend. Other distributions running on my computer using mainline or stable kernels did. Eventually, I found <a href="https://steamdeck-packages.steamos.cloud/archlinux-mirror/sources/">the sources for Valve’s kernel</a> (it’s weird, I’ll explain when we get there) and started a <code>git bisect</code>, leading me to a commit that seems to fix resume from suspend on Steam Deck hardware, but ultimately breaks it on mine. Needing to revert this commit and do my own build is the ultimate reason I headed down this path.</p>
<p>At several points in this process my partner asked if this made more sense than just using Arch or something else directly. I still don’t know the answer, although I think I still prefer relying on a Valve-tested set of packages than whatever’s current in the Arch repos. If I’m going to have to tinker with a Linux distro for running games, it may as well be one that people actually test their games on.</p>
<p>I apparently have a tendency to make poor choices like this, because you are reading the second post in what has become a series on <a href="https://iliana.fyi/blog/installing-fedora-on-mac-mini/">installing Linux distros onto systems they’re not ready for yet</a>.</p>
<h2>SteamOS in a nutshell</h2>
<p>A SteamOS system has eight partitions. The stage 1 bootloader is stored on the EFI system partition, along with metadata files that describe the two A/B partition sets and how to choose which one to boot. Each of the two partition sets contains a stage 2 bootloader (GRUB), the root filesystem, and a <code>/var</code> partition. Finally, there’s a single <code>home</code> partition that fills the rest of the disk.</p>
<pre><code>Number Start (sector) End (sector) Size Code Name
1 2048 133119 64.0 MiB EF00 esp
2 133120 198655 32.0 MiB 0700 efi-A
3 198656 264191 32.0 MiB 0700 efi-B
4 264192 10749951 5.0 GiB 8304 rootfs-A
5 10749952 21235711 5.0 GiB 8304 rootfs-B
6 21235712 21759999 256.0 MiB 8310 var-A
7 21760000 22284287 256.0 MiB 8310 var-B
8 22284288 4000797319 1.9 TiB 8302 home
</code></pre>
<p>When the system boots, a number of other pseudo-filesystems get mounted. Almost a dozen directories, including <code>/var/log</code>, <code>/root</code>, and <code>/nix</code>, are bind-mounted from <code>/home/.steamos/offload</code> to keep their data persistent.</p>
<p>Perhaps my favorite detail of SteamOS is how it handles <code>/etc</code>: it mounts an overlayfs on top of it, with modifications persisted at <code>/var/lib/overlays/etc/upper</code>. This allows persisting the usual things that need to be persisted in <code>/etc</code> (e.g. the <code>machine-id</code> file, NetworkManager connections) while allowing updates to untouched configuration files. Most Linux package managers have similar behaviors around <code>/etc</code>, only updating config files that haven’t been changed from their defaults, but Valve’s approach makes this work with the A/B partition system without any package manager logic.</p>
<p>A system update is started when the Steam client, or a user in a terminal, runs <code>steamos-update</code>. This runs a Python program, <code>steamos-atomupd-client</code>, which sends a request containing the current OS information and the user’s update channel configuration to the URLs in <code>/etc/steamos-atomupd/client.conf</code> in order to determine whether there is a new update.</p>
<p>If there is, the servers respond with a path to a <a href="https://rauc.io/">RAUC bundle</a>, which the client downloads and runs <code>rauc install</code> on. RAUC verifies the signature of the bundle and looks for the <code>rootfs.img.caibx</code> file, then runs <code>casync extract</code> to download all the necessary pieces of the new image and write them to the inactive rootfs partition. RAUC then runs a post-install script that selectively<sup><a href="#user-content-fn-selectively" id="user-content-fnref-selectively" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> synchronizes data from the active <code>/var</code> partition to the inactive <code>/var</code> partition, and modifies the stage 1 bootloader configuration on the EFI system partition to boot into the newly-written partition set.</p>
<h2>Patching the kernel</h2>
<p>Valve uses a heavily-modified Linux kernel in SteamOS. We can know this because we can readily download the sources. It’s a little more convoluted than <code>git clone</code>, but not by much. Their <a href="https://steamdeck-packages.steamos.cloud/archlinux-mirror/">pacman mirror</a> can be found in <code>/etc/pacman.d/mirrorlist</code>, and the sources used for current (as of writing) SteamOS images are in the <code>sources/holo-3.5</code> and <code>sources/jupiter-3.5</code>. Right now, the current stable image’s kernel is <code>6.1.52-valve9-1-neptune-61</code>, whose source lives at <a href="https://steamdeck-packages.steamos.cloud/archlinux-mirror/sources/jupiter-3.5/linux-neptune-61-6.1.52.valve9-1.src.tar.gz">https://steamdeck-packages.steamos.cloud/archlinux-mirror/sources/jupiter-3.5/linux-neptune-61-6.1.52.valve9-1.src.tar.gz</a>.</p>
<p>This is a 2.9 GiB tarball. Why is it that big? Because there’s an entire Linux Git tree in here.</p>
<pre><code>$ tar xvf linux-neptune-61-6.1.52.valve9-1.src.tar.gz
linux-neptune-61/
linux-neptune-61/config
linux-neptune-61/config-neptune
linux-neptune-61/PKGBUILD
linux-neptune-61/archlinux-linux-neptune/
linux-neptune-61/.SRCINFO
linux-neptune-61/archlinux-linux-neptune/hooks/
linux-neptune-61/archlinux-linux-neptune/branches/
linux-neptune-61/archlinux-linux-neptune/HEAD
linux-neptune-61/archlinux-linux-neptune/config
linux-neptune-61/archlinux-linux-neptune/description
linux-neptune-61/archlinux-linux-neptune/objects/
linux-neptune-61/archlinux-linux-neptune/refs/
linux-neptune-61/archlinux-linux-neptune/info/
linux-neptune-61/archlinux-linux-neptune/packed-refs
linux-neptune-61/archlinux-linux-neptune/info/exclude
linux-neptune-61/archlinux-linux-neptune/refs/tags/
linux-neptune-61/archlinux-linux-neptune/refs/heads/
linux-neptune-61/archlinux-linux-neptune/objects/pack/
linux-neptune-61/archlinux-linux-neptune/objects/info/
linux-neptune-61/archlinux-linux-neptune/objects/pack/pack-6e0e2b73767937e4f217e55f6d3628af296eecfc.idx
linux-neptune-61/archlinux-linux-neptune/objects/pack/pack-6e0e2b73767937e4f217e55f6d3628af296eecfc.pack
linux-neptune-61/archlinux-linux-neptune/objects/pack/pack-6e0e2b73767937e4f217e55f6d3628af296eecfc.rev
...
</code></pre>
<p>This was likely generated by <code>makepkg --allsource</code> from a PKGBUILD with:</p>
<pre><code>_tag=6.1.52-valve9
# ...
source=(
"$_srcname::git+ssh://git@gitlab.steamos.cloud/jupiter/linux-integration.git#tag=$_tag"
config # Upstream Arch Linux kernel configuration file, DO NOT EDIT!!!
config-neptune # Jupiter: the neptune kernel fragment file (overrides 'config' above)
)
</code></pre>
<p>So we can’t clone directly from their private GitLab repo or link to various commits, but we can get regular snapshots of every tag with full commit history in the repo from their <code>makepkg</code> sources. This is very useful for, say, bisecting which commit breaks suspend on your living room PC.</p>
<p>The source tarball isn’t a working Git tree that you can <code>cd</code> into and start hacking on. It’s a <a href="https://git-scm.com/docs/gitrepository-layout/2.22.0">bare repository</a>, which you can clone into a normal working tree<sup><a href="#user-content-fn-worktree" id="user-content-fnref-worktree" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup>. My recommendation is to maintain your own branch of changes, tag your releases, and push them to your favorite Git host so that you can use it in the PKGBUILD file. <a href="https://git.iliana.fyi/linux">Here’s mine!</a></p>
<pre><code>wget https://steamdeck-packages.steamos.cloud/archlinux-mirror/sources/jupiter-3.5/linux-neptune-61-6.1.52.valve9-1.src.tar.gz
tar xvzf linux-neptune-61-6.1.52.valve9-1.src.tar.gz
git clone linux-neptune-61/archlinux-linux-neptune/ linux-neptune
cd linux-neptune
git switch --create my-branch 6.1.52-valve9
</code></pre>
<p>Fix up your PKGBUILD (pointing to your Git repo, not mine):</p>
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/PKGBUILD</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/PKGBUILD</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -3,5 +3,5 @@</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> pkgbase=linux-neptune-61</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-_tag=6.1.52-valve9</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+_tag=6.1.52-valve9-iliana1</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> pkgver=${_tag//-/.}</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> pkgrel=1</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -19,5 +19,5 @@</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> options=('!strip' '!debug')</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> _srcname=archlinux-linux-neptune</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> source=(</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- "$_srcname::git+ssh://git@gitlab.steamos.cloud/jupiter/linux-integration.git#tag=$_tag"</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ "$_srcname::git+https://git.iliana.fyi/linux#tag=$_tag"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> config # Upstream Arch Linux kernel configuration file, DO NOT EDIT!!!</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> config-neptune # Jupiter: the neptune kernel fragment file (overrides 'config' above)</span>
</code></pre>
<p>Then <code>makepkg</code> should spit out a package. (Tip: <code>makepkg MAKEFLAGS=-j$(nproc)</code>, or updating <code>/etc/makepkg.conf</code>, is a good idea if you are not building in a tiny virtual machine.) This same general process should apply for any SteamOS-specific packages; all the ones I’ve looked at similarly use a Git repository as their first source.</p>
<p>To make the next steps easier, I set up a pacman repo containing my package outputs. This also helps the <code>steamos-devmode</code> tool work properly if you choose to run that in the future. This is <a href="https://archlinux.org/pacman/repo-add.8.html">very simple</a>: put the packages in a directory, run <code>repo-add $REPO_NAME.db.tar.zst [PACKAGES...]</code> in that directory, and upload the directory to a web host somewhere.</p>
<h2>Repacking the root filesystem</h2>
<p>The update client is perfectly-readable Python, and the sources for the rest of the packages on the system can be found adjacent to Valve’s pacman repos, but I haven’t yet found any release engineering scripts. Reverse engineering these would be fraught, time-consuming, and beyond what I believe my attention span would allow, so I decided to take the existing root filesystem and “repack” it to fit my needs.</p>
<p>If you want my scripts without the explanations and commentary, you can find them at <a href="https://git.iliana.fyi/fauxlo/tree/">https://git.iliana.fyi/fauxlo/tree/</a>.</p>
<h3>Getting the root filesystem</h3>
<ili-callout role="note">
<p>The normal way to get a copy of the SteamOS root filesystem image is to buy a Steam Deck or download the recovery image from <a href="https://store.steampowered.com/steamos/download?ver=steamdeck">https://store.steampowered.com/steamos/download?ver=steamdeck</a>, both of which require agreeing to the Steam End User License Agreement<sup><a href="#user-content-fn-agreement" id="user-content-fnref-agreement" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup>. The methods I describe below don’t make you do this, but now you’re aware of it.</p>
</ili-callout>
<p>First, we need the root filesystem. You could install SteamOS, run an update, and pull it off the disk, but that is kind of obnoxious, especially if you don’t have any hardware to install it on.</p>
<p>Every build of a Steam Deck image can be found at <a href="https://steamdeck-images.steamos.cloud/steamdeck/">https://steamdeck-images.steamos.cloud/steamdeck/</a>, but to find the current release version you can look at what appears to be a fallback URL for the updates system, <a href="https://steamdeck-atomupd.steamos.cloud/meta/steamos/amd64/snapshot/steamdeck.json">https://steamdeck-atomupd.steamos.cloud/meta/steamos/amd64/snapshot/steamdeck.json</a> (or <a href="https://steamdeck-atomupd.steamos.cloud/meta/steamos/amd64/snapshot/steamdeck-beta.json">this path for the preview channel</a>). As of writing, the current stable version is <a href="https://steamdeck-images.steamos.cloud/steamdeck/20231122.1/">https://steamdeck-images.steamos.cloud/steamdeck/20231122.1/</a>.</p>
<p>To download the root filesystem, we follow the same steps <code>steamos-atomupd-client</code> does: first, download the RAUC bundle (the <code>.raucb</code> file). Then extract the <code>rootfs.img.caibx</code> from inside; these RAUC bundles are SquashFS filesystems with a signature at the end, so you can either mount it or use unsquashfs (from squashfs-tools) or p7zip to extract it without mounting. Finally, use casync to fetch the image using the <code>.castr</code> store adjacent to the <code>.raucb</code> bundle:</p>
<pre><code>casync extract \
--store=https://steamdeck-images.steamos.cloud/steamdeck/20231122.1/steamdeck-20231122.1-3.5.7.castr \
rootfs.img.caibx rootfs.img
</code></pre>
<p>The casync store URL is the RAUC bundle URL, but with <code>.raucb</code> replaced with <code>.castr</code> (this is hardcoded in <code>steamos-atomupd</code>).</p>
<p><a href="https://git.iliana.fyi/fauxlo/tree/fetch-current.sh?id=c50eb80f23c08f1ffc824d4983d5b9a740fb1273">Here is a script I use to do all this.</a></p>
<ili-tangent role="note">
<p>The adjacent <code>.img.zip</code> and <code>.img.zst</code> files are not the root filesystem, sadly, but are separate bootable recovery images:</p>
<pre><code>$ sgdisk --print disk.img
Disk disk.img: 15125000 sectors, 7.2 GiB
[...]
Number Start (sector) End (sector) Size Code Name
1 34 131071 64.0 MiB EF00 esp
2 131072 393215 128.0 MiB 0700 efi-A
3 655360 11141119 5.0 GiB 8304 rootfs-A
4 11141120 11665407 256.0 MiB 8310 var-A
5 11665408 15124966 1.6 GiB 8302 home
</code></pre>
<p>You could extract the rootfs partition and use it in the next steps, but for some reason it’s not a bit-for-bit copy of the image that is downloaded via RAUC and casync, and doing this doesn’t save you from having to use those tools as we need them to turn our repacked image back into an update bundle.</p>
</ili-tangent>
<h3>Mounting the root filesystem</h3>
<p>First, we should randomize the filesystem UUID. If you update from a currently-released SteamOS image to your customized one without randomizing the filesystem UUID, you will end up with two distinct filesystems with the same UUID. This can cause problems.</p>
<pre><code>btrfstune -fu rootfs.img
</code></pre>
<p>Valve is currently using Btrfs images with zstd compression, so to maintain that compression as we change the image, we need to mount it with the appropriate option:</p>
<pre><code>mkdir rootfs
mount -o compress=zstd rootfs.img rootfs
</code></pre>
<p>SteamOS uses Btrfs’s <code>readonly</code> subvolume property; clear that flag:</p>
<pre><code>btrfs property set -ts rootfs ro false
</code></pre>
<p>Modifying certain packages, such as the Linux kernel, triggers scripts that want /dev and /proc, so mount those:</p>
<pre><code>mount -t devtmpfs dev rootfs/dev
mount -t proc proc rootfs/proc
</code></pre>
<p>It’s also a good idea to prevent writes to directories that will be mounted by the booted system. We can mount tmpfs to these directories:</p>
<pre><code>mount -t tmpfs tmpfs rootfs/tmp
mount -t tmpfs -o mode=755 tmpfs rootfs/run
mount -t tmpfs -o mode=755 tmpfs rootfs/var
mount -t tmpfs -o mode=755 tmpfs rootfs/home
</code></pre>
<p>In this example we’ll be installing packages via pacman repositories in a chroot (using pacman’s <code>--sysroot</code> option). Networking functions fine in a chroot, but name resolution still relies on a correct /etc/resolv.conf, so bind mount one in:</p>
<pre><code>mount --bind "$(realpath /etc/resolv.conf)" rootfs/etc/resolv.conf
</code></pre>
<h3>Replacing packages</h3>
<p>To add your custom repository, make it the first repository entry in <code>/etc/pacman.conf</code>. This will ensure your packages override any newer-versioned ones from Valve’s repositories. It also allows your packages to be reinstalled by <code>steamos-devmode</code> if you run that on this image. I used this stanza:</p>
<pre><code>[fauxlo]
Server = https://fauxlo.ili.fyi/pacman/$arch
SigLevel = Never
</code></pre>
<p><code>SigLevel = Never</code> allows the packages to have no signatures. If you want to GPG-sign your packages, go for it, but that’s beyond what I have patience for.</p>
<ili-tangent role="note">
<p>If you do end up installing GPG-signed packages, you’ll need to populate the pacman keyring. I think it’s best to avoid messing with the empty keyring in <code>/etc/pacman.d/gnupg</code> by populating a new keyring on a tmpfs:</p>
<pre><code>chroot rootfs pacman-key --gpgdir /tmp/gnupg --init
chroot rootfs pacman-key --gpgdir /tmp/gnupg --populate
# These start a gpg-agent, which we need to stop before we can unmount at the end.
chroot rootfs gpgconf --homedir /tmp/gnupg --kill all
</code></pre>
<p>Then add <code>--gpgdir /tmp/gnupg</code> to your pacman incantation.</p>
</ili-tangent>
<p>Then, we install:</p>
<pre><code>pacman --sysroot rootfs --noconfirm -Sy linux-neptune-61
</code></pre>
<ili-tangent role="note">
<p>In my script, I avoid using <code>-y</code> and instead synchronize my repository’s database behind pacman’s back before running the install command:</p>
<pre><code>curl -Ro rootfs/usr/lib/holo/pacmandb/sync/fauxlo.db \
https://fauxlo.ili.fyi/pacman/x86_64/fauxlo.db
</code></pre>
<p>This keeps the state of the other repositories on disk frozen at the same point in time when the image was originally built. I don’t think this actually matters, but it reduces the changes that show up if I diff my image against Valve’s.</p>
</ili-tangent>
<h3>Changing the build ID</h3>
<p><code>steamos-atomupd</code> reads from <code>/lib/steamos-atomupd/manifest.json</code>, or if that is somehow missing, <code>/etc/os-release</code>, to determine the version and build ID of the current image. It will refuse to perform an update if the server says the available update’s build ID is the same as the current image. It’s also good to know what image you’re running.</p>
<p>The build ID <em>must</em> be of the form YYYYMMDD.N. If it isn’t, <code>steamos-atomupd</code> exits with a Python traceback upon encountering it. To avoid having to remember to count up manually, I set N to a timestamp; either HHMMSS or a Unix timestamp would work fine.</p>
<p>Update the <code>buildid</code> field in <code>rootfs/lib/steamos-atomupd/manifest.json</code> and the <code>BUILD_ID</code> field in <code>rootfs/etc/os-release</code> with whatever you pick. You can steal from <a href="https://git.iliana.fyi/fauxlo/tree/repack.sh?id=c50eb80f23c08f1ffc824d4983d5b9a740fb1273#n47">the Bash script I wrote to do this</a>.</p>
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/lib/steamos-atomupd/manifest.json</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/lib/steamos-atomupd/manifest.json</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -4,7 +4,7 @@</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "variant": "steamdeck",</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "arch": "amd64",</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "version": "3.5.7",</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- "buildid": "20231122.1",</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ "buildid": "20231219.55534",</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "checkpoint": false,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "estimated_size": 0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> }</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/etc/os-release</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/etc/os-release</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -11,4 +11,4 @@</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> LOGO=steamos</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> VARIANT_ID=steamdeck</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> VERSION_ID=3.5.7</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-BUILD_ID=20231122.1</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+BUILD_ID=20231219.55534</span>
</code></pre>
<p>Keep a copy of the updated <code>manifest.json</code> handy, as it’s useful in building the updates server later.</p>
<h3>Changing the update URLs and signing keys</h3>
<p>RAUC uses X.509 certificates to establish trust. The trusted certificate lives at <code>/etc/rauc/keyring.pem</code>. You can make an overcomplicated PKI scheme, such as the <a href="https://github.com/rauc/rauc/blob/master/test/openssl-ca-create.sh">one generated in RAUC’s tests</a>, but <a href="https://rauc.readthedocs.io/en/latest/examples.html#pki-setup">a simple self-signed certificate</a> is fine. Install your new certificate at <code>rootfs/etc/rauc/keyring.pem</code>.</p>
<p>You’ll need to modify the URLs in <code>rootfs/etc/steamos-atomupd/client.conf</code> with your own:</p>
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/rootfs/etc/steamos-atomupd/client.conf</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/rootfs/etc/steamos-atomupd/client.conf</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -1,5 +1,5 @@</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> [Server]</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-QueryUrl = https://steamdeck-atomupd.steamos.cloud/updates</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-ImagesUrl = https://steamdeck-images.steamos.cloud/</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-MetaUrl = https://steamdeck-atomupd.steamos.cloud/meta</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+QueryUrl = https://fauxlo.ili.fyi/updates</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ImagesUrl = https://fauxlo.ili.fyi/</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+MetaUrl = https://fauxlo.ili.fyi/meta</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> Variants = rel;rc;beta;bc;main</span>
</code></pre>
<h3>Other changes</h3>
<p>You can make pretty much any change you want at this point as long as you don’t run out of space in a 5 GiB Btrfs image. For instance, if you want your SteamOS device to be resolvable as <code>hostname.local</code> on your network, you could remove <code>rootfs/usr/lib/systemd/resolved.conf.d/00-disable-mdns.conf</code>. This <em>can</em> be overridden with a configuration in the <code>/etc</code> overlay, but it’s kind of a pain in the ass.</p>
<p>In general, my philosophy here is that you should avoid making changes that are trivial to perform without modifying the image. You <em>could</em> install Firefox in the root filesystem this way, instead of using Flatpak or Nix, but then you’d need to repack the image every time you want to install a Firefox security update.</p>
<h3>Unmounting the root filesystem</h3>
<p>Mark the filesystem read-only once again:</p>
<pre><code>btrfs property set -ts rootfs ro true
</code></pre>
<p>Discard any unused blocks:</p>
<pre><code>fstrim -v rootfs
</code></pre>
<p>Then, unmount. <code>--recursive</code> is particularly helpful here to take care of all the pseudo-filesystems we mounted in:</p>
<pre><code>umount --recursive rootfs
</code></pre>
<h2>Creating the RAUC bundle</h2>
<p>First we need to create the casync store and blob index. We can do this with:</p>
<pre><code>mkdir bundle
casync make --store=rootfs.img.castr \
bundle/rootfs.img.caibx rootfs.img
</code></pre>
<p>The RAUC bundle needs two other files. The first is <code>manifest.raucm</code>:</p>
<pre><code class="language-sh" data-syntax-highlighted><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">cat</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">></span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">bundle/manifest.raucm</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49"><<</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">EOF</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">[update]</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">compatible=steamos-amd64</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">version=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$version</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">[image.rootfs]</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">sha256=$(</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">sha256sum</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> rootfs.img </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">|</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">awk</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> '{ print $1 }')</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">size=$(</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">stat</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-c</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> %s rootfs.img)</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">filename=rootfs.img.caibx</span>
<span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">EOF</span>
</code></pre>
<p>The second is a <code>UUID</code> file containing the filesystem UUID:</p>
<pre><code>blkid -s UUID -o value rootfs.img >bundle/UUID
</code></pre>
<p>With those three files:</p>
<pre><code>$ ls bundle
manifest.raucm rootfs.img.caibx UUID
</code></pre>
<p>we can now call <code>rauc bundle</code>:</p>
<pre><code>rauc bundle \
--signing-keyring=cert.pem --cert=cert.pem --key=key.pem \
bundle rootfs.img.raucb
</code></pre>
<p>Upload <code>rootfs.img.raucb</code> and <code>rootfs.img.caibx</code> to the web server specified by <code>ImagesUrl</code> in <code>rootfs/etc/steamos-atomupd/client.conf</code>. These need to be in the same directory.</p>
<h2>Final update server setup</h2>
<p>The web server you used for <code>QueryUrl</code> and <code>MetaUrl</code> in <code>rootfs/etc/steamos-atomupd/client.conf</code> will need to serve a JSON file. This doesn’t need to be fancy; what I do is write a <code>live.json</code> file with these contents:</p>
<pre><code class="language-json" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">{</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"minor"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"release"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"holo"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"candidates"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"image"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"product"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"steamos"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"release"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"holo"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"variant"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"steamdeck"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"arch"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"amd64"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"version"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"3.5.7"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"buildid"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"20231219.55534"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"checkpoint"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">false</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"estimated_size"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> },</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"update_path"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"rootfs.img.raucb"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> }</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> ]</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> }</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">}</span>
</code></pre>
<p>Note that the object at <code>.minor.candidates[0].image</code> should be the same as <code>/lib/steamos-atomupd/manifest.json</code> in your image. <code>update_path</code> is what the updates client will append to your <code>ImagesUrl</code> to download the bundle.</p>
<p>I use the following Caddy configuration to rewrite the requests <code>steamos-atomupd</code> makes to <code>QueryUrl</code> and <code>MetaUrl</code> to the above <code>live.json</code>:</p>
<pre><code>root * /var/www/fauxlo.ili.fyi
rewrite /updates /live.json
rewrite /meta/*/*/*/*.json /live.json
rewrite /meta/*/*/*/*/*.json /live.json
file_server browse
</code></pre>
<p>The real SteamOS <code>QueryUrl</code> and <code>MetaUrl</code> seem to have quite a bit more logic to them, but this is sufficient to get <code>steamos-atomupd</code> to find the new update. It has logic to avoid updating if it’s already running the image advertised as currently available.</p>
<h2>Updating!</h2>
<p>Once you have all this in place, you can update an existing SteamOS installation to this by modifying <code>/etc/rauc/keyring.pem</code> and <code>/etc/steamos-atomupd/client.conf</code>. (No <code>steamos-readonly disable</code> required, as your changes will land on the <code>/etc</code> overlay; after you run <code>steamos-update</code>, consider cleaning those changes out of <code>/var/lib/overlays/etc/upper</code>.)</p>
<p>You can also probably install your modified SteamOS by modifying one of Valve’s recovery images, replacing their rootfs with your own. I haven’t tested this, but I also haven’t seen anything that would contradict this.</p>
<ili-callout role="note">
<p>Thanks for reading! 2023 has been a busier-than-usual year for my blog, and I’m pretty happy about getting more writing out there. If you’ve found any of my blog posts helpful, give a trans person all of your money. See you next year!</p>
</ili-callout>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-fragile">
<p>The similarities don’t end with just having an AMD CPU and GPU and an NVMe SSD; similar to how Valve says <a href="https://www.youtube.com/watch?v=Dxnr2FAADAs">you shouldn’t open your Steam Deck</a> because it will immediately make it less structurally resilient, you also shouldn’t open my living room PC because you might damage the precision-bent PCI slot cover plate keeping the graphics hovering above the case fans I had to use to replace the GPU fan shroud that wouldn’t fit in the case. <a href="#user-content-fnref-fragile" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-selectively">
<p>The <a href="https://gist.github.com/iliana/44c53bfa7eab4f04e952d6387d3e70ae#file-post-install-sh-L216-L243"><code>sync_var_mountpoints</code> function in <code>/usr/lib/rauc/post-install.sh</code></a> has an excellent comment explaining why some files need to be excluded when synchronizing <code>/var</code> between the partition sets. <a href="#user-content-fnref-selectively" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-worktree">
<p>You could also use <a href="https://git-scm.com/docs/git-worktree">git-worktree(1)</a> for this: <code>cd linux-neptune-61/archlinux-linux-neptune</code> then <code>git worktree add -B my-branch ../../linux-neptune 6.1.52-valve9</code>. <a href="#user-content-fnref-worktree" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-agreement">
<p>If anyone from Valve is reading this, you might want to update your EULA to point to somewhere more useful than <a href="https://gitlab.steamos.cloud/">https://gitlab.steamos.cloud/</a>, which has barely any public repositories and certainly does not have a “list of contained packages along with their respective FOSS or proprietary licenses as well as source code for FOSS packages”. <a href="#user-content-fnref-agreement" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>Building an x86 Linux kernel that works with both systemd-boot and kexeciliana etaoin2023-10-22T19:00:00+00:002023-10-22T19:00:00+00:00https://iliana.fyi/blog/kexec-systemd-boot-kernel-image/<div class="[tab-size:8]">
<p>I’m working on a project that uses <a href="https://buildroot.org/">Buildroot</a> to build an embedded Linux system for a relatively standard x86 server board. I’ve configured it to produce a single Linux kernel image with an embedded initramfs; Buildroot has special support (via <code>BR2_TARGET_ROOTFS_INITRAMFS</code>) to build the kernel a second time using Linux’s <code>CONFIG_INITRAMFS_SOURCE</code> option and embed the root filesystem into the kernel code. This is pretty neat, as it allows you to have a single compressed, bootable file containing the entire system. With <code>CONFIG_EFI_STUB</code>, it’s even EFI bootable, and it works out of the box with <a href="https://lwn.net/Articles/15468/"><code>kexec</code></a>.</p>
<p>I decided I wanted to see if I could get these images working with <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-boot.html">systemd-boot</a>. systemd-boot refers to <a href="https://uapi-group.org/specifications/specs/boot_loader_specification/">The Boot Loader Specification</a>, which describes the two methods systemd-boot uses to look for something to boot; the first is somewhat similar to existing x86 bootloaders which have a list of entries and their associated kernels, initrds, command lines, and other metadata. The second method refers to an “EFI Unified Kernel Image” (abbreviated to “UKI” in places other than the Boot Loader Specification). I already have an EFI image with my initramfs and command line embedded within it; how hard could it be?</p>
<p>A unified kernel image is a Portable Executable, like all EFI images. The Boot Loader Specification requires that the PE binary contain an <code>.osrel</code> section consisting of the contents of the os-release file for the system; this is used by systemd-boot to display a name and version of the OS. In the systemd cinematic universe, this is usually provided by <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-stub.html">systemd-stub</a>, a UEFI kernel boot stub that also defines separate sections for the Linux kernel itself, an initrd, kernel version information, binary devicetree, kernel command line, and boot splash. This is advantageous for some use cases with traditional distributions, where a kernel is normally built by a distribution and the initrd is built by the end-user; an end-user can combine everything into a single, signed UEFI binary. But a UKI is not a kernel image, as far as <code>kexec</code> is concerned; <a href="https://lore.kernel.org/kexec/20230911052535.335770-1-kernel@jfarr.cc/T/">perhaps it could be someday</a>.</p>
<p>So if we want to learn to live with both of these constraints, we need to add an <code>.osrel</code> PE section to our existing kernel image. It is tempting to reach for <code>objcopy</code>, which can add sections to PE binaries, but note that a kernel image built with <code>CONFIG_EFI_STUB</code> is bootable by both BIOS bootloaders and UEFI; it is a polyglot! x86 has a rich and wild history and <a href="https://www.kernel.org/doc/html/v5.6/x86/boot.html">the Linux/x86 boot protocol</a> reflects this. The kernel setup code relies on absolute offsets in this file, so a tool that believes the kernel image is a true PE binary will render it universally unbootable.</p>
<p>Instead we need to reach straight into the guts of our boot executable. (Links and patches below apply to Linux 6.1.51.) The PE binary header is faked into the Linux <code>bzImage</code> in <a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/arch/x86/boot/header.S?h=v6.1.51"><code>arch/x86/boot/header.S</code></a>:</p>
<pre><code class="language-asm" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">#</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">ifdef</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> CONFIG_EFI_STUB</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">pe_header:</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long PE_MAGIC</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # [... snipped from original for brevity ...]</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # Section table</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">section_table:</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> #</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # The offset & size fields are filled in by build.c.</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> #</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .ascii ".setup"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">byte</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">byte</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0x0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # startup_{32,64}</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # Size of initialized data</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # on disk</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0x0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # startup_{32,64}</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToRelocations</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToLineNumbers</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">word</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # NumberOfRelocations</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">word</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # NumberOfLineNumbers</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long IMAGE_SCN_CNT_CODE | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_MEM_READ | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_MEM_EXECUTE | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_ALIGN_16BYTES </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # Characteristics</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> #</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # The EFI application loader requires a relocation section</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # because EFI applications must be relocatable. The .reloc</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # offset & size fields are filled in by build.c.</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> #</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .ascii ".reloc"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">byte</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">byte</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # SizeOfRawData</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToRawData</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToRelocations</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToLineNumbers</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">word</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # NumberOfRelocations</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">word</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # NumberOfLineNumbers</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long IMAGE_SCN_CNT_INITIALIZED_DATA | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_MEM_READ | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_MEM_DISCARDABLE | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_ALIGN_1BYTES </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # Characteristics</span>
</code></pre>
<p>We add our own section below <code>.reloc</code> (and before the <code>.compat</code> section defined if <code>CONFIG_EFI_MIXED</code> is set):</p>
<pre><code class="language-asm" data-syntax-highlighted><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> #</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # systemd-boot requires an .osrel section containing the</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # contents of /etc/os-release. The .osrel offset & size</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # fields are filled in by build.c.</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> #</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .ascii ".osrel"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">byte</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">byte</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # SizeOfRawData</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToRawData</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToRelocations</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # PointerToLineNumbers</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">word</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # NumberOfRelocations</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">word</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # NumberOfLineNumbers</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .long IMAGE_SCN_CNT_INITIALIZED_DATA | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_MEM_READ | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_MEM_DISCARDABLE | \</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> IMAGE_SCN_ALIGN_1BYTES </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"> # Characteristics</span>
</code></pre>
<ili-tangent role="note">
<p>The PE header requires listing the number of sections in the section header, but lucky for us <a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/arch/x86/boot/header.S?h=v6.1.51#n276">someone already set it up to dynamically update</a>:</p>
<pre><code class="language-asm" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .set section_count, (. - section_table) / </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">40</span>
</code></pre>
</ili-tangent>
<p>As the comment we wrote indicates, we also need to update <a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/arch/x86/boot/tools/build.c?h=v6.1.51"><code>arch/x86/boot/tools/build.c</code></a>, which fills real values into all the placeholders in <code>header.S</code>.</p>
<p>For my purposes, I decided I would copy in the os-release contents after the kernel build, so I reserved a 256-byte section for it:</p>
<pre><code class="language-c" data-syntax-highlighted><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">#define</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">PECOFF_OSREL_RESERVE</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">0x</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">100</span>
</code></pre>
<ili-tangent role="note">
<p>If I were considering upstreaming this, I would probably add an option named <code>CONFIG_EFI_OSREL</code> that takes a path to an os-release file to embed, and change <code>build.c</code> to read that path and reserve only the space necessary instead of hardcoding the size at <code>0x100</code>. But I am not, and I don’t want to figure out how that would work within Buildroot either.</p>
</ili-tangent>
<p>In <code>update_pecoff_setup_and_reloc</code>, we update the offset calculations for the surrounding sections:</p>
<!-- In Firefox, these tabs with leading diff characters still render as 8 characters, instead of stopping at the next tab stop. -->
<div class="[tab-size:7]">
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> static void update_pecoff_setup_and_reloc(unsigned int size)</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> u32 setup_offset = 0x200;</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- u32 reloc_offset = size - PECOFF_RELOC_RESERVE - PECOFF_COMPAT_RESERVE;</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ u32 reloc_offset = size - PECOFF_RELOC_RESERVE - PECOFF_OSREL_RESERVE - PECOFF_COMPAT_RESERVE;</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ u32 osrel_offset = reloc_offset + PECOFF_RELOC_RESERVE;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> #ifdef CONFIG_EFI_MIXED</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- u32 compat_offset = reloc_offset + PECOFF_RELOC_RESERVE;</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ u32 compat_offset = osrel_offset + PECOFF_OSREL_RESERVE;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> #endif</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> u32 setup_size = reloc_offset - setup_offset;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> update_pecoff_section_header(".setup", setup_offset, setup_size);</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> update_pecoff_section_header(".reloc", reloc_offset, PECOFF_RELOC_RESERVE);</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> /*</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> * Modify .reloc section contents with a single entry. The</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> * relocation is applied to offset 10 of the relocation section.</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> */</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> put_unaligned_le32(reloc_offset + 10, &buf[reloc_offset]);</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> put_unaligned_le32(10, &buf[reloc_offset + 4]);</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ update_pecoff_section_header(".osrel", osrel_offset, PECOFF_OSREL_RESERVE);</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> #ifdef CONFIG_EFI_MIXED</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> update_pecoff_section_header(".compat", compat_offset, PECOFF_COMPAT_RESERVE);</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> /*</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> * Put the IA-32 machine type (0x14c) and the associated entry point</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> * address in the .compat section, so loaders can figure out which other</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> * execution modes this image supports.</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> */</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> buf[compat_offset] = 0x1;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> buf[compat_offset + 1] = 0x8;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> put_unaligned_le16(0x14c, &buf[compat_offset + 2]);</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> put_unaligned_le32(efi32_pe_entry + size, &buf[compat_offset + 4]);</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> #endif</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> }</span>
</code></pre>
<p>And then in <code>main</code>, we need to reserve the <code>.osrel</code> section:</p>
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> c += reserve_pecoff_compat_section(c);</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ memset(buf+c, 0, PECOFF_OSREL_RESERVE);</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ c += PECOFF_OSREL_RESERVE;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> c += reserve_pecoff_reloc_section(c);</span>
</code></pre>
</div><!-- /[tab-size:7] -->
<p>That covers all the changes we need to make to have an reserved 256-byte <code>.osrel</code> section in the kernel image. To fill it in, we still need to avoid <code>objcopy</code>; I wrote a short Python script to do the job. Take note of the fact that we need to shorten <em>both</em> the <code>size</code> and <code>datasz</code> fields to fit the actual length of the os-release file; <a href="https://github.com/systemd/systemd/blob/e8dc52766e1fdb4f8c09c3ab654d1270e1090c8d/src/shared/bootspec.c#L841-L843">systemd-boot will not accept an <code>.osrel</code> section with multiple null bytes</a>!<sup><a href="#user-content-fn-nl" id="user-content-fnref-nl" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup> (Perhaps the Boot Loader Specification could specify this?)</p>
<pre><code class="language-python" data-syntax-highlighted><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">import</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> collections</span>
<span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">import</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> mmap</span>
<span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">import</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> struct</span>
<span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">import</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> sys</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">Section </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> collections.namedtuple(</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"Section"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, [</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"name"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"size"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"vma"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"datasz"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"offset"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">])</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># Usage: python osrel.py OS_RELEASE_FILE KERNEL_BZIMAGE</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">osrel </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">open</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">(sys.argv[</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">1</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">], </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"rb"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">).read()</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">kernel </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">open</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">(sys.argv[</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">2</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">], </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"r+b"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">)</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">mm </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> mmap.mmap(kernel.fileno(), </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">)</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">pe_header </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> struct.unpack(</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"<i"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, mm[</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">0x</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">3C</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">:</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">0x</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">40</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">])[</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">]</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">num_sections </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> struct.unpack(</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"<h"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, mm[pe_header </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">6</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> : pe_header </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">8</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">])[</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">]</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">section_table </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> pe_header </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">0x</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">B8</span>
<span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">for</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> start </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">in</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">map</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">(</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">lambda</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> i: section_table </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> i </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">40</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">range</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">(num_sections)):</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> s </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> Section(</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">struct.unpack(</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"<8s4i"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, mm[start : start </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">24</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">]))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">if</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> s.name </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">==</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">b</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">".osrel</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">\0\0</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">:</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> size </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">min</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">(</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">len</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">(osrel), s.size)</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> s </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> s._replace(</span><span style="--hl-color-dark:#FFAB70;--hl-color-light:#E36209">size</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">size, </span><span style="--hl-color-dark:#FFAB70;--hl-color-light:#E36209">datasz</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">size)</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> mm[s.offset : s.offset </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> s.size] </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> osrel[: s.size]</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> mm[start : start </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">24</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">] </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> struct.pack(</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"<8s4i"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">, </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">s)</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">break</span>
</code></pre>
<p>After all is said and done, we can indeed verify the section looks right with <code>objdump</code>:</p>
<pre><code>$ objdump -s -j .osrel output/images/bzImage-latest
output/images/bzImage-latest: file format pei-x86-64
Contents of section .osrel:
1003d00 4e414d45 3d576f62 7363616c 65205275 NAME=Wobscale Ru
1003d10 690a4944 3d727569 0a49445f 4c494b45 i.ID=rui.ID_LIKE
1003d20 3d627569 6c64726f 6f740a56 45525349 =buildroot.VERSI
1003d30 4f4e5f49 443d3230 32332e30 322e352d ON_ID=2023.02.5-
1003d40 37342d67 66346563 34313065 34340a42 74-gf4ec410e44.B
1003d50 55494c44 5f49443d 66346563 34313065 UILD_ID=f4ec410e
1003d60 34343434 36333333 32366262 30323935 4444633326bb0295
1003d70 32346166 35336161 37326531 63363064 24af53aa72e1c60d
1003d80 0a .
</code></pre>
<p>And with <code>bootctl</code>, after placing our binary in the magic location on the EFI System Partition:</p>
<pre><code>Default Boot Loader Entry:
type: Boot Loader Specification Type #2 (.efi)
title: Wobscale Rui (2023.02.5-74-gf4ec410e44)
id: rui-2023.02.5-74-gf4ec410e44.efi
source: /boot/EFI/Linux/rui-2023.02.5-74-gf4ec410e44.efi
sort-key: rui
version: 2023.02.5-74-gf4ec410e44
linux: EFI/Linux/rui-2023.02.5-74-gf4ec410e44.efi
</code></pre>
<p>And, finally, <code>kexec</code> is happy with it too:</p>
<pre><code># kexec --kexec-file-syscall --load /boot/EFI/Linux/rui-2023.02.5-74-gf4ec410e44.efi
# echo $?
0
</code></pre>
<ili-tangent role="note">
<p><a href="efi-stub-osrel-section.patch">Here’s a full, ready-to-<code>git am</code> patch</a> of my Linux changes, with more correct handling for building without <code>CONFIG_EFI_STUB</code>. Feel free to adapt to your needs, or improve and upstream it; just keep the <code>Signed-off-by:</code> line around.</p>
</ili-tangent>
</div><!-- /[tab-size:8] -->
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-nl">
<p>If you don’t want to resize the section, you could pad the file with empty lines, I guess. <a href="#user-content-fnref-nl" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>Getting my library cards onto my phone the hard wayiliana etaoin2023-08-06T19:00:00+00:002023-08-06T19:00:00+00:00https://iliana.fyi/blog/ios-wallet-library-card/<p>Our local libraries, The Seattle Public Library and the King County Library System, issue pieces of plastic with barcodes printed on the back assigned to your borrower account. These cards are not <em>strictly</em> necessary in 2023; most everything at Seattle libraries is self-service, including circulation, and these self-service entrypoints usually have a way to type in a library barcode manually. But having the barcode is far more convenient, and I’d like to have it without having to keep yet another plastic card I rarely use in my wallet.</p>
<p>So I put it on my phone, in my iPhone’s Wallet app. This became extremely silly extremely quickly, so I’ve decided to document it here for myself and others.</p>
<h2>A brief introduction to passes</h2>
<p>The Wallet app can manage many things: payment cards, government/employee/student IDs, house/car/hotel room keys; none of these were part of what Wallet, initially called Passbook, could do at its 2012 launch. At that time, Passbook only managed “passes”.</p>
<p><a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/index.html">Apple’s documentation on passes</a> covers this in more detail, but they are self-contained zip files full of JSON and PNGs designed to be distributed through email or the web from a vendor to its user. If you have a pass on your phone, you can usually go to Pass Details and find a share icon in the top right, allowing you to send the .pkpass file to somewhere you can unzip it and inspect it.</p>
<p>The contents are pretty simple. <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW52">There’s a specific list of supported images</a>, there’s a <code>pass.json</code> file which describes all of the non-image content of the pass, there’s a <code>manifest.json</code> file which lists the SHA-1 checksum of all the other files, and a <code>signature</code> which is an S/MIME signature of the contents of the <code>manifest.json</code>.</p>
<p>Our first interesting problem is one of barcode formats. Passes support four types of barcodes: <a href="https://en.wikipedia.org/wiki/QR_code">QR code</a>, <a href="https://en.wikipedia.org/wiki/PDF417">PDF417</a> (commonly used on United States driver licenses), <a href="https://en.wikipedia.org/wiki/Aztec_Code">Aztec Code</a> (used for boarding passes by the airline industry), and <a href="https://en.wikipedia.org/wiki/Code_128">Code 128</a> (the only supported linear symbology). My library card uses… <em>[stares at Wikipedia for half an hour]</em> <a href="https://en.wikipedia.org/wiki/Codabar">Codabar</a>, widely used in libraries<sup><a href="#user-content-fn-blood" id="user-content-fnref-blood" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>, and perhaps one of the cutest barcode symbologies (and names) I’ve ever seen. It’s possible that the barcode scanners at the library support other linear symbologies, but Codabar is the only one I know guaranteed to work at all of them. So we will need to fake it by providing some image that functions as a scannable barcode.</p>
<p>Our second interesting problem, which is a much worse, “oh no”-level problem: for some reason, passes are cryptographically signed, and they have to be signed with a key known to one of Apple’s certificate authorities. Cryptographically signing these files makes some sense when you consider that passes were designed to get automatic updates from their vendors; for example, your boarding pass for a flight reflecting gate changes or changing your seat assignment.</p>
<p>If you are already an Apple developer you can get yourself a pass signing key pretty trivially, but I am not, and I do not intend to drop $99 on this.</p>
<h2>Perfection is the enemy of something or other</h2>
<p>There are other people who are already Apple developers who have made various apps for designing passes. They are… passable? Unfortunately I am a perfectionist.</p>
<p>For one thing, there is the matter of the logo in the top left of the pass. Apple has designed this somewhat flexibly, with a maximum height of 50 device-independent pixels, but a square logo with text to the right side is going to most comfortably fit at about 40 pixels tall. Pass developers are expected to provide correctly-sized logos at <code>logo.png</code>, <code>logo@2x.png</code>, <code>logo@3x.png</code><sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> for different device pixel ratios, but these apps tend to let you select a single logo and not give you any control over how it’s scaled. If you give it a 40-pixel image, it’ll be blurry on any currently-supported iPhone; if you give it an 80-pixel image, it’ll be too large. Not great!</p>
<p>For another thing, I’d really like to have the screen be brighter as I open the pass. Passes with normal, supported barcodes do this to support scanners that need better contrast. To me, the ideal situation here is to trick iOS into making the screen brighter without actually having a non-functional barcode present. I’m not going to be able to get away with this kind of JSON fuzzing without digging into the JSON myself.</p>
<p>And, these apps tend to be free to download, but only let you save a limited number of passes to Wallet before asking you to pay up. I am not here to judge the developers for doing this but I am probably also not going to pay for your app unless it does what I want it to (and unfortunately what I want is kind of extreme).</p>
<h2>Finding a key</h2>
<p>Well, I did just download half a dozen free-to-start pass generator apps.</p>
<p>You could make these one of two ways. Probably the “correct” way is to have some web service which performs the signing, so that you don’t ship a private key with the application itself. But surely one of these apps I’ve downloaded lets you generate passes offline? Sometimes you want the app to work without having to also maintain a web service; that sounds like a one-way ticket to dealing with a ton of bad reviews and refunds when it inevitably goes down.</p>
<p>I turned on Airplane Mode, turned off WiFi, and tried them all. Sure enough, at least one does. I’m not going to draw attention to the specific app I used in this post because I don’t want their key to get revoked<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup>. But it was kind of funny how simple the process was:</p>
<ol>
<li>Download the app on my Mac, since Apple silicon Macs let you run iOS apps.</li>
<li>In the wrapped iOS app bundle, observe that there is a very obvious <code>.p12</code> file.</li>
<li>Run <code>strings</code> on the main binary and look for anything that might be a password (as PKCS#12 files require an import password).</li>
</ol>
<p>And, well:</p>
<pre><code>$ openssl pkcs12 -info -in [redacted].p12 -legacy -nodes
Enter Import Password:
MAC: sha1, Iteration 1
MAC length: 20, salt length: 8
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Certificate bag
Bag Attributes
friendlyName: Pass Type ID: [redacted]
localKeyID: [redacted]
subject=UID = [redacted], CN = Pass Type ID: [redacted], OU = [redacted], O = [redacted], C = [redacted]
issuer=CN = Apple Worldwide Developer Relations Certification Authority, OU = G4, O = Apple Inc., C = US
...
</code></pre>
<p>We also need a certificate chain; this certificate is signed with an intermediate. The app needs it too, so it’s probably somewhere in the bundle, but the certificate contains within its X.509 extension fields the URL to download the intermediate if you need it (you can view a certificate’s various fields with <code>openssl x509 -noout -text -in whatever.pem</code>).</p>
<h2>Laying out the pass</h2>
<p>First we need to pick a <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW45">pass style</a> out of “boarding pass”, “coupon”, “event ticket”, “generic”, or “store card”. We want a layout that lets us put a large horizontal image across the pass somewhere. This limits us to layouts that support the “strip” image: coupon, event ticket, or store card. Out of these three, the store card is most skeuomorphic to our physical library card.</p>
<p>Let’s type up the start of a <code>pass.json</code>. The documentation for this file is found at the <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html">PassKit Package Format Reference</a>.</p>
<pre><code class="language-json" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">{</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"passTypeIdentifier"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"[redacted]"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"teamIdentifier"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"[redacted]"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"formatVersion"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">1</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"serialNumber"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"whatever"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"organizationName"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"me!"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"logoText"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"The Seattle Public Library"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"description"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"Library Card"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"storeCard"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"headerFields"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [],</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"primaryFields"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [],</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"backFields"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [],</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"secondaryFields"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [],</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"auxiliaryFields"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: []</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> },</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"backgroundColor"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"rgb(255, 255, 255)"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"foregroundColor"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"rgb(0, 0, 0)"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"sharingProhibited"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">false</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">}</span>
</code></pre>
<p>The <code>passTypeIdentifier</code> and <code>teamIdentifier</code> must match the <code>UID</code> and <code>OU</code> fields, respectively, of the certificate subject you got from Apple and/or found lying around. <code>serialNumber</code> needs to be unique for each pass you generate with the same <code>passTypeIdentifier</code>. <code>organizationName</code> is ostensibly supposed to be who made and signed the pass, but if you’re never distributing the pass then it probably doesn’t matter.</p>
<p>Now for some images. <code>icon.png</code> is required but is not shown on the pass itself. <code>logo.png</code> is the logo displayed at the top left. I generated three logo files: a 40×40 <code>logo.png</code>, an 80×80 <code>logo@2x.png</code>, and a 120×120 <code>logo@3x.png</code>; then I copied <code>logo@3x.png</code> to <code>icon.png</code>.</p>
<p>Finally, we’ll need the <code>strip.png</code>, which will contain our pre-generated barcode.</p>
<h2>Generating the barcode</h2>
<p>Fortunately iOS scales and crops the <code>strip.png</code> we generate to fit whatever size box it is on a device, so we don’t need to worry about making three different versions of it.</p>
<p>Both my library cards use “A” and “D” as the start and stop symbols. If you already have a barcode scanner handy this is the easiest way to figure out what your start and stop symbols are, but you can also compare the beginning and end of the barcode against <a href="https://en.wikipedia.org/wiki/Codabar#Encoding">the symbology table on Wikipedia</a> by eye pretty easily.</p>
<p>There aren’t many ready-to-use Codabar generators online, but the format is pretty simple to implement yourself. While prototyping I used <a href="https://lib.rs/crates/barcoders">the Barcoders library for Rust</a> to generate an SVG, then tweaked the SVG and exported a PNG. After some experimentation I settled on the following layout (where 1 unit is the width of a narrow bar):</p>
<ul>
<li>The barcode height is, in units, twice the number of total symbols (including the start and stop symbols) in the barcode. (For example: a 13-digit barcode number is 15 symbols, and so I made my barcode height 30 units tall.)</li>
<li>15 units of quiet space is placed before the start and after the end of the barcode. (Various reader documentation I’ve seen suggests 10 units is sufficient but I had a harder time scanning it with my fancy 2D barcode scanner.)</li>
<li>50 units of padding are placed above and below the barcode. This is overkill, but helps ensure the image is cropped on the top and bottom, not on the left and right.</li>
<li>Each unit is scaled up to 8 pixels to ensure iOS is always scaling the image down. (This makes the final image size 1040 pixels tall and, for my example 15-symbol barcode, the barcode 240 pixels tall.)</li>
</ul>
<p>Codabar has such a simple encoding that I felt an overwhelming urge to write a 69-line shell script that generates a bitmap of an encoded Codabar barcode in the above layout:</p>
<pre><code class="language-bash" data-syntax-highlighted><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D">#!/usr/bin/env bash</span>
<span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">if</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> [[ </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$#</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">-ne</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">2</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> ]]; </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">then</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">>&2</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">echo</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"usage: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$0</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> BARCODE OUTPUT"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">exit</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">1</span>
<span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">fi</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">scale_factor</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">8</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># needs to be multiple of 4 for BMP reasons</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">quiet_space</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">15</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">vert_padding</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">50</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_black</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">() { </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">head</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-c</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> scale_factor </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">3</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">/dev/zero</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; }</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_white</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">() { </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_black</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">|</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> LANG</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">C</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">tr</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'\0'</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'\377'</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; }</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">encode_long</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">() {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">for</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> __x </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">in</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 0 8 16 24; </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">do</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">echo</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-en</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"\x$(</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">printf</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> %x $(((</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">>></span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> __x) </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">%</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">256</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">)))"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">done</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">}</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">workdir</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$(</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">mktemp</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-d</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">)</span>
<span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">trap</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'rm -rf "$workdir"'</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">EXIT</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">{</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_white</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> $quiet_space</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">echo</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-n</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">|</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">while</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">read</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-r</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-N1</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">symbol</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">do</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">case</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> $symbol </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">in</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 0|2|6|C|</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">\*</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">|B|N|.) bars=0001 ;;&</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 1|-|7|D|E|/) bars=0010 ;;&</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 4|$|8|A|T|:) bars=0100 ;;&</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 5|9|3|+) bars=1000 ;;&</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 0|1|4|5) spaces=001 ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 2|-|$|9) spaces=010 ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> 6|7|8|3) spaces=100 ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> C|</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">\*</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">|D|E|A|T) spaces=011 ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> B|N) spaces=110 ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .|/|:|+) spaces=000 ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">) </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">>&2</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> echo </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$0</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">: warning: ignoring symbol </span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$symbol</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">continue</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> ;;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">esac</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">for</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> i </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">in</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> {</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">0..3}</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">do</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_black</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((${</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">bars</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">:</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$i</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">:</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">} </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_white</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((${</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">spaces</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">:</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$i</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">:</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">} </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">done</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">done</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_white</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((quiet_space </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">-</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">} </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">></span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$workdir</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">/line"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">image_width</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$(($(wc </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">-</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">c </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49"><</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$workdir</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">/</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">line") </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">/</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">3</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">barcode_height</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((${</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">#</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">1</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">} </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">2</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">image_height</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$(((barcode_height </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> vert_padding </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">2</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">) </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> scale_factor))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">{</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># BMP header</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">printf</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'BM'</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">encode_long</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((image_width </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> image_height </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">3</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">+</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">54</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">printf</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'\0\0\0\0\x36\0\0\0\x28\0\0\0'</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">encode_long</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> $image_width</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">encode_long</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> $image_height</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">printf</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'\x01\0\x18\0\0\0\0\0'</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">encode_long</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((image_width </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> image_height </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">3</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">head</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-c</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">16</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">/dev/zero</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_white</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((image_width </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> vert_padding))</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># top vertical padding</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> lines</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">=</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((barcode_height </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> scale_factor))</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">while</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> ((lines</span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">--</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">></span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">0</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">)); </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">do</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">cat</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$workdir</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">/line"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">; </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">done</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">draw_white</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">$((image_width </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62"> vert_padding))</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># bottom vertical padding</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">} </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">></span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$workdir</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">/barcode.bmp"</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># if not on macOS, replace with your image conversion tool of choice</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">sips</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-s</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">format</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">png</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">$workdir</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">/barcode.bmp"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">--out</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">$2</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"</span>
</code></pre>
<p>Writing this script’s output to <code>strip.png</code> is all we need.</p>
<h2>Adding the barcode number</h2>
<p>I also wanted the barcode number to display under the barcode; this is simple enough to do with the secondary fields:</p>
<pre><code class="language-json" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"secondaryFields"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"key"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"number"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"label"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"CARD NUMBER"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"value"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"6942069420"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> }</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> ],</span>
</code></pre>
<h2>Faking the barcode UX the rest of the way</h2>
<p>When a user selects a barcoded pass, the phone screen gets brighter to assist with scanners. iOS doesn’t think we have a barcode yet. I hoped that specifying an <em>empty</em> barcode would do the trick, and… yeah! It does! Specifying this in the top level keys of <code>pass.json</code> works:</p>
<pre><code class="language-json" data-syntax-highlighted><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"barcodes"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: [</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"message"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">""</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"format"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"PKBarcodeFormatCode128"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">,</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">"messageEncoding"</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">"iso-8859-1"</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> }</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> ],</span>
</code></pre>
<p>It’s the perfect workaround: no barcode is displayed at the bottom of the pass, but the phone acts like there’s a barcode present.</p>
<h2>Signing and packaging the pass</h2>
<p>Once all our files are in place, we need to generate the <code>manifest.json</code>, which is an object with filenames as keys and SHA-1 checksums as values. I wrote a terrifying jq filter to generate the manifest for me:</p>
<pre><code class="language-bash" data-syntax-highlighted><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">sha1sum</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">.png</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">pass.json</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">|</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">\</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">jq</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-Rs</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">'split("\n") | [ .[] | select(. != "") | split(" ") | {(.[1]): .[0]} ] | add'</span>
</code></pre>
<p>Now we need to sign the manifest, placing the signature at <code>signature</code>. This is done with the <code>openssl smime</code> command.</p>
<pre><code class="language-bash" data-syntax-highlighted><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">openssl</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">smime</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-binary</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-sign</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">\</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-signer</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">pkpass.crt</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-inkey</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">pkpass.key</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-certfile</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">wwdrg4.pem</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">\</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-in</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">manifest.json</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-outform</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">der</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-out</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">signature</span>
</code></pre>
<p>It’s possible to use whatever signing time you like<sup><a href="#user-content-fn-3" id="user-content-fnref-3" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup> with the <code>-attime</code> option, so if the certificate you got from Apple and/or found lying around is expired, you can still sign with it. The <code>-attime</code> option takes a UNIX epoch.</p>
<p>And now, we create a zip!</p>
<pre><code class="language-bash" data-syntax-highlighted><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">zip</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">-r</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">out.pkpass</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">*</span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">.png</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">pass.json</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">manifest.json</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#9ECBFF;--hl-color-light:#032F62">signature</span>
</code></pre>
<p>macOS comes with a pass previewer tool, so you can open this pass to check that it is valid and mostly looks right. (If it’s not valid, look for an error in Console.app.) The previewer isn’t 100% accurate, but it does have a button to send it to your phone via iCloud, which is pretty cool. You can also get it onto your phone in whatever manner is convenient.</p>
<figure>
<p><img src="pass.png" alt="The final pass, which displays a logo, the text "The Seattle Public Library", a barcode, and an obviously fake barcode number under it."></p>
<figcaption>Not my real card number, sadly.</figcaption>
</figure>
<p>I will note that I have not yet tested this pass in a real library yet, but my barcode scanner can read it off my phone just as well as it can read it from my physical plastic card if I turn the screen brightness all the way up (yes, even further than the zero-length barcode workaround causes the screen to get brighter).</p>
<h2>Closing thoughts</h2>
<p>I think it’s pretty neat that the pass specification has remained pretty much unchanged in a decade. But I wish, like many things within the Apple ecosystem, that this didn’t require a $99 USD/year membership to get a certificate in order to sign an otherwise harmless pile of PNGs and JSON. There’s a few features you can use in passes that I’m glad require signing, but nothing I did here should require it, and I hope that changes someday.</p>
<p>Also I think Apple should add Codabar support to Wallet. I’m not aware of any library that supports using a digital form of a library card in-person, and I think with some tweaks the platform could support libraries without requiring an audit to ensure every barcode scanner across the system can support Code 128.</p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-blood">
<p>And blood banks? <a href="#user-content-fnref-blood" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-1">
<p><code>@3x</code> was news to me! Apparently some newer phones have a 3× ratio now. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-2">
<p>Given that most of these kinds of apps do not make the passes updatable via the internet, that these keys are limited to signing passes, and that the keys are specifically used in “make whatever pass you want” apps, I do not think there’s any reason to revoke the key I found. Unfortunately I do not trust Apple will accept this reasoning. <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-3">
<p><a href="https://stackoverflow.com/questions/66989389/consequences-of-the-expiration-of-the-signing-certificate-for-a-already-issued-p/66989932#comment130045743_66989932">Astute observation, Michael.</a> <a href="#user-content-fnref-3" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>Ceci n’est pas une licenceiliana etaoin2023-07-09T19:00:00+00:002023-07-09T19:00:00+00:00https://iliana.fyi/links/treachery/<p>I created a web page for <a href="/treachery/">“Ceci n’est pas une licence”</a>, an anti-license I wrote a few months ago.</p>Hey, I was using that...iliana etaoin2023-06-13T19:00:00+00:002023-06-13T19:00:00+00:00https://iliana.fyi/blog/i-was-using-that/<p>A couple of months ago <a href="https://iliana.fyi/blog/firefox-pinned-tab-attention-icon/">I blogged about how to stop Firefox pinned tabs from lighting up on title change</a>. Today, I had to update that to use the following userChrome.css:</p>
<pre><code class="language-css" data-syntax-highlighted><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">.tab-stack</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">></span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">.tab-content</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">[</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">pinned</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">][</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">titlechanged</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">] {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">background-image</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">none</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">!important</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">}</span>
</code></pre>
<p>instead of this now-broken userChrome.css:</p>
<pre><code class="language-css" data-syntax-highlighted><span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D">/* this doesn't work anymore :( */</span>
<span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">:root</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#FFAB70;--hl-color-light:#E36209">--lwt-tab-attention-icon-color</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">transparent</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">}</span>
</code></pre>
<p>When I restarted Firefox today, it updated to version 114, and I immediately noticed that my old CSS had failed me. A quick dig into mozilla-central revealed that they removed<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup> <code>--lwt-tab-attention-icon-color</code> because it “was added for colorways and won’t be needed going forward”.<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup></p>
<p>This is, of course, frustrating. It’s frustrating because I dug through the CSS to find this, which I believed to be “less fragile than ever-changing selectors”, hilariously enough. It’s frustrating that Firefox developers spent however much time on Colorways for it to be <a href="https://support.mozilla.org/en-US/kb/personalize-firefox-colorways">axed earlier this year</a>. Most of all, I think it’s frustrating that userChrome.css is the only path I have to make this browser work for my needs.</p>
<p>There’s no preference, or even a hidden <code>about:config</code> toggle, to turn off the title changed light; this would be good to have for improved <a href="https://www.boia.org/blog/what-is-cognitive-accessibility">cognitive accessibility</a>. There’s no way for an addon to change this behavior, because that died with XUL. I know I’m not the only one who needs this.<sup><a href="#user-content-fn-3" id="user-content-fnref-3" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup></p>
<p>At the very least, Firefox at least lets me dig into the internals and <em>fix this</em> for myself without having to build my own copy of Firefox every week. That’s certainly better than <a href="https://superuser.com/questions/1684707/how-to-enable-one-line-ui-on-chromium-based-browsers">some browsers</a>. But was it really necessary to remove what looked like a much better way to override this behavior?</p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p>The commit landed two weeks after my blog post, but the bug has been open since February. This is comforting to the part of my brain that briefly believed someone read my blog post and decided to break my workflow. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-2">
<p>I am not linking the bug because I don’t want an extremely trivial route for people reading this blog to go add unwanted noise to the bug tracker. That’s not helpful to anyone. <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-3">
<p>If I had more time, I would have written <del>a shorter letter</del> a well-written bug report that won’t get prioritized, or a patch that takes dozens of hours for me to get a build environment set up for, test, iterate on, and get feedback for. <a href="#user-content-fnref-3" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>Disabling the “title changed” light on Firefox pinned tabsiliana etaoin2023-04-06T19:00:00+00:002023-06-13T19:00:00+00:00https://iliana.fyi/blog/firefox-pinned-tab-attention-icon/<ili-callout role="note">
<p><strong>Updated June 13, 2023</strong>: Firefox 114 broke my original fix; I’ve updated the solution below with the more tried-and-true approach.</p>
</ili-callout>
<p>Webmail and chat clients often change the page title to tell you if you have unread messages and get your attention. If you pin these tabs in Firefox, the page title is hidden. To ensure pages can still get your attention, Firefox displays a little light under the favicon when the title changes. It looks like this:<sup><a href="#user-content-fn-contrast" id="user-content-fnref-contrast" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup></p>
<p><img src="tab-titlechanged.png" alt=""></p>
<p>As part of an ongoing project to manage my ADHD, I wanted to disable these. Unfortunately Firefox provides no <code>about:config</code> option to do that. But at least userChrome.css is still supported.</p>
<p>Here are some steps to create a userChrome.css file:</p>
<ol>
<li>Set <strong>toolkit.legacyUserProfileCustomizations.stylesheets</strong> to <strong>true</strong> in about:config.</li>
<li>Find your profile folder on about:support.</li>
<li>Navigate to this folder and create a <strong>chrome</strong> folder inside it, if it doesn’t exist.</li>
<li>Create a file named <strong>userChrome.css</strong>.</li>
</ol>
<p>Once you have a userChrome.css file<sup><a href="#user-content-fn-namespace" id="user-content-fnref-namespace" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>, you can put this in it:</p>
<pre><code class="language-css" data-syntax-highlighted><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">.tab-stack</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">></span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">.tab-content</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">[</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">pinned</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">][</span><span style="--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">titlechanged</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">] {</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">background-image</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">: </span><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">none</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> </span><span style="--hl-color-dark:#F97583;--hl-color-light:#D73A49">!important</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">;</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">}</span>
</code></pre>
<p>Restart Firefox, and never see the tab light again.</p>
<p><del>There are several ways to do this; I chose this way because it sets a CSS variable that can be set by browser themes to override the tab color, which seems less fragile than ever-changing selectors. If this doesn’t work for you, you can <a href="https://support.mozilla.org/en-US/questions/1181537">try the selector-based approach</a> (this approach lets you disable it based on other inputs as well, such as the tab title).</del> <strong>Updated June 13, 2023</strong>: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1815900">Oh, the irony.</a></p>
<p>You can find <a href="https://hg.mozilla.org/mozilla-central/file/tip/browser/themes/shared/tabs.css">the relevant CSS in the Firefox source tree</a>.</p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-contrast">
<p>I’m using Firefox on macOS with “Increase contrast” enabled, so the light is (unfortunately for me) brighter. <a href="#user-content-fnref-contrast" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-namespace">
<p>Note that this will not work if you have <code>@namespace url(http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul);</code> in your userChrome.css, because this rule affects browser styles created via HTML. <a href="https://crisal.io/words/2023/03/30/xul-layout-is-gone.html">There is no XUL, only HTML.</a> <a href="#user-content-fnref-namespace" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>The undocumented Android change that led to aCropalypse was reported during the betailiana etaoin2023-03-18T19:00:00+00:002023-03-18T19:00:00+00:00https://iliana.fyi/blog/acropalypse-now/<p><a href="https://www.da.vidbuchanan.co.uk/blog/exploiting-acropalypse.html">Exploiting aCropalypse: Recovering Truncated PNGs</a> (CVE-2023-21036):</p>
<blockquote>
<p>The bug lies in closed-source Google-proprietary code so it’s a bit hard to inspect, but after some patch-diffing I concluded that the root cause was due to this horrible bit of API ‘design’: <a href="https://issuetracker.google.com/issues/180526528">https://issuetracker.google.com/issues/180526528</a>.</p>
<p>Google was passing <code>w</code> to a call to parseMode(), when they should’ve been passing <code>wt</code> (the t stands for truncation). This is an easy mistake, since similar APIs (like POSIX <a href="https://man7.org/linux/man-pages/man3/fopen.3.html">fopen</a>) will truncate by default when you simply pass <code>w</code>. Not only that, but previous Android releases had <code>parseMode("w")</code> truncate by default too! This change wasn’t even documented until some time after the aforementioned <a href="https://issuetracker.google.com/issues/180526528">bug report</a> was made.</p>
</blockquote>
<p>A friend pointed me to <a href="https://issuetracker.google.com/issues/135714729">this issue, filed in 2019 during the Android 10 beta</a>. The developer reports an issue where <a href="https://developer.android.com/reference/android/content/ContentResolver#openFileDescriptor(android.net.Uri,%20java.lang.String)"><code>ContentResolver.openFileDescriptor</code></a> does not truncate the file. Google’s response to the report:</p>
<blockquote>
<p>If you want to truncate the file, you need to pass the ‘t’ open mode, so something like ‘rwt’.</p>
</blockquote>
<p>The developer pointed out:</p>
<blockquote>
<p>Thanks for the tip, use ‘rwt’ mode is working as I expected. But still, this behavior is different from previous Android system. this can cause compatibility issues. … Should also mention this in the Android Q behavior change documentation.</p>
</blockquote>
<p>This bug was closed as obsolete a year later. (A year after that, Google responded to the bug linked in the original post, eventually updated the documentation, and two years later, we’re here now.)</p>
<p>I hope the irony that Google’s undocumented API change had security impacts to a Google-developed app is not lost on you. But this goes beyond cropped images: any Android app using the wrong file modes for <code>ParcelFileDescriptor.parseMode</code>, for any data format, will result in incorrect behavior. At best, you get noticeable file corruption; at worst, you get this.</p>
<p>How many other Android apps are impacted by this API change? How many apps are impacted because they didn’t carefully read the (updated) documentation, saw something that looked like POSIX, and assumed the semantics were the same?</p>How to make a pronouns field worseiliana etaoin2023-03-01T19:00:00+00:002023-03-01T19:00:00+00:00https://iliana.fyi/blog/pronouns-field-lawyers/<p><strong>Step 1:</strong> Ask a corporate lawyer what they think.<sup><a href="#user-content-fn-explanation" id="user-content-fnref-explanation" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup></p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-explanation">
<p>GitHub finally added a pronouns field to profiles today, with the following disclaimer devoid of meaning: <em>“Your pronouns will be visible to fellow users across GitHub, including where local laws restrict using pronouns other than those assigned at birth.”</em> It is rather unfortunate that GitHub chose to tarnish an otherwise good implementation with what reads like a threat.</p>
<p>(This is not the first time I’ve watched corporate lawyers fuck this up.) <a href="#user-content-fnref-explanation" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>There is no “software supply chain”iliana etaoin2022-09-19T19:00:00+00:002022-09-19T19:00:00+00:00https://iliana.fyi/blog/software-supply-chain/<p>In actual supply chains, money is changing hands. A server manufacturer is paying for PCB fabrication, who is paying their suppliers for raw materials and equipment, and so on until the whole thing eventually loops back on itself when a mining company needs to buy a server.</p>
<p>When you take on an additional dependency in a software project, often money does not change hands. <code>npm install</code> and <code>cargo add</code> do not bill your credit card. There is no formal agreement between a maintainer and its downstream users.</p>
<p>There is a lot of attention on securing “software supply chains.” The usual approach is that you want to try to avoid security issues in your underlying components from impacting customers of your product; and when they do, you want to be able to respond quickly to fix the issue. The people who care about this class of problem are often software companies. The class of components that are most concerning these companies are ones where unpaid hobbyist maintainers wrote something for themselves with no maintenance plan.</p>
<p>This is where the supply chain metaphor — and it is just that, a <em>metaphor</em> — breaks down. If a microchip vendor enters an agreement and fails to uphold it, the vendor’s customers have recourse. If an open source maintainer leaves a project unmaintained for whatever reason, that’s not the maintainer’s fault, and the companies that relied on their work are the ones who get to solve their problems in the future. Using the term “supply chain” here dehumanizes the labor involved in developing and maintaining software as a <em>hobby</em>.</p>
<p>Everything that can be said about sponsorship and paying maintainers has already been said. Important work is still unfunded. Some of us, including me, don’t particularly mind that we’re not making money off of our weekend hacks. The problem is when the mere act of publishing software becomes a burden.</p>
<p>You still cannot disable pull requests on a GitHub repository. <a href="https://lwn.net/Articles/900953/">A package repository might deem your software “critical”</a>, adding requirements to publishing updates that you might not want to or be able to comply with. <a href="https://security.googleblog.com/2021/02/know-prevent-fix-framework-for-shifting.html">Google even wanted to disallow anonymous individuals from maintaining critical software and wanted to police the identities of others.</a><sup><a href="#user-content-fn-wanted" id="user-content-fnref-wanted" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup></p>
<p>Or, perhaps a maintainer tells someone that they won’t maintain a project anymore, and <a href="https://github.com/advisories/GHSA-74w3-p89x-ffgh">GitHub notifies thousands of dependent repositories</a>, calling it a “critical severity” advisory.<sup><a href="#user-content-fn-critical" id="user-content-fnref-critical" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> This was obviously a mistake, and GitHub withdrew and re-labeled it as low severity this morning, but it is far from the only time systems built to secure the “software supply chain” have failed to understand the nuances of open source software maintenance.</p>
<p>I just want to publish software that I think is neat so that other hobbyists can use and learn from it, and I otherwise want to be left the hell alone. I should be allowed to decide if something I wrote is “done”. The focus on securing the “software supply chain” has made it even more likely that releasing software for others to use will just mean more work for me that I don’t benefit from. I reject the idea that a concept so tenuous can be secured in the first place.</p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-wanted">
<p>“… our view is that owners and maintainers of critical software must not be anonymous” … “To continue the inclusive nature of open source, we need to be able to trust a wide range of identities, but still with verified integrity. This implies a federated model for identities, perhaps similar to how we support federated SSL certificates today …”. (I write “wanted” because it’s been 18 months since this post, and I’m not aware of a more current statement either re-affirming or softening this position, so I’m giving Google the benefit of the doubt it does not deserve.) <a href="#user-content-fnref-wanted" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-critical">
<p>Here is a <a href="https://perma.cc/N6XX-G5PE">Google cache copy of when it was labeled critical</a>. <a href="#user-content-fnref-critical" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>An archeaological dive through a software copyright concerniliana etaoin2022-08-22T20:00:00+00:002022-08-22T20:00:00+00:00https://iliana.fyi/blog/licensing-systems/<p>My friend <a href="https://artemis.sh">Artemis</a> wrote about <a href="https://artemis.sh/2022/08/21/this-program-is-illegally-packaged-in-14-distributions.html">unlicensed code finding its way into Linux distributions</a>. How did this happen? Let’s do a little bit of software archaeology<sup><a href="#user-content-fn-archaeology" id="user-content-fnref-archaeology" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>.</p>
<p>The <a href="https://github.com/seletskiy/godiff">unlicensed code in question</a><sup><a href="#user-content-fn-original" id="user-content-fnref-original" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> is a Go library for parsing unified diffs. <a href="https://gitea.com/gitea/tea"><code>tea</code></a>, “the official CLI for Gitea”, uses this library to allow users to <a href="https://gitea.com/gitea/tea/pulls/315">review pull requests from the command line</a>. The feature was merged into <code>tea</code> at the end of 2020; the most recent commit to the library’s original repository is from 2016. <a href="https://github.com/seletskiy/godiff/issues/4">An issue from someone who is likely the <code>tea</code> feature’s author asking for license clarification</a> has gone unanswered.</p>
<p>We’ll discuss some background information, look at where various systems succeeded and failed, and finally ask whether any of this matters.</p>
<ili-tangent role="note">
<p>This parser appears to be unique (among the Go ecosystem) in that it can parse inline comments, like this:</p>
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">index 1d1fa18c..119b19ea 100644</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/nexus/src/external_api/console_api.rs</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/nexus/src/external_api/console_api.rs</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -586,7 +586,13 @@</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> pub async fn console_index_or_login_redirect(</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> // otherwise redirect to idp</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> // put the current URI in the query string to redirect back to after login</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- let uri = rqctx.request.lock().await.uri().to_string();</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ let uri = rqctx</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ .request</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ .lock()</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ .await</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ .uri()</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ .path_and_query()</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+ .map(|p| p.to_string());</span>
<span style="--hl-color-dark:#6A737D;--hl-color-light:#6A737D"># Can you explain why this is necessary in your commit message?</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> Ok(Response::builder()</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> .status(StatusCode::FOUND)</span>
</code></pre>
<p>I had <em>no idea</em> this was a thing, but this is the sort of thing <code>tea</code> needs to enable posting code reviews from the comfort of your terminal, which is pretty neat. I am surprised this is the workflow you might reach for, what with Git being built for LKML-style email workflows:</p>
<pre><code>On Aug 19, 2022 at 14:10, iliana etaoin <iliana@oxide.computer> wrote:
> - let uri = rqctx.request.lock().await.uri().to_string();
> + let uri = rqctx
> + .request
> + .lock()
> + .await
> + .uri()
> + .path_and_query()
> + .map(|p| p.to_string());
Can you explain why this is necessary in your commit message?
</code></pre>
<p>But that’s designed<sup>[<em>citation needed</em>]</sup> for humans to parse, not computers, so I get why you might want to use this.</p>
</ili-tangent>
<h2>Why do we care about licensing?</h2>
<p>In <a href="https://en.wikipedia.org/wiki/Berne_Convention">most countries</a>, works are automatically copyrighted, and there are minimum standards for exclusive rights the author of that work retains. As someone living in a party to the Berne Convention, this blog post I wrote is automatically copyrighted, despite there being no notice informing you of this.</p>
<p>After making the logical leap necessary to apply those minimum standards to the software world, we can understand that software, by default, probably cannot be used, modified, or redistributed without a <em>license</em> from the copyright holder. There is significant case law on the copyrightability of software that we are <em>not</em> going to read, and there are a lot of open questions that we are <em>not</em> going to try to answer, but most of the important open questions have been solved: if you want to use someone’s software, you need a license, and if you want others to be able to use your software without asking permission, you need to grant a license.</p>
<ili-tangent role="note">
<p>If you push code to a public GitHub repository, <a href="https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#3-ownership-of-content-right-to-post-and-license-grants">you grant an implicit, limited license described in the Terms of Service</a>, which allows GitHub to distribute your code and for other users to do a limited set of actions with it. This license is very limited, and does not grant the usual things lawyers want to see when asked about usage and redistribution of software.</p>
<ili-tangent role="note">
<p>Although maybe that limited license is <a href="https://lwn.net/Articles/862769/">not as limited as we thought</a>.</p>
</ili-tangent>
</ili-tangent>
<p>The usual expectation is that you are publishing a copy of your software so that others can use it, but this is not necessarily true; licenses make that explicit. Some licenses, such as <a href="https://en.wikipedia.org/wiki/MIT_License">the MIT License</a>, permit damn near everything as long as you include a notice from the copyright holder; some require more significant responsibility, such as <a href="https://en.wikipedia.org/wiki/Affero_General_Public_License">the AGPL</a>, which requires that you provide a copy of the software to anyone who can access it over a network<sup><a href="#user-content-fn-agpl" id="user-content-fnref-agpl" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup>; some are <a href="https://anticapitalist.software/">purposefully restrictive</a>, or <a href="https://en.wikipedia.org/wiki/Server_Side_Public_License">desperately trying to keep megacorporations from destroying their business</a>, or <a href="https://github.com/supertunaman/cdl/blob/master/COPYING">are jokes purporting to be a legal document</a>.</p>
<p>The short version: you can’t really use software without someone giving you permission, and there are lots of widely-accepted ways to give people permission to use your software.</p>
<ili-tangent role="note">
<p>I must also bring up <a href="https://www.boringcactus.com/2021/09/29/anti-license-manifesto.html">boringcactus’s Anti-License Manifesto</a>, which I wholeheartedly agree with:</p>
<blockquote>
<p>software licenses are unavoidably a legal tool. the legal system, in the US and approximately everywhere else, is not a machine that leads to justice. therefore, software licenses do not lead to justice.</p>
<p>we cannot software license our way to a better world. as such, we should and must software license our way to a stranger world. permissive licenses and copyleft licenses are both tools of the corporate status quo. we therefore reject all conventional software licenses, and instead champion the weird, the experimental, the decorative, the hostile, the absurd, the useless, the straight up unhinged.</p>
</blockquote>
</ili-tangent>
<h2>What does a Linux distribution do, exactly?</h2>
<p>A Linux distribution distributes Linux, which is software. As a side effect, Linux distributions<sup><a href="#user-content-fn-homebrew" id="user-content-fnref-homebrew" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup> usually also distribute additional software that is compatible with Linux, so you can check that your copy of Linux is working correctly, and also so you can make use of it at all.</p>
<p>The groups of people who build and maintain Linux distributions have many goals, but they usually boil down to two things:</p>
<ol>
<li>Ensure that they are allowed to distribute the software contained in the distribution.</li>
<li>Ensure that their users are allowed to distribute the software, too.</li>
</ol>
<p>And so, being a Linux distribution maintainer involves a <em>lot</em> of reading software licenses and asking lawyers about them, if you’re lucky enough to have lawyers. The bureaucracy of Linux distros is overwhelming, and requires <a href="https://www.debian.org/doc/debian-policy/">writing policy manuals</a>, <a href="https://docs.fedoraproject.org/en-US/legal/">providing legal guidelines</a>, and <a href="https://github.com/bottlerocket-os/bottlerocket-sdk/tree/develop/license-scan">building automation</a> to make what is usually a volunteer-driven system work at all (and licensing is somehow one of the least complex topics in distribution development).</p>
<ili-tangent role="note">
<p>Remember our earlier tangent about <a href="https://www.boringcactus.com/2021/09/29/anti-license-manifesto.html">anti-licenses</a>? Linux distributions usually wish to avoid preventing the corporate status quo from being unable to use parts of their distros, and as a policy disallow these sorts of licenses. After all, many of the most popular Linux distributions are <em>built by</em> the corporate status quo.</p>
</ili-tangent>
<h2>Rigid systems are resistant to change</h2>
<p>Go and other languages<sup><a href="#user-content-fn-rust" id="user-content-fnref-rust" data-footnote-ref="" aria-describedby="footnote-label">5</a></sup> that statically compile dependencies prove to be difficult for many Linux distributions. A lot of distribution maintainers suggest that <a href="https://blogs.gentoo.org/mgorny/2021/02/19/the-modern-packagers-security-nightmare/">static linking creates security issues down the road</a>, but this is really a tertiary problem; the explosive dependency trees, including using multiple versions of the same package at once, break the assumptions that Linux distributions made in past eras<sup><a href="#user-content-fn-compat" id="user-content-fnref-compat" data-footnote-ref="" aria-describedby="footnote-label">6</a></sup>. Perl and Python packaging are difficult, and the npm, Go, and Rust ecosystems exacerbate the fragility of Linux distribution infrastructure even further.</p>
<p>It is becoming more difficult each day to keep an eye on the license of every single piece of software that gets slurped into a distribution. Automation helps, but is expensive to write and maintain, and every ecosystem is subtly different with so, so many exceptions to the rules. For more popular distributions (such as Debian and Fedora) that ultimately feed into ecosystems that are sold to customers (such as Ubuntu and RHEL), policies and guidelines help, but other distributions might not have (or want!) the procedures necessary to ensure what they ship is properly licensed.</p>
<ili-tangent role="note">
<p>Another thing that makes statically-compiled languages difficult: attribution. Linux packagers tend to distribute the original license information with the package; many licenses require attribution or notice of some kind, and it’s easiest to just ship every license with the software than figure out which ones you actually need to ship. But if you install, say, <code>ripgrep</code><sup><a href="#user-content-fn-ripgrep" id="user-content-fnref-ripgrep" data-footnote-ref="" aria-describedby="footnote-label">7</a></sup> or <code>docker</code>, you aren’t installing the dozens of dependency packages, because they’re just source code you don’t need. You’re also not installing the attribution notices for those dependencies, which you also don’t need, but are often required to be distributed with copies of the software. Oops.</p>
</ili-tangent>
<p>So. Where did these systems succeed?</p>
<ul>
<li>Someone wanted to use some unlicensed software, and <a href="https://github.com/seletskiy/godiff/issues/4">asked the authors</a> to clarify a license.</li>
<li><a href="https://artemis.sh/2022/08/21/this-program-is-illegally-packaged-in-14-distributions.html">Someone trying to package software for a distribution</a> found the licensing problem.</li>
</ul>
<p>Where did they fail?</p>
<ul>
<li>The person who wanted to use some unlicensed software used it anyway after the authors didn’t respond.</li>
<li>Maintainers of <code>tea</code> did not check or inquire about the licensing issue (but I’m not really sure if that’s their responsibility).</li>
<li>Packagers and maintainers for <a href="https://repology.org/project/gitea-tea/versions">several distributions</a> did not catch the problem, likely for a lack of tools or advice to check licenses of dependencies.</li>
<li>Even when dependencies are licensed, attributions or notices required by the license aren’t always distributed with the final binary artifacts.</li>
<li>I wrote this blog post instead of filing any issues with Gitea or the distributions that ship <code>tea</code>. (It sounds like a lot of work.)</li>
</ul>
<h2>Does any of this actually matter?</h2>
<p>No.</p>
<p>Think about it. All of the <a href="https://repology.org/project/gitea-tea/versions">distributors of this code tracked in Repology</a> are community-operated, as is the Gitea project; the risk-reward ratio of theoretical copyright litigation is not good, especially when <a href="https://blog.opensource.org/gnome-patent-troll-stripped-of-patent-rights/">litigating against open source projects tends to paint a target on your back</a>.</p>
<p>The larger distros with a focus on making redistribution painless are not shipping this code — not for any license auditing reasons, as far as I can tell; just that packaging is hard, there’s a lot of software, and not a lot of people who care to do the work.</p>
<p>Distros can mitigate the issue by removing the code review functionality from the CLI. Anybody could take a better-licensed unified diff parser and add comment support to replace the unlicensed code. Or nothing could happen at all, and it would probably be fine.</p>
<ili-callout role="note">
<p><em>If you liked this writing, you can support me writing more things through <a href="https://github.com/sponsors/iliana">my GitHub Sponsors page</a>. Thanks for your consideration!</em></p>
</ili-callout>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-archaeology">
<p>Wikipedia <a href="https://en.wikipedia.org/wiki/Software_archaeology">defines software archaeology</a> as “the study of poorly documented or undocumented legacy software implementations, as part of software maintenance.” I do not like this definition; we can study all software, including well-documented and modern software, to study the inner workings of computer systems and the intentions of its authors. <a href="#user-content-fnref-archaeology" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-original">
<p>This is the original source; <code>tea</code> uses <a href="https://gitea.com/noerw/unidiff-comments">this fork with a number of changes</a>. <a href="#user-content-fnref-original" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-agpl">
<p>The AGPL is <a href="https://web.archive.org/web/20220823013449/https://twitter.com/ilianathewitch/status/1511768411772137477">arguably impossible to comply with</a> unless authors go out of their way to make compliance the default, but pretty much all corporations refuse to touch AGPL-licensed code, so, it;s impossible to say if its bad or not, <a href="#user-content-fnref-agpl" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-homebrew">
<p>I’m referring to Linux distributions a lot here, but packaging ecosystems like <a href="https://www.pkgsrc.org/">pkgsrc</a>, <a href="https://brew.sh">Homebrew</a>, and <a href="https://chocolatey.org">Chocolatey</a> ultimately share the same problem space while targeting a system that somebody <em>else</em> is building. <a href="#user-content-fnref-homebrew" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-rust">
<p>Yes, this includes Rust. <a href="#user-content-fnref-rust" data-footnote-backref="" aria-label="Back to reference 5" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-compat">
<p>Except… were those even good assumptions? I’ve spent far too many days of my life building packages to keep binaries that linked against older shared libraries. <a href="#user-content-fnref-compat" data-footnote-backref="" aria-label="Back to reference 6" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-ripgrep">
<p>ripgrep depends on both <code>atty</code> and <code>strsim</code>, libraries that <a href="https://github.com/softprops/atty/blob/6633c0e1446aa19e6cd00e00e39770da43081bda/LICENSE">are</a> <a href="https://github.com/dguo/strsim-rs/blob/65eac453cbd10ba4e13273002c843e95c81ae93f/LICENSE">licensed</a> only under the MIT License, which requires the <em>copyright notice</em> (the part with someone’s name in it) is included in all copies of the software; their copyright notices are nowhere to be found on my Debian system with ripgrep installed. (I’m not picking on ripgrep arbitrarily; it just happens to be the single static-binary-ecosystem case I’m aware of that’s installed on all my systems.) <a href="#user-content-fnref-ripgrep" data-footnote-backref="" aria-label="Back to reference 7" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>Installing a pop-up blocker on a GameBoy at the last minuteiliana etaoin2022-07-28T19:00:00+00:002022-07-28T19:00:00+00:00https://iliana.fyi/links/cohost-gif-plays-pkmn/<p>While working with friends on a handheld emulator playable from a cohost post, I found <a href="https://cohost.org/iliana/post/68021-installing-a-pop-up">an interesting workaround for a usability issue</a>.</p>Oxide Computer Company: “Benefits as a reflection of values”iliana etaoin2022-04-20T19:00:00+00:002022-04-20T19:00:00+00:00https://iliana.fyi/links/oxide-benefits/<p>I wrote about <a href="https://oxide.computer/blog/benefits-as-a-reflection-of-values">Oxide’s employee benefits</a>, which include direct reimbursement for trans benefits because I asked, despite the insurance industry.</p>Tailscale’s human-scale networks are still controlled by Google and Microsoftiliana etaoin2022-04-04T19:00:00+00:002022-04-04T19:00:00+00:00https://iliana.fyi/blog/tailscale-auth-and-threat-modeling/<p>I’m not sure if they realize it yet, but Tailscale seems to work extremely well for polycules. Each user can have their own single-user <a href="https://tailscale.com/kb/1136/tailnet/">Tailnet</a> and <a href="https://tailscale.com/kb/1084/sharing/">explicitly share specific machines</a> with other people. Both parties have to consent to sharing a device; either party can revoke this consent. The device owner can further restrict accessible ports through <a href="https://tailscale.com/kb/1018/acls/">ACLs</a>. Tailscale runs on <a href="https://artemis.sh/2022/02/16/tailscale-on-netbsd-proof-of-concept.html">pretty</a> <a href="https://blog.shalman.org/tailscale-for-illumos/">much</a> anything if you try hard and believe in yourself. This entire use case, up to this point, fits in Tailscale’s free tier.</p>
<p>As an apparent product decision, Tailscale does not store passwords or manage 2FA. It relies on third-party authentication services; at the free tier, this is restricted to Google, Microsoft, and GitHub (a Microsoft subsidiary). If you want 2FA, you enable 2FA on your account with those providers or enforce it in your organization.</p>
<p>This is in line with Tailscale’s goal of <a href="https://tailscale.com/blog/free-plan/">keeping operational costs as low as possible by design</a>; today, Tailscale-the-company does not handle or store any information that could be used to gain access to your account (apart from API keys) or decrypt data going over your Tailnet. Instead of constantly auditing that their storage of user credentials is good enough for their largest customers, they simply do not store credentials and tick the box on the audit report.</p>
<p>Our polycule does a lot of threat modeling. We don’t push a lot of paperwork around, but we do constantly consider some important threats around the systems we use to keep connected:</p>
<ol>
<li>Is there a threat of information disclosure to an unknown third party (e.g. eavesdropping, rogue sysadmin, APT) or a <em>known</em> third party (e.g. someone you had a bad breakup with)?</li>
<li>Is there a threat of identity impersonation (e.g. someone is able to send messages as you)?</li>
<li>Is there a threat of losing access to your connections due to a third party for an unrelated reason (e.g. feature change, identity change<sup><a href="#user-content-fn-username" id="user-content-fnref-username" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>, account closure, service downtime)?</li>
</ol>
<p>And, unfortunately, <em>requiring</em> the use of authentication systems owned by Google and Microsoft invoke <em>all</em> of these threats and more<sup><a href="#user-content-fn-ice" id="user-content-fnref-ice" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>, but most concerning is the third: all of these services can terminate your account for pretty much any reason, and it is <a href="/blog/everything-that-lives-is-designed-to-end/">apparently very easy to violate the GitHub Terms of Service on accident</a>. If you lose access to the account for your Tailnet, you eventually lose access to your machines and machines shared with you (probably not immediately, but you can no longer log in to manage your machines, and API keys last at most for 90 days).</p>
<p>Corporate users of Tailscale pay a company to handle their <abbr title="identity provider">IdP</abbr>. Our loose amalgamation of people cannot and will not (and Tailscale, rightfully so, wants you to pay them to integrate a custom IdP into their systems). But if Tailscale is meant to support ”<a href="https://tailscale.com/company/">human-scale networks</a>,” why is this threat so apparent in their product?</p>
<p>There’s not really a good option here for Tailscale. Even if you could bring your own IdP as a free-tier customer, you still have to apply this same threat modeling to that particular provider, even if you know the people who run it, and would put you in the same Tailnet as everyone else in your IdP, replacing the feature of <em>explicitly</em> sharing individual machines with the need for network admins to manage this sharing for the users. It’d be nice if they supported a username/password/2FA flow without the need for third parties, but this is expensive to implement correctly.</p>
<p>I just want to have a single-user Tailnet and explicitly share machines with my partners, without having to trust that GitHub won’t ban me tomorrow. I wish that wasn’t too much to ask of even the paid Personal Pro plan.</p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-username">
<p>It’s unclear what happens to your Tailnet if you change your email or username with the identity provider, since Tailscale appears to make this a one-to-one mapping to your Tailnet. <a href="https://christine.website/blog/identity-model-software-2021-01-31">Identity is a fraught topic for trans people and plural systems</a>, whether these users are on the free tier or an enterprise account. <a href="#user-content-fnref-username" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-ice">
<p>Both of these companies <a href="https://www.businessinsider.com/google-amazon-microsoft-ice-cbp-third-party-contracts-cloud-2021-10">still have contracts with ICE</a> and are regularly involved in union-busting, so these are somewhat concerning companies to force your free-tier customers to use. <a href="#user-content-fnref-ice" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>Identity fluidity and keysmashingiliana etaoin2020-07-02T19:00:00+00:002020-07-02T19:00:00+00:00https://iliana.fyi/blog/etaoin/<p>I <a href="/blog/fitting-rooms-for-your-name/">previously wrote</a> about systems, such as the Amazon employee directory, that can be designed with identity fluidity in mind. I also wrote about the last name <em>destroyer of worlds</em>, one that I’m fond of, but has a number of non-trivial issues that make me think twice before making it my legal name.</p>
<p>I’ve decided to “try on” a new name, again: <em>iliana etaoin</em>.</p>
<p>I don’t think I’ve told the story of my first name very widely, but it’s a name that stuck with me for years — ten years, in fact, until I decided to assume it. I never understood why until I came out to <em>myself</em>. I rejected any other suggested names whether I actually had a reason to or not. <em>iliana</em> was going to be it, regardless of what others thought or even the issues surrounding its typography (which I’ve patched around by <a href="/lowercase/">demanding it be written in lowercase</a>); the only question was how I was going to spell it.</p>
<p>My father was educated as a journalist and worked on various rural daily newspapers from their last hurrahs to their final gasps for air before being <a href="https://www.vanityfair.com/news/2020/02/hedge-fund-vampire-alden-global-capital-that-bleeds-newspapers-dry-has-chicago-tribune-by-the-throat">systematically dismantled and stripped to a husk by private capital</a>. I spent countless hours sitting behind him at his desks of those rural newsrooms, watching him meticulously adjust spacing between words to fit a story into an appropriate section of a page. This period formed most of my design and typography opinions; I was unsurprised when I learned the font I fell in love with and licensed for my website was designed for one of the last remaining newspapers that can afford to commission one. It would have also been a valuable learning experience had I not steered myself away from following in his footsteps, being already aware — first-hand — of print news’s impending destruction.</p>
<p>At the start of his career, Linotype machines were being quickly replaced with modern conveniences, and he watched a pre-press department a wall away from him transform from the hot-lead-slinging machines to entirely digital printing plate preparation. Linotypes are incredibly intricate mechanical machines built of their time; no person today would ever need one except in the most extreme form of nostalgia. I watched a relic operated by a long-retired typesetter as part of a field trip when I was young, and it’s stuck with me for all this time. You can watch <a href="https://www.youtube.com/watch?v=EzilaRwoMus">a video on how mechanical typesetters like the Linotype work</a>, which I highly recommend watching on at <em>least</em> 150% speed.</p>
<p>Because the Linotype requires the metal letter molds to run the entire way through the machine so they can be properly re-sorted, there’s no mechanism to short-circuit a mistyped line. So to quickly finish the line and type it again, you’d hit keys until the line filled up, then you’d (hopefully) remove it from the stack of lines later. If Linotypes used typewriter-style keyboards, you’d see the home row, <em>asdfkjl</em>. Thanks to the layout of the Linotype keyboard, you’d see the first two columns, <em>etaoin shrdlu</em>.</p>
<p>I learned about this and immediately thought it’d be a great name for a trans person, and then it kind of stuck in the same way <em>iliana</em> did. Nowadays my brain doesn’t let me think about a name for ten years before picking it, though.</p>
<p>As far as <em>family</em> names go, <em>iliana etaoin</em> tells a stronger and better story than merely sharing the same family name of a line of dozens of fathers. And so, for now, <em>iliana etaoin</em> I shall be.</p>
<p>Étaín, a sun goddess of Irish mythology according to <a href="https://en.wikipedia.org/wiki/%C3%89ta%C3%ADn">some website I found</a>, can alternately be spelled Etaoin, so I’m choosing to pick <a href="https://twitter.com/kixiQu/status/1270840111748591624">an Irish pronunciation</a> over the pronunciation of <a href="https://www.nytimes.com/video/insider/100000004687429/farewell-etaoin-shrdlu.html">some of the last mechanical typesetters</a>.</p>
<p><em>destroyer of worlds</em> isn’t a deadname. Neither is my father’s last name. If I have the choice, I’ll often end up using a mononym. I’ll use various last names where I see fit.</p>Installing Fedora 31 on a 2018 Mac miniiliana etaoin2020-01-12T20:00:00+00:002020-01-12T20:00:00+00:00https://iliana.fyi/blog/installing-fedora-on-mac-mini/<p>Within the last couple of years I decided to ditch my 2U rackmount desktop (named <code>g2-2xlarge</code>, complete with a GPU on a PCIe riser card) in favor of taking up less space in my house with computers.</p>
<p>I started looking at the mini PC market about four days before the new Mac mini was announced and it just blew me away. Four Thunderbolt 3 ports!</p>
<p>Of course, getting Linux running on new Mac hardware is fraught with peril; this time even more so.</p>
<h2>Hardware support in Linux 5.4</h2>
<p>My goal was to get Fedora stable packages booting on my Mac mini (no matter how they had to be installed), with the internal SSD and Thunderbolt ports working, and thermal management working enough to not halt or catch fire.</p>
<p>The T2 Security Chip implements a lot of functionality for modern Macs, including what appears to be a software NVMe controller with some interesting quirks. <a href="https://lore.kernel.org/linux-nvme/20190807075122.6247-1-benh@kernel.crashing.org/">The T2 nvme-pci patchset from Benjamin Herrenschmidt</a> landed in Linux 5.4.</p>
<p>A patch to <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=0d53827d7c172f1345140f7638fe658bda1bb25d">fix a Thunderbolt code path on Titan Ridge hardware</a> was also necessary to get Thunderbolt devices showing up on the system.</p>
<p>A few months back, I tried the first Fedora Rawhide compose with these patchsets and confirmed everything worked as expected. I wasn’t inclined to run Rawhide on my desktop though, and waited for Linux 5.4 to make its way into a stable Fedora repository. On 6 January 2020, v5.4.7 was in Fedora 31 stable.</p>
<h2>Installation process</h2>
<p>Since the Mac mini was sitting on a shelf for several months, I decided to make sure all its firmware was up-to-date by running the macOS Software Update until it told me there weren’t anymore. After that I entered <a href="https://support.apple.com/en-us/HT201314">macOS Recovery</a> to <a href="https://support.apple.com/en-us/HT208330">disable Secure Boot and enable External Boot</a>.</p>
<p>The Fedora 31 netinst ISO doesn’t have Linux 5.4, so I opted to install using a Fedora Rawhide network installer. It’s possible to install prior versions of Fedora with Anaconda by overriding the repository URLs. <code>dd</code> the ISO to a USB drive and boot it by holding down <kbd class="key">⌥ Option</kbd>/<kbd class="key">Alt</kbd>, then selecting the orange disk labeled “EFI Boot”.</p>
<p>After performing some failed installations and doing some research, I found out about two additional quirks. The first: attempting to add a new boot entry via <code>efibootmgr</code> will immediately display an <code>invalid opcode</code> message from the kernel and hang the system. A workaround is adding <code>efi=noruntime</code> to the kernel command line in both Anaconda and the installed system.</p>
<p>The second: the EFI system partition must be FAT32, which is normally not weird, except that this is a change from past Mac EFI hardware. Working around this is best done with a quick-and-dirty Anaconda patch, distributed via an updates.img. (Note that this patch may cause problems if trying to dual-boot.) This patch applies to the <a href="https://github.com/rhinstaller/anaconda/tree/anaconda-32.18-1"><code>anaconda-32.18-1</code></a> tag:</p>
<pre><code class="language-diff" data-syntax-highlighted><span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">diff --git a/pyanaconda/bootloader/efi.py b/pyanaconda/bootloader/efi.py</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">index f8dd84a4f..30bee3756 100644</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/pyanaconda/bootloader/efi.py</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/pyanaconda/bootloader/efi.py</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -197,10 +197,6 @@</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> class MacEFIGRUB(EFIGRUB):</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> def is_valid_stage1_device(self, device, early=False):</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> valid = super().is_valid_stage1_device(device, early)</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- # Make sure we don't pick the OSX root partition</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- if valid and getattr(device.format, "name", "") != "Linux HFS+ ESP":</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- valid = False</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> if hasattr(device.format, "name"):</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> log.debug("device.format.name is '%s'", device.format.name)</span>
<span style="--hl-color-dark:#79B8FF;--hl-color-light:#005CC5">diff --git a/pyanaconda/platform.py b/pyanaconda/platform.py</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E">index 60db687ad..d9968a860 100644</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">--- a/pyanaconda/platform.py</span>
<span style="--hl-color-dark:#85E89D;--hl-color-light:#22863A">+++ b/pyanaconda/platform.py</span>
<span style="--hl-bold-dark:bold;--hl-bold-light:bold;--hl-color-dark:#B392F0;--hl-color-light:#6F42C1">@@ -162,21 +162,12 @@</span><span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> class EFI(Platform):</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> class MacEFI(EFI):</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- _boot_stage1_format_types = ["macefi"]</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- _boot_efi_description = N_("Apple EFI Boot Partition")</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> _non_linux_format_types = ["macefi"]</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> _packages = ["mactel-boot"]</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> _boot_stage1_missing_error = N_("For a UEFI installation, you must include "</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "a Linux HFS+ ESP on a GPT-formatted "</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> "disk, mounted at /boot/efi.")</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- def set_platform_bootloader_reqs(self):</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- ret = super().set_platform_bootloader_reqs()</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- ret.append(PartSpec(mountpoint="/boot/efi", fstype="macefi",</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- size=Size("200MiB"), max_size=Size("600MiB"),</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- grow=True))</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">- return ret</span>
<span style="--hl-color-dark:#FDAEB7;--hl-color-light:#B31D28">-</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> class Aarch64EFI(EFI):</span>
<span style="--hl-color-dark:#E1E4E8;--hl-color-light:#24292E"> _non_linux_format_types = ["vfat", "ntfs"]</span>
</code></pre>
<p>Building updates.img is done simply by running <code>scripts/makeupdates</code>. Upload it to a web server and add <code>inst.updates=https://...</code> to the kernel command line.</p>
<p>For me, <kbd class="key">Ctrl</kbd>+<kbd class="key">X</kbd> did not boot from the boot arguments editor, but <kbd class="key">F10</kbd> did.</p>
<p>With these two workarounds combined, the install proceeds normally, except a prompt about being unable to add the boot entry (which can be answered “Yes” to continue).</p>
<p>If Fedora is the only OS on the machine, there’s no need to add a boot entry. If dual-booting, it might be possible to <a href="https://wiki.archlinux.org/index.php/Unified_Extensible_Firmware_Interface#bcfg">add an entry with the EFI shell’s <code>bcfg</code> command</a>.</p>
<h2>Success!</h2>
<p>Say hello to <code>drifblim</code>, my new desktop machine:</p>
<p><img src="drifblim-stack@1800px.jpg" alt="A Mac mini (2018) with several Thunderbolt 3 cables, a Thunderbolt dock above it, and a Schiit Modi 2 Uber and Magni 2 Uber above that"></p>
<p>Apart from adding <code>efi=noruntime</code> to the kernel command line, and a rare issue where the system will freeze and halt during boot (then come up fine the next time), this machine is wonderful. It’s very quiet at high temperatures compared to what it replaced (an Intel NUC8i5BEK) and seems to perform much better too.</p>
<p><img src="drifblim-neofetch.png" alt="neofetch output with pixel art of the Pokémon Drifblim"></p>
<p>Special thanks to <a href="https://github.com/mikeeq/mbp-fedora">mikeeq/mbp-fedora</a>; while I didn’t use that version of the Live CD as it did a lot more than I needed, the information about the above quirks was the help I needed to get this done, and readers might find it useful for installing Fedora on a modern MacBook Pro.</p>Fitting rooms, but for your nameiliana etaoin2019-08-15T19:00:00+00:002019-08-15T19:00:00+00:00https://iliana.fyi/blog/fitting-rooms-for-your-name/<p>At Amazon, the company directory (“Phone Tool”) allows you to set your name to pretty much anything you please. I remember getting the company-wide email sent announcing this ability. A few of my coworkers wondered why anybody would need such a feature. For me, I read that email and realized that functionality was for people like me.</p>
<p>At the time I was playing around with the idea that I might be trans. Idly thinking about femme names in the shower every morning, wondering when I was going to say “fuck it” and shave off my awful beard. That email was when I finally realized that, yeah, I could actually go through with this and come out okay, because there are people out there like me who are looking out for me.</p>
<p>A company directory where you can change your name whenever you want, without the court order and lengthy lines at the Social Security Administration, was so incredibly transformative to me when I came out. I changed my name in the directory, and a couple hours later was able to get a spiffy new badge with my spiffy new name.</p>
<p>And then, a year and a half later, I finally realized that I can have <em>any name I want</em> at work without The Process, and I can <em>try them on</em> for as long as I like.</p>
<p>I went through a lot of different names (I’ll get to that below), trying to feel out if anything really felt like “me”. About a year ago I ended up changing it out to “iliana destroyer of worlds”, during a time when I was changing the last name every few weeks. I couldn’t bring myself to change it again because I liked it too damn much.</p>
<p>I started making Git commits with that name, then I changed my email signature to that name, and then I changed my personal website to that name. I tried on the name for a bit and I liked it so much that I still think about getting a legal name change for it despite <a href="https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/">being aware of the multitude of systems it would break</a>. (It won’t even fit on most credit cards, since I’d refuse to make any of it the middle name.)</p>
<p>I’d like to see, within my lifetime, more ways for folx to try on new names and have that be as far reaching as they like. Real name policies must be destroyed as soon as possible, and not just on Facebook, but everywhere. Payment cards shouldn’t have names, but if they must, we need to be able to put anything we like in that field and have it update without shipping a new card. As long as I establish the link between myself and the name, I should be able to have as many names as I want and still be able to file my tax return.</p>
<p>All these things are possible. It is policy, law, and cisnormativity that prevents it. Let’s fix that.</p>
<p>By the way: you don’t have to be trans to change your name to whatever you like.</p>
<h2>Epilogue: A brief history of iliana’s last names</h2>
<p>I’ll admit, I spent a good while trying to break things with Phone Tool’s preferred name field, and ended up learning a lot about how names are distributed across the company.</p>
<p>Emoji are sadly not allowed (not even symbols later retconned as emoji). But <a href="https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms">fullwidth forms</a> are.</p>
<p><img src="ili-fullwidth.png" alt="A badge displayed in Phone Tool, reading "iliana weller" with "weller" written using full width text"></p>
<p>I then learned about the many ways where applications get people’s names. For Amazon users, <a href="https://docs.aws.amazon.com/chime/latest/ag/active_directory.html">Chime uses Active Directory</a>, and whatever syncs names from Phone Tool to AD has some interesting conversion logic.</p>
<p><img src="ili-fullwidth-chime.png" alt="A message in Chime that reads "is this real life"; a reply from "??????, iliana" replying "LOL""></p>
<p>After spending some time trying to figure out if there was a way around HTML tag stripping (because who wouldn’t want <code><script>alert("sup")</script></code> as their last name), I decided to go with “iliana weller’;DROP TABLE users;—”, until someone with the wrong kind of mind for security (three weeks later) emailed my director. (The single quote was specifically selected because there are plenty of people with apostrophes in their names, and the load bearing symbols didn’t even make it through AD conversion.)</p>
<p>After that was “iliana :)”, which Active Directory kindly transformed to ”), iliana”. And then, “iliana destroyer of worlds”.</p>the [E]nd of eeeeeiliana etaoin2018-12-11T20:00:00+00:002018-12-11T20:00:00+00:00https://iliana.fyi/blog/everything-that-lives-is-designed-to-end/<p><em>(This is a follow-up to ”<a href="/blog/e98e/">A tale of 132 e’s</a>”. Post updated to reflect permanent archive location.)</em></p>
<p>I archived <a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee">e30e/e98e</a>, effectively making it read-only.</p>
<p>This was a decision I made this morning after reading <a href="github-email-1.txt">GitHub Support’s first response</a>, regardless of how they continued to respond. I informed the other maintainers of my decision about two hours ago.</p>
<p>I became incredibly disillusioned not just with how GitHub handled the situation, but with the negative responses I received (detailed in <a href="../e98e/">the previous post</a>).</p>
<p>GitHub initially disabled access to the repository at around 2018-12-10 20:30 UTC and informed me via an automated email, and it took them 19 hours to respond for any clarification whatsoever, writing that the repository ”<em>was disabled following reports of abuse of GitHub’s Terms of Service. Keep in mind, people use GitHub to learn, to work, and to be productive. Generating disruptive content on GitHub can go against our prohibition against spam.</em>”</p>
<p>Please keep your e’s to a minimum, folks, there are people trying to perform capitalism here.</p>
<p>A few hours later, <a href="github-email-2.txt">GitHub clarified its concerns</a>, stating that the reason for disabling the repository was that it affected “the UI of other GitHub users who haven’t starred or otherwise interacted with the repository”, excluding the Explore page and its related emails, and that it had generated an unsustainable level of support load.</p>
<p>I understand this, and made it clear that I was willing to rename the repository if they could clarify what their limit (even if temporary) actually was. I had no intention of playing chicken with GitHub Support over the number of e’s I was allowed to use.</p>
<p>They finally clarified it:</p>
<blockquote>
<p><a href="github-email-3.txt"><em>We’d ask that you limit the organization and repository names to 10 characters each for now.</em></a></p>
</blockquote>
<p>This is absurd. It took me <a href="https://github.com/rust-lang-nursery/edition-guide">approximately five seconds to find a legitimate repository that fails this limit</a>.</p>
<p>But none of that matters, and there’s nothing GitHub Support can or should do at this point. There’s a loud minority of GitHub users that really believe that GitHub is not a venue for this kind of activity, and they’re willing to show their face to us as they tell us to go fuck ourselves.</p>
<p><a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/issues/435"><img src="github-asshole.png" alt=""Good luck to you :)", and an email response from GitHub Support from an email with the subject "TBH, it's an abuse repo""></a></p>
<p>And despite the tone with GitHub Support improving, and despite us shortening the repo name or the UI fixes they can make, there will still be this loud group of assholes who want to see this wonderful accidental art project gone.</p>
<p>I don’t have the energy to maintain a repository that’s this popular, and I don’t have the energy to do it knowing that at any time GitHub might bend to the whim of a hater and it’ll take me an entire day to get the repository back.</p>
<p><a href="https://buttslol.net/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.git.zip">A repository archive is available as a zip file</a> (<a href="e98e-sha512sum.txt">signed checksum file</a>), which contains the default branch, all PRs (regardless of state), and a snapshot of accessible forks. It can also be cloned:</p>
<pre><code>git clone -b eeeeeeeeeeeeeeeeeeeeeeee https://buttslol.net/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.git
</code></pre>
<p><a href="https://www.archiveteam.org/index.php?title=ArchiveBot">ArchiveBot</a> was summoned to take care of capturing the web part, and it’ll go to the Internet Archive in due time. Sorry if we didn’t merge or respond to your pull request.</p>
<p>If GitHub wants to rename the repository to something shorter, they can. I won’t comply with an excessively restrictive demand. <a href="github-email-4.txt">My final email to support explains our position and our concerns.</a></p>
<p>I want to thank everybody who contributed to make e98e what it is today. It’s been a fun few days. But it’s time to move on.</p>A tale of 132 e’siliana etaoin2018-12-10T20:00:00+00:002018-12-10T20:00:00+00:00https://iliana.fyi/blog/e98e/<p><em>(Updates moved to follow-up post: ”<a href="/blog/everything-that-lives-is-designed-to-end/">the <span class="font-feature-case">[E]nd</span> of eeeee</a>”.)</em></p>
<p>A few weeks ago <a href="https://github.com/ilianaw">my</a> most popular repository on GitHub was <a href="https://github.com/ilianaw/rust-crowbar">crowbar</a>, a Rust crate that I’m particularly proud of. It wasn’t necessarily <em>good</em>, but it did something interesting. It has 168 stars.</p>
<p>It was overtaken by <a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee">a repository named by the maximum number of e’s</a>.</p>
<p>It cannot be cloned.</p>
<pre><code>Cloning into 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'...
fatal: remote error:
is not a valid repository name
Email support@github.com for help
</code></pre>
<h2>“Why?”</h2>
<p>“Why?” is the most frequently-asked question about this repository. Here is as good of an answer as you’ll get:</p>
<p>At some point I wanted a GitHub organization named by 32 a’s. This was taken, so I tried with “e”. Now I had an otherwise useless organization.</p>
<p>A few weeks later I noticed I owned this organization. I then did the most chaotic neutral thing I could and created the longest possible repository name.</p>
<p>That’s the whole story.</p>
<p>I created it as an empty repository, as I like to make the first commit myself. This was when I learned I couldn’t push to this repository via git. I couldn’t fork it because it was empty, so I had to delete it and recreate it with a README.md.</p>
<p>I then posted about it <a href="https://cybre.space/@iliana">on the fediverse</a> and some folks had fun — five of the first six pull requests are my friends.</p>
<h2>The unstoppable e98e</h2>
<p>Some folks found it again around November 16ish and <a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/pull/33/files">provided extremely valuable community contributions</a>.</p>
<p>Friday evening (December 7), it started receiving an incredible amount of attention from an unknown source. <a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/pull/53/files">PR #53 proposed a number of programs that infinitely printed “e”</a>, and once that was merged dozens of others managed to help turn the repo into a Dadaist <a href="http://www.rosettacode.org/wiki/Rosetta_Code">Rosetta Code</a>.</p>
<p>Around that time, it hit <a href="https://github.com/trending">GitHub’s trending page</a>. I’m writing this early Monday morning and it’s been there for at least 36 hours, and at the top for 24. It was wonderful to watch it overtake <a href="https://github.com/MicrosoftEdge/MSEdge">Microsoft’s announcement that it would move Edge to the Chromium engine</a>.</p>
<p>Apparently <a href="https://twitter.com/wstrinz/status/1071846553558663168">we broke GitHub’s daily trending repositories email, too</a>.</p>
<h2>It’s okay to not understand</h2>
<p>It wasn’t too long after we hit trending for a significant amount of negative feedback to be hurled our way.</p>
<p>Some of it was perhaps defensible, alarmed that the “best” (?!) repo on GitHub that day was an absurd celebration of obnoxiously long repository names. Of course, GitHub’s trending page is merely a discovery tool, not a daily awards show, and like any computer-generated list of “what’s popular” should be read critically.</p>
<p>Someone asked <a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/issues/30#issuecomment-438941358">“what it is supposed to represent that an employee of a large company is making such a garbage repository”</a> as if my employment at a massive dystopian megacorporation quickly taking over the world has anything to do with this.</p>
<p>Another user told me to go fuck myself after spreading their shitty attempts at humor over dozens of issues. They remain blocked by the e30e organization. We unilaterally added a code of conduct shortly after that incident, which brought the sorts of trolls you’d expect out to play too.</p>
<p>A tweet called the repository a “waste of public resources”, which may be a mistranslation, but is otherwise a gross misunderstanding of GitHub’s business model.</p>
<p>Many more people looked at our repository and didn’t understand. Instead of walking away, they clicked the issues tab and opened an issue asking what the hell we were doing.</p>
<p>Our industry teaches a lot of people that there is nothing they cannot learn, and we have a critical mass of arrogant people in tech and on GitHub. It’s incredibly toxic and I never want to maintain a <em>real</em> repository this large on GitHub, paid or not.</p>
<p>I see the double-digit number of pull requests on the repository, roll my eyes, and wonder if I should just throw in the towel and archive the project.</p>
<p>But without fail, every time I think that, <a href="https://github.com/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/pull/330">there’s an absolutely beautiful pull request waiting</a>.</p>
<p><a href="conway-eeeee.png"><img src="conway-eeeee.png" alt="Conway's Game of Life ticker spitting out lowercase e's"></a></p>
<p><em>(Updates moved to follow-up post: ”<a href="/blog/everything-that-lives-is-designed-to-end/">the <span class="font-feature-case">[E]nd</span> of eeeee</a>”.)</em></p>