<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">

<channel>
<pubDate>Wed, 17 Jun 2026 13:46:10 GMT</pubDate>
<title>Alexander Gromnitsky&#39;s Blog</title>
<description>Пограмування, ойті, витяги, цитати</description>
<link>https://sigwait.org/~alex/blog</link>
<language>en</language>
<itunes:image href="https://sigwait.org/~alex/blog/itunes.png"/>
<itunes:explicit>false</itunes:explicit>

  <itunes:category text="Drama" />


<itunes:author>ag</itunes:author>



<item>
  <title>Linux Apps That Maybe Run</title>
  <link>https://sigwait.org/~alex/blog/2026/06/13/qAp2Gu.html</link>
  <guid>https://sigwait.org/~alex/blog/2026/06/13/qAp2Gu.html</guid>
  <pubDate>Wed, 17 Jun 2026 13:46:10 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>ойті</category>
  
  <description><![CDATA[<p>According to some prominent fellows, whom I won't name to protect the
guilty, the only viable way to distribute software for Linux is to
ship a container. Not to make an rpm/deb &amp; serve it from a repo, like
some insignificant companies that control half of the Internet do, but
to collect all (or most) dependencies &amp; put them alongside the
application. Only this way, experts say, guarantees a high probability
of surviving in the Linux wilderness.</p>
<p>Another set of leading figures argue that shipping a container is
necessary but not sufficient: in the days of rogue AI agents &amp;
catastrophic vulnerabilities hiding in every software corner, an
application should (some insists on the word <em>must</em>) be incapable of
doing anything without the user's permission.</p>
<p>I thought about that a little, &amp; came to the conclusion that a
layperson couldn't care less. I'm not talking about a guy who feels
personally offended when an ntp client dares to send a udp packet
without asking first, I'm talking about an individual who downloads an
"app" not to enjoy perusing a granular permission model, but to
accomplish <em>a</em> task.</p>
<p>I noticed how not a few Windows developers are at a loss to what to make
of Linux software distribution models. This post is for them.</p>
<h3>Not a GUI</h3>
<p><em>Theoretically</em>, if you write simple network microservices, you can
abandon all 3rd-party libraries, even libc itself, &amp; stick to syscalls
of the Linux kernel ABI. As long as Linus is in charge, your programs
will always work.</p>
<p>Out of curiosity, I once tried to write a simple nolibc TCP server &amp;
<a href="https://henry-flower.dreamwidth.org/521944.html">described the
experience</a>. (For
convenience, the text is in Ukrainian.)</p>
<p>Now, let's get back to real life.</p>
<h3>DLLs &amp; sonames</h3>
<p>In the Linux world a DLL is called a <em>shared library</em>. A plain stock
Fedora Live ISO with no developer tools installed, contains thousands
(3977 in f44) of them.</p>
<p>The 2 major Linux GUI toolkits (GTK &amp; Qt) use a multitude of 3rd-party
shared libraries that they don't control, &amp; when you link against a
GUI library in a particular distro, there is 0 guarantees that your
program will work in the next distro release. Even if the shared
libraries of the GUI toolkit stay the same, some transitive dependency
may acquire a <em>soname bump</em>, &amp; a dynamic linker will refuse to run
your program.</p>
<p>A shared library named <em>foo</em> has actually several names, that look
different to various library consumers:</p>
<ul>
<li>you use <code>-lfoo</code> argument when you invoke a linker to link your
program against the <em>foo</em> library;</li>
<li>during that building step, the linker searches for <em>libfoo.so</em> file
(in a set of known directories);</li>
<li>while the source code of the library could live across many files of
arbitrary names, a build step that produces the library itself, puts
the result in <em>libfoo.so.x.y.z</em> file.</li>
<li>when you invoke your program, the dynamic linker searches for
<em>libfoo.so.x</em> file (in a set of known directories).</li>
</ul>
<p>The last name, <em>libfoo.so.x</em>, e.g., <code>libfoo.so.3</code>, is called the
<em>soname</em>, &amp; contains the major version number. Thus, the infamous
<em>soname bump</em> means an increment of that number, due to incompatible
changes in ABI. As soon as it happens, you're required to rebuild your
program. On certain occasions you may cheat, symlinking <code>libfoo.so.3</code>
to a new <code>libfoo.so.4</code>, but no user does that, your application just
stops working after an OS update.</p>
<p>Here is an example for <code>giflib</code> package on Fedora. As usual, to
irritate newcomers, the library is split between 2 packages:</p>
<pre><code>$ rpm -ql giflib giflib-devel | grep .so | xargs stat -c %N
'/usr/lib64/libgif.so.7' -&gt; 'libgif.so.7.1.0'
'/usr/lib64/libgif.so.7.1.0'
'/usr/lib64/libgif.so' -&gt; 'libgif.so.7'

$ objdump -p /usr/lib64/libgif.so.7.1.0 | grep -i soname
  SONAME               libgif.so.7
</code></pre>
<p>The program ld.so (the dynamic linker, at the time of writing
<code>/lib64/ld-linux-x86-64.so.2</code>) looks for <code>libgif.so.7</code>. A linker,
during a build step, uses <code>libgif.so</code> (no version number). The latter
file is absent in the user-faced <code>giflib</code> package.</p>
<h3>Living on the edge</h3>
<p>If you listen to proponents of assorted form of containerisation long
enough, you may think that soname bumps (or other events of ruinous
nature) happen every week. In reality, even rather old versions of
fairly complex programs like Google Chrome, despite being designed to
be updated daily, run ~fine on current Linux.</p>
<p>E.g., I fished out a .deb variant of Chrome 71 (Dec of 2018), unpacked
it &amp; successfully ran<a class="footnote" href="#qAp2Gu-1"><sup>1</sup></a> the ancient browser on Fedora 44. It
was linked against GTK3 (modern Chrome doesn't use GTK), a toolkit
that any distro will continue to ship for at least 20 years.</p>
<p>Not all applications are that lucky, though. If you have a program
compiled during the forgotten v1.0.x OpenSSL years, chances that a
regular user would have skills to find (or more precisely, bother to
look for, unless extremely motivated) a compatible .so file is
practically nil.</p>
<p>Here comes a point, where 2 schools of thought appear:</p>
<ol>
<li><p>You rely on dependencies target distros provide, rebuilding the
program when the time comes. The movement attracts too few
adherents nowadays, &amp; we won't talk about it, for this
philosophical tradition is considered passé in high society.</p>
</li>
<li><p>You protect yourself from the disappearance of old shared libraries
from the repos by shipping the most fragile of them with the
program. How do you know which ones should be brought in? You
don't, &amp; resort to guessing.</p>
</li>
</ol>
<h3>AppImage</h3>
<p>In the Windows world you can put necessary DLLs in the same directory
with the executable. In Linux, by default, the dynamic linker searches
for .so files exclusively in a predefined set of directories,
reconfigurable only by a superuser.</p>
<p>A temporal (meaning, it has a long life) fix for changing the
directory lookup is to invoke ld.so manually, or set <code>LD_LIBRARY_PATH</code>
env variable. This requires a tiny shell wrapper for a program
executable, &amp; this is what every developer starts with when he thinks
about the chilling <em>software distribution on Linux</em>.</p>
<p>What do you do after putting everything under 1 directory? In the
distant past, folks would create a .tar.gz file with a content like
so:</p>
<pre><code>foobar/ # many files in this directory
foobar.sh
README
</code></pre>
<p>The user would unpack the archive, ignore the readme, &amp; run <code>foobar.sh</code>.</p>
<p>By today's standards this is regarded as too confusing. First of all,
what is this .tar.gz, a Japanese nesting doll? Second, a user vainly
tries to start <code>foobar.sh</code> from within a GUI archiver app.</p>
<p>In 2010s, there was a popular feeling that "something has to be done"
about motley tarballs, hence a bunch of solutions appeared. One of the
few survivors is AppImage, even though back in ~2016, when it hit the
Linux scene hard, many decided it was <a href="https://news.ycombinator.com/item?id=11187622">"unnecessarily complicated"</a>.</p>
<p>I don't find it complicated at all. To show how it works, we'll ①
write a hello world program that acquires the word "World" from a
shared library, ② make an "appimage" from the program.</p>
<p>Our source code:</p>
<pre><code>$ cat libfoo.c
char* greeting() { return "World"; }
$ cat app.c
#include &lt;stdio.h&gt;
char* greeting();
int main() { printf("Hello, %s!\n", greeting()); }
</code></pre>
<p>We <a href="https://sigwait.org/~alex/blog/2026/06/13/appimage-shared-lib/app.mk">compile it</a> like so:</p>
<pre><code>cc -fPIC   -c -o libfoo.o libfoo.c
cc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 libfoo.o
ln -sfn libfoo.so.1.0.0 libfoo.so.1
ln -sfn libfoo.so.1 libfoo.so
cc   -L. -lfoo  app.c   -o app
</code></pre>
<p>The app consists of an executable &amp; a shared library with soname
<code>libfoo.so.1</code>:</p>
<pre><code>$ stat -c %N *.so* app
'libfoo.so' -&gt; 'libfoo.so.1'
'libfoo.so.1' -&gt; 'libfoo.so.1.0.0'
'libfoo.so.1.0.0'
'app'
</code></pre>
<p>If we run it as is, it predictably fails:</p>
<pre><code>$ ./app
./app: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
</code></pre>
<p>So we write a wrapper:</p>
<pre><code>$ cat AppRun
#!/bin/sh

dir=$(dirname "$(readlink -f "$0")")
export LD_LIBRARY_PATH=$dir
exec "$dir/app" "$@"
</code></pre>
<p>An appimage is a squashfs image with a prepended "runtime". The latter
is a <a href="https://github.com/AppImage/type2-runtime/releases">static ELF executable</a> that mounts its payload (the squashfs
image) &amp; runs a program with a hard-coded name "AppRun". At no point
root is required.</p>
<p>You make a ready-to-go runnable "appimage" in 3 steps:</p>
<ol>
<li><p>Create a squashfs image <code>app.sqsh</code>:</p>
<pre><code> $ mksquashfs app AppRun libfoo.so.* app.sqsh -quiet -no-progress
 $ unsquashfs -l app.sqsh
 squashfs-root
 squashfs-root/AppRun
 squashfs-root/app
 squashfs-root/libfoo.so.1
 squashfs-root/libfoo.so.1.0.0
</code></pre>
</li>
<li><p>Catenate the "runtime" with it:</p>
<pre><code> $ cat type2-runtime app.sqsh &gt; app.appimage
</code></pre>
</li>
<li><p>Add the executable bit:</p>
<pre><code> $ chmod +x app.appimage
</code></pre>
</li>
</ol>
<p>That's it.</p>
<pre><code>$ ./app.appimage
Hello, World!
</code></pre>
<p>Obviously, the .appimage file extension isn't strictly required. You
can use .exe if you're so maliciously inclined.</p>
<p>GUI archivers won't show the content of an appimage, therefore
bewildered users cannot start <code>AppRun</code> from within the archiver.</p>
<p>The general disadvantages of the approach are:</p>
<ul>
<li>a downloaded .appimage from the web won't have an executable bit;</li>
<li>it requires a suid <code>fusermount</code> utility on the host;</li>
<li>there is no requirements for sandboxing of any kind: by downloading
an .appimage you can't know without extracting its payload whether
the author thought about one;</li>
<li>no protection from missing dependencies: if you didn't put a
necessary library inside the image, you're in the same boat with
the suppliers of a mere tarball.</li>
</ul>
<p>Still, for me it's the je ne sais quoi that makes the AppImage format
charming. The construct is so simple to implement, that you can
replace the static runtime binary with a 22-lines shell script:</p>
<pre><code>$ cat type2-lunch.sh
#!/bin/sh

set -e
self=`readlink -f "$0"`
offset=$(grep -abo -m1 "$(printf 'hsqs\002')" "$self" | cut -d: -f1)

[ "$offset" ] || { echo No squashfs image attached 1&gt;&amp;2; exit 1; }
[ "$PRINT_OFFSET" ] &amp;&amp; { echo "$offset"; exit 0; }

tmp=`mktemp -d /tmp/appimage.XXXXXX`

clean() {
    set +e
    fusermount -u "$tmp"
    rmdir "$tmp"
}

trap clean 0 1 2 15

squashfuse_ll -o offset="$offset" "$self" "$tmp"
"$tmp"/AppRun "$@"
exit $?
</code></pre>
<p>It looks for a magic number position &amp; uses it as an offset when
mounting an image. The step #2 from above thus can be replaced as:</p>
<pre><code>$ cat type2-lunch.sh app.sqsh &gt; app.appimage
</code></pre>
<h2>Not a container</h2>
<p>Unfortunately, DLLs are not the only dependencies you'll
encounter. GTK programs, for example, won't function properly or even
start without compiled GSettings schemas, MIME database caches, icon
sets, &amp; so on.</p>
<p>This is where the proponents of containerisation smile very widely: you
can copy beloved .so files all day long, but when any of them have a
hard-coded path to <code>/lib64/gdk-pixbuf-2.0/2.10.0/loaders.cache</code> file,
all your backbreaking work becomes useless if a target host lacks one.</p>
<p>Without resorting to heavyweight Docker-like hammers, you can employ
<em>mount namespaces</em>, a feature that has been in Linux &gt; 20 years, but
only gained momentum among regular folks since the rise of
Flatpak. One of the core components of the latter is a sandboxing
utility called <a href="https://manpages.debian.org/unstable/bubblewrap/bwrap.1.en.html">bwrap</a>. From a user perspective, it does a glorified
chroot(2), but without the need of superuser privileges. bwrap is
shipped by default in all desktop variations of 3 major distros<a class="footnote" href="#qAp2Gu-2"><sup>2</sup></a>. You don't need to
touch or rely on Flatpak to use it.</p>
<p>If we <a href="https://sigwait.org/~alex/blog/2026/06/13/appimage-shared-lib/bwrap.mk">make a minimal directory tree</a>
for the hello world program above (this includes the dynamic linker
too)</p>
<pre><code>container
├── bin -&gt; usr/bin
├── lib64 -&gt; usr/lib64
└── usr
    ├── bin
    │&nbsp;&nbsp; └── app
    └── lib64
        ├── ld-linux-x86-64.so.2
        ├── libc.so.6
        ├── libfoo.so -&gt; libfoo.so.1
        ├── libfoo.so.1 -&gt; libfoo.so.1.0.0
        └── libfoo.so.1.0.0
</code></pre>
<p>then we can run our "app" executable without additional wrappers that
export <code>LD_LIBRARY_PATH</code>:</p>
<pre><code>$ bwrap --bind container / /bin/app
Hello, World!
</code></pre>
<p>How far you can go with that? To prove to myself that this works not
only for toy programs, I employed Fedora's dnf repos to fetch all the
dependencies for Celluloid (an mpv frontend) in such a way, that a
resulting "guest" included the necessary libraries for hardware video
decoding &amp; pipewire/pulseaudio/alsa communication with the host.</p>
<p>It worked, although the size of such a "guest" failed to inspire:</p>
<pre><code>$ du -hs container/
993M    container/
</code></pre>
<p>If you zip it, it contracts to 358 MB. How does this compare to a
Flatpak version?</p>
<img src="https://sigwait.org/~alex/blog/2026/06/13/flathub-celluloid.png" alt="flathub screenshot" style="display: block; max-width: 34em; width: 100%; margin: 0 auto;">

<p>Although the advantage of Flatpak here is in decoupling of what they
call "runtime" from an application itself, I'm not a huge fan of it
for the <a href="https://lwn.net/Articles/1020571/">amount of bloat</a> it brings
into a Linux desktop mess.</p>
<p>Anyhow, the fake container approach could be a solution if you don't
care about disk space. The <a href="https://github.com/gromnitsky/not-a-container">github example</a> with which I did the mpv
experiment, contains several spec examples for more lightweight
programs.</p>
<p>For the sake of pedantry, I also tried the 2026 celluloid "container"
on a Debian 9.0 (2017) VM with its 4.9.0 kernel. I had to compile
bwrap myself &amp; set sysctl <code>kernel.unprivileged_userns_clone</code> to 1, but
the thing worked flawlessly. No rational user, of course, will ever do
that for any application, but it's the principle that counts.</p>
<hr>

<ol>
<li id="qAp2Gu-1"><pre>$ f=google-chrome-stable_71.0.3578.98-1_amd64.deb
$ sha1sum $f | awk '{print $1}'
a4bffb66d9fe055a9baab366d4dd94c96ce47d24
$ dpkg-deb -x $f .
$ opt/*/*/google-chrome --no-sandbox --user-data-dir=`pwd`/1</pre>
</li>
<li id="qAp2Gu-2">Ubuntu, Fedora, Debian.</li>
</ol>
]]></description>
  
</item>

<item>
  <title>Making Debian or Fedora persistent live images</title>
  <link>https://sigwait.org/~alex/blog/2026/05/28/smdBC8.html</link>
  <guid>https://sigwait.org/~alex/blog/2026/05/28/smdBC8.html</guid>
  <pubDate>Sun, 31 May 2026 16:51:45 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>ойті</category>
  
  <description><![CDATA[<p>When you download a 'live' ISO, dd it to a USB drive, you notice that
all your tweaks or installed packages vanish after a reboot. If you
think about how most such 'live' ISOs work, it becomes apparent
why:</p>
<pre><code>$ parted -s Fedora-Xfce-Live-44-1.7.x86_64.iso print free | grep '^[PN ]'
Partition Table: gpt
Number  Start   End     Size    File system  Name       Flags
 1      32.8kB  2897MB  2897MB               ISO9660    hidden, msftdata
 2      2897MB  2929MB  31.5MB  fat16        Appended2  boot, esp
        2929MB  2929MB  512B    Free Space
</code></pre>
<p>ISO9660 is a read-only filesystem, &amp; the fact it was written onto a
writable medium is irrelevant: its fs driver contains no
implementation for writing data blocks, &amp; the Linux VFS layer immediately
returns <code>EROFS</code> (code 30, <em>Read-only file system</em>) when it sees that a
fs was mounted read-only.</p>
<p>A common workaround is to use OverlayFS; in the case of 'live' ISOs, to
do an overlay with a chunk of RAM.</p>
<p>Obviously, you can do an overlay with a filesystem that supports write
operations instead, like ext4, but inside the Live ISO there isn't
one, &amp; hence there is nothing to do an overlay with.</p>
<p>While you can always create an ext4 partition manually, how do you tell
the 'live' OS to use it during boot? This distro corner has no
standardisation whatsoever, &amp; everybody is doing it in their own
unique way. E.g., Debian &amp; Ubuntu have diverged so much throughout the
years that even the kernel parameters for their 'persistence'
implementations differ. While it may seem logical to an impartial
spectator to keep at least the user-facing interface the same between the
distros, it's not how it is done in practice.</p>
<h3>Ubuntu</h3>
<ul>
<li>Kernel parameter: "persistent".</li>
<li>An (empty) partition must have the label "casper-rw".</li>
</ul>
<p>What is annoying is that it's surprisingly non-obvious to detect
whether such a trick worked: if your partition is <code>/dev/sda4</code>, &amp;
Ubuntu does <em>not</em> show it as mounted, &amp; <code>/cow</code> is roughly the size of
<code>/dev/sda4</code>:</p>
<pre><code>$ df -h | grep cow
/cow           9.8G  161M  9.2G   2% /
</code></pre>
<p>then persistence is <em>on</em>. If, on the other hand, you see this:</p>
<pre><code>$ df -h | grep casper
/dev/disk/by-label/casper-rw  9.8G  161M  9.2G   2% /var/log
</code></pre>
<p>you most likely mistyped the word <em>persistent</em>.</p>
<p>The next issue is how to save grub parameters in the .iso. As it's
absolutely useless to mount it to modify files, you can either extract
everything from the .iso, edit what you want in <code>grub.cfg</code>, &amp; recreate
the image, or, alternatively, do a simple 12-byte to 12-byte swap:</p>
<pre><code>$ export LANG=C
$ sed -i 's/quiet splash/persistent  /' xubuntu-26.04-desktop-amd64.iso
</code></pre>
<p>It's amusingly hacky, but works. If your replacement string is not equal
in length to the pattern, you'll corrupt the ISO9660 filesystem, &amp;
grub will refuse to boot the kernel.</p>
<p>(See <a href="https://github.com/gromnitsky/mk-debian-live-iso-persistent">a github sample</a> for a script that does all this; it assumes a
Linux host &amp; injects an ext4 partition into a copy of an .iso. You can
always resize the partition (&amp; its filesystem) in real time using the
Disks utility that the .iso ships with.)</p>
<h3>Debian</h3>
<ul>
<li>Kernel parameter: "persistence".</li>
<li>A partition must have:<ul>
<li>the label "persistence";</li>
<li>a file named <code>persistence.conf</code> in the root of the partition with
a line akin to "/ union".</li>
</ul>
</li>
</ul>
<p>Notice that it was "persisten<strong>t</strong>" for Ubuntu, but it's
"persisten<strong>ce</strong>" for Debian. Why not.</p>
<p>The same mechanism of rude byte swapping in the .iso applies here
too:</p>
<pre><code>$ export LANG=C
$ sed -i 's/splash quiet/persistence /' debian-live-13.5.0-amd64-xfce.iso
</code></pre>
<p>Detecting a successful overlay is easier:</p>
<pre><code>$ mount | grep sda3
/dev/sda3 on /run/live/persistence/sda3 type ext4 (rw,noatime)
overlay on / type overlay (rw,noatime,lowerdir=/run/live/rootfs/filesystem.squashfs/,upperdir=/run/live/persistence/sda3/rw,workdir=/run/live/persistence/sda3/work,redirect_dir=on)
</code></pre>
<h3>Fedora</h3>
<ul>
<li>Kernel parameters: "<code>selinux=0 rd.live.overlay=LABEL=foo:/bar</code>".</li>
<li>A partition must have:<ul>
<li>a label "foo" (choose whatever you want, but it must correspond to
the value in the kernel parameter);</li>
<li>a "bar" directory (again, see the kernel parameter);</li>
<li>an "ovlwork" directory (this is a hardcoded name).</li>
</ul>
</li>
</ul>
<p>To check:</p>
<pre><code>$ df -h | grep sdb1
/dev/sdb1        9.8G  134M  9.1G   2% /run/initramfs/overlayfs
$ mount | grep Live
LiveOS_rootfs on / type overlay (rw,relatime,lowerdir=/run/rootfsbase,upperdir=/run/overlayfs,workdir=/run/ovlwork)
$ file /run/overlayfs
/run/overlayfs: symbolic link to /run/initramfs/overlayfs/bar
</code></pre>
<p>In the case of Fedora, this is all mostly useless. Its 'linux' loader
command in <code>grub.cfg</code> menu entries contains no space to sacrifice for
a different 40-byte-long string. You, of course, can delete one menu
entry completely &amp; substitute it with your own, but this would be
rather fragile: if, in the next version of Fedora, the size of
<code>grub.cfg</code> changes, your script will corrupt the underlying ISO9660
filesystem.</p>
<p>If the only reliable way here is to extract the rootfs from the .iso
to edit it, why bother with overlays then? This is what Fedora Live
mounts during boot:</p>
<pre><code>$ isoinfo -i Fedora-Xfce-Live-44-1.7.x86_64.iso -Jf | grep -i liveos/
/LiveOS/squashfs.img
</code></pre>
<p>Despite its name, it's a 2.6GB EROFS image file (<a href="https://lore.kernel.org/lkml/863d726d-e919-9ee1-56e4-994c8ab09f4b@huawei.com/">the name is a
pun</a> on a generic EROFS error code).</p>
<p>The image contains everything, including the kernel &amp; initramfs. We
can just create 2 image files:</p>
<ol>
<li>a FAT32 one to hold <code>EFI/BOOT/BOOTX64.EFI</code>, alongside the kernel &amp;
initramfs;</li>
<li>an ext4 one with a label, say "Fedora-Live", into which we extract
the contents of <code>squashfs.img</code>.</li>
</ol>
<p>The ext4 partition can be of any length, &amp; our 'live' Fedora image
will have space to hold user files without any shenanigans with
overlays.</p>
<p>After creating these 2 images, we combine them into 1 (with a GPT
layout), &amp; dd it onto a USB drive.</p>
<p><code>grub.cfg</code> can be as short as:</p>
<pre><code>set timeout=3
menuentry "Fedora Live" {
  linux /vmlinuz rd.live.image root=LABEL=Fedora-Live rw noresume
  initrd /initramfs
}
</code></pre>
<p><code>rd.live.image</code> parameter is required for systemd to start
<a href="https://pagure.io/livesys-scripts">livesys</a> service, otherwise, no
<code>liveuser</code> will be created.</p>
<p>The mechanism works for any official Fedora
<a href="https://fedoraproject.org/torrents/">spin</a>.</p>
<p>See <a href="https://github.com/gromnitsky/mk-fedora-live-image-persistent">another github sample</a> for a script that does all this. For a
quick test in QEMU, you'll need to specify a UEFI bios:</p>
<pre><code>$ sudo ./mflip 10G Fedora-Xfce-Live-44-1.7.x86_64.iso out.img

$ alias qemu3d='qemu-kvm -machine q35 \
   -bios /usr/share/OVMF/OVMF_CODE.fd -m 4G \
   -display gtk,gl=on -smp 2 \
   -device virtio-vga-gl,hostmem=2G,blob=true,venus=true'

$ qemu3d out.img
</code></pre>
]]></description>
  
</item>

<item>
  <title>Obfuscating Image Links</title>
  <link>https://sigwait.org/~alex/blog/2025/12/07/qYkza8.html</link>
  <guid>https://sigwait.org/~alex/blog/2025/12/07/qYkza8.html</guid>
  <pubDate>Sun, 07 Dec 2025 14:19:47 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>ойті</category>
  
  <description><![CDATA[<p>I noticed this recently, though it started happening about a year ago.
On some websites (archive.org's bookreader), a normal <code>&lt;img&gt;</code> tag
suddenly began to look like some insane MS Internet Explorer extension
from 1998:</p>
<pre><code>&lt;img src="blob:https://example.com/b501e863-fe43-4b63-ae5d-dac14cac097e"&gt;
</code></pre>
<p>The web page that contains it renders the image fine, but when a
fairly naïve user posts that link into a chat, it results in
nothing: the blob referenced by <code>&lt;img&gt;</code> exists only in the memory of a
specific browser instance, &amp; the server will return 404 for
any <code>https://example.com/UUID</code>.</p>
<p>Why do they do this? Every <em>n</em> years some people get scared of
hotlinking (<em>bastards keep stealing our traffic</em>), AI luddites try to
ruin business of evil corporations, &amp; fans of toy-level DRM amuse
themselves with a new scheme.</p>
<p>If you look at the <code>fetch</code>-requests of such a page, you'll see
resources that look like images but actually aren't:</p>
<pre>$ url='<a href="https://ia800206.us.archive.org/BookReader/BookReaderPreview.php?id=parkinsonlaw00park&amp;subPrefix=parkinsonlaw00park&amp;itemPath=/31/items/parkinsonlaw00park&amp;server=ia800206.us.archive.org&amp;page=leaf8&amp;scale=1">https://ia800206.us.archive.org/🙈.jpg</a>'

$ curl -sI "$url" | grep -e type -e length -e obfuscate
content-type: image/jpeg
content-length: 267470
x-obfuscate: 1|uoEV6/PZOWtGhOZdVM898w==

$ curl -s "$url" | head -c25 | file -
/dev/stdin: data
</pre>

<p>Such a .jpg is encrypted. Part of the key is in the <code>X-Obfuscate</code>
header, but neither a random AI scraper nor any social network knows
about this. The cipher is also not disclosed, &amp; every website may use
any scheme it wants: following any recommendations would defeat the
entire purpose of obfuscation.</p>
<p>The image-rendering algorithm then becomes:</p>
<ol>
<li><p>download the encrypted file;</p>
</li>
<li><p>decrypt it using the key from the corresponding header &amp; push the
result into a blob;</p>
</li>
<li><p>create a link to the blob using <code>URL.createObjectURL</code> function;</p>
</li>
<li><p>inject into the DOM an <code>img</code> element whose <em>src</em> attribute is equal
to the newly created link.</p>
</li>
</ol>
<p>We can increase entropy further by <a href="https://sigwait.org/~alex/demo/misc/img-blob/">writing our own custom element</a>:</p>
<pre><code>&lt;img-blob alt="a fluffy cat" src="cat.bin"&gt;&lt;/img-blob&gt;
</code></pre>
<p>that will do all of the above on its own. (I <em>terser</em>'ed the source
code of the example for I absolutely don't want you to use it in
anything serious: the entire approach is extremely user-hostile &amp;
anti-web.)</p>
<p>For efficiency, archive.org AES-CTR-encrypts only the first 1024 bytes
of the image. Browsers know about AES but strictly require a secure
context that can be annoying during testing; hence, for mickey mouse
DRM we can simply use XOR-encryption.</p>
<p>The <code>X-Obfuscate</code> header itself can be obfuscated even more, e.g.:</p>
<pre><code>x-obfuscate: rlW2MKWmnJ9hVwbtZFjtVzgyrFV6VPVkZwZ0AFW9Pt==
</code></pre>
<p>looks like a base64 string, but:</p>
<pre><code>$ echo rlW2MKWmnJ9hVwbtZFjtVzgyrFV6VPVkZwZ0AFW9Pt== | base64 -d | xxd
base64: invalid input
00000000: ae55 b630 a5a6 9c9f 6157 06ed 6458 ed57  .U.0....aW..dX.W
00000010: 3832 ac55 7a54 f564 6706 7400 55bd 3e    82.UzT.dg.t.U.&gt;
</code></pre>
<p>I checked if DeepSeek could figure it out: it spent 9 minutes &amp; left
2 villages in Zhejiang province without water, was several times
very close to the target, but ultimately failed.</p>
<p>The string had been post-processed with rot13:</p>
<pre><code>$ alias rot13="tr 'A-Za-z' 'N-ZA-Mn-za-m'"
$ echo rlW2MKWmnJ9hVwbtZFjtVzgyrFV6VPVkZwZ0AFW9Pt== | rot13 | base64 -d
{"version": 1, "key": "12345"}
</code></pre>
<p>As homework, you can add an equivalent of <code>loading="lazy"</code> to the
custom element using the Intersection Observer API.</p>
]]></description>
  
</item>

<item>
  <title>Offline Math: Converting LaTeX to SVG with MathJax</title>
  <link>https://sigwait.org/~alex/blog/2025/10/07/3t8acq.html</link>
  <guid>https://sigwait.org/~alex/blog/2025/10/07/3t8acq.html</guid>
  <pubDate>Wed, 08 Oct 2025 14:10:31 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>ойті</category>
  
  <description><![CDATA[<p>Pandoc can <em>prepare</em> LaTeX math for MathJax via its eponymous
<code>--mathjax</code> option. It wraps formulas in <code>&lt;span class="math"&gt;</code>
elements and injects a <code>&lt;script&gt;</code> tag that points to
<em>cdn.jsdelivr.net</em>, which means rendering won't work offline or in
case of the 3rd-party server failure. You can mitigate this by
providing your own copy of the MathJax library, but the mechanism
still fails when the target device doesn't support JavaScript (e.g.,
many epub readers).</p>
<p>At the same time, practically all browsers support MathML. Use it
(pandoc's <code>--mathml</code> option), if you care only about the information
superhighway: your formulas will look good on every modern device and
scale delightfully. Otherwise, SVGs are the only truly portable
option.</p>
<p>Now, how can we transform the html produced by</p>
<pre><code>$ echo 'Ohm'\''s law: $I = \frac{V}{R}$.' |
  pandoc -s -f markdown --mathjax
</code></pre>
<p>into a fully standalone document where the formula gets converted into
SVG nodes?</p>
<ol>
<li>Use an html parser like Nokogiri, and replace each <code>&lt;span class="math"&gt;</code> node with an image. There are multiple ways to
convert a TeX-looking string to an SVG: using MathJax itself (which
provides a <a href="https://github.com/mathjax/MathJax-demos-node/blob/master/mjs/simple/tex2svg">corresponding CLI example</a>), or by doing it in a
'classical' fashion with pdflatex. (You can read more about this
method in <a href="https://sigwait.org/~alex/p/pg2e/"><em>A practical guide to EPUB</em></a>, chapters 3.4 and 4.6.)</li>
</ol>
<ol start="2">
<li>Alternatively, load the page into a headless browser, inject
MathJax scripts, and serialise the modified DOM back to html.</li>
</ol>
<p>I tried the 2nd approach in 2016 with the now-defunct phantomjs.  It
worked, but debugging was far from enjoyable due to the strangest bugs
in phantomjs. I can still run the old code, but it depends on an
ancient version of the MathJax library that, for obvious reasons,
isn't easily upgradable within the phantomjs pre-es6 environment.</p>
<p>Nowadays, Puppeteer would certainly do, but for this kind of task
I prefer something more lightweight.</p>
<p>There's also jsdom. Back in 2016, I tried it as well, but it was much
slower than running phantomjs. Recently, I gave jsdom another try and
was pleasantly surprised. I'm not sure what exactly tipped the scales:
computers, v8, or jsdom itself, but it no longer feels slow in
combination with MathJax.</p>
<pre><code>$ wc -l *js *conf.json
  24 loader.js
 105 mathjax-embed.js
  12 mathjax.conf.json
 141 total
</code></pre>
<p>Roughly 50% of the code is nodejs infrastructure junk (including CL
parsing), the rest is a MathJax config and jsdom interactions:</p>
<pre><code>let dom = new JSDOM(html, {
  url: `file://${base}/`,
  runScripts: /* very */ 'dangerously',
  resources: new MyResourceLoader(), // block ext. absolute urls
})

dom.window.my_exit = function() {
  cleanup(dom.window.document) // remove mathjax &lt;script&gt; tags
  console.log(dom.serialize())
}

dom.window.my_mathjax_conf = mathjax_conf // user-provided

let script = new Script(read(`${import.meta.dirname}/loader.js`))
let vmContext = dom.getInternalVMContext()
script.runInContext(vmContext)
</code></pre>
<p>The most annoying step here is setting <code>url</code> property that jsdom uses
to resolve paths to relative resources. <code>my_exit()</code> function is called
by MathJax when its job is supposedly finished. <code>loader.js</code> script is
executed in the context of the loaded html:</p>
<pre><code>window.MathJax = {
  output: { fontPath: '@mathjax/%%FONT%%-font' },
  startup: {
    ready() {
      MathJax.startup.defaultReady()
      MathJax.startup.promise.then(window.my_exit)
    }
  }
}

Object.assign(window.MathJax, window.my_mathjax_conf)

function main() {
  var script = document.createElement('script')
  script.src = 'mathjax/startup.js'
  document.head.appendChild(script)
}

document.addEventListener('DOMContentLoaded', main)
</code></pre>
<p>The full source is on <a href="https://github.com/gromnitsky/mathjax-embed">Github</a>.</p>
<p>Intended use is as follows:</p>
<pre><code>$ echo 'Ohm'\''s law: $I = \frac{V}{R}$.' |
  pandoc -s -f markdown --mathjax |
  mathjax-embed &gt; 1.html
</code></pre>
<p>The resulting html doesn't use JavaScript and doesn't fetch any
external MathJax resources. <code>mathjax-embed</code> script itself always works
offline.</p>
]]></description>
  
</item>

<item>
  <title>The Size of Adobe Reader Installers Through The Years</title>
  <link>https://sigwait.org/~alex/blog/2025/08/25/zw6z4E.html</link>
  <guid>https://sigwait.org/~alex/blog/2025/08/25/zw6z4E.html</guid>
  <pubDate>Tue, 26 Aug 2025 01:33:40 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>ойті</category>
  
  <description><![CDATA[<p>At the time of writing, the most recent Adobe Reader 25.x.y.z 64-bit
installer for Windows 11 weights <strong>687,230,424 bytes</strong>. After
installation, the program includes 'AI' (of course), an auto-updater,
sprinkled ads for <em>Acrobat online services</em> everywhere, and 2 GUIs:
'new' and 'old'.</p>
<p>For comparison, the size of SumatraPDF-3.5.2 installer is 8,246,744
bytes. It has no 'AI', no auto-updater (though it can check for new
versions, which I find unnecessary, for anyone sane would install it
via scoop anyway), and no ads for 'cloud storage'.</p>
<p>The following chart shows how the Adobe Reader installer has grown in
size over the years. When possible, 64-bit versions of installers were
used.</p>
<img style="width: 100%" src="https://sigwait.org/~alex/blog/2025/08/25/adobe_reader-vs-sumatrapdf.svg" alt="adobe reader vs sumatrapdf">

<p><strong>Next Day Update:</strong></p>
<p>Best comment on Hacker News: <em>"Looks like a chart crime scene."</em></p>
<p>Alright, here's your linear graph, along with <a href="https://sigwait.org/~alex/blog/2025/08/25/installers.tar">the
source</a> from which both graphs were generated. All
point labels are version numbers.</p>
<img style="width: 100%" src="https://sigwait.org/~alex/blog/2025/08/25/adobe_reader-vs-sumatrapdf.linear.svg" alt="adobe reader vs sumatrapdf (linear scale)">
]]></description>
  
</item>

<item>
  <title>Not a bug</title>
  <link>https://sigwait.org/~alex/blog/2025/08/15/9pQ3o1.html</link>
  <guid>https://sigwait.org/~alex/blog/2025/08/15/9pQ3o1.html</guid>
  <pubDate>Mon, 25 Aug 2025 08:39:06 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>ойті</category>
  
  <description><![CDATA[<p>Peter Weinberger (the "w" in awk), while working at Bell Labs,
wrote an experimental
implementation of a network file system. Included with Research Unix
v8 (Feb 1985, licensed strictly for educational use), it allowed to
share / (yes) with other machines running v8 by specifying a mapping
between a local uid/gui and the desired view from the LAN.</p>
<p>Weinberger described peculiarities of his <em>netfs</em> as</p>
<blockquote>
<p>"If A mounted B's file system somewhere, and B mounted A's, then the
directory tree was infinite. That's mathematics, not a bug."</p>
</blockquote>
<p>His /usr/src/netfs/TODO contained an existential question:</p>
<blockquote>
<p>'why does it get out of synch?'</p>
</blockquote>
<p>The connection of this <em>netfs</em> and Sun's NFS is murky.</p>
<p>Steve Johnson:</p>
<blockquote>
<p>"I remember Bill Joy visiting Bell Labs and getting a very complete
demo of RFS and being very impressed.  Within a year, Sun announced
NFS."</p>
</blockquote>
<p>Unix System V SVR3, released by AT&amp;T in 1987, included a different
version of <em>netfs</em>, which they officially began calling RFS. Appearing
18 months after Sun announced NFS, it briefly attempted to compete,
but failed on 2 fronts simultaneously: ⓐ big vendors (Dec, IBM, HP)
disliked its licensing terms, and ⓑ the protocol's brittleness
discouraged ports to non-Unix systems. NFS won, becoming widely
used--even by NeXTSTEP.</p>
<p>Lyndon Nerenberg:</p>
<blockquote>
<p>'We ran RFS on a "cluster" of four 3B2s [AT&amp;T microcomputers], and
while it worked, to varying degrees, the statefulness of the
protocol inevitably led to the whole thing locking up, requiring a
reboot of all four machines to recover.'</p>
</blockquote>
]]></description>
  
</item>

<item>
  <title>Batocera 35 and Vontar X3</title>
  <link>https://sigwait.org/~alex/blog/2025/07/06/Gih6An.html</link>
  <guid>https://sigwait.org/~alex/blog/2025/07/06/Gih6An.html</guid>
  <pubDate>Mon, 07 Jul 2025 13:32:04 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>untagged</category>
  
  <description><![CDATA[<p>In the dining room there is an old (2013) 1080p telly with an old (2020)
Android TV box connected to it. The TV box contains an ancient
<a href="https://www.cnx-software.com/2019/04/12/amlogic-s905x3-specifications-block-diagram/">Amlogic S905X3</a> SoC inside. It has just enough power to play
Youtube &amp; 1080p movies from a network drive but not much else.</p>
<p>Some time ago I heard about repurposing this particular model (Vontar
X3) as a retro-gaming console, but anticipating battles similar to
those with openwrt-on-outers--where 2 devices with the exact same name
have slightly different hardware revisions (&amp; as a result, nothing
works as expected)--I've been putting off the adventure.</p>
<p>The easiest Linux gaming distro to deploy is a French one called
Batocera.<a class="footnote" href="#Gih6An-1"><sup>1</sup></a> Its
wiki describes perfomance of a particular device in terms of console
generation support:</p>
<table>
<thead>
<tr>
<th>Gén</th>
<th>Consoles</th>
</tr>
</thead>
<tbody><tr>
<td>3</td>
<td>NES</td>
</tr>
<tr>
<td>4</td>
<td>SNES, Sega Mega Drive</td>
</tr>
<tr>
<td>5</td>
<td>PlayStation (psx), PlayStation Portable (psp)</td>
</tr>
</tbody></table>
<p>(I've skipped the irrelevant generations.)</p>
<p>In my tests, while the modest S905X3 runs most psx &amp; psp games
acceptably, some titles have such a perceived frame drop (that do not
occur on a desktop PC running the same emulator as Batocera) that it
makes them unplayable. The prominent unsuccessful examples are <em>CTR:
Crash Team Racing</em> (psx) and <em>MotorStorm: Arctic Edge</em> (psp).</p>
<p>The last Batocera version for the Vontar X3 is 35 (the OS images for
this device haven't been updated since '22). I dd'ed
<a href="https://mirrors.o2switch.fr/batocera/tvbox-gen3/stable/35/">batocera-s905gen3-tvbox-gen3-35-20220910.img</a> onto a 32GB SD card,
inserted the card into the TV box, pressed its reset button with a
toothpick, plugged in the power cable, &amp; saw this:</p>
<p><video preload="metadata" controls="" src="https://sigwait.org/~alex/blog/2025/07/06/interlaced.mp4"></video></p>
<p>The TV's info popups indicated that the resolution of this shaky image
was 1080i (interlaced?) instead of the expected 1080p. I then tried 2
completely different (albeit much newer) TVs, as well as a capture
card--none of them had any problems negotiating a proper resolution
with Batocera.</p>
<p>After pointlessly suffering with various kernel parameters, I ended up
with the following kludge to disable the interlaced mode:</p>
<ol>
<li><p>Connect the device to your LAN via the ethernet port. Batocera runs
Avahi, hence we can just say</p>
<pre><code> $ ssh root@batocera.local
</code></pre>
<p>(The password is 'linux'.)</p>
</li>
<li><p>Run</p>
<pre><code> # batocera-resolution listModes | head -10
 max-1920x1080:maximum 1920x1080
 max-640x480:maximum 640x480
 0.0.1920x1080.60:HDMIA 1920x1080 60Hz (1920x1080i)
 0.1.1920x1080.60:HDMIA 1920x1080 60Hz (1920x1080)
 0.2.1920x1080.60:HDMIA 1920x1080 60Hz (1920x1080)
 0.3.1920x1080.60:HDMIA 1920x1080 60Hz (1920x1080i)
 0.4.1920x1080.50:HDMIA 1920x1080 50Hz (1920x1080)
 0.5.1920x1080.50:HDMIA 1920x1080 50Hz (1920x1080i)
 0.6.1920x1080.24:HDMIA 1920x1080 24Hz (1920x1080)
 0.7.1920x1080.24:HDMIA 1920x1080 24Hz (1920x1080)
</code></pre>
<p> inside Batocera. Take a note of a mode you'd like to see.</p>
</li>
<li><p>Turn the device off. Extract the SD card out of it &amp; insert it into
a PC. The card has 2 partition: the 1st one is fat32 that has
<code>batocera-boot.conf</code> file. Add a line to it:</p>
<pre><code> es.resolution=0.1.1920x1080.60
</code></pre>
</li>
</ol>
<p>The picture will still incessantly jerk from left to right, but only
during the boot phase:</p>
<p><video preload="metadata" controls="" src="https://sigwait.org/~alex/blog/2025/07/06/fixed.mp4"></video></p>
<pre><code># batocera-info
Disk format: ext4
Temperature: 66°C
Architecture: tvbox-gen3
Model: Shenzhen Haochuangyi Technology Co., Ltd H96 Max
System: Linux 5.10.134
Available memory: 624/932 MB
Cpu model: ARMv8 Processor rev 0 (v8l)
Cpu number: 4
Cpu max frequency: 1908 MHz
</code></pre>
<p>There is little to add here. You copy your .nes/.sfc/.chd/.iso files
to <code>/userdata/roms/{nes,snes,psx,psp}</code> either directly onto the 2nd
partition of the SD card, or via ssh, or even smb, for Batocera runs
Samba.</p>
<hr>

<ol>

<li id="Gih6An-1">I couldn't find any guidance on how to pronouce it:
there is a type of beetles called /bə'tosərə/, but some youtubers say
it as /bαto'sɛrα/.</li>

</ol>
]]></description>
  
</item>

<item>
  <title>Sea-sickness</title>
  <link>https://sigwait.org/~alex/blog/2025/05/19/lzBnKb.html</link>
  <guid>https://sigwait.org/~alex/blog/2025/05/19/lzBnKb.html</guid>
  <pubDate>Mon, 19 May 2025 18:45:46 GMT</pubDate>
  
    <author>alexander.gromnitsky@gmail.com (ag)</author>
  
  
    <category>quote</category>
  
    <category>usa</category>
  
  <description><![CDATA[<blockquote>
<p>'[1852] In the regiment there was a Lieutenant Slaughter who was
very liable to sea-sickness. It almost made him sick to see the wave
of a table-cloth when the servants were spreading it.</p>
<p>'Soon after his graduation, Slaughter was ordered to California and
took passage by a sailing vessel going around Cape Horn. The vessel
was seven months making the voyage, and Slaughter was sick every
moment of the time, never more so than while lying at anchor after
reaching his place of destination.</p>
<p>'On landing in California he found orders which had come by the
Isthmus, notifying him of a mistake in his assignment; he should
have been ordered to the northern lakes. He started back by the
Isthmus route and was sick all the way. But when he arrived at the
East he was again ordered to California, this time definitely, and
at this date was making his third trip. He was as sick as ever, and
had been so for more than a month while lying at anchor in the bay.</p>
<p>'I remember him well, seated with his elbows on the table in front
of him, his chin between his hands, and looking the picture of
despair. At last he broke out, "I wish I had taken my father's
advice; he wanted me to go into the navy; if I had done so, I should
not have had to go to sea so much." Poor Slaughter! it was his last
sea voyage. He was killed by Indians in Oregon.'</p>
</blockquote>
<p>(From <em>Personal Memoirs of U.S. Grant</em>, Ch. XIV by Ulysses S. Grant.)</p>
]]></description>
  
</item>


</channel>
</rss>