<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>Planet GNOME</title>
    <link>https://planet.gnome.org/atom.xml</link>
    <description>Planet GNOME - https://planet.gnome.org/</description>
    <atom:link href="https://planet.gnome.org/atom.xml" rel="self"/>
    <docs>http://www.rssboard.org/rss-specification</docs>
    <generator>python-feedgen</generator>
    <language>en</language>
    <lastBuildDate>Wed, 13 May 2026 18:17:42 +0000</lastBuildDate>
    <item>
      <title>Colin Walters: LLMs and core software: human driven</title>
      <link>https://blog.verbum.org/2026/03/18/llms-and-core-software-human-driven/</link>
      <description>&lt;div class="wp-block-jetpack-markdown"&gt;&lt;p&gt;It’s clear LLMs are one of the biggest changes in technology ever. The rate
of progress is astounding: recently due to a configuration mistake
I accidentally used Claude Sonnet 3.5 (released ~2 years ago)
instead of Opus 4.6 for a task and looked at the output and thought “what is
this garbage”?&lt;/p&gt;
&lt;p&gt;But daily now: Opus 4.6 is able to generate reasonable PoC level Rust
code for complex tasks for me. It’s not perfect – it’s a combination
of exhausting and exhilarating to find the 10% absolutely bonkers/broken
code that still makes it past subagents.&lt;/p&gt;
&lt;p&gt;So yes I use LLMs every day, but I will be clear: if I could push a button
to “un-invent” them I &lt;em&gt;absolutely&lt;/em&gt; would because I think the long term
issues in larger society (not being able to trust any media, and many
of the things from &lt;a href="https://www.darioamodei.com/essay/the-adolescence-of-technology"&gt;Dario’s recent blog&lt;/a&gt; etc.)
will outweigh the benefits.&lt;/p&gt;
&lt;p&gt;But since we can’t un-invent them: here’s my opinion on how they should be
used. As a baseline, I agree with a lot from &lt;a href="https://rfd.shared.oxide.computer/rfd/0576"&gt;this doc from Oxide about LLMs&lt;/a&gt;.
What I want to talk about is especially around some of the norms/tools
that I see as important for LLM use, following principles similar to those.&lt;/p&gt;
&lt;p&gt;On framing: there’s “core” software vs “bespoke”. An entirely new
capability of course is for e.g. a nontechnical restaurant owner to
use an LLM to generate (“vibe code”) a website (excepting hopefully online
orderings and payments!). I’m not overly concerned about this.&lt;/p&gt;
&lt;p&gt;Whereas “core” software is what organizations/businesses provide/maintain
for others. I work for a company (Red Hat) that produces a lot of this.
I am sure no one would want to run for real an operating system, cluster filesystem,
web browser, monitoring system etc. that was primarily “vibe coded”.&lt;/p&gt;
&lt;p&gt;And while I respect people and groups that are trying to entirely ban LLM
use, I don’t think that’s viable for at least my space.&lt;/p&gt;
&lt;p&gt;Hence the subject of this blog is my perspective on how LLMs should be used
for “core” software: not vibe coding, but using LLMs responsibly and
intelligently – and always under human control and review.&lt;/p&gt;
&lt;h2&gt;Agents should amplify and be controlled by humans&lt;/h2&gt;
&lt;p&gt;I think most of the industry would agree we can’t give responsibility
to LLMs. That means they must be overseen by humans. If they’re
overseen by a human, then I think they should be &lt;em&gt;amplifying&lt;/em&gt;
what that human thinks/does as a baseline – intersected with
the constraints of the task of course.&lt;/p&gt;
&lt;p&gt;On “amplification”: Everyone using a LLM to generate content should inject their own
system prompt (e.g. &lt;a href="https://agents.md/"&gt;AGENTS.md&lt;/a&gt;) or equivalent.
&lt;a href="https://github.com/cgwalters/homegit/blob/main/dotfiles/.config/AGENTS.md"&gt;Here’s mine&lt;/a&gt; – notice
I turn off all the emoji etc. and try hard to tune down bulleted lists
because that’s not my style. This is a truly baseline thing to do.&lt;/p&gt;
&lt;p&gt;Now most LLM generated content targeted for core software is
still going to need review, but just ensuring that the baseline
matches what the human does helps ensure alignment.&lt;/p&gt;
&lt;h2&gt;Pull request reviews&lt;/h2&gt;
&lt;p&gt;Let’s focus on a very classic problem: pull request reviews. Many
projects have wired up a flow such that when a PR comes in,
it gets reviewed by a model automatically. Many projects and
tools pitch this. We use one on some of my projects.&lt;/p&gt;
&lt;p&gt;But I want to get away from this because in my experience these
reviews are a combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extremely insightful and correct things (there’s some amazing
fine-tuning and tool use that must have happened to find some
issues pointed out by some of these)&lt;/li&gt;
&lt;li&gt;Annoying nitpicks that no one cares about (not handling spaces
in a filename in a shell script used for tests)&lt;/li&gt;
&lt;li&gt;Broken stuff like getting confused by things that happened after its training cutoff
(e.g. Gemini especially seems to get confused by referencing
the current date, and also is unaware of newer Rust features, etc)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, we just want the first of course.&lt;/p&gt;
&lt;p&gt;How I think it should work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A pull request comes in&lt;/li&gt;
&lt;li&gt;It gets auto-assigned to a human on the team for review&lt;/li&gt;
&lt;li&gt;A human contributing to that project is running their own agents
(wherever: could be local or in the cloud) &lt;em&gt;using their own configuration&lt;/em&gt; (but of course
still honoring the project’s default development setup and the
project’s AGENTS.md etc)&lt;/li&gt;
&lt;li&gt;A new containerized/sandboxed agent may be spawned automatically,
or perhaps the human needs to click a button to do so – or
perhaps the human sees the PR come in and thinks “this one needs
a deeper review, didn’t we hit a perf issue with the database before?”
and adds that to a prompt for the agent.&lt;/li&gt;
&lt;li&gt;The agent prepares a &lt;em&gt;draft&lt;/em&gt; review that only the human can see.&lt;/li&gt;
&lt;li&gt;The human reviews/edits the draft PR review, and has the opportunity
to remove confabulations, add their own content etc. And to send the agent back to look more closely
at some code (i.e. this part can be a loop)&lt;/li&gt;
&lt;li&gt;When the human is happy they click the “submit review” button.&lt;/li&gt;
&lt;li&gt;Goal: it is 100% clear what parts are LLM generated vs human generated for the reader.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wrote &lt;a href="https://github.com/bootc-dev/agent-skills/tree/main/perform-forge-review"&gt;this agent skill&lt;/a&gt;
to try to make this work well, and if you search you can see it in action
in a few places, though I haven’t truly tried to scale this up.&lt;/p&gt;
&lt;p&gt;I think the above matches the vision of LLMs amplifying humans.&lt;/p&gt;
&lt;h2&gt;Code Generation&lt;/h2&gt;
&lt;p&gt;There’s no doubt that LLMs can be amazing code generators, and I use
them every day for that. But for any “core” software I work on,
I absolutely review all of the output – not just superficially,
and changes to core algorithms very closely.&lt;/p&gt;
&lt;p&gt;At least in my experience the reality is still there’s that percentage
of the time when the agent decided to reimplement base64 encoding
for no reason, or disable the tests claiming “the environment didn’t support it”
etc.&lt;/p&gt;
&lt;p&gt;And to me it’s still a baseline for “core” software to require
another human review to merge (per above!) with their own customized
LLM assisting them (ideally a different model, etc).&lt;/p&gt;
&lt;h2&gt;FOSS vs closed&lt;/h2&gt;
&lt;p&gt;Of course, my position here is biased a bit by working on FOSS – I
still very much believe in that, and working in a FOSS context can
be quite different than working in a “closed environment” where
a company/organization may reasonably want to (and be able to)
apply uniform rules across a codebase.&lt;/p&gt;
&lt;p&gt;While for sure LLMs &lt;em&gt;allow&lt;/em&gt; organizations to create their own
Linux kernel filesystems or bespoke Kubernetes forks or virtual
machine runtime or whatever – it’s not clear to me that it
is a good idea for most to do so. I think shared (FOSS) infrastructure
that is productized by various companies, provided as a service
and maintained by human experts in that problem domain still makes sense.
And how we develop that matters a lot.&lt;/p&gt;
&lt;/div&gt;</description>
      <guid isPermaLink="false">https://blog.verbum.org/2026/03/18/llms-and-core-software-human-driven/</guid>
      <pubDate>Wed, 18 Mar 2026 20:17:23 +0000</pubDate>
    </item>
    <item>
      <title>Sam Thursfield: Status update, 21st March 2026</title>
      <link>https://samthursfield.wordpress.com/2026/03/21/status-update-21st-march-2026/</link>
      <description>&lt;p class="wp-block-paragraph"&gt;Hello there,&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;If you’re an avid reader of blogs, you’ll know this medium is basically dead now. Everyone switched to  making YouTube videos, complete with cuts and costume changes every few seconds because, I guess, our brains work much faster now. &lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;The YouTube recommendation algorithm, problematic as it is, does turn up some interesting stuff, such this video entitled &lt;a href="https://www.youtube.com/watch?v=f7iloI8V6Z8"&gt;“Why Work is Starting to Look Medieval”&lt;/a&gt;:&lt;/p&gt;
&lt;figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"&gt;&lt;div class="wp-block-embed__wrapper"&gt;
&lt;div class="jetpack-video-wrapper"&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p class="wp-block-paragraph"&gt;It is 15 minutes long, but it does include lots of short snippets and some snipping scissors, so maybe you’ll find it a fun 15 minutes. The key point, I guess, is that before we were wage slaves we used to be craftspeople, more deeply connected to our work and with a sense of purpose. The industrial revolution marked a shift from cottage industry, where craftspeople worked with their own tools in their own house or workshop, to modern capitalism where the owners of the tools are the 1%, and the rest of us are reduced to selling our labour at whatever is the going rate.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;Then she posits that, since the invention of the personal computer, influencers and independent content creators have begun to transcend the structures of 20th century capitalism, and are returning to a more traditional relationship with work. Hence, perhaps, why nearly everyone under 18 wants to be a YouTuber. Maybe that’s a stretch.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;This message resonated with &lt;em&gt;me&lt;/em&gt; after 20 years in the open source software world, and hopefully you can see the link. Software development is a craft. And the Free Software movement has always been in tacit opposition to capitalism, with its implied message that anyone working on a computer should have some &lt;em&gt;ownership&lt;/em&gt; of the software tools we use: let me use it, let me improve it, and let me share it.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I’ve read many many takes on AI-generated code this year, and its really only March. I’m guilty one of these myself: &lt;a href="https://samthursfield.wordpress.com/2026/01/24/ai-predictions-for-2026/"&gt;AI Predictions for 2026&lt;/a&gt;, in which I made a link between endless immersion in LLM-driven coding and more traditional drug addictions that has now been corroborated by Steve Yegge himself. See his update &lt;a href="https://steve-yegge.medium.com/the-ai-vampire-eda6e4f07163"&gt;“The AI Vampire”&lt;/a&gt; (which is also something of a critique of capitalism).&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I’ve read several takes that the Free Software movement has won now because it is much easier to understand, share and modify programs than ever before. See, for example, &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7436143626042339328/"&gt;this one from Bruce Perens on Linquedin&lt;/a&gt;:&lt;em&gt; “The advent of AI and its capability to create software quickly, with human guidance, means that we can probably have almost anything we want as Free Software.”&lt;/em&gt;.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I’ve also seen takes that, in fact, the capitalism has won. Such as the (fictional) &lt;a href="https://malus.sh/"&gt;MALUSCorp&lt;/a&gt;: &lt;em&gt;“Our proprietary AI robots independently recreate any open source project from scratch. The result? &lt;strong&gt;Legally distinct code&lt;/strong&gt; with corporate-friendly licensing. No attribution. No copyleft. No problems.”.&lt;/em&gt;&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;One take I haven’t seen is what this means for people who love the &lt;em&gt;craft&lt;/em&gt; of building software. Software is a craft, and our tools are the operating system and the compiler. Programmers working on open source, where code serves as reference material and can live in the open for decades, will show much more pride in their code than programmers in academia and industry, whose prototypes or products just need to get the job done. The programmer is a craftsperson, just like the seamstress, the luthier and the blacksmith. But unlike clothes, guitars and horseshoes, the stuff we build is intangible. Perhaps as a result, society sees us less like craftspeople and more like weird, unpopular wizards.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;figure class="wp-block-image size-large is-resized"&gt;&lt;a href="https://samthursfield.wordpress.com/wp-content/uploads/2026/03/image.png"&gt;&lt;img alt="" class="wp-image-3506" height="768" src="https://samthursfield.wordpress.com/wp-content/uploads/2026/03/image.png?w=768" style="width: 377px; height: auto;" width="768"/&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;p class="wp-block-paragraph"&gt;I’ve spent a lot of my career building and testing open source operating systems, as you can see from these &lt;a href="https://samthursfield.wordpress.com/category/build-and-test-tools/"&gt;30 different blog posts&lt;/a&gt;, which include the blockbuster &lt;a href="https://samthursfield.wordpress.com/2015/10/20/some-cmake-tips/"&gt;“Some CMake Tips”&lt;/a&gt;, the satisfying &lt;a href="https://samthursfield.wordpress.com/2017/05/22/tracker-%f0%9f%92%99-meson/"&gt;“Tracker &lt;img alt="💙" class="wp-smiley" src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f499.png" style="height: 1em;"/&gt; Meson”&lt;/a&gt;, and or largely obsolete &lt;a href="https://samthursfield.wordpress.com/2018/01/29/how-buildstream-uses-ostree/"&gt;“How BuildStream uses OSTree”&lt;/a&gt;.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;It’s really not that I have some deep-seated desire to rewrite all of the world’s Makefiles. My interest in operating systems and build tools has always came from a desire to &lt;em&gt;democratize&lt;/em&gt; these here computers. To free us from being locked into fixed ways of working designed by Apple, Google, Microsoft. Open source tools are great, yes, but I’m more interested in whether someone can access the full power of their computer without needing a university education. This is why I’ve found GNOME interesting over the years: it’s accessible to non-wizards, and the code is right there in the open, for anyone to change. That said, I’ve always wished we GNOME focus more on &lt;em&gt;customizability&lt;/em&gt;, and I don’t mean adding more preferences. Look, here’s me &lt;em&gt;in 2009&lt;/em&gt; discovering &lt;a href="https://samthursfield.wordpress.com/2009/10/31/nix/"&gt;Nix for the first time&lt;/a&gt; and jumping straight to this: &lt;em&gt;“So Nix could give us beautiful support for testing and hacking on bits of GNOME”&lt;/em&gt;.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;So what happened? Plenty has changed, but I feel that hacking on bits of GNOME hasn’t become meaningfully easier in the intervening 17 years. And perhaps we can put that largely down to the tech industry’s relentless drive to sell us new computers, and our own hunger to do everything faster and better. In the 1980s, an operating system could reasonably get away with running only one program at a time. In the 1990s, you had multitasking but there was still just the one CPU, at least in my PC. I don’t think there was any point in the 2000s when I owned a GPU. In the 2010s, my monitor was small enough that I never worried about fractional scaling. And so on. For every one person working to simplify, there are a hundred more paid to innovate. &lt;a href="https://terriblesoftware.org/2026/03/03/nobody-gets-promoted-for-simplicity/"&gt;Nobody gets promoted for simplicity&lt;/a&gt;.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I can see a steadily growing interest in tech from people who aren’t necessarily interested in programming. If you’re not tired of videos yet, here’s a harp player discussing the firmware of a digital guitar pedal (cleverly titled &lt;a href="https://www.youtube.com/watch?v=WRcm2b877ek"&gt;“What pedal makers don’t want you to see”&lt;/a&gt;). Here’s another musician discussing STM32 chips and mesh networks under the title &lt;a href="https://youtu.be/W_F4rEaRduk?si=xVtsqy2dAOvviqwr"&gt;“Gadgets For People Who Don’t Trust The Government”&lt;/a&gt;. This one does &lt;em&gt;not &lt;/em&gt;have costume changes every few seconds.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;So we’re at an inflection point.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;The billions pumped into the AI bubble come from a desire by rich men to take back control of computing. It’s a feature, not a bug, that you can’t run ChatGPT on a consumer GPU, and that AI companies &lt;a href="https://www.mooreslawisdead.com/post/sam-altman-s-dirty-dram-deal"&gt;need absolutely all of the DRAM&lt;/a&gt;. They could spend that money on a programme like &lt;a href="https://www.outreachy.org/blog/2024-08-14/outreachy-needs-your-help/"&gt;Outreachy&lt;/a&gt;, supporting people to learn and understand today’s software tools … but you don’t consolidate power through education. (The book Careless People, which I &lt;a href="https://samthursfield.wordpress.com/2025/05/18/book-club-2025-edition/"&gt;recommend last year&lt;/a&gt;, will show you how much tech CEOs crave raw power).&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;In another sense, AI models are a new kind of operating system, exposing the capabilities of a GPU in a radical new interface. The computer now contains a facility that can translate instructions in your native language into any well-known programming language. (Just &lt;a href="https://esolang-bench.vercel.app/"&gt;don’t ask it to generate Whitespace&lt;/a&gt;). By now you must know &lt;em&gt;someone&lt;/em&gt; non-technical who has nevertheless automated parts of their job away by prompting ChatGPT to generate Excel macros. This &lt;em&gt;is &lt;/em&gt;the future we were aiming for, guys! &lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I’m no longer sure if the craft I care about is writing software, or getting computers to do things, or both. And I’m really not sure what this craft is going to look like in 10 or 20 years. What topics will be universally understood, what work will be open to individual craftspeople, and what tools will be available only to states and mega-corporations?  Will basic computer tools be universally available and understood, like knives and saucepans in a kitchen? Will they require small scale investment and training, like a microbrewery? Or will the whole world come to depend on a few enourmous facilities in China?&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;And most importantly, will I be able to share my passion for software without feeling like a weird, unpopular wizard any time soon?&lt;/p&gt;</description>
      <guid isPermaLink="false">https://samthursfield.wordpress.com/2026/03/21/status-update-21st-march-2026/</guid>
      <pubDate>Sat, 21 Mar 2026 18:29:27 +0000</pubDate>
    </item>
    <item>
      <title>Matthew Garrett: SSH certificates and git signing</title>
      <link>https://codon.org.uk/~mjg59/blog/p/ssh-certificates-and-git-signing/</link>
      <description>&lt;p&gt;When you’re looking at source code it can be helpful to have some evidence
indicating who wrote it. Author tags give a surface level indication, &lt;a class="link" href="https://github.com/torvalds/linux/commit/ac632c504d0b881d7cfb44e3fdde3ec30eb548d9" rel="noopener" target="_blank"&gt;but
it turns out you can just
lie&lt;/a&gt;
and if someone isn’t paying attention when merging stuff there’s certainly a
risk that a commit could be merged with an author field that doesn’t
represent reality. Account compromise can make this even worse - a PR being
opened by a compromised user is going to be hard to distinguish from the
authentic user. In a world where supply chain security is an increasing
concern, it’s easy to understand why people would want more evidence that
code was actually written by the person it’s attributed to.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://git-scm.com/" rel="noopener" target="_blank"&gt;git&lt;/a&gt; has support for cryptographically signing
commits and tags. Because git is about choice even if Linux isn’t, you can
do this signing with OpenPGP keys, X.509 certificates, or SSH keys. You’re
probably going to be unsurprised about my feelings around OpenPGP and the
web of trust, and X.509 certificates are an absolute nightmare. That leaves
SSH keys, but bare cryptographic keys aren’t terribly helpful in isolation -
you need some way to make a determination about which keys you trust. If
you’re using someting like &lt;a class="link" href="https://github.com" rel="noopener" target="_blank"&gt;GitHub&lt;/a&gt; you can extract that
information from the set of keys associated with a user account&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://codon.org.uk/~mjg59/blog/index.xml#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;, but
that means that a compromised GitHub account is now also a way to alter the
set of trusted keys and also when was the last time you audited your keys
and how certain are you that every trusted key there is still 100% under
your control?  Surely there’s a better way.&lt;/p&gt;
&lt;h2 id="ssh-certificates"&gt;SSH Certificates
&lt;/h2&gt;&lt;p&gt;And, thankfully, there is. &lt;a class="link" href="https://openssh.com" rel="noopener" target="_blank"&gt;OpenSSH&lt;/a&gt; supports
certificates, an SSH public key that’s been signed by some trusted party and
so now you can assert that it’s trustworthy in some form. SSH Certificates
also contain metadata in the form of Principals, a list of identities that
the trusted party included in the certificate. These might simply be
usernames, but they might also provide information about group
membership. There’s also, unsurprisingly, native support in SSH for
forwarding them (using the agent forwarding protocol), so you can keep your
keys on your local system, ssh into your actual dev system, and have access
to them without any additional complexity.&lt;/p&gt;
&lt;p&gt;And, wonderfully, you can use them in git! Let’s find out how.&lt;/p&gt;
&lt;h2 id="local-config"&gt;Local config
&lt;/h2&gt;&lt;p&gt;There’s two main parameters you need to set. First,&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre class="chroma" tabindex="0"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git config set gpg.format ssh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;because unfortunately for historical reasons all the git signing config is
under the &lt;code&gt;gpg&lt;/code&gt; namespace even if you’re not using OpenPGP. Yes, this makes
me sad. But you’re also going to need something else. Either
&lt;code&gt;user.signingkey&lt;/code&gt; needs to be set to the path of your certificate, or you
need to set &lt;code&gt;gpg.ssh.defaultKeyCommand&lt;/code&gt; to a command that will talk to an
SSH agent and find the certificate for you (this can be helpful if it’s
stored on a smartcard or something rather than on disk). Thankfully for you,
I’ve &lt;a class="link" href="https://gitlab.com/mjg59/find-ssh-certificate" rel="noopener" target="_blank"&gt;written one&lt;/a&gt;. It will
talk to an SSH agent (either whatever’s pointed at by the &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt;
environment variable or with the &lt;code&gt;-agent&lt;/code&gt; argument), find a certificate
signed with the key provided with the &lt;code&gt;-ca&lt;/code&gt; argument, and then pass that
back to git. Now you can simply pass &lt;code&gt;-S&lt;/code&gt; to &lt;code&gt;git commit&lt;/code&gt; and various other
commands, and you’ll have a signature.&lt;/p&gt;
&lt;h2 id="validating-signatures"&gt;Validating signatures
&lt;/h2&gt;&lt;p&gt;This is a bit more annoying. Using native git tooling ends up calling out to
&lt;code&gt;ssh-keygen&lt;/code&gt;&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://codon.org.uk/~mjg59/blog/index.xml#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;, which validates signatures against a file in a format
that looks somewhat like &lt;code&gt;authorized-keys&lt;/code&gt;. This lets you add something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre class="chroma" tabindex="0"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* cert-authority ssh-rsa AAAA…
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;which will match all principals (the wildcard) and succeed if the signature
is made with a certificate that’s signed by the key following
cert-authority. I recommend you don’t read the &lt;a class="link" href="https://github.com/git/git/blob/ca1db8a0f7dc0dbea892e99f5b37c5fe5861be71/gpg-interface.c#L461" rel="noopener" target="_blank"&gt;code that does this in
git&lt;/a&gt;
because I made that mistake myself, but it does work. Unfortunately it
doesn’t provide a lot of granularity around things like “Does the
certificate need to be valid at this specific time” and “Should the user
only be able to modify specific files” and that kind of thing, but also if
you’re using GitHub or GitLab you wouldn’t need to do this at all because
they’ll just do this magically and put a “verified” tag against anything
with a valid signature, right?&lt;/p&gt;
&lt;p&gt;Haha. No.&lt;/p&gt;
&lt;p&gt;Unfortunately while both GitHub and GitLab support using SSH certificates
for authentication (so a user can’t push to a repo unless they have a
certificate signed by the configured CA), there’s currently no way to say
“Trust all commits with an SSH certificate signed by this CA”. I am unclear
on why. So, I &lt;a class="link" href="https://gitlab.com/mjg59/validate-git-signatures" rel="noopener" target="_blank"&gt;wrote my
own&lt;/a&gt;. It takes a range of
commits, and verifies that each one is signed with either a certificate
signed by the key in &lt;code&gt;CA_PUB_KEY&lt;/code&gt; or (optionally) an OpenPGP key provided in
&lt;code&gt;ALLOWED_PGP_KEYS&lt;/code&gt;. Why OpenPGP? Because even if you sign all of your own
commits with an SSH certificate, anyone using the API or web interface will
end up with their commits signed by an OpenPGP key, and if you want to have
those commits validate you’ll need to handle that.&lt;/p&gt;
&lt;p&gt;In any case, this should be easy enough to integrate into whatever CI
pipeline you have. This is currently very much a proof of concept and I
wouldn’t recommend deploying it anywhere, but I am interested in merging
support for additional policy around things like expiry dates or group
membership.&lt;/p&gt;
&lt;h2 id="doing-it-in-hardware"&gt;Doing it in hardware
&lt;/h2&gt;&lt;p&gt;Of course, certificates don’t buy you any additional security if an attacker
is able to steal your private key material - they can steal the certificate
at the same time. This can be avoided on almost all modern hardware by
storing the private key in a separate cryptographic coprocessor - a &lt;a class="link" href="https://en.wikipedia.org/wiki/Trusted_Platform_Module" rel="noopener" target="_blank"&gt;Trusted
Platform Module&lt;/a&gt; on
PCs, or the &lt;a class="link" href="https://support.apple.com/guide/security/the-secure-enclave-sec59b0b31ff/web" rel="noopener" target="_blank"&gt;Secure
Enclave&lt;/a&gt;
on Macs. If you’re on a Mac then &lt;a class="link" href="https://secretive.dev/" rel="noopener" target="_blank"&gt;Secretive&lt;/a&gt; has
been around for some time, but things are a little harder on Windows and
Linux - there’s various things you can do with
&lt;a class="link" href="https://en.wikipedia.org/wiki/PKCS_11" rel="noopener" target="_blank"&gt;PKCS#11&lt;/a&gt; but you’ll hate yourself
even more than you’ll hate me for suggesting it in the first place, and
there’s &lt;a class="link" href="https://github.com/Foxboron/ssh-tpm-agent" rel="noopener" target="_blank"&gt;ssh-tpm-agent&lt;/a&gt; except
it’s Linux only and quite tied to Linux.&lt;/p&gt;
&lt;p&gt;So, obviously, I wrote &lt;a class="link" href="https://gitlab.com/mjg59/attestation-tpm-agent" rel="noopener" target="_blank"&gt;my
own&lt;/a&gt;. This makes use of the
&lt;a class="link" href="https://github.com/google/go-attestation" rel="noopener" target="_blank"&gt;go-attestation&lt;/a&gt; library my team
at Google wrote, and is able to generate TPM-backed keys and export them
over the SSH agent protocol. It’s also able to proxy requests back to an
existing agent, so you can just have it take care of your TPM-backed keys
and continue using your existing agent for everything else. In theory it
should also work on Windows&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://codon.org.uk/~mjg59/blog/index.xml#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt; but this is all in preparation for a
&lt;a class="link" href="https://sched.co/2E1g3" rel="noopener" target="_blank"&gt;talk&lt;/a&gt; I only found out I was giving about two weeks
beforehand, so I haven’t actually had time to test anything other than that
it builds.&lt;/p&gt;
&lt;p&gt;And, delightfully, because the agent protocol doesn’t care about where the
keys are actually stored, this still works just fine with forwarding - you
can ssh into a remote system and sign something using a private key that’s
stored in your local TPM or Secure Enclave. Remote use can be as transparent
as local use.&lt;/p&gt;
&lt;h2 id="wait-attestation"&gt;Wait, attestation?
&lt;/h2&gt;&lt;p&gt;Ah yes you may be wondering why I’m using go-attestation and why the term
“attestation” is in my agent’s name. It’s because when I’m generating the
key I’m also generating all the artifacts required to prove that the key was
generated on a particular TPM. I haven’t actually implemented the other end
of that yet, but if implemented this would allow you to verify that a key
was generated in hardware before you issue it with an SSH certificate - and
in an age of agentic bots accidentally exfiltrating whatever they find on
disk, that gives you a lot more confidence that a commit was signed on
hardware you own.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Using SSH certificates for git commit signing is great - the tooling is a
bit rough but otherwise they’re basically better than every other
alternative, and also if you already have infrastructure for issuing SSH
certificates then you can just reuse it&lt;sup id="fnref:4"&gt;&lt;a class="footnote-ref" href="https://codon.org.uk/~mjg59/blog/index.xml#fn:4"&gt;4&lt;/a&gt;&lt;/sup&gt; and everyone wins.&lt;/p&gt;
&lt;div class="footnotes"&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Did you know you can just download people’s SSH pubkeys from github from &lt;code&gt;https://github.com/&amp;lt;username&amp;gt;.keys&lt;/code&gt;? Now you do &lt;a class="footnote-backref" href="https://codon.org.uk/~mjg59/blog/index.xml#fnref:1"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Yes it is somewhat confusing that the &lt;code&gt;keygen&lt;/code&gt; command does things
other than generate keys &lt;a class="footnote-backref" href="https://codon.org.uk/~mjg59/blog/index.xml#fnref:2"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;This is &lt;a class="link" href="https://mjg59.dreamwidth.org/67402.html" rel="noopener" target="_blank"&gt;more difficult than it sounds&lt;/a&gt; &lt;a class="footnote-backref" href="https://codon.org.uk/~mjg59/blog/index.xml#fnref:3"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;And if you don’t, by implementing this you now have infrastructure
for issuing SSH certificates and can use that for SSH authentication as
well. &lt;a class="footnote-backref" href="https://codon.org.uk/~mjg59/blog/index.xml#fnref:4"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description>
      <guid isPermaLink="false">https://codon.org.uk/~mjg59/blog/p/ssh-certificates-and-git-signing/</guid>
      <pubDate>Sat, 21 Mar 2026 19:38:07 +0000</pubDate>
    </item>
    <item>
      <title>Colin Walters: Agent security is just security</title>
      <link>https://blog.verbum.org/2026/03/23/agent-security-is-just-security/</link>
      <description>&lt;div class="wp-block-jetpack-markdown"&gt;&lt;p&gt;Suddenly I have been hearing the term Landlock more in (agent) security
circles. To me this is a bit weird because while &lt;a href="https://landlock.io"&gt;Landlock&lt;/a&gt;
is absolutely a useful Linux security tool, it’s been a bit obscure
and that’s for good reason. It feels to me a lot like the how weird
&lt;a href="https://arxiv.org/html/2412.11385v1"&gt;prevalence of the word delve&lt;/a&gt;
became a clear tipoff that LLMs were the ones writing, not a human.&lt;/p&gt;
&lt;p&gt;Here’s my opinion: &lt;em&gt;Agentic LLM AI security is just security&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;We do not need to reinvent any fundamental technologies for this. Most uses of
agents one hears about provide the ability to execute arbitrary code as a feature.
It’s how OpenCode, Claude Code, Cursor, OpenClaw and many more work.&lt;/p&gt;
&lt;p&gt;Especially let me emphasize since OpenClaw is popular for some reason
right now: You should &lt;em&gt;absolutely not&lt;/em&gt; give any LLM tool blanket read &lt;em&gt;and write&lt;/em&gt;
access to your full user account on your computer. There are many issues with that, but
everyone using an LLM needs to understand just how dangerous
&lt;a href="https://simonwillison.net/tags/prompt-injection/"&gt;prompt injection&lt;/a&gt; can be.
&lt;a href="https://simonwillison.net/2026/Mar/18/"&gt;This post&lt;/a&gt; is just one of many
examples. Even global read access is dangerous because an attacker
could exfiltrate your browser cookies or other files.&lt;/p&gt;
&lt;p&gt;Let’s go back to Landlock – one prominent place I’ve seen it
mentioned is in this project &lt;code&gt;nono.sh&lt;/code&gt; pitches itself as a new sandbox for agents.
It’s not the only one, but indeed it heavily leans on Landlock on Linux.
Let’s dig into &lt;a href="https://alwaysfurther.ai/blog/why-i-built-nono"&gt;this blog post&lt;/a&gt;
from the author. First of all, I’m glad they are working on agentic
security. We both agree: unsandboxed OpenClaw (and other tools!) is a bad idea.&lt;/p&gt;
&lt;p&gt;Here’s where we disagree:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With AI agents, the core issue is access without boundaries. We give agents our full filesystem permissions because that’s how Unix works. We give them network access because they need to call APIs. We give them access to our SSH keys, our cloud credentials, our shell history, our browser cookies – not because they need any of that, but because we haven’t built the tooling to say “you can have this, but not that.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No. We have had usable tooling for “you can have this, but not that”
for well over a decade. Docker kicked off a revolution for a reason:
&lt;code&gt;docker run &amp;lt;app&amp;gt;&lt;/code&gt; is “reasonably completely isolated” from the host system.
Since then of course, there’s many OCI runtime implementations,
from &lt;a href="https://podman.io"&gt;podman&lt;/a&gt; to &lt;a href="https://github.com/apple/container"&gt;apple/container&lt;/a&gt; on MacOS
and more.&lt;/p&gt;
&lt;p&gt;If you want to provide the app some credentials, you can just
use bind mounts to provide them like &lt;code&gt;docker|podman|ctr -v ~/.config/somecred.json:/etc/cred.json:ro&lt;/code&gt;.
Notice there the &lt;code&gt;ro&lt;/code&gt; which makes it readonly. Yes, it’s
that straightforward to have “this but not that”.&lt;/p&gt;
&lt;p&gt;Other tools like &lt;a href="https://flatpak.org"&gt;Flatpak&lt;/a&gt; on Linux
have leveraged Linux kernel namespacing similar to this
to streamline running GUI apps in an isolated way
from the host. For a decade.&lt;/p&gt;
&lt;p&gt;There’s far more sophisticated tooling built on top
of similar container runtimes since then, from
having them transparently backed by virtual machines,
Kubernetes and similar projects are all about running
containers at scale with lots of built up security
knowledge.&lt;/p&gt;
&lt;p&gt;That doesn’t need reinventing. It’s generic workload
technology, and agentic AI is just another workload
from the perspective of kernel/host level isolation.
There absolutely are some new, novel risks and issues
of course: but again the core principle here is
we don’t need to reinvent anything from the kernel level up.&lt;/p&gt;
&lt;p&gt;Security here really needs to start from defaulting
to &lt;em&gt;fully&lt;/em&gt; isolating (from the host and other apps),
and then only allow-listing in what is needed. That’s again how
&lt;code&gt;docker run&lt;/code&gt; worked from the start. Also on this topic,
&lt;a href="https://docs.flatpak.org/en/latest/basic-concepts.html#portals"&gt;Flatpak portals&lt;/a&gt;
are a cool technology for dynamic resource access on a single
host system.&lt;/p&gt;
&lt;p&gt;So why do I think Landlock is obscure? Basically
because &lt;em&gt;most&lt;/em&gt; workloads should already be isolated already
per above, and Landlock has &lt;em&gt;heavy&lt;/em&gt; overlap with the wide
variety of Linux kernel security mechanisms already in
use in containers.&lt;/p&gt;
&lt;p&gt;The primary pitch of Landlock is more for an &lt;em&gt;application&lt;/em&gt; to
further isolate itself – it’s at its best when it’s a &lt;em&gt;complement&lt;/em&gt;
coarse-grained isolation techniques like virtualization or containers.
One way to think of it is that often container runtimes don’t
grant privileges needed for an application to further spawn
its own sub-containers (for kernel attack surface reasons), but
Landlock is absolutely a reasonable thing for an app to use
to e.g. disable networking from a sub-process that doesn’t need
it, etc.&lt;/p&gt;
&lt;p&gt;Of course the challenge is that not every app is easy to run
in a container or virtual machine. Some workloads are most
convenient with that “ambient access” to all of your data
(like an IDE or just a file browser).&lt;/p&gt;
&lt;p&gt;But giving that ambient access by default to agentic AI is a terrible
idea. So don’t do it: use (OCI) containers and allowlist in
what you need.&lt;/p&gt;
&lt;p&gt;(There’s other things nono is doing here that I find
dubious/duplicative; for example I don’t see the need for
a new filesystem snapshotting system when we have both git and OCI)&lt;/p&gt;
&lt;p&gt;But I’m not specifially trying to pick on nono – just in the last
two weeks I had to point out similar problems in &lt;em&gt;two&lt;/em&gt; different projects
I saw go by also pitched for AI security. One used bubblewrap,
but with insufficient sandboxing, and the other was also trying
to use Landlock.&lt;/p&gt;
&lt;p&gt;On the other hand, I do think the credential problem (that nono and others are
trying to address in differnet ways) is somewhat specific
to agentic AI, and likely does need new tooling.
When deploying a typical containerized
app usually one just provisions a few relatively static
credentials. In contrast, developer/user agentic AI is often a lot
more freeform and dynamic, and while it’s hard to
get most apps to leak credentials without completely compromising
it, it’s much easier with agentic AI and prompt injection.
I have thoughts on credentials, and absolutely more work
here is needed.&lt;/p&gt;
&lt;p&gt;It’s great that people want to work on FOSS security, and AI
could certainly use more people thinking about security.
But I don’t think we need “next generation” security here:
we should build on top of the “previous generation”.
I actually use plain separate Unix users for isolation for some things, which
works quite well! Running OpenShell in a &lt;em&gt;secondary&lt;/em&gt; user account
where one only logs into a select few things (i.e. not your email and online banking)
is much more reasonable, although clearly a lot of care is still needed.
Landlock is a fine technology but is just not there as
a &lt;em&gt;replacement&lt;/em&gt; for other sandboxing techniques. So just use
containers and virtual machines because these are proven technologies.
And if you take one message away from this: absolutely don’t wire up an LLM
via OpenShell or a similar tool to your complete digital life with
no sandboxing.&lt;/p&gt;
&lt;/div&gt;</description>
      <guid isPermaLink="false">https://blog.verbum.org/2026/03/23/agent-security-is-just-security/</guid>
      <pubDate>Mon, 23 Mar 2026 13:51:25 +0000</pubDate>
    </item>
    <item>
      <title>Christian Schaller: Using AI to create some hardware tools and bring back the past</title>
      <link>https://blogs.gnome.org/uraeus/2026/03/23/using-ai-to-create-some-hardware-tools-and-bring-back-the-past/</link>
      <description>&lt;p&gt;As I talked about in a couple of blog posts now I been working a lot with AI recently as part of my day to day job at Red Hat, but also spending a lot of evenings and weekend time on this (sorry kids pappa has switched to 1950’s mode for now). One of the things I spent time on is trying to figure out what the limitations of AI models are and what kind of use they can have for Open Source developers.&lt;/p&gt;
&lt;p&gt;One thing to mention before I start talking about some of my concrete efforts is that I more and more come to conclude that AI is an incredible tool to hypercharge someone in their work, but I feel it tend to fall short for fully autonomous systems. In my experiments AI can do things many many times faster than you ordinarily could, talking specifically in the context of coding here which is what is most relevant for those of us in the open source community. &lt;/p&gt;
&lt;p&gt;So one annoyance I had for years as a Linux user is that I get new hardware which has features that are not easily available to me as a Linux user. So I have tried using AI to create such applications for some of my hardware which includes an Elgato Light and a Dell Ultrasharp Webcam.&lt;/p&gt;
&lt;p&gt;I found with AI and this is based on using Google Gemini, Claude Sonnet and Opus and OpenAI codex, they all required me to direct and steer the AI continuously, if I let the AI just work on its own, more often than not it would end up going in circles or diverging from the route it was supposed to go, or taking shortcuts that makes wanted output useless.On the other hand if I kept on top of the AI and intervened and pointed it in the right direction it could put together things for me in very short time spans.&lt;br/&gt;
My projects are also mostly what I would describe as end leaf nodes, the kind of projects that already are 1 person projects in the community for the most part. There are extra considerations when contributing to bigger efforts, and I think a point I seen made by others in the community too is that you need to own the patches you submit, meaning that even if an AI helped your write the patch you still need to ensure that what you submit is in a state where it can be helpful and is merge-able. I know that some people feel that means you need be capable of reviewing the proposed patch and ensuring its clean and nice before submitting it, and I agree that if you expect your patch to get merged that has to be the case. On the other hand I don’t think AI patches are useless even if you are not able to validate them beyond ‘does it fix my issue’. &lt;/p&gt;
&lt;p&gt;My friend and PipeWire maintainer Wim Taymans and I was talking a few years ago about what I described at the time as the problem of ‘bad quality patches’, and this was long before AI generated code was a thing. Wim response to me which I often thought about afterwards was “a bad patch is often a great bug report”. And that would hold true for AI generated patches to. If someone makes a patch using AI, a patch they don’t have the ability to code review themselves, but they test it and it fixes their problem, it might be a good bug report and function as a clearer bug report than just a written description by the user submitting the report. Of course they should be clear in their bug report that they don’t have the skills to review the patch themselves, but that they hope it can be useful as a tool for pinpointing what isn’t working in the current codebase. &lt;/p&gt;
&lt;p&gt;Anyway, let me talk about the projects I made. &lt;strong&gt;They are all found on my personal website &lt;a class="external" href="http:www.linuxrising.org"&gt;Linuxrising.org&lt;/a&gt; a website that I also used AI to update after not having touched the site in years. &lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
&lt;strong&gt;&lt;a class="external" href="https://gitlab.com/cschalle/gnome-shell-elgato-light-controller"&gt;Elgato Light GNOME Shell extension&lt;/a&gt;&lt;/strong&gt;&lt;br/&gt;
&lt;div class="wp-caption alignright" id="attachment_11287" style="width: 310px;"&gt;&lt;img alt="Elgato Light GNOME Shell extension" class="size-medium wp-image-11287" height="230" src="https://blogs.gnome.org/uraeus/files/2025/10/elgatognomeshellextension-300x230.png" width="300"/&gt;&lt;p class="wp-caption-text" id="caption-attachment-11287"&gt;Elgato Light GNOME Shell extension&lt;/p&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;The first project I worked on is a GNOME Shell extension for controlling my &lt;a class="external" href="https://www.elgato.com/us/en/p/key-light?srsltid=AfmBOoqqz6bVkKSzZmI15jnh6qUz2EIL1avuw_KhfK2z383-rOttBt_6"&gt;Elgato Key Wifi Lamp&lt;/a&gt;. The Elgato lamp is basically meant for podcasters and people doing a lot of video calls to be able to easily configure light in their room to make a good recording. The lamp announces itself over mDNS, and thus can be controlled via Avahi. For Windows and Mac the vendor provides software to control their lamp, but unfortunately not for Linux. &lt;/p&gt;
&lt;p&gt;There had been GNOME Shell extensions for controlling the lamp in the past, but they had not been kept up to date and their feature set was quite limited. Anyway, I grabbed one of these old extensions and told Claude to update it for latest version of GNOME. It took a few iterations of testing, but we eventually got there and I had a simple GNOME Shell extension that could turn the lamp off and on and adjust hue and brightness. This was a quite straightforward process because I had code that had been working at some point, it just needed some adjustments to work with current generation of GNOME Shell.&lt;/p&gt;
&lt;p&gt;Once I had the basic version done I decided to take it a bit further and try to recreate the configuration dialog that the windows application offers for the full feature set which took me quite a bit of back and forth with Claude. I found that if I ask Claude to re-implement from a screenshot it recreates the functionality of the user interface first, meaning that it makes sure that if the screenshot has 10 buttons, then you get a GUI with 10 buttons. You then have to iterate both on the UI design, for example telling Claude that I want a dark UI style to match the GNOME Shell, and then I also had to iterate on each bit of functionality in the UI. Like most of the buttons in the UI didn’t really do anything from the start, but when you go back and ask Claude to add specific functionality per button it is usually able to do so.&lt;br/&gt;
&lt;div class="wp-caption alignright" id="attachment_11290" style="width: 277px;"&gt;&lt;img alt="Elgato Light Settings Application" class="size-medium wp-image-11290" height="300" src="https://blogs.gnome.org/uraeus/files/2025/10/elgato-settingsscreen-other-267x300.png" width="267"/&gt;&lt;p class="wp-caption-text" id="caption-attachment-11290"&gt;Elgato Light Settings Application&lt;/p&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;So this was probably a fairly easy thing for the AI because all the functionality of the lamp could be queried over Avahi, there was no ‘secret’ USB registers to be set or things like that.&lt;br/&gt;
Since the application was meant to be part of the GNOME Shell extension I didn’t want to to have any dependency requirements that the Shell extension itself didn’t have, so I asked Claude to make this application in JavaScript and I have to say so far I haven’t seen any major differences in terms of the AIs ability to generate different languages. The application now reproduce most of the functionality of the Windows application. Looking back I think it probably took me a couple of days in total putting this tool together. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://gitlab.com/cschalle/appfordellwebcam"&gt;Dell Ultrasharp Webcam 4K&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class="wp-caption alignright" id="attachment_11292" style="width: 310px;"&gt;&lt;img alt="Dell UltraSharp 4K settings application for Linux" class="size-medium wp-image-11292" height="209" src="https://blogs.gnome.org/uraeus/files/2025/10/dellultrasharplinux-300x209.png" width="300"/&gt;&lt;p class="wp-caption-text" id="caption-attachment-11292"&gt;Dell UltraSharp 4K settings application for Linux&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;The second application on the list is a controller application for my &lt;a class="external" href="https://www.dell.com/en-us/shop/dell-ultrasharp-webcam-wb7022-4k-uhd/apd/319-bbhp/pc-accessories"&gt;Dell UltraSharp Webcam 4K UHD (WB7022)&lt;/a&gt;. This is a high end Webcam I that have been using for a while and it is comparable to something like the Logitech BRIO 4K webcam. It has mostly worked since I got it with the generic UVC driver and I been using it for my Google Meetings and similar, but since there was no native Linux control application I could not easily access a lot of the cameras features. To address this I downloaded the windows application installer and installed it under Windows and then took a bunch of screenshots showcasing all features of the application. I then fed the screenshots into Claude and told it I wanted a GTK+ version for Linux of this application. I originally wanted to have Claude write it in Rust, but after hitting some issues in the PipeWire Rust bindings I decided to just use C instead. &lt;/p&gt;
&lt;p&gt;I took me probably 3-4 days with intermittent work to get this application working and Claude turned out to be really good and digging into Windows binaries and finding things like USB property values. Claude was also able to analyze the screenshots and figure out the features the application needed to have. It was a lot of trial and error writing the application, but one way I was able to automate it was by building a screenshot option into the application, allowing it to programmatically take screenshots of itself. That allowed me to tell Claude to try fixing something and then check the screenshot to see if it worked without me having to interact with the prompt. Also to get the user interface looking nicer, once I had all the functionality in I asked Claude to tweak the user interface to follow the guidelines of the GNOME Human Interface Guidelines, which greatly improved the quality of the UI.&lt;/p&gt;
&lt;p&gt;At this point my application should have almost all the features of the Windows application. Since it is using PipeWire underneath it is also tightly integrated with the PipeWire media graph, allowing you to see it connect and work with your application in PipeWire patchbay applications like Helvum. The remaining features are software features of Dell’s application, like background removal and so on, but I think that if I decided to to implement that it should be as a standalone PipeWire tool that can be used with any camera, and not tied to this specific one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://gitlab.com/cschalle/redhat-planet"&gt;Red Hat Planet&lt;/a&gt;&lt;/strong&gt;&lt;br/&gt;
&lt;div class="wp-caption alignright" id="attachment_11347" style="width: 310px;"&gt;&lt;img alt="Red Hat Vulkan Globe" class="size-medium wp-image-11347" height="272" src="https://blogs.gnome.org/uraeus/files/2026/03/globeshot-300x272.png" width="300"/&gt;&lt;p class="wp-caption-text" id="caption-attachment-11347"&gt;The application shows the worlds Red Hat offices and include links to latest Red Hat news.&lt;/p&gt;&lt;/div&gt;&lt;br/&gt;
The next application on my list is called Red Hat Planet. It is mostly a fun toy, but I made it to partly revisit the &lt;a href="https://blogs.gnome.org/uraeus/2025/07/29/artificial-intelligence-and-the-linux-community/"&gt;Xtraceroute modernisation&lt;/a&gt; I blogged about earlier. So as I mentioned in that blog, Xtraceroute while cute isn’t really very useful IMHO, since the way the modern internet works rarely have your packets jump around the world. Anyway, as people pointed out after I posted about the port is that it wasn’t an actual Vulkan application, it was a GTK+ application using the GTK+ Vulkan backend. The Globe animation itself was all software rendered. &lt;/p&gt;
&lt;p&gt;I decided if I was going to revisit the Vulkan problem I wanted to use a different application idea than traceroute. The idea I had was once again a 3D rendered globe, but this one reading the coordinates of Red Hats global offices from a file and rendering them on the globe. And alongside that provide clickable links to recent Red Hat news items. So once again maybe not the worlds most useful application, but I thought it was a cute idea and hopefully it would allow me to create it using actual Vulkan rendering this time.&lt;/p&gt;
&lt;p&gt;Creating this turned out to be quite the challenge (although it seems to have gotten easier since I started this effort), with Claude Opus 4.6 being more capable at writing Vulkan code than Claude Sonnet, Google Gemini or OpenAI Codex was when I started trying to create this application.&lt;br/&gt;
When I started this project I had to keep extremely close tabs on the AI and what is was doing in order to force it to keep working on this as a Vulkan application, as it kept wanting to simplify with Software rendering or OpenGL and sometimes would start down that route without even asking me. That hasn’t happened more recently, so maybe that was a problem of AI of 5 Months ago.&lt;/p&gt;
&lt;p&gt;I also discovered as part of this that rendering Vulkan inside a GTK4 application is far from trivial and would ideally need the GTK4 developers to create such a widget to get rendering timings and similar correct. It is one of the few times I have had Claude outright say that writing a widget like that was beyond its capabilities (haven’t tried again so I don’t know if I would get the same response today). So I started moving the application to SDL3 first, which worked as I got a spinning globe with red dots on, but came with its own issues, in the sense that SDL is not a UI toolkit as such. So while I got the globe rendered and working the AU struggled badly with the news area when using SDL.&lt;/p&gt;
&lt;p&gt;So I ended up trying to port the application to Qt, which again turned out to be non-trivial in terms of how much time it took with trial and error to get it right. I think in my mind I had a working globe using Vulkan, how hard could it be to move it from SDL3 to Qt, but there was a million rendering issues. In fact I ended up using the Qt Vulkan rendering example as a starting point in the end and then ‘porting’ the globe over bit by bit, testing it for each step, to finally get a working version. The current version is a Vulkan+Qt app and it basically works, although it seems the planet is not spinning correctly on AMD systems at the moment, while it seems to work well on Intel and NVIDIA systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://gitlab.com/cschalle/wmdock"&gt;WMDock&lt;/a&gt;&lt;/strong&gt;&lt;br/&gt;
&lt;div class="wp-caption alignright" id="attachment_11361" style="width: 253px;"&gt;&lt;img alt="WmDock fullscreen with config application" class="size-medium wp-image-11361" height="300" src="https://blogs.gnome.org/uraeus/files/2026/03/wmdockfullscreen-243x300.png" width="243"/&gt;&lt;p class="wp-caption-text" id="caption-attachment-11361"&gt;WmDock fullscreen with config application.&lt;/p&gt;&lt;/div&gt;&lt;br/&gt;
This project came out of a chat with Matthias Clasen over lunch where I mused about if Claude would be able to bring the old Window Maker dockapps to GNOME and Wayland. Turns out the answer is yes although the method of doing so changed as I worked on it. &lt;/p&gt;
&lt;p&gt;My initial thought was for Claude to create a shim that the old dockapps could be compiled against, without any changes. That worked, but then I had a ton of dockapps showing up in things like the alt+tab menu. It also required me to restart my GNOME Shell session all the time as I was testing the extension to house the dockapps. In the end I decided that since a lot of the old dockapps don’t work with modern Linux versions anyway, and thus they would need to be actively ported, I should accept that I ship the dockapps with the tool and port them to work with modern linux technologies. This worked well and is what I currently have in the repo, I think the wildest port was porting the old dockapp webcam app from V4L1 to PipeWire. Although updating the soundcontroller from ESD to PulesAudio was also a generational jump.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://gitlab.com/cschalle/xmms-resuscitated"&gt;XMMS resuscitated&lt;/a&gt;&lt;/strong&gt;&lt;br/&gt;
&lt;div class="wp-caption alignright" id="attachment_11362" style="width: 310px;"&gt;&lt;img alt="XMMS brought back to life" class="size-medium wp-image-11362" height="127" src="https://blogs.gnome.org/uraeus/files/2026/03/xmms-300x127.png" width="300"/&gt;&lt;p class="wp-caption-text" id="caption-attachment-11362"&gt;XMMS brought back to life&lt;/p&gt;&lt;/div&gt;&lt;br/&gt;
So the last effort I did was reviving the old XMMS media player. I had tried asking Claude to do this for Months and it kept failing, but with Opus 4.6 it plowed through it and had something working in a couple of hours, with no input from me beyond kicking it off. This was a big lift,moving it from GTK2 and Esound, to GTK4, GStreamer and PipeWire. One thing I realized is that a challenge with bringing an old app back is that since keeping the themeable UI is a big part of this specific application adding new features is a little kludgy. Anyway I did set it up to be able to use network speakers through PipeWire and also you can import your Spotify playlists and play those, although you need to run the Spotify application in the background to be able to play sound on your local device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://gitlab.com/cschalle/monkey-bubble"&gt;Monkey Bubble&lt;/a&gt;&lt;/strong&gt;&lt;br/&gt;
&lt;img alt="Monkey Bubble game" class="alignright size-medium wp-image-11369" height="251" src="https://blogs.gnome.org/uraeus/files/2026/03/monkeybubble-300x251.png" width="300"/&gt;&lt;br/&gt;
Monkey Bubble was a game created in the heyday of GNOME 2 and while I always thought it was a well made little game it had never been updated to never technologies. So I asked Claude to port it to GTK4 and use GStreamer for audio.This port was fairly straightforward with Claude having little problems with it. I also asked Claude to add highscores using the libmanette library and network game discovery with Avahi. So some nice little.improvements.&lt;/p&gt;
&lt;p&gt;All the applications are available either as Flatpaks or Fedora RPMS, through the gitlab project page, so I hope people enjoy these applications and tools. And enoy the blasts from the past as much as I did.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Worries about Artifical Intelligence&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
When I speak to people both inside Red Hat and outside in the community I often come across negativity or even sometimes anger towards Artificial Intelligence in the coding space. And to be clear I to worry about where things could be heading and how it will affect my livelihood too, so I am not unsympathetic to those worries at all. I probably worry about these things at least a few times a day. At the same time I don’t think we can hide from or avoid this change, it is happening with or without us. We have to adapt to a world where this tool exists, just like our ancestors have adapted to jobs changing due to industrialization and science before. So do I worry about the future, yes I do. Do I worry about how I might personally get affected by this? yes, I do. Do I worry about how society might change for the worse due to this? yes, I do. But I also remind myself that I don’t know the future and that people have found ways to move forward before and society has survived and thrived. So what I can control is that I try to be on top of these changes myself and take advantage of them where I can and that is my recommendation to the wider open source community on this too. By leveraging them to move open source forward and at the same time trying to put our weight on the scale towards the best practices and policies around Artificial Intelligence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Next Test and where AI might have hit a limit for me.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
So all these previous efforts did teach me a lot of tricks and helped me understand how I can work with an AI agent like Claude, but especially after the success with the webcam I decided to up the stakes and see if I could use Claude to help me create a driver for my &lt;a class="external" href="https://plustek.com/us/products/film-photo-scanners/opticfilm-8200i-ai/"&gt;Plustek OpticFilm 8200i&lt;/a&gt; scanner. So I have zero backround in any kind of driver development and probably less than zero in the field of scanner driver specifically. So I ended up going down a long row of deadends on this journey and I to this day has not been able to get a single scan out of the scanner with anything that even remotely resembles the images I am trying to scan.&lt;/p&gt;
&lt;p&gt;My idea was to have Claude analyse the Windows and Mac driver and build me a SANE driver based on that, which turned out to be horribly naive and lead nowhere. One thing I realized is that I would need to capture USB traffic to help Claude contextualize some of the findings it had from looking at the Windows and Mac drivers.I started out with Wireshark and feeding Claude with the Wireshark capture logs. Claude quite soon concluded that the Wireshark logs wasn’t good enough and that I needed lower level traffic capture. Buying a USB packet analyzer isn’t cheap so I had the idea that I could use one of the ARM development boards floating around the house as a USB relay, allowing me to perfectly capture the USB traffic. With some work I did manage to set up my &lt;a class="external" href="https://libre.computer/products/aml-s905d3-cc/"&gt;LibreComputer Solitude AML-S905D3-CC&lt;/a&gt; arm board going and setting it in device mode. I also had a usb-relay daemon going on the board. After a lot of back and forth, and even at one point trying to ask Claude to implement a missing feature in the USB kernel stack, I realized this would never work and I ended up ordering a Beagle USB 480 USB hardware analyzer. &lt;/p&gt;
&lt;p&gt;At about the same time I came across the chipset documentation for the Genesys Logic GL845 chip in the scanner. I assumed that between my new USB analyzer and the chipset docs this would be easy going from here on, but so far no. I even had Claude decompile the windows driver using &lt;a class="external" href="https://github.com/NationalSecurityAgency/ghidra"&gt;ghidra&lt;/a&gt; and then try to extract the needed information needed from the decompiled code.&lt;br/&gt;
I bought a network controlled electric outlet so that Claude can cycle the power of the scanner on its own.&lt;/p&gt;
&lt;p&gt;So the problem here is that with zero scanner driver knowledge I don’t even know what I should be looking for, or where I should point Claude to, so I keept trying to brute force it by trial and error. I managed to make SANE detect the scanner and I managed to get motor and lamp control going, but that is about it. I can hear the scanner motor running and I ask for a scan, but I don’t know if it moves correctly. I can see light turning on and off inside the scanner, but I once again don’t know if it is happening at the correct times and correct durations. And Claude has of course no way of knowing either, relying on me to tell it if something seems like it has improved compared to how it was.&lt;/p&gt;
&lt;p&gt;I have now used Claude to create two tools for Claude to use, once using a camera to detect what is happening with the light inside the scanner and the other recording sound trying to compare the sound this driver makes compared to the sounds coming out when doing a working scan with the MacOS X application. I don’t know if this will take me to the promised land eventually, but so far I consider my scanner driver attempt a giant failure. At the same time I do believe that if someone actually skilled in scanner driver development was doing this they could have guided Claude to do the right things and probably would have had a working driver by now.&lt;/p&gt;
&lt;p&gt;So I don’t know if I hit the kind of thing that will always be hard for an AI to do, as it has to interact with things existing in the real world, or if newer versions of Claude, Gemini or Codex will suddenly get past a threshold and make this seem easy, but this is where things are at for me at the moment.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/uraeus/2026/03/23/using-ai-to-create-some-hardware-tools-and-bring-back-the-past/</guid>
      <pubDate>Mon, 23 Mar 2026 16:07:43 +0000</pubDate>
    </item>
    <item>
      <title>GNOME Foundation News: Introducing the GNOME Fellowship program</title>
      <link>https://blogs.gnome.org/foundation/2026/03/24/introducing-gnome-fellowship/</link>
      <description>&lt;p&gt;&lt;a href="https://blogs.gnome.org/foundation/files/2026/03/fellows-social.png"&gt;&lt;img alt="" class="size-full wp-image-20095 aligncenter" height="348" src="https://blogs.gnome.org/foundation/files/2026/03/fellows-social.png" width="662"/&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p class="part"&gt;&lt;em&gt;Sustaining GNOME by directly funding contributors&lt;/em&gt;&lt;/p&gt;
&lt;p class="part"&gt;The GNOME Foundation is excited to announce the &lt;a class="external" href="https://fellowship.gnome.org/" rel="noopener" target="_blank"&gt;GNOME Fellowship&lt;/a&gt; program, a new initiative to fund community members working on the long-term sustainability of the GNOME project. We’re now accepting applications for our inaugural fellowship cycle, beginning around May 2026.&lt;/p&gt;
&lt;p class="part"&gt;GNOME has always thrived because of its contributors: people who invest their time and expertise to build and maintain the desktop, applications, and platform that millions rely on. But open source contribution often depends on volunteers finding time alongside other commitments, or on companies choosing to fund development amongst competing priorities. Many important areas of the project – the less glamorous but critical infrastructure work – can go underinvested.&lt;/p&gt;
&lt;p class="part"&gt;The fellowship program changes that. Thanks to the generous support of &lt;a class="external" href="https://donate.gnome.org/" rel="noopener" target="_blank"&gt;Friends of GNOME&lt;/a&gt; donors, we can now directly fund contributors to focus on what matters most for GNOME’s future. Programs such as this rely on ongoing support from our donors, so if you would like to see this and similar programs continue in future, please consider &lt;a class="external" href="https://donate.gnome.org/" rel="noopener" target="_blank"&gt;setting up a recurring donation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 class="part" id="What’s-a-Fellowship"&gt;What’s a Fellowship?&lt;/h2&gt;
&lt;p class="part"&gt;A fellowship is funding for an individual to spend dedicated time over a 12 month period working in an area where they have expertise. Unlike traditional contracts with rigid scopes and deliverables, fellowships are built on trust. We’re backing people and the type of work they do, giving them the flexibility to tackle problems as they find them.&lt;/p&gt;
&lt;p class="part"&gt;This approach reduces bureaucratic overhead for both contributors and the Foundation. It lets talented people do what they do best: identify important problems and solve them.&lt;/p&gt;
&lt;h2 class="part" id="Focus-Sustainability"&gt;Focus: Sustainability&lt;/h2&gt;
&lt;p class="part"&gt;For this first cycle, we’re seeking proposals focused on sustainability work that makes GNOME more maintainable, efficient, and productive for developers. This includes areas like build systems, CI/CD infrastructure, testing frameworks, developer tooling, documentation, accessibility, and reducing technical debt.&lt;/p&gt;
&lt;p class="part"&gt;We’re not funding new features this round. Instead, we want to invest in the foundations that make future development and contributions easier and faster. The goal is for each fellowship to leave the project in better shape than we found it.&lt;/p&gt;
&lt;h2 class="part" id="Apply-Now"&gt;Apply Now&lt;/h2&gt;
&lt;p class="part"&gt;We have funding for at least one 12-month fellowship paid between $70,000 and $100,000 USD per year based on experience and location. Applicants can propose full-time, half-time work, or either – half-time proposals may allow us to support multiple fellows.&lt;/p&gt;
&lt;p class="part"&gt;Applications are open to anyone with a track record in GNOME or relevant experience, with some restrictions due to US sanctions compliance. A GNOME Foundation Board committee will review applications and select fellows for this inaugural cycle.&lt;/p&gt;
&lt;p class="part"&gt;Full details, application requirements, and FAQ are available at &lt;a class="external" href="http://fellowship.gnome.org" rel="noopener" target="_blank"&gt;fellowship.gnome.org&lt;/a&gt;. Applications close on 20th April 2026.&lt;/p&gt;
&lt;h2 class="part" id="Thank-You-to-Friends-of-GNOME"&gt;Thank You to Friends of GNOME&lt;/h2&gt;
&lt;p class="part"&gt;This program is possible because of the individuals and organizations who support GNOME through Friends of GNOME donations. When we ask for donations, funding contributor work is exactly the kind of initiative we have in mind. If you’d like to sustain this program beyond its first year, consider becoming a &lt;a class="external" href="https://donate.gnome.org/" rel="noopener" target="_blank"&gt;Friend of GNOME&lt;/a&gt;. A recurring donation, no matter how small, gives us the predictability to expand this program and others like it.&lt;/p&gt;
&lt;h2 class="part" id="Looking-Ahead"&gt;Looking Ahead&lt;/h2&gt;
&lt;p class="part"&gt;This is a pilot program. We’re optimistic, and if it succeeds, we hope to sustain and grow the fellowship program in future years, funding more contributors across more areas of GNOME. We believe this model can become a sustainable way to invest in the project’s long-term health.&lt;/p&gt;
&lt;p class="part"&gt;We can’t wait to see your proposals!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/foundation/2026/03/24/introducing-gnome-fellowship/</guid>
      <pubDate>Tue, 24 Mar 2026 12:26:19 +0000</pubDate>
    </item>
    <item>
      <title>Lennart Poettering: Mastodon Stories for systemd v260</title>
      <link>https://0pointer.net/blog/mastodon-stories-for-systemd-v260.html</link>
      <description>&lt;p&gt;On March 17 we released systemd v260 &lt;a href="https://github.com/systemd/systemd/releases/tag/v260"&gt;into the wild&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the weeks leading up to that release (and since then) I have posted
a series of serieses of posts to Mastodon about key new features in
this release, under the
&lt;a href="https://mastodon.social/@pid_eins/tagged/systemd260"&gt;#systemd260&lt;/a&gt;
hash tag. In case you aren't using Mastodon, but would like to
read up, here's a list of all 21 posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Post #1: &lt;a href="https://mastodon.social/@pid_eins/116143441607125263"&gt;NvPCR Measurements for Activated DDIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #2: &lt;a href="https://mastodon.social/@pid_eins/116158446582239764"&gt;Varlink Transport Plugins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #3: &lt;a href="https://mastodon.social/@pid_eins/116167363137246748"&gt;Well-Known Varlink Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #4: &lt;a href="https://mastodon.social/@pid_eins/116169755712847601"&gt;.mstack Overlay Mount Stacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #5: &lt;a href="https://mastodon.social/@pid_eins/116175685679588363"&gt;RefreshOnReload= in Service Units&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #6: &lt;a href="https://mastodon.social/@pid_eins/116198232706709156"&gt;FANCY_NAME= in /etc/os-release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #7: &lt;a href="https://mastodon.social/@pid_eins/116207200815338698"&gt;BindNetworkInterface= in Service Units&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #8: &lt;a href="https://mastodon.social/@pid_eins/116209272590086800"&gt;importctl pull-oci for Acquiring OCI Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #9: &lt;a href="https://mastodon.social/@pid_eins/116220986239256436"&gt;systemd-report and Metrics API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #10: &lt;a href="https://mastodon.social/@pid_eins/116249087900074872"&gt;udev's tpm2_id built-in and the TPM2 Quirks Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #11: &lt;a href="https://mastodon.social/@pid_eins/116249601419824877"&gt;Devicetree/CHID Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #12: &lt;a href="https://mastodon.social/@pid_eins/116254613033058686"&gt;Varlink IPC for systemd-networkd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #13: &lt;a href="https://mastodon.social/@pid_eins/116254632376293505"&gt;systemd-vmspawn knows --ephemeral now&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #14: &lt;a href="https://mastodon.social/@pid_eins/116277375277269773"&gt;systemd-loginds's xaccess Concept&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #15: &lt;a href="https://mastodon.social/@pid_eins/116284331662553931"&gt;Unprivileged Portable Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #16: &lt;a href="https://mastodon.social/@pid_eins/116284380064883282"&gt;Image Policy Improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #17: &lt;a href="https://mastodon.social/@pid_eins/116288723454722754"&gt;LUKS Volume Key Fixation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #18: &lt;a href="https://mastodon.social/@pid_eins/116299869233876251"&gt;Journal Varlink Access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #19: &lt;a href="https://mastodon.social/@pid_eins/116299887458427182"&gt;Nested UID Range Delegation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #20: &lt;a href="https://mastodon.social/@pid_eins/116299916605898399"&gt;PrivateUsers=managed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Post #21: &lt;a href="https://mastodon.social/@pid_eins/116299971280176013"&gt;bootctl install as Varlink API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I intend to do a similar series of serieses of posts for the next systemd
release (v261), hence if you haven't left tech Twitter for Mastodon yet, now is
the opportunity.&lt;/p&gt;
&lt;p&gt;My series for v261 will begin in a few weeks most likely, under the
&lt;a href="https://mastodon.social/@pid_eins/tagged/systemd261"&gt;#systemd261&lt;/a&gt;
hash tag.&lt;/p&gt;
&lt;p&gt;In case you are interested, &lt;a href="https://0pointer.net/blog/mastodon-stories-for-systemd-v259.html"&gt;here is the corresponding blog story for
systemd v259&lt;/a&gt;,
&lt;a href="https://0pointer.net/blog/mastodon-stories-for-systemd-v258.html"&gt;here for
v258&lt;/a&gt;,
&lt;a href="https://0pointer.net/blog/announcing-systemd-v257.html"&gt;here for
v257&lt;/a&gt;,
and &lt;a href="https://0pointer.net/blog/announcing-systemd-v256.html"&gt;here for
v256&lt;/a&gt;.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://0pointer.net/blog/mastodon-stories-for-systemd-v260.html</guid>
      <pubDate>Thu, 26 Mar 2026 23:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Sebastian Wick: Three Little Rust Crates</title>
      <link>https://blog.sebastianwick.net/posts/three-little-rust-crates/</link>
      <description>&lt;p&gt;I published three Rust crates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://crates.io/crates/name-to-handle-at"&gt;name-to-handle-at&lt;/a&gt;&lt;/strong&gt;: Safe, low-level Rust bindings for Linux &lt;code&gt;name_to_handle_at&lt;/code&gt; and &lt;code&gt;open_by_handle_at&lt;/code&gt; system calls&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://crates.io/crates/pidfd-util"&gt;pidfd-util&lt;/a&gt;&lt;/strong&gt;: Safe Rust wrapper for Linux process file descriptors (pidfd)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://crates.io/crates/listen-fds"&gt;listen-fds&lt;/a&gt;&lt;/strong&gt;: A Rust library for handling systemd socket activation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They might seem like rather arbitrary, unconnected things – but there is a connection!&lt;/p&gt;
&lt;p&gt;systemd socket activation passes file descriptors and a bit of metadata as environment variables to the activated process. If the activated process exec’s another program, the file descriptors get passed along because they are not &lt;code&gt;CLOEXEC&lt;/code&gt;. If that process then picks them up, things could go very wrong. So, the activated process is supposed to mark the file descriptors &lt;code&gt;CLOEXEC&lt;/code&gt;, and unset the socket activation environment variables. If a process doesn’t do this for whatever reason however, the same problems can arise. So there is another mechanism to help prevent it: another bit of metadata contains the PID of the target. Processes can check it against their own PID to figure out if they were the target of the activation, without having to depend on all other processes doing the right thing.&lt;/p&gt;
&lt;p&gt;PIDs however are racy because they wrap around pretty fast, and that’s why nowadays we have pidfds. They are file descriptors which act as a stable handle to a process and avoid the ID wrap-around issue. Socket activation with systemd nowadays also passes a pidfd ID. A pidfd ID however is not the same as a pidfd file descriptor! It is the 64 bit inode of the pidfd file descriptor on the pidfd filesystem. This has the advantage that systemd doesn’t have to install another file descriptor in the target process which might not get closed. It can just put the pidfd ID number into the &lt;code&gt;$LISTEN_PIDFDID&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;p&gt;Getting the inode of a file descriptor doesn’t sound hard. &lt;code&gt;fstat(2)&lt;/code&gt; fills out &lt;code&gt;struct stat&lt;/code&gt; which has the &lt;code&gt;st_ino&lt;/code&gt; field. The problem is that it has a type of &lt;code&gt;ino_t&lt;/code&gt;, which is 32 bits on some systems so we might end up with a process identifier which wraps around pretty fast again.&lt;/p&gt;
&lt;p&gt;We can however use the &lt;code&gt;name_to_handle&lt;/code&gt; syscall on the pidfd to get a &lt;code&gt;struct file_handle&lt;/code&gt; with a &lt;code&gt;f_handle&lt;/code&gt; field. The man page helpfully says that “the caller should treat the file_handle structure as an opaque data type”. We’re going to ignore that, though, because at least on the pidfd filesystem, the first 64 bits are the 64 bit inode. With systemd already depending on this and the kernel rule of “don’t break user-space”, this is now API, no matter what the man page tells you.&lt;/p&gt;
&lt;p&gt;So there you have it. It’s all connected.&lt;/p&gt;
&lt;p&gt;Obviously both pidfds and &lt;code&gt;name_to_handle&lt;/code&gt; have more exciting uses, many of which serve my broader goal: making Varlink services a first-class citizen. More about that another time.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blog.sebastianwick.net/posts/three-little-rust-crates/</guid>
      <pubDate>Fri, 27 Mar 2026 00:15:49 +0000</pubDate>
    </item>
    <item>
      <title>Gedit Technology: gedit 50.0 released</title>
      <link>https://gedit-text-editor.org/blog/2026-03-28-gedit-50-0.html</link>
      <description>&lt;p&gt;
&lt;a href="https://gedit-text-editor.org/" target="_blank"&gt;gedit&lt;/a&gt; 50.0 has
  been released! Here are the highlights since version 49.0 from January.
  (Some sections are a bit technical).
&lt;/p&gt;
&lt;h3&gt;No Large Language Models AI tools&lt;/h3&gt;
&lt;p&gt;
  The gedit project now
  &lt;a href="https://gitlab.gnome.org/World/gedit/gedit/-/commit/d2488f3179343f383d0645ce21dee3ad5cc5f665" target="_blank"&gt;disallows the use of LLMs&lt;/a&gt;
  for contributions.
&lt;/p&gt;
&lt;p&gt;
  The rationales:
&lt;/p&gt;
&lt;p&gt;
  Programming can be seen as a discipline between art and engineering. Both art
  and engineering require practice. It's the action of doing - modifying the
  code - that permits a deep understanding of it, to ensure correctness and
  quality.
&lt;/p&gt;
&lt;p&gt;
  When generating source code with an LLM tool, the real sources are the inputs
  given to it: the training dataset, plus the human commands.
&lt;/p&gt;
&lt;p&gt;
  Adding something generated to the version control system (e.g., Git) is
  usually frown upon. Moreover, we aim for reproducible results (to follow the
  best-practices of reproducible builds, and reproducible science more
  generally). Modifying afterwards something generated is also a bad practice.
&lt;/p&gt;
&lt;h3&gt;Releasing earlier, releasing more often&lt;/h3&gt;
&lt;p&gt;
  To follow more closely the
  &lt;a href="https://en.wikipedia.org/wiki/Release_early,_release_often" target="_blank"&gt;release early, release often&lt;/a&gt;
  mantra, gedit aims for a faster release cadence in 2026, to have smaller
  deltas between each version. Future will tell how it goes.
&lt;/p&gt;
&lt;h3&gt;The website is now responsive&lt;/h3&gt;
&lt;p&gt;
  Since last time, we've made some efforts to the website. Small-screen-device
  readers should have a more pleasant experience.
&lt;/p&gt;
&lt;h3&gt;libgedit-amtk becomes "The Good Morning Toolkit"&lt;/h3&gt;
&lt;p&gt;
  Amtk originally stands for "Actions, Menus and Toolbars Kit". There was a
  desire to expand it to include other GTK extras that are useful for gedit
  needs.
&lt;/p&gt;
&lt;p&gt;
  A more appropriate name would be libgedit-gtk-extras. But renaming the module
  - not to mention the project namespace - is more work. So we've chosen to
  simply continue with the name Amtk, just changing its scope and definition.
  And - while at it - sprinkle a bit of fun :-)
&lt;/p&gt;
&lt;p&gt;
  So there are now four libgedit-* modules:
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gedit-text-editor.org/developer-docs/libgedit-gfls-1/" target="_blank"&gt;libgedit-gfls&lt;/a&gt;,
    aka "libgedit-glib-extras", currently for "File Loading and Saving";
  &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gedit-text-editor.org/developer-docs/libgedit-amtk-5/" target="_blank"&gt;libgedit-amtk&lt;/a&gt;,
    aka "libgedit-gtk-extras" - it extends GTK for gedit needs at the exception
    of GtkTextView;
  &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gedit-text-editor.org/developer-docs/libgedit-gtksourceview-300/" target="_blank"&gt;libgedit-gtksourceview&lt;/a&gt;
    - it extends GtkTextView and is a fork of GtkSourceView, to evolve the
    library for gedit needs;
  &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gedit-text-editor.org/developer-docs/libgedit-tepl-6/" target="_blank"&gt;libgedit-tepl&lt;/a&gt;
    - the Text Editor Product Line library, it provides a high-level API,
    including an application framework for creating more easily new text
    editors.
  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
  Note that all of these are still constantly in construction.
&lt;/p&gt;
&lt;h3&gt;Some code overhaul&lt;/h3&gt;
&lt;p&gt;
  Work continues steadily inside libgedit-gfls and libgedit-gtksourceview to
  streamline &lt;strong&gt;document loading&lt;/strong&gt;.
&lt;/p&gt;
&lt;p&gt;
  You might think that it's a &lt;em&gt;problem solved&lt;/em&gt; (for many years), but it's
  actually not the case for gedit. Many improvements are still possible.
&lt;/p&gt;
&lt;p&gt;
  Another area of interest is the &lt;strong&gt;completion framework&lt;/strong&gt; (part of
  libgedit-gtksourceview), where changes are still needed to make it fully
  functional under Wayland. The popup windows are sometimes misplaced. So
  between gedit 49.0 and 50.0 some progress has been made on this. The
  Word Completion gedit plugin works fine under Wayland, while the LaTeX
  completion with
  &lt;a href="https://gitlab.gnome.org/World/gedit/enter-tex" target="_blank"&gt;Enter TeX&lt;/a&gt;
  is still buggy since it uses more features from the completion system.
&lt;/p&gt;</description>
      <guid isPermaLink="false">https://gedit-text-editor.org/blog/2026-03-28-gedit-50-0.html</guid>
      <pubDate>Sat, 28 Mar 2026 10:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Matthew Garrett: Self hosting as much of my online presence as practical</title>
      <link>https://codon.org.uk/~mjg59/blog/p/self-hosting-as-much-of-my-online-presence-as-practical/</link>
      <description>&lt;p&gt;Because I am bad at giving up on things, I’ve been running my own email
server for over 20 years. Some of that time it’s been a PC at the end of a
DSL line, some of that time it’s been a Mac Mini in a data centre, and some
of that time it’s been a hosted VM. Last year I decided to bring it in
house, and since then I’ve been gradually consolidating as much of the rest
of my online presence as possible on it. I mentioned this &lt;a class="link" href="https://nondeterministic.computer/@mjg59/116321518908968091" rel="noopener" target="_blank"&gt;on
Mastodon&lt;/a&gt; and a
couple of people asked for more details, so here we are.&lt;/p&gt;
&lt;p&gt;First: &lt;a class="link" href="https://www.monkeybrains.net/" rel="noopener" target="_blank"&gt;my ISP&lt;/a&gt; doesn’t guarantee a static
IPv4 unless I’m on a business plan and that seems like it’d cost a bunch
more, so I’m doing what I &lt;a class="link" href="https://mjg59.dreamwidth.org/72095.html" rel="noopener" target="_blank"&gt;described
here&lt;/a&gt;: running a Wireguard link
between a box that sits in a cupboard in my living room and the smallest
&lt;a class="link" href="https://us.ovhcloud.com/" rel="noopener" target="_blank"&gt;OVH&lt;/a&gt; instance I can, with an additional IP
address allocated to the VM and NATted over the VPN link. The practical
outcome of this is that my home IP address is irrelevant and can change as
much as it wants - my DNS points at the OVH IP, and traffic to that all ends
up hitting my server.&lt;/p&gt;
&lt;p&gt;The server itself is pretty uninteresting. It’s a refurbished HP EliteDesk
which idles at 10W or so, along 2TB of NVMe and 32GB of RAM that I found
under a pile of laptops in my office. We’re not talking rackmount Xeon
levels of performance, but it’s entirely adequate for everything I’m doing
here.&lt;/p&gt;
&lt;p&gt;So. Let’s talk about the services I’m hosting.&lt;/p&gt;
&lt;h2 id="web"&gt;Web
&lt;/h2&gt;&lt;p&gt;This one’s trivial. I’m not really hosting much of a website right now, but
what there is is served via Apache with a Let’s Encrypt certificate. Nothing
interesting at all here, other than the proxying that’s going to be relevant
later.&lt;/p&gt;
&lt;h2 id="email"&gt;Email
&lt;/h2&gt;&lt;p&gt;Inbound email is easy enough. I’m running Postfix with a pretty stock
configuration, and my MX records point at me. The same Let’s Encrypt
certificate is there for TLS delivery. I’m using Dovecot as an IMAP server
(again with the same cert). You can find plenty of guides on setting this
up.&lt;/p&gt;
&lt;p&gt;Outbound email? That’s harder. I’m on a residential IP address, so if I send
email directly nobody’s going to deliver it. Going via my OVH address isn’t
going to be a lot better. I have a Google Workspace, so in the end I just
made use of &lt;a class="link" href="https://knowledge.workspace.google.com/admin/gmail/advanced/route-outgoing-smtp-relay-messages-through-google" rel="noopener" target="_blank"&gt;Google’s SMTP relay
service&lt;/a&gt;. There’s
various commerical alternatives available, I just chose this one because it
didn’t cost me anything more than I’m already paying.&lt;/p&gt;
&lt;h2 id="blog"&gt;Blog
&lt;/h2&gt;&lt;p&gt;My blog is largely static content generated by
&lt;a class="link" href="https://gohugo.io/" rel="noopener" target="_blank"&gt;Hugo&lt;/a&gt;. Comments are &lt;a class="link" href="https://remark42.com/" rel="noopener" target="_blank"&gt;Remark42&lt;/a&gt;
running in a Docker container. If you don’t want to handle even that level
of dynamic content you can use a third party comment provider like
&lt;a class="link" href="https://disqus.com" rel="noopener" target="_blank"&gt;Disqus&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="mastodon"&gt;Mastodon
&lt;/h2&gt;&lt;p&gt;I’m deploying Mastodon pretty much along the lines of the &lt;a class="link" href="https://github.com/mastodon/mastodon/blob/main/docker-compose.yml" rel="noopener" target="_blank"&gt;upstream compose
file&lt;/a&gt;. Apache
is proxying /api/v1/streaming to the websocket provided by the streaming
container and / to the actual Mastodon service. The only thing I tripped
over for a while was the need to set the “X-Forwarded-Proto” header since
otherwise you get stuck in a redirect loop of Mastodon receiving a request
over http (because TLS termination is being done by the Apache proxy) and
redirecting to https, except that’s where we just came from.&lt;/p&gt;
&lt;p&gt;Mastodon is easily the heaviest part of all of this, using around 5GB of RAM
and 60GB of disk for an instance with 3 users. This is more a point of
principle than an especially good idea.&lt;/p&gt;
&lt;h2 id="bluesky"&gt;Bluesky
&lt;/h2&gt;&lt;p&gt;I’m arguably cheating here. Bluesky’s federation model is quite different to
Mastodon - while running a Mastodon service implies running the webview and
other infrastructure associated with it, Bluesky has split that into
&lt;a class="link" href="https://docs.bsky.app/docs/advanced-guides/federation-architecture" rel="noopener" target="_blank"&gt;multiple
parts&lt;/a&gt;. User
data is stored on Personal Data Servers, then aggregated from those by
Relays, and then displayed on Appviews. Third parties can run any of these,
but a user’s actual posts are stored on a PDS. There are various reasons to
run the others, for instance to implement alternative moderation policies,
but if all you want is to ensure that you have control over your data,
running a PDS is sufficient. I followed &lt;a class="link" href="https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/" rel="noopener" target="_blank"&gt;these
instructions&lt;/a&gt;,
other than using Apache as the frontend proxy rather than nginx, and it’s
all been working fine since then. In terms of ensuring that my data remains
under my control, it’s sufficient.&lt;/p&gt;
&lt;h2 id="backups"&gt;Backups
&lt;/h2&gt;&lt;p&gt;I’m using &lt;a class="link" href="https://torsion.org/borgmatic/" rel="noopener" target="_blank"&gt;borgmatic&lt;/a&gt;, backing up to a local
Synology NAS and also to my parents’ home (where I have another HP EliteDesk
set up with an equivalent OVH IPv4 fronting setup). At some point I’ll check
that I’m actually able to restore them.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Most of what I post is now stored on a system that’s happily living under a
TV, but is available to the rest of the world just as visibly as if I used a
hosted provider. Is this necessary? No. Does it improve my life? In no
practical way. Does it generate additional complexity? Absolutely. Should
you do it? Oh good heavens no. But you can, and once it’s working it largely
just keeps working, and there’s a certain sense of comfort in knowing that
my online presence is carefully contained in a small box making a gentle
whirring noise.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://codon.org.uk/~mjg59/blog/p/self-hosting-as-much-of-my-online-presence-as-practical/</guid>
      <pubDate>Wed, 01 Apr 2026 02:35:43 +0000</pubDate>
    </item>
    <item>
      <title>GNOME Shell and Mutter Development: What is new in GNOME Kiosk 50</title>
      <link>https://blogs.gnome.org/shell-dev/2026/04/01/what-is-new-in-gnome-kiosk-50/</link>
      <description>&lt;p&gt;GNOME Kiosk, the lightweight, specialized compositor continues to evolve In GNOME 50 by adding new configuration options and improving accessibility.&lt;/p&gt;
&lt;div&gt;
&lt;h3&gt;Window configuration&lt;/h3&gt;
&lt;div&gt;
&lt;h4&gt;User configuration file monitoring&lt;/h4&gt;
&lt;p&gt;The user configuration file gets reloaded when it changes on disk, so that it is not necessary to restart the session.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4&gt;New placement options&lt;/h4&gt;
&lt;p&gt;New configuration options to constrain windows to monitors or regions on screen have been added:&lt;/p&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lock-on-monitor&lt;/code&gt;: lock a window to a monitor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lock-on-monitor-area&lt;/code&gt;: lock to an area relative to a monitor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lock-on-area&lt;/code&gt;: lock to an absolute area.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p&gt;These options are intended to replicate the legacy „&lt;em&gt;Zaphod&lt;/em&gt;“ mode from X11, where windows could be tied to a specific monitor. It even goes further than that, as it allows to lock windows on a specific area on screen.&lt;/p&gt;
&lt;p&gt;The window/monitor association also remains true when a monitor is disconnected. Take for example a setup where each monitor, on a multiple monitors configuration, shows different timetables. If one of the monitors is disconnected (for whatever reason), the timetable showing on that monitor should not be moved to another remaining monitor. The &lt;code&gt;lock-on-monitor&lt;/code&gt; option prevents that.&lt;/p&gt;
&lt;div&gt;
&lt;h4&gt;Initial map behavior was tightened&lt;/h4&gt;
&lt;div&gt;
&lt;p&gt;Clients can resize or change their state  before the window is mapped, so size, position, and fullscreen as set from the configuration could be skipped. Kiosk now makes sure to apply configured size, position, and fullscreen on first map when the initial configuration was not applied reliably.&lt;/p&gt;
&lt;div&gt;
&lt;h4&gt;Auto-fullscreen heuristics were adjusted&lt;/h4&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;Only &lt;em&gt;normal&lt;/em&gt; windows are considered when checking whether another window already covers the monitor (avoids false positives from e.g. &lt;code&gt;xwaylandvideobridge&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The current window is excluded when scanning “&lt;em&gt;other&lt;/em&gt;” fullscreen sized windows (fixes Firefox restoring monitor-sized geometry).&lt;/li&gt;
&lt;li&gt;Maximized or fullscreen windows are no longer treated as non-resizable so toggling fullscreen still works when the client had already maximized.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;h2&gt;Compositor behavior and command-line options&lt;/h2&gt;
&lt;p&gt;New command line options have been added:&lt;/p&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--no-cursor&lt;/code&gt;: hides the pointer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--force-animations&lt;/code&gt;: forces animations to be enabled.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--enable-vt-switch&lt;/code&gt;: restores VT switching with the keyboard.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;--no-cursor&lt;/code&gt; option can be used to hide the pointer cursor entirely for setups where user input does not involve a pointing device (it is similar to the &lt;code&gt;-nocursor&lt;/code&gt; option in Xorg).&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Animations can now be disabled using the desktop settings, and will also be automatically disabled when the backend reports no hardware-accelerated rendering for performance purpose. The option &lt;code&gt;--force-animations&lt;/code&gt; can be used to forcibly enable animations in that case, similar to GNOME Shell.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;The native keybindings, which include VT switching keyboard shortcuts are now disabled by default for kiosk hardening. Applications that rely on the user being able to switch to another console VT on Linux, such as e.g Anaconda, will need to explicit re-enable VT switching using &lt;code&gt;--enable-vt-switch&lt;/code&gt; in their session.&lt;/p&gt;
&lt;p&gt;These options need to be passed from the command line starting &lt;code&gt;gnome-kiosk&lt;/code&gt;, which would imply updating the systemd definitions files, or better, create a custom one (taking example on the the ones provided with the GNOME Kiosk sessions).&lt;/p&gt;
&lt;div&gt;
&lt;h2&gt;Accessibility&lt;/h2&gt;
&lt;div&gt;
&lt;h4&gt;Accessibility panel&lt;/h4&gt;
&lt;div&gt;An example of an accessibility panel is now included, to control the platform accessibility settings with a GUI. It is a simple Python application using GTK4.&lt;/div&gt;
&lt;div&gt;&lt;img alt="" class="alignnone size-medium wp-image-3364" height="202" src="https://blogs.gnome.org/shell-dev/files/2026/03/gnome-kiosk-a11y-panel-300x202.png" width="300"/&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;(The &lt;code&gt;gsettings&lt;/code&gt; options are also documented in the &lt;a class="external" href="https://gitlab.gnome.org/GNOME/gnome-kiosk/-/blob/main/CONFIG.md"&gt;CONFIG.md&lt;/a&gt; file.)&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4&gt;Screen magnifier&lt;/h4&gt;
&lt;div&gt;
&lt;p&gt;Desktop magnification is now implemented, using the same settings as the rest of the GNOME desktop (namely &lt;code&gt;screen-magnifier-enabled&lt;/code&gt;, &lt;code&gt;mag-factor&lt;/code&gt;, see the &lt;a class="external" href="https://gitlab.gnome.org/GNOME/gnome-kiosk/-/blob/main/CONFIG.md#screen-magnifier"&gt;CONFIG.md&lt;/a&gt; file for details).&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p&gt;It can can be enabled from the accessibility panel or from the keyboard shortcuts through the gnome-settings-daemon’s “mediakeys” plugin.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;img alt="" class="alignnone size-medium wp-image-3365" height="202" src="https://blogs.gnome.org/shell-dev/files/2026/03/gnome-kiosk-a11y-magnify-300x202.png" width="300"/&gt;&lt;/div&gt;
&lt;h4&gt;Accessibility settings&lt;/h4&gt;
&lt;div&gt;
&lt;p&gt;The default systemd session units now start the gnome-settings-daemon accessibility plugin so that Orca (the screen reader) can be enabled through the dedicated keyboard shortcut.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;h3&gt;Notifications&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A new, optional notification daemon implements &lt;code&gt;org.freedesktop.Notifications&lt;/code&gt; and &lt;code&gt;org.gtk.Notifications&lt;/code&gt; using GTK 4 and libadwaita.&lt;/li&gt;
&lt;li&gt;A small utility to send notifications via &lt;code&gt;org.gtk.Notifications&lt;/code&gt; is also provided.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;img alt="" class="alignnone size-medium wp-image-3366" height="202" src="https://blogs.gnome.org/shell-dev/files/2026/03/gnome-kiosk-notifications-300x202.png" width="300"/&gt;&lt;/div&gt;
&lt;h3&gt;Input sources&lt;/h3&gt;
&lt;div&gt;GNOME Kiosk was ported to the new Mutter’s keymap API which allows remote desktop servers to mirror the keyboard layout used on the client side.&lt;/div&gt;
&lt;h3&gt;Session files and systemd&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X-GDM-SessionRegister&lt;/code&gt; is now set to &lt;code&gt;false&lt;/code&gt; in kiosk sessions as GNOME Kiosk does not register the session itself (unlike GNOME Shell). That fixes a hang when terminating the session.&lt;/li&gt;
&lt;li&gt;Script session: systemd is no longer instructed to restart the session when the script exits, so that users can logout of the script session when the script terminates.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/shell-dev/2026/04/01/what-is-new-in-gnome-kiosk-50/</guid>
      <pubDate>Wed, 01 Apr 2026 09:06:16 +0000</pubDate>
    </item>
    <item>
      <title>Andy Wingo: wastrel milestone: full hoot support, with generational gc as a treat</title>
      <link>https://wingolog.org/archives/2026/04/09/wastrel-milestone-full-hoot-support-with-generational-gc-as-a-treat</link>
      <description>&lt;div&gt;&lt;p&gt;Hear ye, hear ye: Wastrel and Hoot means REPL!&lt;/p&gt;&lt;p&gt;Which is to say, &lt;a href="https://codeberg.org/andywingo/wastrel"&gt;Wastrel&lt;/a&gt; can
now make native binaries out of WebAssembly files as produced by the
&lt;a href="https://spritely.institute/hoot"&gt;Hoot&lt;/a&gt; Scheme toolchain, up
to and including a full read-eval-print loop.  Like the &lt;a href="https://spritely.institute/hoot"&gt;REPL on the
Hoot web page&lt;/a&gt;, but instead of
requiring a browser, you can just run it on your console.  Amazing stuff!&lt;/p&gt;&lt;h3&gt;try it at home&lt;/h3&gt;&lt;p&gt;First, we need the latest Hoot.  &lt;a href="https://codeberg.org/spritely/hoot#building-from-source"&gt;Build it from
source&lt;/a&gt;, then
compile a simple REPL:&lt;/p&gt;&lt;pre&gt;echo '(import (hoot repl)) (spawn-repl)' &amp;gt; repl.scm
./pre-inst-env hoot compile -fruntime-modules -o repl.wasm repl.scm
&lt;/pre&gt;&lt;p&gt;This takes about a minute.  The resulting wasm file has a pretty full
standard library including a full macro expander and evaluator.&lt;/p&gt;&lt;p&gt;Normally Hoot would do some aggressive
&lt;a href="https://wingolog.org/archives/2023/11/24/tree-shaking-the-horticulturally-misguided-algorithm"&gt;tree-shaking&lt;/a&gt;
to discard any definitions not used by the program, but with a REPL we don’t know what we might need.  So, we pass
&lt;tt&gt;-fruntime-modules&lt;/tt&gt; to instruct Hoot to record all modules and their
bindings in a central registry, so they can be looked up at run-time.
This results in a 6.6 MB Wasm file; with tree-shaking we would have been
at 1.2 MB.&lt;/p&gt;&lt;p&gt;Next, &lt;a href="https://codeberg.org/andywingo/wastrel#building"&gt;build Wastrel from
source&lt;/a&gt;, and compile
our new &lt;tt&gt;repl.wasm&lt;/tt&gt;:&lt;/p&gt;&lt;pre&gt;wastrel compile -o repl repl.wasm
&lt;/pre&gt;&lt;p&gt;This takes about 5 minutes on my machine: about 3 minutes to generate
all the C, about 6.6MLOC all in all, split into a couple hundred files
of about 30KLOC each, and then 2 minutes to compile with GCC and
link-time optimization (parallelised over 32 cores in my case).  I have
some ideas to golf the first part down a bit, but the the GCC side will
resist improvements.&lt;/p&gt;&lt;p&gt;Finally, the moment of truth:&lt;/p&gt;&lt;pre&gt;$ ./repl
Hoot 0.8.0

Enter `,help' for help.
(hoot user)&amp;gt; "hello, world!"
=&amp;gt; "hello, world!"
(hoot user)&amp;gt;
&lt;/pre&gt;&lt;h3&gt;statics&lt;/h3&gt;&lt;p&gt;When I first got the REPL working last week, I gasped out loud: it’s
alive, it’s alive!!!  Now that some days have passed, I am finally able
to look a bit more dispassionately at where we’re at.&lt;/p&gt;&lt;p&gt;Firstly, let’s look at the compiled binary itself.  By default, Wastrel
passes the &lt;tt&gt;-g&lt;/tt&gt; flag to GCC, which results in binaries with embedded
debug information.  Which is to say, my &lt;tt&gt;./repl&lt;/tt&gt; is chonky: 180 MB!!
Stripped, it’s “just” 33 MB.  92% of that is in the &lt;tt&gt;.text&lt;/tt&gt; (code)
section.  I would like a smaller binary, but it’s what we got for now:
each byte in the Wasm file corresponds to around 5 bytes in the x86-64
instruction stream.&lt;/p&gt;&lt;p&gt;As for dependencies, this is a pretty minimal binary, though dynamically
linked to &lt;tt&gt;libc&lt;/tt&gt;:&lt;/p&gt;&lt;pre class="pre-$ ldd repl"&gt;linux-vdso.so.1 (0x00007f6c19fb0000)
libm.so.6 =&amp;gt; /gnu/store/…-glibc-2.41/lib/libm.so.6 (0x00007f6c19eba000)
libgcc_s.so.1 =&amp;gt; /gnu/store/…-gcc-15.2.0-lib/lib/libgcc_s.so.1 (0x00007f6c19e8d000)
libc.so.6 =&amp;gt; /gnu/store/…-glibc-2.41/lib/libc.so.6 (0x00007f6c19c9f000)
/gnu/store/…-glibc-2.41/lib/ld-linux-x86-64.so.2 (0x00007f6c19fb2000)
&lt;/pre&gt;&lt;p&gt;Our compiled &lt;tt&gt;./repl&lt;/tt&gt; includes a garbage collector from
&lt;a href="https://github.com/wingo/whippet"&gt;Whippet&lt;/a&gt;, about which, more in a
minute.  For now, we just note that our use of Whippet introduces no
run-time dependencies.&lt;/p&gt;&lt;h3&gt;dynamics&lt;/h3&gt;&lt;p&gt;Just running the REPL with &lt;tt&gt;WASTREL_PRINT_STATS=1&lt;/tt&gt; in the environment,
it seems that the REPL has a peak live data size of 4MB or so, but for
some reason uses 15 MB total.  It takes about 17 ms to start up and then
exit.&lt;/p&gt;&lt;p&gt;These numbers I give are consistent over a choice of particular garbage
collector implementations: the default
&lt;tt&gt;--gc=stack-conservative-parallel-generational-mmc&lt;/tt&gt;, or the
non-generational &lt;tt&gt;stack-conservative-parallel-mmc&lt;/tt&gt;, or the
Boehm-Demers-Weiser &lt;tt&gt;bdw&lt;/tt&gt;.  Benchmarking collectors is a bit gnarly
because the dynamic heap growth heuristics aren’t the same between the
various collectors; by default, the heap grows to 15 MB or so with all
collectors, but whether it chooses to collect or expand the heap in
response to allocation affects startup timing.  I get the above startup
numbers by setting &lt;tt&gt;GC_OPTIONS=heap-size=15m,heap-size-policy=fixed&lt;/tt&gt; in
the environment.&lt;/p&gt;&lt;p&gt;Hoot implements &lt;a href="https://gnu.org/s/guile"&gt;Guile Scheme&lt;/a&gt;, so we can also
benchmark Hoot against Guile.  Given the following test program that
sums the leaf values for ten thousand quad trees of height 5:&lt;/p&gt;&lt;pre&gt;(define (quads depth)
  (if (zero? depth)
      1
      (vector (quads (- depth 1))
              (quads (- depth 1))
              (quads (- depth 1))
              (quads (- depth 1)))))
(define (sum-quad q)
  (if (vector? q)
      (+ (sum-quad (vector-ref q 0))
         (sum-quad (vector-ref q 1))
         (sum-quad (vector-ref q 2))
         (sum-quad (vector-ref q 3)))
      q))

(define (sum-of-sums n depth)
  (let lp ((n n) (sum 0))
    (if (zero? n)
        sum
        (lp (- n 1)
            (+ sum (sum-quad (quads depth)))))))


(sum-of-sums #e1e4 5)
&lt;/pre&gt;&lt;p&gt;We can cat it to our &lt;tt&gt;repl&lt;/tt&gt; to see how we do:&lt;/p&gt;&lt;pre class="pre-cat test.scm | WASTREL_PRINT_STATS=1 ./repl"&gt;Hoot 0.8.0

Enter `,help' for help.
(hoot user)&amp;gt; =&amp;gt; 10240000
(hoot user)&amp;gt;
Completed 3 major collections (281 minor).
4445.267 ms total time (84.214 stopped); 4556.235 ms CPU time (189.188 stopped).
0.256 ms median pause time, 0.272 p95, 7.168 max.
Heap size is 28.269 MB (max 28.269 MB); peak live data 9.388 MB.
&lt;/pre&gt;&lt;p&gt;That is to say, 4.44s, of which 0.084s was spent in garbage collection
pauses.  The default collector configuration is generational, which can
result in some odd heap growth patterns; as it happens, this workload
runs fine in a 15MB heap.  Pause time as a percentage of total
run-time is very low, so all the various GCs perform the same, more or
less; we seem to be benchmarking &lt;tt&gt;eval&lt;/tt&gt; more than the GC itself.&lt;/p&gt;&lt;p&gt;Is our Wastrel-compiled &lt;tt&gt;repl&lt;/tt&gt; performance good?  Well, we can evaluate
it in two ways.  Firstly, against Chrome or Firefox, which can run the same
program; if I paste in the above program in the REPL over at &lt;a href="https://spritely.institute/hoot/"&gt;the Hoot
web site&lt;/a&gt;, it takes about 5 or 6 times as
long to complete, respectively.  Wastrel wins!&lt;/p&gt;&lt;p&gt;I can also try this program under Guile itself: if I &lt;tt&gt;eval&lt;/tt&gt; it in Guile,
it takes about 3.5s.  Granted, Guile’s implementation of the same source
language is different, and it benefits from a number of representational
tricks, for example using just two words for a pair instead of four on
Hoot+Wastrel.  But these numbers are in the same ballpark, which is
heartening.  Compiling the test program instead of interpreting is about 10× faster with both Wastrel and Guile, with a similar relative ratio.&lt;/p&gt;&lt;p&gt;Finally, I should note that Hoot’s binaries are pretty well optimized in
many ways, but not in all the ways.  Notably, they use too many locals,
and the &lt;a href="https://codeberg.org/spritely/hoot/src/branch/main/module/wasm/optimize.scm#L25-L47"&gt;post-pass to fix this is
unimplemented&lt;/a&gt;,
and last time I checked (a long time ago!), &lt;tt&gt;wasm-opt&lt;/tt&gt; didn’t work on
our binaries.  I should take another look some time.&lt;/p&gt;&lt;h3&gt;generational?&lt;/h3&gt;&lt;p&gt;This week I dotted all the t’s and crossed all the i’s to emit write
barriers when we mutate the value of a field to store a new GC-managed
data type, allowing me to enable the &lt;a href="https://wingolog.org/archives/2022/10/22/the-sticky-mark-bit-algorithm"&gt;sticky
mark-bit&lt;/a&gt;
variant of the Immix-inspired &lt;a href="https://github.com/wingo/whippet/blob/main/doc/collector-mmc.md"&gt;mostly-marking
collector&lt;/a&gt;.
It seems to work fine, though &lt;a href="https://wingolog.org/archives/2025/02/09/baffled-by-generational-garbage-collection"&gt;this kind of generational collector still
baffles me
sometimes&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;With all of this, Wastrel’s GC-using binaries use a
&lt;a href="https://github.com/wingo/whippet/blob/main/doc/collector-mmc.md#conservative-stack-scanning"&gt;stack-conservative&lt;/a&gt;,
&lt;a href="https://github.com/wingo/whippet/blob/main/doc/collector-mmc.md#parallel-tracing"&gt;parallel&lt;/a&gt;,
&lt;a href="https://github.com/wingo/whippet/blob/main/doc/collector-mmc.md#generations"&gt;generational&lt;/a&gt;
collector that can compact the heap as needed.  This collector supports
multiple concurrent mutator threads, though Wastrel doesn’t do threading
yet.  Other collectors can be chosen at compile-time, though
always-moving collectors are off the table due to not emitting stack
maps.&lt;/p&gt;&lt;p&gt;The neat thing is that any language that compiles to Wasm can have any
of these collectors!  And when the
&lt;a href="https://github.com/wingo/whippet"&gt;Whippet&lt;/a&gt; GC library gets another
collector or another mode on an existing collector, you can have that
too.&lt;/p&gt;&lt;h3&gt;missing pieces&lt;/h3&gt;&lt;p&gt;The biggest missing piece for Wastrel and Hoot is some kind of
asynchrony, similar to &lt;a href="https://v8.dev/blog/jspi"&gt;JavaScript Promise Integration
(JSPI)&lt;/a&gt;, and somewhat related to &lt;a href="https://github.com/WebAssembly/stack-switching"&gt;stack
switching&lt;/a&gt;.  You want
Wasm programs to be able to wait on external events, and Wastrel doesn’t
support that yet.&lt;/p&gt;&lt;p&gt;Other than that, it would be lovely to experiment with &lt;a href="https://github.com/WebAssembly/shared-everything-threads"&gt;Wasm
shared-everything
threads&lt;/a&gt; at
some point.&lt;/p&gt;&lt;h3&gt;what’s next&lt;/h3&gt;&lt;p&gt;So I have an ahead-of-time Wasm compiler.  It does GC and lots of neat
things.  Its performance is state-of-the-art.  It implements a few
standard libraries, including WASI 0.1 and Hoot.  It can make a pretty
good standalone Guile REPL.  &lt;i&gt;But what the hell is it for?&lt;/i&gt;&lt;/p&gt;&lt;p&gt;Friends, I... I don’t know!  It’s really cool, but I don’t yet know who
needs it.  I have a few purposes of my own (pushing Wasm standards,
performance work on Whippet, etc), but you or someone you know needs a
wastrel, do let me know at &lt;tt&gt;wingo@igalia.com&lt;/tt&gt;: I would love to be able
to spend more time hacking in this area.&lt;/p&gt;&lt;p&gt;Until next time, happy compiling to all!&lt;/p&gt;&lt;/div&gt;</description>
      <guid isPermaLink="false">https://wingolog.org/archives/2026/04/09/wastrel-milestone-full-hoot-support-with-generational-gc-as-a-treat</guid>
      <pubDate>Thu, 09 Apr 2026 13:48:04 +0000</pubDate>
    </item>
    <item>
      <title>Bilal Elmoussaoui: goblint: A Linter for GObject C Code</title>
      <link>https://belmoussaoui.com/blog/23-goblin-linter/</link>
      <description>&lt;p&gt;Over the past week, I’ve been building &lt;strong&gt;goblint&lt;/strong&gt;, a linter specifically designed for GObject-based C codebases.&lt;/p&gt;
&lt;p&gt;If you know Rust’s &lt;code&gt;clippy&lt;/code&gt; or Go’s &lt;code&gt;go vet&lt;/code&gt;, think of goblint as the same thing for GObject/GLib.&lt;/p&gt;
&lt;h2 id="why-this-exists"&gt;Why this exists&lt;/h2&gt;
&lt;p&gt;A large part of the Linux desktop stack (GTK, Mutter, Pango, NetworkManager) is built on GObject. These projects have evolved over decades and carry a lot of patterns that predate newer GLib helpers, are easy to misuse, or encode subtle lifecycle invariants that nothing verifies.&lt;/p&gt;
&lt;p&gt;This leads to issues like missing &lt;code&gt;dispose&lt;/code&gt;/&lt;code&gt;finalize&lt;/code&gt;/&lt;code&gt;constructed&lt;/code&gt; chain-ups (memory leaks or undefined behavior), incorrect property definitions, uninitialized &lt;code&gt;GError*&lt;/code&gt; variables, or function declarations with no implementation.&lt;/p&gt;
&lt;p&gt;These aren’t theoretical. &lt;a href="https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/9789" rel="nofollow noreferrer"&gt;This GTK merge request&lt;/a&gt; recently fixed several missing chain-ups in example code.&lt;/p&gt;
&lt;p&gt;Despite this, the C ecosystem lacks a linter that understands GObject semantics. goblint exists to close that gap.&lt;/p&gt;
&lt;h2 id="what-goblint-checks"&gt;What goblint checks&lt;/h2&gt;
&lt;p&gt;goblint ships with &lt;a href="https://github.com/bilelmoussaoui/goblint/blob/main/RULES.md" rel="nofollow noreferrer"&gt;&lt;strong&gt;35 rules&lt;/strong&gt;&lt;/a&gt; across different categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correctness&lt;/strong&gt;: Real bugs like non-canonical property names, uninitialized &lt;code&gt;GError*&lt;/code&gt;, missing &lt;code&gt;PROP_0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suspicious&lt;/strong&gt;: Likely mistakes like missing implementations or redundant NULL checks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style&lt;/strong&gt;: Idiomatic GLib usage (&lt;code&gt;g_strcmp0&lt;/code&gt;, &lt;code&gt;g_str_equal()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complexity&lt;/strong&gt;: Suggests modern helpers (&lt;code&gt;g_autoptr&lt;/code&gt;, &lt;code&gt;g_clear_*&lt;/code&gt;, &lt;code&gt;g_set_str()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Optimizations like &lt;code&gt;G_PARAM_STATIC_STRINGS&lt;/code&gt; or &lt;code&gt;g_object_notify_by_pspec()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pedantic&lt;/strong&gt;: Consistency checks (macro semicolons, matching declare/define pairs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;23 out of 35 rules are auto-fixable.&lt;/strong&gt; You should apply fixes one rule at a time to review the changes:&lt;/p&gt;
&lt;pre class="language-bash" style="background-color: #2e3440; color: #d8dee9;"&gt;&lt;code class="language-bash"&gt;&lt;span style="color: #88c0d0;"&gt;goblint&lt;/span&gt;&lt;span&gt; --fix --only use_g_strcmp0
&lt;/span&gt;&lt;span style="color: #88c0d0;"&gt;goblint&lt;/span&gt;&lt;span&gt; --fix --only use_clear_functions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="ci-cd-integration"&gt;CI/CD Integration&lt;/h2&gt;
&lt;p&gt;goblint fits into existing pipelines.&lt;/p&gt;
&lt;h3 id="github-actions"&gt;GitHub Actions&lt;/h3&gt;
&lt;pre class="language-yaml" style="background-color: #2e3440; color: #d8dee9;"&gt;&lt;code class="language-yaml"&gt;&lt;span&gt;- &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;name&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;Run goblint
&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;run&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;goblint --format sarif &amp;gt; goblint.sarif
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;name&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;Upload SARIF results
&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;uses&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;github/codeql-action/upload-sarif@v3
&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;with&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;:
&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;sarif_file&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;goblint.sarif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Results show up in the Security tab under "Code scanning" and inline on pull requests.&lt;/p&gt;
&lt;h3 id="gitlab-ci"&gt;GitLab CI&lt;/h3&gt;
&lt;pre class="language-yaml" style="background-color: #2e3440; color: #d8dee9;"&gt;&lt;code class="language-yaml"&gt;&lt;span style="color: #8fbcbb;"&gt;goblint&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;:
&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;image&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;ghcr.io/bilelmoussaoui/goblint:latest
&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;script&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;:
&lt;/span&gt;&lt;span&gt;    - &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;goblint --format sarif &amp;gt; goblint.sarif
&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;artifacts&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;:
&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;reports&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;:
&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;span style="color: #8fbcbb;"&gt;sast&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;: &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;goblint.sarif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Results appear inline in merge requests.&lt;/p&gt;
&lt;h2 id="configuration"&gt;Configuration&lt;/h2&gt;
&lt;p&gt;Rules default to &lt;code&gt;warn&lt;/code&gt;, and can be tuned via &lt;code&gt;goblint.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="language-toml" style="background-color: #2e3440; color: #d8dee9;"&gt;&lt;code class="language-toml"&gt;&lt;span style="color: #81a1c1;"&gt;min_glib_version &lt;/span&gt;&lt;span&gt;= &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"2.40"  &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Auto-disable rules for newer versions
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;[rules]
&lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;g_param_spec_static_name_canonical &lt;/span&gt;&lt;span&gt;= &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"error"  &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Make critical
&lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;use_g_strcmp0 &lt;/span&gt;&lt;span&gt;= &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"warn"  &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Keep as warning
&lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;use_g_autoptr_inline_cleanup &lt;/span&gt;&lt;span&gt;= &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"ignore"  &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Disable
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Per-rule ignore patterns
&lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;missing_implementation &lt;/span&gt;&lt;span&gt;= { &lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;level &lt;/span&gt;&lt;span&gt;= &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"error"&lt;/span&gt;&lt;span style="color: #eceff4;"&gt;, &lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;ignore &lt;/span&gt;&lt;span&gt;= [&lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"src/backends/**"&lt;/span&gt;&lt;span&gt;] }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can adopt it gradually without fixing everything at once.&lt;/p&gt;
&lt;h2 id="try-it"&gt;Try it&lt;/h2&gt;
&lt;pre class="language-bash" style="background-color: #2e3440; color: #d8dee9;"&gt;&lt;code class="language-bash"&gt;&lt;span style="color: #616e88;"&gt;# Run via container
&lt;/span&gt;&lt;span style="color: #88c0d0;"&gt;podman&lt;/span&gt;&lt;span&gt; run --rm -v &lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;"&lt;/span&gt;&lt;span style="color: #81a1c1;"&gt;$&lt;/span&gt;&lt;span&gt;PWD&lt;/span&gt;&lt;span style="color: #a3be8c;"&gt;:/workspace:Z"&lt;/span&gt;&lt;span&gt; ghcr.io/bilelmoussaoui/goblint:latest
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Install locally
&lt;/span&gt;&lt;span style="color: #88c0d0;"&gt;cargo&lt;/span&gt;&lt;span&gt; install --git https://github.com/bilelmoussaoui/goblint goblint
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Usage
&lt;/span&gt;&lt;span style="color: #88c0d0;"&gt;goblint              &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Lint current directory
&lt;/span&gt;&lt;span style="color: #88c0d0;"&gt;goblint&lt;/span&gt;&lt;span&gt; --fix        &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Apply automatic fixes
&lt;/span&gt;&lt;span style="color: #88c0d0;"&gt;goblint&lt;/span&gt;&lt;span&gt; --list-rules &lt;/span&gt;&lt;span style="color: #616e88;"&gt;# Inspect available rules
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://github.com/bilelmoussaoui/goblint" rel="nofollow noreferrer"&gt;project&lt;/a&gt; is early, so feedback is especially valuable (false positives, missing checks, workflow issues, etc.).&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The project was originally named "goblin" but was renamed to "goblint" to avoid conflicts with the existing &lt;a href="https://crates.io/crates/goblin" rel="nofollow noreferrer"&gt;goblin&lt;/a&gt; crate for parsing binary formats.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://belmoussaoui.com/blog/23-goblin-linter/</guid>
      <pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Peter Hutterer: Huion devices in the desktop stack</title>
      <link>http://who-t.blogspot.com/2026/04/huion-devices-in-desktop-stack.html</link>
      <description>&lt;p&gt;
  This post attempts to explain how Huion tablet devices currently integrate into the desktop stack. I'll touch a bit on the Huion driver and the OpenTablet driver but primarily this explains the &lt;em&gt;intended&lt;/em&gt; integration[1]. While I have access to some Huion devices and have seen reports from others, there are likely devices that are slightly different. Huion's vendor ID is also used by other devices (UCLogic and Gaomon) so this applies to those devices as well.
&lt;/p&gt;
&lt;p&gt;This post was written without AI support, so any errors are organic artisian hand-crafted ones. Enjoy.&lt;/p&gt;
&lt;h1&gt;The graphics tablet stack&lt;/h1&gt;
&lt;p&gt;
  First, a short overview of the ideal graphics tablet stack in current desktops. At the bottom is the physical device which contains a significant amount of firmware. That device provides something resembling the &lt;a href="https://who-t.blogspot.com/2018/12/understanding-hid-report-descriptors.html"&gt;HID protocol&lt;/a&gt; over the wire (or bluetooth) to the kernel. The kernel typically handles this via the generic HID drivers [2] and provides us with an &lt;code&gt;/dev/input/event&lt;/code&gt; evdev node, ideally one for the pen (and any other tool) and one for the pad (the buttons/rings/wheels/dials on the physical tablet). libinput then interprets the data from these event nodes, passes them on to the compositor which then passes them via Wayland to the client. Here's a simplified illustration of this:
&lt;div class="separator" style="clear: both;"&gt;&lt;a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVYmEs-CeH1xuV0NCGBLu90fyncRp0XJGrLnYGrj1tKSk-b1s7PtdckX1rFey_a75SkvIw7epJVhe4NmDb4SLp8C8rOBdbFKOYpLae2ibMGjPtgj7J92NHFGVFrUoXB3i_0e0xSR_dHBKto-uXZkDQLXXxvlPOlZzjGddOcnBodbHQ-yKLzKvC1ZWdMfA1/s1621/input-stack.png" style="display: block; padding: 1em 0; text-align: center;"&gt;&lt;img alt="" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVYmEs-CeH1xuV0NCGBLu90fyncRp0XJGrLnYGrj1tKSk-b1s7PtdckX1rFey_a75SkvIw7epJVhe4NmDb4SLp8C8rOBdbFKOYpLae2ibMGjPtgj7J92NHFGVFrUoXB3i_0e0xSR_dHBKto-uXZkDQLXXxvlPOlZzjGddOcnBodbHQ-yKLzKvC1ZWdMfA1/s400/input-stack.png" width="400"/&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;
  Unlike the X11 api, libinput's API works both per-tablet and per-tool basis. In other words, when you plug in a tablet you get a libinput device that has a tablet tool capability and (optionally) a tablet pad capability. But the tool will only show up once you bring it into proximity. Wacom tools have sufficient identifiers that we can a) know what tool it is and b) get a unique serial number for that particular device. This means you can, if you wanted to, track your physical tool as it is used on multiple devices. No-one [3] does this but it's possible. More interesting is that because of this you can also configure the tools individually, different pressure curves, etc. This was possible with the xf86-input-wacom driver in X but only with some extra configuration, libinput provides/requires this as the default behaviour.
&lt;/p&gt;
&lt;p&gt;
  The most prominent case for this is the eraser which is present on virtually all pen-like tools though some will have an eraser at the tail end and others (the numerically vast majority) will have it hardcoded on one of the buttons. Changing to eraser mode will create a new tool (the eraser) and bring it into proximity - that eraser tool is logically separate from the pen tool and can thus be configured differently. [4]
&lt;/p&gt;
&lt;p&gt;
  Another effect of this per-tool behaviour is also that we know exactly what a tool can do. If you use two different styli with different capabilities (e.g. one with tilt and 2 buttons, one without tilt and 3 buttons), they will have the right bits set. This requires libwacom - a library that tells us, simply: any tool with id 0x1234 has N buttons and capabilities A, B and C. libwacom is just a bunch of static text files with a C library wrapped around those. Without libwacom, we cannot know what any individual tool can do - the firmware and kernel always expose the capability set of all tools that can be used on any particular tablet. For example: wacom's devices support an airbrush tool so any tablet plugged in will announce the capabilities for an airbrush even though &amp;gt;99% of users will never use an airbrush [5].
&lt;/p&gt;
&lt;p&gt;
  The compositor then takes the libinput events, modifies them (e.g. pressure curve handling is done by the compositor) and passes them via the Wayland protocol to the client. That protocol is a pretty close mirror of the libinput API so it works mostly the same. From then on, the rest is up to the application/toolkit.
&lt;/p&gt;
&lt;p&gt;
  Notably, libinput is a hardware abstraction layer and conversion of hardware events into others is generally left to the compositor. IOW if you want a button to generate a key event, that's done either in the compositor or in the application/toolkit. But the current versions of libinput and the Wayland protocol do support all hardware features we're currently aware of: the various stylus types (including Wacom's lens cursor and mouse-like "puck" devices) and buttons, rings, wheels/dials, and touchstrips on pads. We even support the rather once-off Dell Canvas Totem device.
&lt;/p&gt;
&lt;h1&gt;Huion devices&lt;/h1&gt;
&lt;p&gt;
  Huion's devices are HID compatible which means they "work" out of the box but they come in two different modes, let's call them firmware mode and tablet mode. Each tablet device pretends to be three HID devices on the wire and depending on the mode some of those devices won't send events.
&lt;/p&gt;
&lt;h2&gt;Firmware mode&lt;/h2&gt;
&lt;p&gt;
  This is the default mode after plugging the device in. Two of the HID devices exposed look like a tablet stylus and a keyboard. The tablet stylus is usually correct (enough) to work OOTB with the generic kernel drivers, it exports the buttons, pressure, tilt, etc. The buttons and strips/wheels/dials on the tablet are configured to send key events. For example, the Inspiroy 2S I have sends b/i/e/Ctrl+S/space/Ctrl+Alt+z for the buttons and the roller wheel sends Ctrl-/Ctrl= depending on direction. The latter are often interpreted as zoom in/out so hooray, things work OOTB. Other Huion devices have similar bindings, there is quite some overlap but not all devices have exactly the same key assignments for each button. It does of course get a lot more interesting when you want a button to do something different - you need to remap the key event (ideally without messing up your key map lest you need to type an 'e' later).
&lt;/p&gt;
&lt;p&gt;
  The userspace part is effectively the same, so here's a simplified illustration of what happens in kernel land:
  
  &lt;div class="separator" style="clear: both;"&gt;&lt;a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiH6gEv85WQ8Vkz3RqAgMVMB7dPIbrSQuuig4ShIuiR9rmMvEikI7UwKcjkryXO7EmTYhte8K9KT41ucretIsQBCx72iJhjMMn1g7p4FdphkWU81bmWR1cw8r8kW8rYY5la5GE1-lYp7iNZdjHd35NcMNfnaTkzA1ohzKtjsXDJEwyfjlFNgAoZc4gddmuo/s862/kernel-stack.png" style="display: block; padding: 1em 0; text-align: center;"&gt;&lt;img alt="" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiH6gEv85WQ8Vkz3RqAgMVMB7dPIbrSQuuig4ShIuiR9rmMvEikI7UwKcjkryXO7EmTYhte8K9KT41ucretIsQBCx72iJhjMMn1g7p4FdphkWU81bmWR1cw8r8kW8rYY5la5GE1-lYp7iNZdjHd35NcMNfnaTkzA1ohzKtjsXDJEwyfjlFNgAoZc4gddmuo/s320/kernel-stack.png" width="320"/&gt;&lt;/a&gt;&lt;/div&gt;

Any vendor-specific data is discarded by the kernel (but in this mode that HID device doesn't send events anyway).
&lt;/p&gt;
&lt;h2&gt;Tablet mode&lt;/h2&gt;
&lt;p&gt;
  If you read a special USB string descriptor from the English language ID, the device switches into tablet mode. Once in tablet mode, the HID tablet stylus and keyboard devices will stop sending events and instead all events from the device are sent via the third HID device which consists of a single vendor-specific report descriptor (read: 11 bytes of "here be magic"). Those bits represent the various features on the device, including the stylus features and all pad features as buttons/wheels/rings/strips (and not key events!). This mode is the one we want to handle the tablet properly. The kernel's hid-uclogic driver switches into tablet mode for supported devices, in userspace you can use e.g. &lt;a href="https://github.com/whot/huion-switcher"&gt;huion-switcher&lt;/a&gt;. The device cannot be switched back to firmware mode but will return to firmware mode once unplugged.
&lt;/p&gt;
&lt;p&gt;
  Once we have the device in tablet mode, we can get true tablet data and pass it on through our intended desktop stack. Alas, like ogres there are layers.
&lt;/p&gt;
&lt;h3&gt;hid-uclogic and udev-hid-bpf&lt;/h3&gt;
&lt;p&gt;
  Historically and thanks in large parts to the now-discontinued &lt;a href="https://digimend.github.io/"&gt;digimend project&lt;/a&gt;, the hid-uclogic kernel driver did do the switching into tablet mode, followed by report descriptor mangling (inside the kernel) so that the resulting devices can be handled by the generic HID drivers. The more modern approach we are pushing for is to use &lt;a href="https://who-t.blogspot.com/2024/04/udev-hid-bpf-quickstart-tooling-to-fix.html"&gt;udev-hid-bpf&lt;/a&gt; which is quite a bit easer to develop for. But both do effectively the same thing: they overlay the vendor-specific data with a normal HID report descriptor so that the incoming data can be handled by the generic HID kernel drivers. This will look like this:
&lt;/p&gt;
&lt;div class="separator" style="clear: both;"&gt;&lt;a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidmzeph_EUqGRnlOajchCbphHENd4p8Kpk53Hk-pnrXWH-r6LUCTcaGbZF6vYeFYz66EH0TU5t5xTURoUj_nM8pVUZQFn2FDEX1uOTd8uX7bDEsk-gs3027e9Kc3st1Ov_kp5dkRJRcgahOvSzTQLTiknyK_h56Wjt9Vg6_i5zWQVOcGa39mdP6rsUN1qm/s1081/kernel-stack.png" style="display: block; padding: 1em 0; text-align: center;"&gt;&lt;img alt="" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidmzeph_EUqGRnlOajchCbphHENd4p8Kpk53Hk-pnrXWH-r6LUCTcaGbZF6vYeFYz66EH0TU5t5xTURoUj_nM8pVUZQFn2FDEX1uOTd8uX7bDEsk-gs3027e9Kc3st1Ov_kp5dkRJRcgahOvSzTQLTiknyK_h56Wjt9Vg6_i5zWQVOcGa39mdP6rsUN1qm/s320/kernel-stack.png" width="320"/&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;
  Notable here: the stylus and keyboard may still exist and get event nodes but never send events[6] but the uclogic/bpf-enabled device will be proper stylus/pad event nodes that can be handled by libinput (and thus the rest), with raw hardware data where buttons are buttons.
&lt;/p&gt;
&lt;h1&gt;Challenges&lt;/h1&gt;
&lt;p&gt;Because in true manager speak we don't have problems, just challenges. And oh boy, we collect challenges as if we'd be organising the olypmics.&lt;/p&gt;
&lt;h2&gt;hid-uclogic and libinput&lt;/h2&gt;
&lt;p&gt;
  First and probably most embarrassing is that hid-uclogic has a different way of exposing event nodes than what libinput expects. This is largely my fault for having focused on Wacom devices and internalized their behaviour for long years. The hid-uclogic driver exports the wheels and strips on separate event nodes - libinput doesn't handle this correctly (or at all). That'd be fixable but the compositors also don't really expect this so there's a bit more work involved but the immediate effect is that those wheels/strips will likely be ignored and not work correctly. Buttons and pens work.
&lt;/p&gt;
&lt;div class="separator" style="clear: both;"&gt;&lt;a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ5Fuh4nc-tE79ov3K6KbbZ4CjU0D368lcLqTmtmbk8zNKQS7Q7iqqLwtfMYfcb71NEh_a3WQK_mBvUwr8O7OKFxwxFaHGc8wMJFb0SiRd6_9r8vnA3QZiGbDws8hNd3iUa_tIJHUzhKDjQ8cuIhxq4ZasHdvFuhl6j-V4qsYVQWzqLVUbdsfeK0fX2Pjw/s980/kernel-stack.png" style="display: block; padding: 1em 0; text-align: center;"&gt;&lt;img alt="" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ5Fuh4nc-tE79ov3K6KbbZ4CjU0D368lcLqTmtmbk8zNKQS7Q7iqqLwtfMYfcb71NEh_a3WQK_mBvUwr8O7OKFxwxFaHGc8wMJFb0SiRd6_9r8vnA3QZiGbDws8hNd3iUa_tIJHUzhKDjQ8cuIhxq4ZasHdvFuhl6j-V4qsYVQWzqLVUbdsfeK0fX2Pjw/s320/kernel-stack.png" width="320"/&gt;&lt;/a&gt;&lt;/div&gt;
&lt;h2&gt;udev-hid-bpf and huion-switcher&lt;/h2&gt;
&lt;p&gt;
  hid-uclogic being a kernel driver has access to the underlying USB device. The HID-BPF hooks in the kernel currently do not, so we cannot switch the device into tablet mode from a BPF, we need it in tablet mode already. This means a userspace tool (read: huion-switcher) triggered via udev on plug-in and before the udev-hid-bpf udev rules trigger. Not a problem but it's one more moving piece that needs to be present (but boy, does this feel like the unix way...).
&lt;/p&gt;
&lt;h2&gt;Huion's precious product IDs&lt;/h2&gt;
&lt;p&gt;
  By far the most annoying part about anything Huion is that until relatively recently (I don't have a date but maybe until 2 years ago) &lt;em&gt;all&lt;/em&gt; of Huion's devices shared the same few USB product IDs. For &lt;em&gt;most&lt;/em&gt; of these devices we worked around it by matching on device names but there were devices that had the same product id &lt;em&gt;and&lt;/em&gt; device name. At some point libwacom and the kernel and huion-switcher had to implement firmware ID extraction and matching so we could differ between devices with the same 0256:006d usb IDs. Luckily this seems to be in the past now with modern devices now getting new PIDs for each individual device. But if you have an older device, expect difficulties and, worse, things to potentially break after firmware updates when/if the firmware identification string changes. udev-hid-bpf (and uclogic) rely on the firmware strings to identify the device correctly.
&lt;/p&gt;
&lt;p&gt;&lt;i&gt;edit: and of course less than 24h after posting this I process a bug report about two completely different new devices sharing one of the product IDs&lt;/i&gt;&lt;/p&gt;
&lt;h2&gt;udev-hid-bpf and hid-uclogic&lt;/h2&gt;
&lt;p&gt;
  Because we have a changeover from the hid-uclogic kernel driver to the udev-hid-bpf files there are rough edges on "where does this device go". The general rule is now: if it's not a shared product ID (see above) it should go into udev-hid-bpf and not the uclogic driver. Easier to maintain, much more fire-and-forget. Devices already supported by udev-hid-bpf will remain there, we won't implement BPFs for those (older) devices, doubly so because of the aforementioned libinput difficulties with some hid-uclogic features.
&lt;/p&gt;
&lt;h2&gt;Reverse engineering required&lt;/h2&gt;
&lt;p&gt;
  The newer tablets are always slightly different so we basically need to reverse-engineer each tablet to get it working. That's common enough for any device but we do rely on volunteers to do this. Mind you, the udev-hid-bpf approach is much simpler than doing it in the kernel, much of it is now copy-paste and I've even had quite some success to get e.g. Claude Code to spit out a 90% correct BPF on its first try. At least the advantage of our approach to change the report descriptor means once it's done it's done forever, there is no maintenance required because it's a static array of bytes that doesn't ever change.
&lt;/p&gt;
&lt;h2&gt;Plumbing support into userspace&lt;/h2&gt;
&lt;p&gt;
   Because we're abstracting the hardware, userspace needs to be fully plumbed. This was a problem last year for example when we (slowly) got support for relative wheels into libinput, then wayland, then the compositors, then the toolkits to make it available to the applications (of which I think none so far use the wheels). Depending on how fast your distribution moves, this may mean that support is months and years off even when everything has been implemented. On the plus side these new features tend to only appear once every few years. Nonetheless, it's not hard to see why the "just sent Ctrl=, that'll do" approach is preferred by many users over "probably everything will work in 2027, I'm sure".
&lt;/p&gt;
&lt;h2&gt;So, what stylus is this?&lt;/h2&gt;
&lt;p&gt;
  A currently unsolved problem is the lack of tool IDs on all Huion tools. We cannot know if the tool used is the two-button + eraser PW600L or the three-button-one-is-an-eraser-button PW600S or the two-button PW550 (I don't know if it's really 2 buttons or 1 button + eraser button). We always had this problem with e.g. the now quite old Wacom Bamboo devices but those pens all had the same functionality so it just didn't matter. It would matter less if the various pens would only work on the device they ship with but it's apparently quite possible to use a 3 button pen on a tablet that shipped with a 2 button pen OOTB. This is not difficult to solve (pretend to support all possible buttons on all tools) but it's frustrating because it removes a bunch of UI niceties that we've had for years - such as the pen settings only showing buttons that actually existed. Anyway, a problem currently in the "how I wish there was time" basket.
&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;
  Overall, we are in an ok state but not as good as we are for Wacom devices. The lack of tool IDs is the only thing not fixable without Huion changing the hardware[7]. The delay between a new device release and driver support is really just dependent on one motivated person reverse-engineering it (our BPFs can work across kernel versions and you can literally &lt;a href="https://libevdev.pages.freedesktop.org/udev-hid-bpf/installing-from-ci.html"&gt;download them from a successful CI pipeline&lt;/a&gt;). 
  The hid-uclogic split should become less painful over time and the same as the devices with shared USB product IDs age into landfill and even more so if libinput gains support for the separate event nodes for wheels/strips/... (there is currently no plan and I'm somewhat questioning whether anyone really cares). But other than that our main feature gap is really the ability for much more flexible configuration of buttons/wheels/... in &lt;em&gt;all&lt;/em&gt; compositors - having that would likely make the requirement for OpenTabletDriver and the Huion tablet disappear.
&lt;/p&gt;
&lt;h1&gt;OpenTabletDriver and Huion's own driver&lt;/h1&gt;
&lt;p&gt;The final topic here: what about the existing non-kernel drivers?&lt;p&gt;
&lt;p&gt;
    Both of these are userspace HID input drivers which all use the same approach: read from a &lt;code&gt;/dev/hidraw&lt;/code&gt; node, create a uinput device and pass events back. On the plus side this means you can do literally anything that the input subsystem supports, at the cost of a context switch for every input event. Again, a diagram on how this looks like (mostly) below userspace:
  &lt;/p&gt;
&lt;div class="separator" style="clear: both;"&gt;&lt;a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIhVZRSnyoFzf-_Zc7CrWP96B3itgaTpHZNIfQnCma4Wp8ra8FmYyaPq295uEoIsTeAhFjXClZsev_1sgp21kYFn1CAk3bZFoxvH9V_kiMUvarTHMwSdIP24dhgZf-sGqHlKRgdkRpw2ybFUeDIMRP-ukDzY9k1xyYMRZIA05SdZ_X25b9JES_gVseC45w/s1173/kernel-stack.png" style="display: block; padding: 1em 0; text-align: center;"&gt;&lt;img alt="" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIhVZRSnyoFzf-_Zc7CrWP96B3itgaTpHZNIfQnCma4Wp8ra8FmYyaPq295uEoIsTeAhFjXClZsev_1sgp21kYFn1CAk3bZFoxvH9V_kiMUvarTHMwSdIP24dhgZf-sGqHlKRgdkRpw2ybFUeDIMRP-ukDzY9k1xyYMRZIA05SdZ_X25b9JES_gVseC45w/s320/kernel-stack.png" width="320"/&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;
    Note how the kernel's HID devices are not exercised here at all because we parse the vendor report, create our own custom (separate) uinput device(s) and then basically re-implement the HID to evdev event mapping. This allows for great flexibility (and control, hence the vendor drivers are shipped this way) because any remapping can be done before you hit uinput. I don't immediately know whether OpenTabletDriver switches to firmware mode or maps the tablet mode but architecturally it doesn't make much difference.
  &lt;/p&gt;
&lt;p&gt;
    From a security perspective: having a userspace driver means you either need to run that driver daemon as root or (in the case of OpenTabletDriver at least) you need to allow uaccess to &lt;code&gt;/dev/uinput&lt;/code&gt;, usually via udev rules. Once those are &lt;em&gt;installed&lt;/em&gt;, anything can create uinput devices, which is a risk but how much is up for interpretation.
  &lt;/p&gt;
&lt;p&gt;
&lt;small&gt;
    [1] As is so often the case, even the intended state does not necessarily spark joy&lt;br/&gt;
    [2] Again, we're talking about the &lt;em&gt;intended&lt;/em&gt; case here...&lt;br/&gt;
    [3] fsvo "no-one"&lt;br/&gt;
    [4] The xf86-input-wacom driver always initialises a separate eraser tool even if you never press that button&lt;br/&gt;
    [5] For historical reasons those are also multiplexed so getting ABS_Z on a device has different meanings depending on the tool currently in proximity&lt;br/&gt;
    [6] In our udev-hid-bpf BPFs we hide those devices so you really only get the correct event nodes, I'm not immediately sure what hid-uclogic does&lt;br/&gt;
    [7] At which point Pandora will once again open the box because most of the stack is not yet ready for non-Wacom tool ids&lt;br/&gt;
&lt;/small&gt;
&lt;/p&gt;&lt;/p&gt;&lt;/p&gt;</description>
      <guid isPermaLink="false">http://who-t.blogspot.com/2026/04/huion-devices-in-desktop-stack.html</guid>
      <pubDate>Mon, 13 Apr 2026 06:47:00 +0000</pubDate>
    </item>
    <item>
      <title>Felipe Borges: RHEL 10 (GNOME 47) Accessibility Conformance Report</title>
      <link>https://blogs.gnome.org/feborges/rhel10-gnome47-acr/</link>
      <description>&lt;p&gt;Red Hat just published the&lt;a class="external" href="https://dxp-dpp-prod.apps.ext-waf.drop.prod.us-west-2.aws.paas.redhat.com/sites/products/files/2026-04/accessibility-conformance-report-rhel-10.0-gnome-desktop-environment.pdf"&gt; Accessibility Conformance Report (ACR) for Red Hat Enterprise Linux 10&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://access.redhat.com/compliance/accessibility"&gt;Accessibility Conformance Reports&lt;/a&gt; basically document how our software measures up against accessibility standards like &lt;a class="external" href="https://www.w3.org/TR/WCAG22/"&gt;WCAG&lt;/a&gt; and &lt;a class="external" href="https://www.access-board.gov/ict/"&gt;Section 508&lt;/a&gt;. Since RHEL 10 is built on GNOME 47, this report is a good look at how our stack handles various accessibility things from screen readers to keyboard navigation.&lt;/p&gt;
&lt;p&gt;Getting a desktop environment to meet these requirements is a huge task and it’s only possible because of the work done by our community in projects like: Orca, GTK, Libadwaita, Mutter, GNOME Shell, core apps, etc…&lt;/p&gt;
&lt;p&gt;Kudos to everyone in the GNOME project that cares about &lt;a class="external" href="https://developer.gnome.org/documentation/guidelines/accessibility.html"&gt;improving accessibility&lt;/a&gt;. We all know there’s a long way to go before desktop computing is fully accessible to everyone, but we are surely working on that.&lt;/p&gt;
&lt;p&gt;If you’re curious about the state of accessibility in the 47 release or how these audits work, you can find the full PDF&lt;a class="external" href="https://dxp-dpp-prod.apps.ext-waf.drop.prod.us-west-2.aws.paas.redhat.com/sites/products/files/2026-04/accessibility-conformance-report-rhel-10.0-gnome-desktop-environment.pdf"&gt; here&lt;/a&gt;.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/feborges/rhel10-gnome47-acr/</guid>
      <pubDate>Mon, 13 Apr 2026 10:00:49 +0000</pubDate>
    </item>
    <item>
      <title>Adrien Plazas: Monster World IV: Disassembly and Code Analysis</title>
      <link>https://adrienplazas.com/blog/2026/04/14/monster-world-iv-disassembly-and-code-analysis.html</link>
      <description>&lt;p&gt;This winter I was bored and needed something new, so I spent lots of my free
time disassembling and analysing Monster World IV for the SEGA Mega Drive.
More specifically, I looked at the 2008 Virtual Console revision of the game,
which adds an English translation to the original 1994 release.&lt;/p&gt;
&lt;p&gt;My long term goal would be to fully disassemble and analyse the game, port it to
C or Rust as I do, and then port it to the Game Boy Advance.
I don’t have a specific reason to do that, I just think it’s a charming game
from a dated but charming series, and I think the Monaster World series would be
a perfect fit on the Game Boy Advance.
Since a long time, I also wanted to experiment with disassembling or decompiling
code, understanding what doing so implies, understanding how retro computing
systems work, and understanding the inner workings of a game I enjoy.
Also, there is not publicly available disassembly of this game as far as I know.&lt;/p&gt;
&lt;p&gt;As Spring is coming, I sense my focus shifting to other projets, but I don’t
want this work to be gone forever and for everyone, especially not for future
me.
Hence, I decided to publish what I have here, so I can come back to it later or
so it can benefit someone else.&lt;/p&gt;
&lt;p&gt;First, here is the
&lt;a href="https://adrienplazas.com/assets/blog/2026-04-14-monster-world-iv-disassembly-and-code-analysis/mw4_2026_04_14.gar"&gt;Ghidra project archive&lt;/a&gt;.
It’s the first time I used Ghidra and I’m certain I did plenty of things wrong,
feedback is happily welcome!
While I tried to rename things as my understanding of the code grew, it is still
quite a mess of clashing name conventions, and I’m certain I got plenty of
things wrong.&lt;/p&gt;
&lt;p&gt;Then, here is the Rust-written
&lt;a href="https://adrienplazas.com/assets/blog/2026-04-14-monster-world-iv-disassembly-and-code-analysis/mw4.tar.xz"&gt;data extractor&lt;/a&gt;.
It documents how some systems work, both as code and actual documentation.
It mainly extracts and documents graphics and their compression methods, glyphs
and their compression methods, character encodings, and dialog scripts.
Similarly, I’m not a Rust expert, I did my best but I’m certain there is area
for improvement, and everything was constantly changing anyway.&lt;/p&gt;
&lt;p&gt;There is more information that isn’t documented and is just floating in my head,
such as how the entity system works, but I yet have to refine my understanding
of it.
Same goes for the optimimzations allowed by coding in assembly, such as using
specific registers for commonly used arguments.
Hopefully I will come back to this project and complete it, at least when it
comes to disassembling and documenting the game’s code.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://adrienplazas.com/blog/2026/04/14/monster-world-iv-disassembly-and-code-analysis.html</guid>
      <pubDate>Mon, 13 Apr 2026 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Sjoerd Stendahl: Announcing the upcoming Graphs 2.0</title>
      <link>https://blogs.gnome.org/sstendahl/2026/04/14/announcing-the-upcoming-graphs-2-0/</link>
      <description>&lt;p&gt;It’s been a while since we last shared a major update of Graphs. We’ve had a few minor releases, but the last time we had a substantial feature update was over two years ago.&lt;/p&gt;
&lt;p&gt;This does not mean that development has stalled, to the contrary. But we’ve been working hard on some major changes that took some time to get completely right. Now after a long development cycle, we’re finally getting close enough to a release to be able to announce an official beta period. In this blog, I’ll try to summarize most of the changes in this release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New data types&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In previous version of Graphs, all data types are treated equally. This means that an equation is actually just regular data that is generated when loading. Which is fine, but it also means that the span of the equation is limited, the equation cannot be changed afterward, and operations on the equation will not be reflected in the equation name. In Graphs 2.0, we have three distinct data types: Datasets, Generated Datasets and Equations.&lt;/p&gt;
&lt;p&gt;Datasets are the regular, imported data that you all know and love. Nothing really has changed here. Generated Datasets are essentially the same as regular datasets, but the difference is that these datasets are generated from an equation. They work the same as regular datasets, but for generated datasets you can change the equation, step size and the limits &lt;em&gt;after &lt;/em&gt;creating the item. Finally, the major new addition is the concept of equations. As the name implies, equations are generated based on an equation you enter, but they span an infinite range. Furthermore, operations you perform on equations are done analytically. Meaning if you translate the equation `y = 2x + 3` with 3 in the y-direction, it will change to `y = 2x + 6`. If you perform a derivative, the equation will change to `y = 2x` etcetera. This is a long-requested feature, and has been made possible thanks to the magic of sympy and some trickery on the canvas. Below, there’s a video that demonstrates these three data types.&lt;/p&gt;
&lt;div class="wp-video" style="width: 660px;"&gt;&lt;video class="wp-video-shortcode" controls="controls" height="358" id="video-114-1" preload="metadata" width="660"&gt;&lt;source src="https://blogs.gnome.org/sstendahl/files/2026/04/Screencast-From-2026-04-14-12-23-56.webm?_=1" type="video/webm"/&gt;&lt;a href="https://blogs.gnome.org/sstendahl/files/2026/04/Screencast-From-2026-04-14-12-23-56.webm"&gt;https://blogs.gnome.org/sstendahl/files/2026/04/Screencast-From-2026-04-14-12-23-56.webm&lt;/a&gt;&lt;/video&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Revamped Style Editor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We have redesigned the style editor, where we now show a live preview of the edited styles. This has been a pain point in the past, when you edit styles you cannot see how it actually affects the canvas. Now the style editor immediately tells you how it will affect a canvas, making it much easier to change the style exactly to your preferences.&lt;/p&gt;
&lt;p&gt;We have also added the ability to import styles. Since Graphs styles are based on matplotlib styles, most features from a matplotlib style generally work. Similarly, you can now export your styles as well making it easier to share your style or simply to send it to a different machine. Finally, the style editor can be opened independently of Graphs. By opening a Graphs style from your file explorer, you can change the style without having to open Graphs.&lt;/p&gt;
&lt;p&gt;We also added some new options, such as the ability to style the new error bars. But also the option to draw tick labels (so the values) on all axes that have ticks.&lt;/p&gt;
&lt;figure class="wp-caption alignnone" id="attachment_119" style="width: 622px;"&gt;&lt;img alt="A screenshot of the Graphs style editor, on the left you can see the different settings as in the previous version. On the right you can see the live preview" class="wp-image-119" height="373" src="https://blogs.gnome.org/sstendahl/files/2026/04/Screenshot-From-2026-04-14-11-17-51-300x180.png" width="622"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-119"&gt;The revamped style editor&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Improved data import&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We have completely reworked the way data is imported. Under the hood, our modules are completely modular making it possible to add new parsers without having to mess with the code. Thanks to this rework, we have added support for spreadsheets (LibreOffice .ods and Microsoft Office .xlxs) and for sqlite databases files. The UI automatically updates accordingly. For example for spreadsheets, columns are imported by the column name (alphabetical letter) instead of an index, while sqlite imports show the tables present in the database.&lt;/p&gt;
&lt;figure class="wp-caption alignnone" id="attachment_118" style="width: 608px;"&gt;&lt;img alt="The new import dialog for Graphs. You can see how multiple different types of items are about the be imported, as well as new settings" class="wp-image-118" height="365" src="https://blogs.gnome.org/sstendahl/files/2026/04/Screenshot-From-2026-04-14-11-13-34-300x180.png" width="608"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-118"&gt;The new import dialog&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Furthermore, the import dialog has been improved. It is not possible to add multiple files at once, or import multiple datasets from the same file. Settings can be adjusted for each dataset individually. And you can even import just from a single column. We also added the ability to import error-bars on either axes, and added some pop-up buttons that explain certain settings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Error bars&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I mentioned this in the previous paragraph, but as it’s a feature that’s been requested multiple times I thought it’d be good to state this explicitly as well. We now added support for error bars. Error bars can easily be set on the import dialog, and turned on and off for each axis when editing the item.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Singularity handling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The next version of Graph will also finally handle singularities properly, so equations that have infinite values in them will be rendered as they should be. What was happening in the old version, was that for equations with values that go to infinity and then flip sign, that the line was drawn from the maximum value to the minimum value. Even though there are no values in between. Furthermore, since we render a finite amount of datapoints, the lines don’t go up to infinity either, giving misleading Graphs.&lt;/p&gt;
&lt;p&gt;This is neatly illustrated in the pictures below. The values go all the way up to infinity like they should, and Graphs neatly knows that the line is not continuous, so it does not try to draw a straight line going from plus to minus infinity.&lt;/p&gt;
&lt;figure class="wp-caption alignnone" id="attachment_145" style="width: 708px;"&gt;&lt;img alt="The old version of Graphs trying to render tan(x). Lines don't go all the way to plus/minus infinity, and they also draw a line between the high and low values." class="wp-image-145" height="425" src="https://blogs.gnome.org/sstendahl/files/2026/04/eq_old-300x180.png" width="708"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-145"&gt;The old version of Graphs trying to render tan(x). Lines don’t go all the way to plus/minus infinity, and they also draw a line between the high and low values.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure class="wp-caption alignnone" id="attachment_144" style="width: 663px;"&gt;&lt;img alt="" class="wp-image-144" height="398" src="https://blogs.gnome.org/sstendahl/files/2026/04/eq-300x180.png" width="663"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-144"&gt;The upcoming version of Graphs, were equations such as tan(x) are drawn properly.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Reworked Curve fitting&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The curve fitting has been reworked completely under the hood. While the changes may not be that obvious as a user, the code has basically been completely replaced. The most important change is that the confidence band is now calculated completely correctly using the delta-method. Previously a naive approach was used where the limits were calculated using the standard deviation each parameter. This does not hold up well in most cases though. The parameter values that are given are also no longer rounded in the new equation names (e.g. 421302 used to be rounded to 421000). More useful error messages are provided when things go wrong, custom equations now have an apply button which improves smoothness when entering new equations, the root mean squared error is added as a second goodness-of-fit measure, you can now check out the residuals of your fit. The residuals can be useful to check if your fit is physically correct. A good fit will show residuals scattered randomly around zero with no visible pattern. A systematic pattern in the residuals, such as a curve or a trend suggests that the chosen model may not be appropriate for the data.&lt;/p&gt;
&lt;figure class="wp-caption alignnone" id="attachment_123" style="width: 653px;"&gt;&lt;img alt="" class="wp-image-123" height="392" src="https://blogs.gnome.org/sstendahl/files/2026/04/09a5161cf6ae1b2f-300x180.png" width="653"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-123"&gt;The old version of Graphs with the naive calculation of the confidence band&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure class="wp-caption alignnone" id="attachment_122" style="width: 650px;"&gt;&lt;img alt="" class="wp-image-122" height="390" src="https://blogs.gnome.org/sstendahl/files/2026/04/9533aa0645091bce-300x180.png" width="650"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-122"&gt;The new version of Graphs with the proper calculation of the confidence band.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;UI changes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We’ve tweaked the UI a bit all over the place. But one particular change that is worth to highlight, is that we have moved the item and figure settings to the sidebar. The reason for this, is that the settings are typically used to affect the canvas so you don’t want to lose sight of how your setting affects the canvas while you’re updating. For example, when setting the axes limits, you want to see how your graph looks with the new limit, having a window obstructing the view does not help.&lt;/p&gt;
&lt;p&gt;Another nice addition is that you can now simply click on a part of the canvas, such as the limits, and it will immediately bring you to the figure settings with the relevant field highlighted. See video below.&lt;/p&gt;
&lt;div class="wp-video" style="width: 660px;"&gt;&lt;video class="wp-video-shortcode" controls="controls" height="395" id="video-114-2" preload="metadata" width="660"&gt;&lt;source src="https://blogs.gnome.org/sstendahl/files/2026/04/Screencast-From-2026-04-14-11-30-21.webm?_=2" type="video/webm"/&gt;&lt;a href="https://blogs.gnome.org/sstendahl/files/2026/04/Screencast-From-2026-04-14-11-30-21.webm"&gt;https://blogs.gnome.org/sstendahl/files/2026/04/Screencast-From-2026-04-14-11-30-21.webm&lt;/a&gt;&lt;/video&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Mobile screen support&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With the upcoming release, we finally have full support for mobile devices. See here a quick demonstration on an old OnePlus 6:&lt;/p&gt;
&lt;div class="wp-video" style="width: 480px;"&gt;&lt;video class="wp-video-shortcode" controls="controls" height="854" id="video-114-3" preload="metadata" width="480"&gt;&lt;source src="https://blogs.gnome.org/sstendahl/files/2026/04/smoll.webm?_=3" type="video/webm"/&gt;&lt;a href="https://blogs.gnome.org/sstendahl/files/2026/04/smoll.webm"&gt;https://blogs.gnome.org/sstendahl/files/2026/04/smoll.webm&lt;/a&gt;&lt;/video&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Figure exporting&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One nice addition is the improved figure export. Instead of simply taking the same canvas as you see on the screen, you can now explicitly set a certain resolution. This is vital if you have a lot of figures in the same work, or need to publish your figures in academic journals, and you need consistency both in size and in font sizes. Of course, you can still use the previous setting and have the same size as in the application.&lt;/p&gt;
&lt;figure class="wp-caption alignnone" id="attachment_124" style="width: 670px;"&gt;&lt;img alt="" class="wp-image-124" height="402" src="https://blogs.gnome.org/sstendahl/files/2026/04/Screenshot-From-2026-04-14-11-44-40-300x180.png" width="670"/&gt;&lt;figcaption class="wp-caption-text" id="caption-attachment-124"&gt;The new export figure dialog&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;More quality of life changes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The above are just a highlight of some major feature updates. But there’s a large amount of features that we added. Here’s a rapid-fire list of other niceties that we added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;Multiple instances of Graphs can now be open at the same time&lt;/li&gt;
&lt;li&gt;Data can now be imported by drag-and-drop&lt;/li&gt;
&lt;li&gt;The subtitle finally shows the full file path, even in the isolated Flatpak&lt;/li&gt;
&lt;li&gt;Custom transformations have gotten more powerful with the addition of new variables to use&lt;/li&gt;
&lt;li&gt;Graphs now inhibits the session when unsaved data is still open&lt;/li&gt;
&lt;li&gt;Added support for base-2 logarithmic scaling&lt;/li&gt;
&lt;li&gt;Warnings are now displayed when trying to open a project from a beta version&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And a whole bunch of bug-fixes, under-the-hood changes, and probably some features I have forgotten about. Overall, it’s our biggest update yet by far, and I am excited to finally be able to share the update soon.&lt;/p&gt;
&lt;p&gt;As always, thanks to everyone who has been involved in this version. Graphs is not a one-person project. The bulk of the maintenance is done by me and Christoph, the other maintainer. And of course, we should thank the entire community. Both within GNOME projects (such as help from the design team, and the translation team), as well as outsiders that come with feedback, report or plain suggestions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Getting the beta&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This release is still in beta while we are ironing out the final issues. The expected release date is somewhere in the second week of may. In the meantime, feel free to test the beta. We are very happy for any feedback, especially in this period!&lt;/p&gt;
&lt;p&gt;You can get the beta directly from flathub. First you need to add the flathub beta remote:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flatpak remote-add &lt;span class="nt"&gt;--if-not-exists&lt;/span&gt; flathub-beta https://flathub.org/beta-repo/flathub-beta.flatpakrepo&lt;br/&gt;
&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then, you can install the application:&lt;br/&gt;
&lt;code&gt;flatpak &lt;span class="nb"&gt;install &lt;/span&gt;flathub-beta se.sjoerd.Graphs&lt;br/&gt;
&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To run the beta version by default, the following command can be used:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;flatpak make-current se.sjoerd.Graphs beta&lt;br/&gt;
&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Note that the sudo is neccesary here, as it sets the current branch on the system level. To install this on a per-user basis, the flag –user can be used in the previous commands. To switch back to the stable version simply run the above command replacing beta with stable.&lt;/p&gt;
&lt;p&gt;The beta branch on update should get updated somewhat regularly. If you don’t feel like using the flathub-beta remote, or want the latest build. You can also get the release from &lt;a class="external" href="https://gitlab.gnome.org/World/Graphs"&gt;the GitLab page&lt;/a&gt;, and build it in GNOME Builder.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/sstendahl/2026/04/14/announcing-the-upcoming-graphs-2-0/</guid>
      <pubDate>Tue, 14 Apr 2026 10:33:47 +0000</pubDate>
    </item>
    <item>
      <title>Steven Deobald: End of 10 Handout</title>
      <link>https://blogs.gnome.org/steven/2026/04/14/end-of-10-handout/</link>
      <description>&lt;p&gt;There was a silly little project I’d tried to encourage many folks to attempt last summer. Sri picked it up back in September and after many months, I decided to wrap it up and publish what’s there.&lt;/p&gt;
&lt;p&gt;The intention is a simple, 2-sided A4 that folks can print and give out at repair cafes, like the &lt;a class="external" href="https://endof10.org/"&gt;End of 10&lt;/a&gt; event series. Here’s the &lt;a class="external" href="https://gitlab.gnome.org/Teams/Engagement/initiatives/End-Of-10-Promotion/-/issues/2#note_2559345"&gt;original issue&lt;/a&gt;, if you’d like to look at the initial thought process.&lt;/p&gt;
&lt;p&gt;When I hear fairly technical folks talk about Linux in 2026, I still consistently hear things like “I don’t want to use the command line.” The fact that Spotify, Discord, Slack, Zoom, and Steam all run smoothly on Linux is far removed from these folks’ conception of the Linux desktop they might have formed back in 2009. Most people won’t come to Linux because it’s free of &lt;img alt="✨" class="wp-smiley" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" style="height: 1em;"/&gt;shlop&lt;img alt="✨" class="wp-smiley" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" style="height: 1em;"/&gt; and ads — they’re accustomed to choking on that stuff. They’ll come to Linux because they can open a spreadsheet for free, play Slay The Spire 2, or install Slack even though they promised themselves they wouldn’t use their personal computer for work.&lt;/p&gt;
&lt;p&gt;The GNOME we all know and love is one we take for granted… and the benefits of which we assume everyone wants. But the efficiency, the privacy, the universality, the hackability, the gorgeous design, and the lack of ads? All these things are the icing on the cake. The cake, like it or not, is installing Discord so you can join the Sunday book club.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://gitlab.gnome.org/steven/new-user-docs/-/blob/main/endof10/win2gnome_1.2.pdf?ref_type=heads"&gt;Here’s the A4&lt;/a&gt;. And here’s a snippet:&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&lt;img alt="An A4 snippet including &amp;quot;where's the start menu?&amp;quot;, &amp;quot;where are my exes?&amp;quot;, and &amp;quot;how do I install programs?&amp;quot;" class="size-medium wp-image-218 aligncenter" height="264" src="https://blogs.gnome.org/steven/files/2026/04/a4-snippet-300x264.png" width="300"/&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;If you try this out at a local repair cafe, I’d love to know which bits work and which don’t. Good luck! &lt;img alt="❤" class="wp-smiley" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2764.png" style="height: 1em;"/&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/steven/2026/04/14/end-of-10-handout/</guid>
      <pubDate>Tue, 14 Apr 2026 21:28:19 +0000</pubDate>
    </item>
    <item>
      <title>Jussi Pakkanen: Multi merge sort, or when optimizations aren't</title>
      <link>https://nibblestew.blogspot.com/2026/04/multi-merge-sort-or-when-optimizations.html</link>
      <description>&lt;p&gt;In our &lt;a href="https://nibblestew.blogspot.com/2026/04/sorting-performance-rabbit-hole.html"&gt;previous episode&lt;/a&gt; we wrote a merge sort implementation that runs a bit faster than the one in stdlibc++. The question then becomes, could it be made even faster. If you go through the relevant literature one potential improvement is to do a multiway merge. That is, instead of merging two arrays into one, you merge four into one using, for example, a priority queue.&lt;/p&gt;&lt;p&gt;This seems like a slam dunk for performance.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ul style="text-align: left;"&gt;&lt;li&gt;Doubling the number of arrays to merge at a time halves the number of total passes needed&lt;/li&gt;&lt;li&gt;The priority queue has a known static maximum size, so it can be put on the stack, which is guaranteed to be in the cache all the time&lt;/li&gt;&lt;li&gt;Processing an element takes only &lt;span style="font-family: courier;"&gt;log(#lists)&lt;/span&gt; comparisons&lt;/li&gt;&lt;/ul&gt;Implementing multimerge was conceptually straightforward but getting all the gritty details right took a fair bit of time. Once I got it working the end result was slower. And not by a little, either, but more than 30% slower. Trying some optimizations made it a bit faster but not noticeably so.&lt;p&gt;&lt;/p&gt;&lt;p&gt;Why is this so? Maybe there are bugs that cause it to do extra work? Assuming that is not the case, what actually is? Measuring seems to indicate that a notable fraction of the runtime is spent in the priority queue code. Beyond that I got very little to nothing.&lt;/p&gt;&lt;p&gt;The best hypotheses I could come up with has to with the number of comparisons made. A classical merge sort does two if statements per output elements. One to determine which of the two lists has a smaller element at the front and one to see whether removing the element exhausted the list. The former is basically random and the latter is always false except when the last element is processed. This amounts to 0.5 mispredicted branches per element per round.&lt;/p&gt;&lt;p&gt;A priority queue has to do a bunch more work to preserve the heap property. The first iteration needs to check the root and its two children. That's three comparisons for value and two checks whether the children actually exist. Those are much less predictable than the comparisons in merge sort. Computers are really efficient at doing simple things, so it may be that the additional bookkeeping is so expensive that it negates the advantage of fewer rounds.&lt;/p&gt;&lt;p&gt;Or maybe it's something else. Who's to say? Certainly not me. If someone wants to play with the code, the implementation is &lt;a href="https://github.com/jpakkane/pystd/blob/master/include/pystd2026_mmsort.hpp"&gt;here&lt;/a&gt;. I'll probably delete it at some point as it does not have really any advantage over the regular merge sort.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://nibblestew.blogspot.com/2026/04/multi-merge-sort-or-when-optimizations.html</guid>
      <pubDate>Fri, 17 Apr 2026 10:41:00 +0000</pubDate>
    </item>
    <item>
      <title>Andrea Veri: GNOME GitLab Git traffic caching</title>
      <link>https://www.dragonsreach.it/2026/04/17/gnome-gitlab-git-pulls-caching-improvements/</link>
      <description>&lt;h2 id="table-of-contents"&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#the-problem"&gt;The problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#architecture-overview"&gt;Architecture overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#the-vcl-layer"&gt;The VCL layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#the-post-to-get-conversion"&gt;The POST-to-GET conversion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#protecting-private-repositories"&gt;Protecting private repositories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#the-lua-layer"&gt;The Lua layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#debugging-the-rollout"&gt;Debugging the rollout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#how-we-got-here"&gt;How we got here&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#conclusions"&gt;Conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;One of the most visible signs that GNOME’s infrastructure has grown over the years is the amount of CI traffic that flows through &lt;a href="https://gitlab.gnome.org"&gt;gitlab.gnome.org&lt;/a&gt; on any given day. Hundreds of pipelines run in parallel, most of them starting with a &lt;code&gt;git clone&lt;/code&gt; or &lt;code&gt;git fetch&lt;/code&gt; of the same repository, often at the same commit. All that traffic was landing directly on GitLab’s webservice pods, generating redundant load for work that was essentially identical.&lt;/p&gt;
&lt;p&gt;GNOME’s infrastructure runs on AWS, which generously provides credits to the project. Even so, data transfer is one of the largest cost drivers we face, and we have to operate within a defined budget regardless of those credits. The bandwidth costs associated with this Git traffic grew significant enough that for a period of time we redirected unauthenticated HTTPS Git pulls to our GitHub mirrors as a short-term cost mitigation. That measure bought us some breathing room, but it was never meant to be permanent: sending users to a third-party platform for what is essentially a core infrastructure operation is not a position we wanted to stay in. The goal was always to find a proper solution on our own infrastructure.&lt;/p&gt;
&lt;p&gt;This post documents the caching layer we built to address that problem. The solution sits between the client and GitLab, intercepts Git fetch traffic, and routes it through Fastly’s CDN so that repeated fetches of the same content are served from cache rather than generating a fresh pack every time. The design went through several iterations — this post presents the final architecture first, then walks through &lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#how-we-got-here"&gt;how we got here&lt;/a&gt; for readers interested in the evolution.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;The Git smart HTTP protocol uses two endpoints: &lt;code&gt;info/refs&lt;/code&gt; for capability advertisement and ref discovery, and &lt;code&gt;git-upload-pack&lt;/code&gt; for the actual pack generation. The second one is the expensive one. When a CI job runs &lt;code&gt;git fetch origin main&lt;/code&gt;, GitLab has to compute and send the entire pack for that fetch negotiation. If ten jobs run the same fetch within a short window, GitLab does that work ten times.&lt;/p&gt;
&lt;p&gt;The tricky part is that &lt;code&gt;git-upload-pack&lt;/code&gt; is a &lt;code&gt;POST&lt;/code&gt; request with a binary body that encodes what the client already has (&lt;code&gt;have&lt;/code&gt; lines) and what it wants (&lt;code&gt;want&lt;/code&gt; lines). Traditional HTTP caches ignore POST bodies entirely. Building a cache that actually understands those bodies and deduplicates identical fetches requires some work at the edge.&lt;/p&gt;
&lt;p&gt;For a fresh clone the body contains only &lt;code&gt;want&lt;/code&gt; lines — one per ref the client is requesting:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0032want 7d20e995c3c98644eb1c58a136628b12e9f00a78
0032want 93e944c9f728a4b9da506e622592e4e3688a805c
0032want ef2cbad5843a607236b45e5f50fa4318e0580e04
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For an incremental fetch the body is a mix of &lt;code&gt;want&lt;/code&gt; lines (what the client needs) and &lt;code&gt;have&lt;/code&gt; lines (commits the client already has locally), which the server uses to compute the smallest possible packfile delta:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;00a4want 51a117587524cbdd59e43567e6cbd5a76e6a39ff
0000
0032have 8282cff4b31dce12e100d4d6c78d30b1f4689dd3
0032have be83e3dae8265fdc4c91f11d5778b20ceb4e2479
0032have 7d46abdf9c5a3f119f645c8de6d87efffe3889b8
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The leading four hex characters on each line are the pkt-line length prefix. The server walks back through history from the wanted commits until it finds a common ancestor with the &lt;code&gt;have&lt;/code&gt; set, then packages everything in between into a packfile. Two CI jobs running the same pipeline at the same commit will produce byte-for-byte identical request bodies and therefore identical responses — exactly the property a cache can help with.&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture overview&lt;/h2&gt;
&lt;p&gt;The current architecture has three components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fastly&lt;/strong&gt; as the user-facing CDN for &lt;code&gt;gitlab.gnome.org&lt;/code&gt;, with custom VCL that intercepts &lt;code&gt;git-upload-pack&lt;/code&gt; traffic, hashes the request body, converts the POST to a GET, and caches the response at edge POPs worldwide&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenResty&lt;/strong&gt; (Nginx + LuaJIT) running as the origin server, with a Lua script that restores the original POST, checks a Valkey denylist for private repositories, and signals cacheability back to Fastly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valkey + webhook&lt;/strong&gt; — a small &lt;a href="https://valkey.io/"&gt;Valkey&lt;/a&gt; instance stores a denylist of private repository paths, kept in sync by a &lt;a href="https://gitlab.gnome.org/GNOME/gitlab-git-cache-webhook"&gt;webhook service&lt;/a&gt; that listens for GitLab project visibility changes&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="mermaid"&gt;
flowchart TD
client["Git client / CI runner"]
edge["Fastly Edge POP (nearest)"]
shield["Fastly Shield POP (IAD)"]
nginx["OpenResty Nginx (origin)"]
lua["Lua: git_upload_pack.lua"]
valkey["Valkey denylist"]
gitlab["GitLab webservice"]
webhook["gitlab-git-cache-webhook"]
gitlab_events["GitLab project events"]
client -- "POST /git-upload-pack" --&amp;gt; edge
edge -- "HIT → serve from edge" --&amp;gt; client
edge -- "MISS → forward to shield" --&amp;gt; shield
shield -- "HIT → return to edge (edge caches)" --&amp;gt; edge
shield -- "MISS → fetch from origin" --&amp;gt; nginx
nginx --&amp;gt; lua
lua -- "authenticated? check denylist" --&amp;gt; valkey
lua -- "denied/error: keep auth, skip cache" --&amp;gt; gitlab
lua -- "allowed: keep auth, signal cacheable" --&amp;gt; gitlab
gitlab -- "packfile response" --&amp;gt; nginx
nginx -- "X-Git-Cacheable: 1 (if allowed)" --&amp;gt; shield
gitlab_events --&amp;gt; webhook
webhook -- "SET/DEL git:deny:" --&amp;gt; valkey
&lt;/pre&gt;
&lt;p&gt;The request flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;POST /git-upload-pack&lt;/code&gt; arrives at the nearest Fastly edge POP.&lt;/li&gt;
&lt;li&gt;VCL checks the body: if &lt;code&gt;Content-Length&lt;/code&gt; exceeds 8 KB (the limit of what Fastly can read from &lt;code&gt;req.body&lt;/code&gt;), or the body does not contain &lt;code&gt;command=fetch&lt;/code&gt;, the request is passed through uncached.&lt;/li&gt;
&lt;li&gt;VCL hashes the body with SHA256 to build the cache key, base64-encodes the body into &lt;code&gt;X-Git-Original-Body&lt;/code&gt;, and converts the request to GET. If the request carries authentication headers (&lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;PRIVATE-TOKEN&lt;/code&gt;, &lt;code&gt;Job-Token&lt;/code&gt;), VCL sets &lt;code&gt;X-Git-Auth-Passthrough&lt;/code&gt; to flag it — but the request still enters the cache lookup.&lt;/li&gt;
&lt;li&gt;On a cache hit at the edge, the packfile is served immediately — regardless of whether the request is authenticated or not.&lt;/li&gt;
&lt;li&gt;On a miss, the request routes to the IAD shield POP. If the shield has it cached, it returns the object and the edge caches it locally.&lt;/li&gt;
&lt;li&gt;On a shield miss, the request reaches Nginx at the origin. Lua detects &lt;code&gt;X-Git-Original-Body&lt;/code&gt; and restores the POST body. If &lt;code&gt;X-Git-Auth-Passthrough&lt;/code&gt; is set, Lua checks the Valkey denylist: if the repo is private (or Valkey is unreachable), the &lt;code&gt;Authorization&lt;/code&gt; header is preserved and cacheability is not signaled — the response passes through uncached. If the repo is not on the denylist, &lt;code&gt;Authorization&lt;/code&gt; is preserved (internal repos need it for GitLab to return 200) and cacheability is signaled.&lt;/li&gt;
&lt;li&gt;For unauthenticated requests (no passthrough flag), Lua strips &lt;code&gt;Authorization&lt;/code&gt; and signals cacheability unconditionally — these are by definition accessing public repositories.&lt;/li&gt;
&lt;li&gt;The response flows back through the shield and the edge. If &lt;code&gt;X-Git-Cacheable: 1&lt;/code&gt; is present, both nodes cache the response. Subsequent requests — authenticated or not — for the same cache key are served directly from cache.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="the-vcl-layer"&gt;The VCL layer&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;vcl_recv&lt;/code&gt; snippet runs at priority 9, before the existing &lt;code&gt;enable_segmented_caching&lt;/code&gt; snippet at priority 10 which would otherwise &lt;code&gt;return(pass)&lt;/code&gt; for non-asset URLs:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Snippet git-cache-vcl-recv : 9
# Edge: convert POST to GET, hash body, encode body in header
if (req.url ~ "/git-upload-pack$" &amp;amp;&amp;amp; req.request == "POST") {
if (std.atoi(req.http.Content-Length) &amp;gt; 8192) {
return(pass);
}
if (req.body !~ "command=fetch") {
return(pass);
}
set req.http.X-Git-Cache-Key = "v3:" digest.hash_sha256(req.body);
set req.http.X-Git-Original-Body = digest.base64(req.body);
# Flag authenticated requests — they still enter the cache lookup,
# but on a miss Lua uses this to decide whether to cache the response
if (req.http.Authorization || req.http.PRIVATE-TOKEN || req.http.Job-Token) {
set req.http.X-Git-Auth-Passthrough = "1";
}
set req.request = "GET";
set req.backend = F_Host_1;
if (req.restarts == 0) {
set req.backend = fastly.try_select_shield(ssl_shield_iad_va_us, F_Host_1);
}
return(lookup);
}
# Shield: request already converted to GET by the edge
if (req.http.X-Git-Cache-Key) {
set req.backend = F_Host_1;
return(lookup);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Authenticated requests — CI runners with &lt;code&gt;Authorization: Basic &amp;lt;gitlab-ci-token:TOKEN&amp;gt;&lt;/code&gt;, API clients with &lt;code&gt;PRIVATE-TOKEN&lt;/code&gt; or &lt;code&gt;Job-Token&lt;/code&gt; — are no longer sent straight to origin. Instead, VCL flags them with &lt;code&gt;X-Git-Auth-Passthrough&lt;/code&gt; and lets them enter the cache lookup. On a cache hit, the packfile is served directly from the edge — no origin contact, no credential validation needed, because the cached object can only exist if a previous request already established that the repository is public (see &lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#protecting-private-repositories"&gt;Protecting private repositories&lt;/a&gt;). On a cache miss, the flagged request reaches origin where Lua checks the Valkey denylist to decide whether the response should be cached.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;command=fetch&lt;/code&gt; filter means only Git protocol v2 fetch commands are cached. The &lt;code&gt;ls-refs&lt;/code&gt; command is excluded because its request body is essentially static — caching it with a long TTL would serve stale ref listings after a push. Fetch bodies encode exactly the SHAs the client wants and already has, making them safe to cache indefinitely.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;v3:&lt;/code&gt; prefix is a cache version string. Bumping it invalidates all existing cache entries without touching Fastly’s purge API.&lt;/p&gt;
&lt;p&gt;The second &lt;code&gt;if&lt;/code&gt; block handles the shield. When a cache miss at the edge forwards the request to the shield POP, the shield runs &lt;code&gt;vcl_recv&lt;/code&gt; again. At that point the request is already a GET (the edge converted it), so the first block’s &lt;code&gt;req.request == "POST"&lt;/code&gt; check will not match. Without the second block, the request would fall through to the &lt;code&gt;enable_segmented_caching&lt;/code&gt; snippet, which returns &lt;code&gt;pass&lt;/code&gt; for any URL that is not an artifact or archive — effectively preventing the shield from ever caching git traffic.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;vcl_hash&lt;/code&gt; snippet overrides the default URL-based hash when a cache key is present:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Snippet git-cache-vcl-hash : 10
if (req.http.X-Git-Cache-Key) {
set req.hash += req.http.X-Git-Cache-Key;
return(hash);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;vcl_fetch&lt;/code&gt; snippet caches 200 responses that carry the &lt;code&gt;X-Git-Cacheable&lt;/code&gt; signal from Nginx:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Snippet git-cache-vcl-fetch : 100
if (req.http.X-Git-Cache-Key) {
if (beresp.status == 200 &amp;amp;&amp;amp; beresp.http.X-Git-Cacheable == "1") {
set beresp.http.Surrogate-Key = "git-cache " regsub(req.url.path, "/git-upload-pack$", "");
set beresp.cacheable = true;
set beresp.ttl = 30d;
set beresp.http.X-Git-Cache-Key = req.http.X-Git-Cache-Key;
unset beresp.http.Cache-Control;
unset beresp.http.Pragma;
unset beresp.http.Expires;
unset beresp.http.Set-Cookie;
return(deliver);
}
set beresp.ttl = 0s;
set beresp.cacheable = false;
return(deliver);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;Surrogate-Key&lt;/code&gt; line tags each cached object with both a global &lt;code&gt;git-cache&lt;/code&gt; key and the repository path. This enables targeted purging — a single repository’s cache can be flushed with &lt;code&gt;fastly purge --key "/GNOME/glib"&lt;/code&gt;, or all git cache at once with &lt;code&gt;fastly purge --key "git-cache"&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The 30-day TTL is deliberately long. Git pack data is content-addressed: a pack for a given set of &lt;code&gt;want&lt;/code&gt;/&lt;code&gt;have&lt;/code&gt; lines will always be the same. As long as the objects exist in the repository, the cached pack is valid. The only case where a cached pack could be wrong is if objects were deleted (force-push that drops history, for instance), which is rare and, on GNOME’s GitLab, made even rarer by the &lt;a href="https://gitlab.gnome.org/GNOME/gitaly-custom-hooks"&gt;Gitaly custom hooks&lt;/a&gt; we run to prevent force-pushes and history rewrites on protected namespaces. In those cases the cache version prefix would force a key change rather than relying on TTL expiry.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;X-Git-Cacheable&lt;/code&gt; header is intentionally &lt;strong&gt;not&lt;/strong&gt; unset in &lt;code&gt;vcl_fetch&lt;/code&gt;. This is important for the shielding architecture: when the shield caches the object, the stored headers include &lt;code&gt;X-Git-Cacheable: 1&lt;/code&gt;. When the edge later fetches this object from the shield, the edge’s own &lt;code&gt;vcl_fetch&lt;/code&gt; sees the header and knows it is safe to cache locally. If &lt;code&gt;vcl_fetch&lt;/code&gt; stripped the header, the edge would never cache — every request would be a local miss that has to travel back to the shield.&lt;/p&gt;
&lt;p&gt;The cleanup happens in &lt;code&gt;vcl_deliver&lt;/code&gt;, which runs last before the response reaches the client:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Snippet git-cache-vcl-deliver : 100
if (req.http.X-Git-Cache-Key) {
set resp.http.X-Git-Cache-Status = if(fastly_info.state ~ "HIT(?:-|\z)", "HIT", "MISS");
unset resp.http.X-Git-Original-Body;
if (!req.http.Fastly-FF) {
unset resp.http.X-Git-Cacheable;
unset resp.http.X-Git-Cache-Key;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;Fastly-FF&lt;/code&gt; check distinguishes between inter-POP traffic (shield-to-edge) and the final client response. &lt;code&gt;Fastly-FF&lt;/code&gt; is set when the request comes from another Fastly node. On the shield, where the request came from the edge, internal headers like &lt;code&gt;X-Git-Cacheable&lt;/code&gt; and &lt;code&gt;X-Git-Cache-Key&lt;/code&gt; are preserved — the edge’s &lt;code&gt;vcl_fetch&lt;/code&gt; needs them. On the edge, where the request came from the actual client, those headers are stripped from the final response. Only &lt;code&gt;X-Git-Cache-Status&lt;/code&gt; is exposed to clients for observability.&lt;/p&gt;
&lt;h2 id="the-post-to-get-conversion"&gt;The POST-to-GET conversion&lt;/h2&gt;
&lt;p&gt;This is probably the most unusual part of the design. Fastly’s consistent hashing and shield routing only works for GET requests. POST requests always go straight to origin. Fastly does provide a way to force POST responses into the cache — by returning &lt;code&gt;pass&lt;/code&gt; in &lt;code&gt;vcl_recv&lt;/code&gt; and setting &lt;code&gt;beresp.cacheable&lt;/code&gt; in &lt;code&gt;vcl_fetch&lt;/code&gt; — but it is a blunt instrument: there is no consistent hashing, no shield collapsing, and no guarantee that two nodes in the same POP will ever share the cached result.&lt;/p&gt;
&lt;p&gt;By converting the POST to a GET in VCL, encoding the body in a header (&lt;code&gt;X-Git-Original-Body&lt;/code&gt;), and using a body-derived SHA256 as the cache key, we get consistent hashing and shield-level request collapsing for free. The VCL uses the &lt;code&gt;X-Git-Cache-Key&lt;/code&gt; header (not the URL or method) as the cache key, so the GET conversion is invisible to the caching logic.&lt;/p&gt;
&lt;p&gt;Fastly’s shield feature routes cache misses through a designated shield node before going to origin. When two different edge nodes both get a MISS for the same cache key simultaneously, the shield node collapses them into a single origin request. This is important because without it, a burst of CI jobs fetching the same commit would all miss, all go to origin in parallel, and GitLab would end up generating the same pack multiple times.&lt;/p&gt;
&lt;h2 id="protecting-private-repositories"&gt;Protecting private repositories&lt;/h2&gt;
&lt;p&gt;Private repository traffic must never be cached — that would mean storing authenticated git content in a third-party cache and serving it to arbitrary clients. The protection relies on two independent layers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1: cache population is restricted.&lt;/strong&gt; The cache can only be populated when Lua signals cacheability via &lt;code&gt;X-Git-Cacheable: 1&lt;/code&gt;. Lua only signals cacheability when the request is either unauthenticated (by definition accessing a public repo) or authenticated for a repo that is not on the Valkey denylist. For private repos, Lua does not signal cacheability, so &lt;code&gt;vcl_fetch&lt;/code&gt; sets &lt;code&gt;ttl=0&lt;/code&gt; and &lt;code&gt;cacheable=false&lt;/code&gt; — the response is delivered but never stored.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 2: the Valkey denylist.&lt;/strong&gt; A &lt;a href="https://gitlab.gnome.org/GNOME/gitlab-git-cache-webhook"&gt;webhook service&lt;/a&gt; listens for GitLab &lt;code&gt;project_create&lt;/code&gt; and &lt;code&gt;project_update&lt;/code&gt; system hooks. When a project’s visibility is set to private (level &lt;code&gt;0&lt;/code&gt;), the webhook sets a &lt;code&gt;git:deny:&amp;lt;path&amp;gt;&lt;/code&gt; key in Valkey. When visibility changes to internal (level &lt;code&gt;10&lt;/code&gt;) or public (level &lt;code&gt;20&lt;/code&gt;), the key is removed. A periodic reconciliation job (&lt;code&gt;reconcile.py&lt;/code&gt;) syncs the full denylist against the GitLab API to correct any drift from missed events.&lt;/p&gt;
&lt;p&gt;On a cache miss for an authenticated request, Lua checks the denylist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repo is on the denylist (private):&lt;/strong&gt; &lt;code&gt;Authorization&lt;/code&gt; is preserved, cacheability is not signaled. The request proxies to GitLab with credentials intact, GitLab validates the token, the response is returned but never cached.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repo is not on the denylist (public/internal):&lt;/strong&gt; &lt;code&gt;Authorization&lt;/code&gt; is preserved (internal repos require it for GitLab to return 200), cacheability is signaled. The response is cached for future requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valkey is unreachable or returns an error:&lt;/strong&gt; treated the same as denied — &lt;code&gt;Authorization&lt;/code&gt; is preserved, cacheability is not signaled. This fail-closed design means infrastructure failures result in cache misses, never in data leaks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The denylist only needs to track private repositories, which are a small fraction of the total on GNOME’s GitLab instance. A private repo’s packfile can never enter the cache through two independent mechanisms: the denylist prevents Lua from signaling cacheability, and even if the denylist were somehow wrong, an unauthenticated request to a private repo returns a 401 from GitLab — which &lt;code&gt;vcl_fetch&lt;/code&gt; does not cache (it only caches &lt;code&gt;200 + X-Git-Cacheable&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="the-lua-layer"&gt;The Lua layer&lt;/h2&gt;
&lt;p&gt;With the VCL handling body hashing, the POST-to-GET conversion, and the cache lookup for all requests, the Lua script runs on cache misses that reach origin. Both authenticated and unauthenticated requests can arrive here. The script’s responsibilities are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Detect that the request arrived from Fastly with an encoded body (the &lt;code&gt;X-Git-Original-Body&lt;/code&gt; header).&lt;/li&gt;
&lt;li&gt;Decode and restore the original POST.&lt;/li&gt;
&lt;li&gt;For authenticated requests, check the Valkey denylist to determine if the repository is private.&lt;/li&gt;
&lt;li&gt;Signal back to Fastly whether the response is safe to cache.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-Lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;redis_helper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"redis_helper"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;redis_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"REDIS_HOST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;redis_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"REDIS_PORT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;encoded_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_headers&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Original-Body"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;encoded_body&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx.decode_base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx.HTTP_POST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_body_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Length"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Original-Body"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_headers&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Auth-Passthrough"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Auth-Passthrough"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx.var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"^/(.+)/git%-upload%-pack$"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%.git$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;denied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_helper.is_denied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis_port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx.WARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"git-cache: Redis error for "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;" — keeping auth, skipping cache"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;denied&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git_cacheable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git_cacheable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The two branches handle the authenticated and unauthenticated paths. When &lt;code&gt;X-Git-Auth-Passthrough&lt;/code&gt; is present, the request came from a CI runner or API client. Lua checks the denylist: if the repo is private or Valkey is unreachable, the script returns early — &lt;code&gt;Authorization&lt;/code&gt; stays on the request (so GitLab can validate it), and &lt;code&gt;git_cacheable&lt;/code&gt; is never set (so the response is not cached). If the repo is not denied, &lt;code&gt;Authorization&lt;/code&gt; is preserved and cacheability is signaled. The &lt;code&gt;Authorization&lt;/code&gt; header is kept rather than stripped because internal repositories (visibility level &lt;code&gt;10&lt;/code&gt;) require authentication for git operations — stripping it would cause GitLab to return a 401. Public repos work with or without credentials, so keeping the header is safe for both.&lt;/p&gt;
&lt;p&gt;For unauthenticated requests (no passthrough flag), &lt;code&gt;Authorization&lt;/code&gt; is stripped and cacheability is signaled unconditionally — these are by definition accessing public repositories.&lt;/p&gt;
&lt;p&gt;The early &lt;code&gt;return&lt;/code&gt; for denied or errored lookups is the fail-closed behavior. The request still proxies to GitLab (the &lt;code&gt;proxy_pass&lt;/code&gt; directive in the Nginx location block runs after Lua), but without the cacheable signal, &lt;code&gt;vcl_fetch&lt;/code&gt; will not store the response.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ngx.ctx.git_cacheable&lt;/code&gt; flag is picked up by the &lt;code&gt;header_filter_by_lua_block&lt;/code&gt; in the Nginx configuration, which translates it into the &lt;code&gt;X-Git-Cacheable: 1&lt;/code&gt; response header that &lt;code&gt;vcl_fetch&lt;/code&gt; checks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-Nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/git-upload-pack$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;client_body_buffer_size&lt;/span&gt; &lt;span class="mi"&gt;5m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;5m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;access_by_lua_file&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/lua/git_upload_pack.lua&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;header_filter_by_lua_block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;ngx.ctx.git_cacheable&lt;/span&gt; &lt;span class="s"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;ngx.header["X-Git-Cacheable"]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://gitlab-webservice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="debugging-the-rollout"&gt;Debugging the rollout&lt;/h2&gt;
&lt;p&gt;The rollout surfaced a few issues worth documenting for anyone building a similar setup on Fastly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shielding introduces a second &lt;code&gt;vcl_recv&lt;/code&gt; execution.&lt;/strong&gt; When the edge forwards a cache miss to the shield, the shield runs the entire VCL pipeline from scratch. The POST-to-GET conversion in &lt;code&gt;vcl_recv&lt;/code&gt; checks for &lt;code&gt;req.request == "POST"&lt;/code&gt;, but on the shield the request is already a GET. Without the fallback &lt;code&gt;if (req.http.X-Git-Cache-Key)&lt;/code&gt; block, the shield’s &lt;code&gt;vcl_recv&lt;/code&gt; would fall through to the segmented caching snippet and &lt;code&gt;return(pass)&lt;/code&gt; — making the shield unable to cache anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Response headers must survive the shield-to-edge hop.&lt;/strong&gt; &lt;code&gt;vcl_fetch&lt;/code&gt; and &lt;code&gt;vcl_deliver&lt;/code&gt; both run on each node independently. If &lt;code&gt;vcl_fetch&lt;/code&gt; on the shield strips a header after caching the object, the stored object will not have that header. When the edge fetches from the shield, the edge’s &lt;code&gt;vcl_fetch&lt;/code&gt; will not see it. The solution is to only strip internal headers in &lt;code&gt;vcl_deliver&lt;/code&gt; on the final client response, using &lt;code&gt;Fastly-FF&lt;/code&gt; to distinguish inter-POP traffic from client traffic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fastly’s &lt;code&gt;req.body&lt;/code&gt; is limited to 8 KB.&lt;/strong&gt; VCL can only inspect the first 8192 bytes of a request body. For the vast majority of git fetch negotiations — especially shallow clones and CI pipelines fetching recent commits — the body is well under this limit. Requests with larger bodies (deep fetches with many &lt;code&gt;have&lt;/code&gt; lines) fall through to &lt;code&gt;return(pass)&lt;/code&gt; and are handled directly by GitLab without caching. This is an acceptable tradeoff: those large-body requests are typically unique negotiations that would not benefit from caching anyway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Git protocol v1 clients are not cached.&lt;/strong&gt; The VCL filters on &lt;code&gt;command=fetch&lt;/code&gt;, which is a Git protocol v2 construct. Protocol v1 uses a different body format (&lt;code&gt;want&lt;/code&gt;/&lt;code&gt;have&lt;/code&gt; lines without the &lt;code&gt;command=&lt;/code&gt; prefix). Since protocol v2 has been the default since git 2.26 (March 2020), the vast majority of traffic benefits from caching. Protocol v1 clients still work correctly — they simply bypass the cache.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Internal repositories require authentication for git operations.&lt;/strong&gt; An early version of the Lua script stripped &lt;code&gt;Authorization&lt;/code&gt; for any repo not on the denylist, assuming that “not private” meant “accessible without credentials.” Internal repositories (visibility level &lt;code&gt;10&lt;/code&gt;) are not on the denylist — their content is not sensitive — but GitLab still requires authentication for git clone/fetch operations on them. Stripping credentials produced a 401 from GitLab. The fix was to preserve &lt;code&gt;Authorization&lt;/code&gt; for all authenticated requests that pass the denylist check, regardless of whether the repo is public or internal. Public repos accept the header harmlessly; internal repos require it.&lt;/p&gt;
&lt;h2 id="how-we-got-here"&gt;How we got here&lt;/h2&gt;
&lt;p&gt;The current architecture is the result of two iterations. The sections above describe the final design; this section documents the path we took to get there.&lt;/p&gt;
&lt;h3 id="iteration-1-separate-cdn-service-with-lua-driven-caching"&gt;Iteration 1: Separate CDN service with Lua-driven caching&lt;/h3&gt;
&lt;p&gt;The first version used a separate Fastly CDN service (&lt;code&gt;cdn.gitlab.gnome.org&lt;/code&gt;) as the cache layer, with Nginx doing most of the heavy lifting in Lua:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart TD
client["Git client / CI runner"]
gitlab_gnome["gitlab.gnome.org (Nginx reverse proxy)"]
nginx["OpenResty Nginx"]
lua["Lua: git_upload_pack.lua"]
cdn_origin["/cdn-origin internal location"]
fastly_cdn["Fastly CDN"]
origin["gitlab.gnome.org via its origin (second pass)"]
gitlab["GitLab webservice"]
valkey["Valkey denylist"]
webhook["gitlab-git-cache-webhook"]
gitlab_events["GitLab project events"]
client --&amp;gt; gitlab_gnome
gitlab_gnome --&amp;gt; nginx
nginx --&amp;gt; lua
lua -- "check denylist" --&amp;gt; valkey
lua -- "private repo: BYPASS" --&amp;gt; gitlab
lua -- "public/internal: internal redirect" --&amp;gt; cdn_origin
cdn_origin --&amp;gt; fastly_cdn
fastly_cdn -- "HIT" --&amp;gt; cdn_origin
fastly_cdn -- "MISS: origin fetch" --&amp;gt; origin
origin --&amp;gt; gitlab
gitlab_events --&amp;gt; webhook
webhook -- "SET/DEL git:deny:" --&amp;gt; valkey
&lt;/pre&gt;
&lt;p&gt;In this design, the Lua script did everything: read the POST body, SHA256-hash it to build a cache key, check a Valkey denylist to exclude private repositories, convert the POST to a GET, encode the body in a header, and perform an internal redirect to a &lt;code&gt;/cdn-origin&lt;/code&gt; location that proxied to the CDN. On a cache miss, the CDN would fetch from &lt;code&gt;gitlab.gnome.org&lt;/code&gt; directly (the “second pass”), where Lua would detect the origin fetch, decode the body, restore the POST, and proxy to GitLab.&lt;/p&gt;
&lt;p&gt;Private repositories were protected by a denylist stored in Valkey. A small FastAPI webhook service (&lt;a href="https://gitlab.gnome.org/GNOME/gitlab-git-cache-webhook"&gt;gitlab-git-cache-webhook&lt;/a&gt;) listened for GitLab system hooks on &lt;code&gt;project_create&lt;/code&gt; and &lt;code&gt;project_update&lt;/code&gt; events, maintaining &lt;code&gt;git:deny:&amp;lt;path&amp;gt;&lt;/code&gt; keys for private repositories (visibility level &lt;code&gt;0&lt;/code&gt;). Internal repositories (level &lt;code&gt;10&lt;/code&gt;) were treated the same as public (level &lt;code&gt;20&lt;/code&gt;) since they are accessible to any authenticated user on the instance.&lt;/p&gt;
&lt;p&gt;The Lua script for this design was substantially more complex:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-Lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;resty_sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"resty.sha256"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;resty_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"resty.string"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;redis_helper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"redis_helper"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;redis_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"REDIS_HOST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;"localhost"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;redis_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"REDIS_PORT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;"6379"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Second pass: request arriving from CDN origin fetch.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_headers&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Cache-Internal"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;encoded_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_headers&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Original-Body"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;encoded_body&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx.decode_base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx.HTTP_POST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_body_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Length"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Original-Body"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And on the first pass, it handled hashing, denylist checks, and the CDN redirect:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-Lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"command=fetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ngx.header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Cache-Status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BYPASS"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resty_sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;body_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resty_str.to_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"v2:"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;body_hash&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;denied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_helper.is_denied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis_port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;denied&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt; &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"X-Git-Original-Body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ngx.encode_base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx.HTTP_GET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ngx.req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_body_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ngx.exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/cdn-origin"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The CDN’s VCL was relatively simple — it used &lt;code&gt;X-Git-Cache-Key&lt;/code&gt; for the hash, routed through a shield, and cached 200 responses for 30 days.&lt;/p&gt;
&lt;p&gt;This architecture worked, but it had two significant limitations that led to the current design.&lt;/p&gt;
&lt;h3 id="iteration-2-edge-caching-with-ci-runner-participation"&gt;Iteration 2: Edge caching with CI runner participation&lt;/h3&gt;
&lt;p&gt;The first problem with the separate CDN service was geographic. Nginx runs in AWS us-east-1, so from Fastly’s perspective the only client of the CDN was that single instance in Virginia. Every request entered through the IAD POP, which meant the CDN’s edge POPs around the world were never populated. A CI runner in Europe would have its request travel from a European Fastly POP to IAD, then to Nginx, then back to Fastly IAD, and then all the way back — crossing the Atlantic twice for every cache miss.&lt;/p&gt;
&lt;p&gt;The fix was to eliminate the separate CDN service and move all the caching logic into the &lt;code&gt;gitlab.gnome.org&lt;/code&gt; Fastly service itself. The key insight was that the POST-to-GET conversion and body hashing could happen in Fastly’s VCL rather than in Lua — Fastly provides &lt;code&gt;digest.hash_sha256()&lt;/code&gt; and &lt;code&gt;digest.base64()&lt;/code&gt; functions that operate directly on &lt;code&gt;req.body&lt;/code&gt;. By doing the conversion at the CDN edge, every POP in the network became a potential cache node for git traffic.&lt;/p&gt;
&lt;p&gt;The second problem was that the original denylist approach had two flaws. First, its error handling was fail-open: a Valkey connection error would cause the Lua script to assume the repo was public and strip credentials — the wrong default. Second, even after briefly replacing the denylist with a simple VCL auth bypass (&lt;code&gt;return(pass)&lt;/code&gt; for any request with &lt;code&gt;Authorization&lt;/code&gt;), CI runners were left completely uncached. GitLab CI always injects a &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; into every job, and the runner authenticates with &lt;code&gt;Authorization: Basic &amp;lt;gitlab-ci-token:TOKEN&amp;gt;&lt;/code&gt; regardless of whether the repository is public or private. With the auth bypass, every CI clone skipped the cache entirely — safe, but it left the biggest source of redundant traffic unserved.&lt;/p&gt;
&lt;p&gt;The current design solves both problems. VCL flags authenticated requests with &lt;code&gt;X-Git-Auth-Passthrough&lt;/code&gt; instead of bypassing the cache, letting them participate in cache lookups. On a hit, the cached packfile is served immediately. On a miss, the request reaches Lua at origin, where the flag triggers a denylist check against Valkey — the same denylist and webhook infrastructure from iteration 1, re-deployed with one critical change: fail-closed error handling. A Valkey error or missing connection causes Lua to preserve &lt;code&gt;Authorization&lt;/code&gt; and skip cacheability signaling. The request still works (GitLab validates the token and serves the packfile), but the response is not cached. Infrastructure failures result in cache misses, never in data leaks.&lt;/p&gt;
&lt;p&gt;The denylist only tracks private repositories (visibility level &lt;code&gt;0&lt;/code&gt;), which are a small fraction of the total on GNOME’s GitLab. Public and internal repositories pass the denylist check, and Lua signals cacheability while preserving the &lt;code&gt;Authorization&lt;/code&gt; header — internal repos require it for GitLab to return 200, and public repos accept it harmlessly.&lt;/p&gt;
&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;The system has been running in production since April 2026 and has gone through two iterations to reach its current form. Packfiles are cached at Fastly edge POPs worldwide — a CI runner in Europe gets a cache hit served from a European POP rather than making a round trip to the US East coast.&lt;/p&gt;
&lt;p&gt;The moving parts are Fastly’s VCL, an OpenResty Nginx instance with a ~30-line Lua script, a Valkey instance storing the private repository denylist, and a small webhook service that keeps the denylist synchronized with GitLab. Private repositories are protected by two independent layers: the Valkey denylist (which prevents cacheability signaling) and GitLab’s own authentication (which rejects unauthenticated access).&lt;/p&gt;
&lt;p&gt;If something goes wrong with the cache layer, requests fall through to GitLab directly — the same path they took before caching existed. There is no failure mode where caching breaks git operations. This also means we don’t redirect any traffic to github.com anymore.&lt;/p&gt;
&lt;p&gt;That should be all for today, stay tuned!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://www.dragonsreach.it/2026/04/17/gnome-gitlab-git-pulls-caching-improvements/</guid>
      <pubDate>Fri, 17 Apr 2026 14:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Allan Day: GNOME Foundation Update, 2026-04-17</title>
      <link>https://blogs.gnome.org/aday/2026/04/17/gnome-foundation-update-2026-04-17/</link>
      <description>&lt;p&gt;Welcome to another update about everything that’s been happening at the GNOME Foundation. It’s been four weeks since my last post, due to a vacation and public holidays, so there’s lots to cover. This period included a major announcement, but there’s also been a lot of other notable work behind the scenes.&lt;/p&gt;
&lt;h2&gt;Fellowship &amp;amp; Fundraising&lt;/h2&gt;
&lt;p&gt;The really big news from the last four weeks was the launch of our new &lt;a class="external" href="https://fellowship.gnome.org/"&gt;Fellowship program&lt;/a&gt;. This is something that the Board has been discussing for quite some time, so we were thrilled to be able to make the program a reality. We are optimistic that it will make a significant difference to the GNOME project.&lt;/p&gt;
&lt;p&gt;If you didn’t see it already, &lt;a href="https://blogs.gnome.org/foundation/2026/03/24/introducing-gnome-fellowship/"&gt;check out the announcement for details&lt;/a&gt;. Also, if you want to apply to be our first Fellow, you have just three days until the application deadline on 20th April!&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://donate.gnome.org"&gt;donate.gnome.org&lt;/a&gt; has been a great success for the GNOME Foundation, and it is only through the support of our existing donors that the Fellowship was possible. Despite these amazing contributions, the GNOME Foundation needs to grow our donations if we are going to be able to support future Fellowship rounds while simultaneously sustaining the organisation.&lt;/p&gt;
&lt;p&gt;To this end, there’s an effort happening to build our marketing and fundraising effort. This is primarily taking place in the &lt;a class="external" href="https://gitlab.gnome.org/Teams/Engagement/gitlab-profile/-/blob/main/README.md"&gt;GNOME Engagement Team&lt;/a&gt;, and we would love help from the community to help boost our outbound comms. If you are interested, please join the Engagement space and look out for announcements.&lt;/p&gt;
&lt;p&gt;Also, if you haven’t already, and are able to do so: &lt;a class="external" href="https://donate.gnome.org/"&gt;please donate&lt;/a&gt;!&lt;/p&gt;
&lt;h2&gt;Conferences&lt;/h2&gt;
&lt;p&gt;We have two major events coming up, with &lt;a class="external" href="https://linuxappsummit.org/"&gt;Linux App Summit in May&lt;/a&gt; and &lt;a class="external" href="https://guadec.org"&gt;GUADEC in July&lt;/a&gt;, so right now is a busy time for conferences.&lt;/p&gt;
&lt;p&gt;The schedules for both of these upcoming events are currently being worked on, and arrangements for catering, photographers, and audio visual services are all in the process of being finalized.&lt;/p&gt;
&lt;p&gt;The Travel Committee has also been busy handling GUADEC travel requests, and has sent out the first batch of approvals. There are some budget pressures right now due to rising flight prices, but budget has been put aside for more GUADEC travel, so &lt;a class="external" href="https://handbook.gnome.org/events/travel.html"&gt;please apply if you want to attend and need support&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;April 2026 Board Meeting&lt;/h2&gt;
&lt;p&gt;This week was the Board’s regular monthly meeting for April. Highlights from the meeting included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I gave a general report on the Foundation’s activities, and we discussed progress on programs and initiatives, including the new Fellowship program and fundraising.&lt;/li&gt;
&lt;li&gt;Deepa gave a finance report for October to December 2025.&lt;/li&gt;
&lt;li&gt;Andrea Veri joined us to give an update on the Membership &amp;amp; Elections Committee, as well as the Infrastructure team. Andrea has been doing this work for a long time and has been instrumental in helping to keep the Foundation running, so this was a great opportunity to thank him for his work.&lt;/li&gt;
&lt;li&gt;One key takeaway from this month’s discussion was the very high level of support that GNOME receives from our infrastructure partners, particularly &lt;a class="external" href="https://aws.amazon.com/"&gt;AWS&lt;/a&gt; and also &lt;a class="external" href="https://www.fastly.com/"&gt;Fastly&lt;/a&gt;. We are hugely appreciative of this support, which represents a major financial contribution to GNOME, and want to make sure that these partners get positive exposure from us and feel appreciated.&lt;/li&gt;
&lt;li&gt;We reviewed the timeline for the upcoming 2026 board elections, which we are tweaking a little this year, in order to ensure that there is opportunity to discuss every candidacy, and reduce some unnecessary delay in final result.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Infrastructure&lt;/h2&gt;
&lt;p&gt;As usual, plenty has been happening on the infrastructure side over the past month. This has included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ongoing work to tune our Fastly configuration and managing the resource usage of GNOME’s infra.&lt;/li&gt;
&lt;li&gt;Deployment of a &lt;a class="external" href="https://www.liberaforms.org/en"&gt;LiberaForms&lt;/a&gt; instance on GNOME infrastructure. This is hooked up to GNOME’s SSO, so is available to anyone with an account who wants to use it – just head over to &lt;a class="external" href="https://forms.gnome.org/"&gt;forms.gnome.org&lt;/a&gt; to give it a try.&lt;/li&gt;
&lt;li&gt;Changes to the Foundation’s internal email setup, to allow easier management of the generic contact email addresses, as well as better organisation of the role-based email addresses that we have.&lt;/li&gt;
&lt;li&gt;New translation support for donate.gnome.org.&lt;/li&gt;
&lt;li&gt;Ongoing work in Flathub, around OAuth and flat-manager.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Admin &amp;amp; Finance&lt;/h2&gt;
&lt;p&gt;On the accounting side, the team has been busy catching up on regular work that got put to one side during last month’s audit. There were some significant delays to our account process as a result of this, but we are now almost up to date.&lt;/p&gt;
&lt;p&gt;Reorganisation of many of our finance processes has also continued over the past four weeks. Progress has included a new structure and cadence for our internal accounting calls, continued configuration of our new payments platform, and new forms for handling reimbursement requests.&lt;/p&gt;
&lt;p&gt;Finally, we have officially kicked off the process of migrating to our new physical mail service. Work on this is ongoing and will take some time to complete. &lt;a class="external" href="https://foundation.gnome.org/contact"&gt;Our new address is on the website&lt;/a&gt;, if anyone needs it.&lt;/p&gt;
&lt;p&gt;That’s it for this report! Thanks for reading, and feel free to use the comments if you have questions!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/aday/2026/04/17/gnome-foundation-update-2026-04-17/</guid>
      <pubDate>Fri, 17 Apr 2026 15:22:14 +0000</pubDate>
    </item>
    <item>
      <title>Matthias Klumpp: Hello old new “Projects” directory!</title>
      <link>https://blog.tenstral.net/2026/04/hello-projects-directory.html</link>
      <description>&lt;p&gt;If you have recently installed a very up-to-date Linux distribution with a desktop environment, or upgraded your system on a rolling-release distribution, you might have noticed that your home directory has a new folder: “Projects”&lt;/p&gt;
&lt;h2 class="wp-block-heading"&gt;Why?&lt;/h2&gt;
&lt;p&gt;With the recent 0.20 release of &lt;a href="https://www.freedesktop.org/wiki/Software/xdg-user-dirs/"&gt;xdg-user-dirs&lt;/a&gt; we enabled the “Projects” directory by default. Support for this has already existed since 2007, but was never formally enabled. This closes a &lt;a href="https://gitlab.freedesktop.org/xdg/xdg-user-dirs/-/work_items/3"&gt;more than 11 year old bug report&lt;/a&gt; that asked for this feature.&lt;/p&gt;
&lt;p&gt;The purpose of the &lt;em&gt;Projects&lt;/em&gt; directory is to give applications a default location to place project files that do not cleanly belong into one of the existing categories (Documents, Music, Pictures, Videos). Examples of this are software engineering projects, scientific projects, 3D printing projects, CAD design or even things like video editing projects, where project files would end up in the “Projects” directory, with output video being more at home in “Videos”.&lt;/p&gt;
&lt;p&gt;By enabling this by default, and subsequently in the coming months adding support to GLib, Flatpak, desktops and applications that want to make use of it, we hope to give applications that do operate in a “project-centric” manner with mixed media a better default storage location. As of now, those tools either default to the home directory, or will clutter the “Documents” folder, both of which is not ideal. It also gives users a default organization structure, hopefully leading to less clutter overall and better storage layouts.&lt;/p&gt;
&lt;h2 class="wp-block-heading"&gt;This sucks, I don’t like it!&lt;/h2&gt;
&lt;figure class="wp-block-image size-full"&gt;&lt;a href="https://blog.tenstral.net/wp-content/uploads/2026/04/woman-pointing-at-projects-folder.avif"&gt;&lt;img alt="" class="wp-image-2055" height="273" src="https://blog.tenstral.net/wp-content/uploads/2026/04/woman-pointing-at-projects-folder.avif" width="548"/&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;As usual, you are in control and can modify your system’s behavior. If you do not like the “Projects” folder, &lt;strong&gt;simply delete it!&lt;/strong&gt; The &lt;em&gt;xdg-user-dirs&lt;/em&gt; utility will not try to create it again, and instead adjust the default location for this directory to your home directory. If you want more control, you can influence exactly what goes where by editing your &lt;code&gt;~/.config/user-dirs.dirs&lt;/code&gt; configuration file.&lt;/p&gt;
&lt;p&gt;If you are a system administrator or distribution vendor and want to set default locations for the default XDG directories, you can edit the &lt;code&gt;/etc/xdg/user-dirs.defaults&lt;/code&gt; file to set global defaults that affect all users on the system (users can still adjust the settings however they like though).&lt;/p&gt;
&lt;h2 class="wp-block-heading"&gt;What else is new?&lt;/h2&gt;
&lt;p&gt;Besides this change, the 0.20 release of &lt;code&gt;xdg-user-dirs&lt;/code&gt; brings full support for the Meson build system (dropping Automake), translation updates, and some robustness improvements to its code. We also fixed the “arbitrary code execution from unsanitized input” bug that the Arch Linux Wiki mentions &lt;a href="https://wiki.archlinux.org/title/XDG_user_directories#Querying_configured_directories"&gt;here&lt;/a&gt; for the &lt;code&gt;xdg-user-dirs&lt;/code&gt; utility, by replacing the shell script with a C binary.&lt;/p&gt;
&lt;p&gt;Thanks to everyone who contributed to this release!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blog.tenstral.net/2026/04/hello-projects-directory.html</guid>
      <pubDate>Sat, 18 Apr 2026 08:06:00 +0000</pubDate>
    </item>
    <item>
      <title>Juan Pablo Ugarte: Casilda 1.2.4 Released!</title>
      <link>https://blogs.gnome.org/xjuan/2026/04/19/casilda-1-2-4-released/</link>
      <description>&lt;p&gt;I am very happy to announce a new version of Casilda!&lt;/p&gt;
&lt;p&gt;A simple Wayland compositor widget for Gtk 4 originally created for &lt;a class="external" href="https://gitlab.gnome.org/jpu/cambalache"&gt;Cambalache&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This release comes with several new features, bug fixes and extra polish that it is making it start to feel like a proper compositor.&lt;/p&gt;
&lt;p&gt;It all started with a quick 1.2 release to port it to wlroots 0.19 because 0.18 was removed from Debian, while doing this on my new laptop I was able to reproduce a &lt;a class="external" href="https://gitlab.gnome.org/jpu/casilda/-/issues/11"&gt;texture leak crash&lt;/a&gt; which lead to 1.2.1 and a &lt;a class="external" href="https://gitlab.gnome.org/GNOME/gtk/-/commit/42c0796a47af6b16a75c7164aba79a5bf14a177a"&gt;fix in Gtk&lt;/a&gt; by Benjamin to support Vulkan drivers that return dmabufs with less fd than planes.&lt;/p&gt;
&lt;p&gt;At this point I was invested to I decided to fix the rest of issues in the backlog…&lt;/p&gt;
&lt;h3&gt;Update:&lt;/h3&gt;
&lt;p&gt;Cambalache 1.0.1 released with Casilda 1.2.4&lt;/p&gt;
&lt;h3&gt;Fractional scale&lt;/h3&gt;
&lt;p&gt;Casilda only supported integer scales not fractional scale so you could set your display scale to 200% but not 125%.&lt;/p&gt;
&lt;p&gt;For reference this is how gtk4-demo looks like at 100% or scale 1 where 1 application/logical pixel corresponds to one device/display pixel.&lt;/p&gt;
&lt;p&gt;*** Keep in mind its preferable to see all the following images without fractional scale itself and at full size ***&lt;/p&gt;
&lt;p&gt;&lt;img alt="" class="alignnone wp-image-6533 size-full" height="797" src="https://blogs.gnome.org/xjuan/files/2026/04/casilda_scale_1.png" width="1052"/&gt;&lt;/p&gt;
&lt;p&gt;Clients would render at the next round scale if the application was started with a fractional scale set…&lt;/p&gt;
&lt;p&gt;&lt;img alt="" class="alignnone wp-image-6529 size-full" height="996" src="https://blogs.gnome.org/xjuan/files/2026/04/casilda_broken_fractional_scale.png" width="1315"/&gt;&lt;/p&gt;
&lt;p&gt;Or the client would render at scale 1 and look blurry if you switched from 1 to a fractional scale.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" class="alignnone wp-image-6530 size-full" height="996" src="https://blogs.gnome.org/xjuan/files/2026/04/casilda_broken_fractional_scale_switching.png" width="1315"/&gt;&lt;/p&gt;
&lt;p&gt;In both cases the input did not matched with the renderer window making the application really broken.&lt;/p&gt;
&lt;p&gt;So if the client application draws a 4 logical pixel border, it will be 5 pixels in the backing texture this means that 1 logical pixel correspond to 1.25 device pixels. So in order for things to look sharp CasildaCompositor needs to make sure the coordinates it uses for position the client window will match to the device pixel grid.&lt;/p&gt;
&lt;p&gt;My first attempt was to do&lt;/p&gt;
&lt;pre&gt;((int)x * scale) / scale&lt;/pre&gt;
&lt;p&gt;but that still looked blurry, and that is because I assumed window coordinate 0,0 was the same as its backing surface coordinates 0,0 but that is not the case because I forgot about the window shadow. Luckily there is API to get the offset, then all you have to do is add the logical position of the compositor widget and you get the surface origin coordinates&lt;/p&gt;
&lt;pre lang="c"&gt;gtk_native_get_surface_transform (GTK_NATIVE (root), &amp;amp;surface_origin_x, &amp;amp;surface_origin_y);

/* Add widget offset */
if (gtk_widget_compute_point (self, GTK_WIDGET (root), &amp;amp;GRAPHENE_POINT_INIT (0, 0), &amp;amp;out_point))
  {
    surface_origin_x += out_point.x;
    surface_origin_y += out_point.y;
  }&lt;/pre&gt;
&lt;p&gt;Once I had that I could finally calculate the right position&lt;/p&gt;
&lt;pre&gt;/* Snap logical coordinates to device pixel grid */
if (scale &amp;gt; 1.0)
  {
    x = floorf ((x + surface_origin_x) * scale) / scale - surface_origin_x;
    y = floorf ((y + surface_origin_y) * scale) / scale - surface_origin_y;
  }&lt;/pre&gt;
&lt;p&gt;And this is how it looks now with 1.25 fractional scale.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" class="alignnone wp-image-6534 size-full" height="996" src="https://blogs.gnome.org/xjuan/files/2026/04/casilda_scale_1.25.png" width="1315"/&gt;&lt;/p&gt;
&lt;h3&gt;Keyboard layouts&lt;/h3&gt;
&lt;p&gt;Another missing feature was support for different keyboard layouts so switching layouts would work on clients too. Not really important for Cambalache but definitely necessary for a generic compositor.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" class="alignnone wp-image-6528 size-full" height="797" src="https://blogs.gnome.org/xjuan/files/2026/04/casilda_layouts.png" width="1052"/&gt;&lt;/p&gt;
&lt;h3&gt;Popups positioners&lt;/h3&gt;
&lt;p&gt;Casilda now send clients all the necessary information for positioning popups in a place where they do not get cut out of the display area which is a nice thing to have.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" class="alignnone wp-image-6527 size-full" height="797" src="https://blogs.gnome.org/xjuan/files/2026/04/casilda_positioner.png" width="1052"/&gt;&lt;/p&gt;
&lt;h3&gt;Cursor shape protocol&lt;/h3&gt;
&lt;p&gt;Current versions of Gtk 4 requires cursor shape protocol on wayland otherwise it fallback to 32×32 pixel size cursors which might not be the same size of your system cursors and look blurry with fractional scales.&lt;/p&gt;
&lt;p&gt;In this case the client send an cursor id instead of a pixel buffer when it wants to change the cursor.&lt;/p&gt;
&lt;p&gt;This was really easy to implement as all I had to do is call&lt;/p&gt;
&lt;pre&gt;gtk_widget_set_cursor_from_name (compositor, wlr_cursor_shape_v1_name (event-&amp;gt;shape));&lt;/pre&gt;
&lt;h3&gt;Greetings&lt;/h3&gt;
&lt;p&gt;As usual this would not be possible without the help of the community, special thanks to emersion, Matthias and Benjamin for their help and support.&lt;/p&gt;
&lt;h3&gt;Release Notes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;Add fractional scale support&lt;/li&gt;
&lt;li&gt;Add viewporter support&lt;/li&gt;
&lt;li&gt;Add support for cursor shape&lt;/li&gt;
&lt;li&gt;Forward keyboard layout changes to clients.&lt;/li&gt;
&lt;li&gt;Improve virtual size calculation&lt;/li&gt;
&lt;li&gt;Fix maximized/fullscreen auto resize on compositor size allocation&lt;/li&gt;
&lt;li&gt;Add support for popups reposition&lt;/li&gt;
&lt;li&gt;Fix GdkTexture leak&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed Issues&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;#5 “Track keymap layout changes”&lt;/li&gt;
&lt;li&gt;#12 “Support for wlroots-0.19”&lt;/li&gt;
&lt;li&gt;#13 “Wrong cursor size on client windows”&lt;/li&gt;
&lt;li&gt;#14 “Support for fractional scaling snap to device grid”&lt;/li&gt;
&lt;li&gt;#19 Add support for popups reposition&lt;/li&gt;
&lt;li&gt;#16 Firefox GTK backdrop/shadow not scaled correctly&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Where to get it?&lt;/h3&gt;
&lt;p&gt;Source code lives on GNOME gitlab &lt;a class="external" href="https://gitlab.gnome.org/jpu/casilda"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;git clone https://gitlab.gnome.org/jpu/casilda.git&lt;/pre&gt;
&lt;h3&gt;Matrix channel&lt;/h3&gt;
&lt;p&gt;Have any question? come chat with us at &lt;a class="external" href="https://matrix.to/#/#cambalache:gnome.org"&gt;#cambalache:gnome.org&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Mastodon&lt;/h3&gt;
&lt;p&gt;Follow me in Mastodon &lt;a class="external" href="https://mastodon.social/@xjuan"&gt;@xjuan&lt;/a&gt; to get news related to Casilda and Cambalache development.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/xjuan/2026/04/19/casilda-1-2-4-released/</guid>
      <pubDate>Sun, 19 Apr 2026 20:07:12 +0000</pubDate>
    </item>
    <item>
      <title>Andy Wingo: on hayek's bastards</title>
      <link>https://wingolog.org/archives/2026/04/20/on-hayeks-bastards</link>
      <description>&lt;div&gt;&lt;p&gt;After &lt;a href="https://wingolog.org/archives/2026/03/26/free-trade-and-the-left-quater-witches"&gt;wrapping up a four-part series on free trade and the
left&lt;/a&gt;,
I thought I was done with neoliberalism.  I had come to the conclusion
that neoliberals were simply not serious people: instead of placing
value in literally any human concern, they value only a network of
trade, and as such, cannot say anything of value.  They should be
ignored in public debate; we can find economists elsewhere.&lt;/p&gt;&lt;p&gt;I based this conclusion partly on Quinn Slobodian’s
&lt;a href="https://www.hup.harvard.edu/books/9780674244849"&gt;&lt;i&gt;Globalists&lt;/i&gt;&lt;/a&gt; (2020),
which describes Friedrich Hayek’s fascination with cybernetics in the
latter part of his life.  But Hayek himself died before the birth of the
WTO, NAFTA, all the institutions “we” fought in Seattle; we fought his
ghost, living on past its time.&lt;/p&gt;&lt;p&gt;Well, like I say, I thought I was done, but then a copy of Slobodian’s
&lt;a href="https://press.princeton.edu/books/hardcover/9781890951917/hayeks-bastards"&gt;&lt;i&gt;Hayek’s
Bastards&lt;/i&gt;&lt;/a&gt;
(2025) arrived in the post.  The book contests the narrative that the
right-wing “populism” that we have seen in the last couple decades is an
exogenous reaction to elite technocratic management under high
neoliberalism, and that actually it proceeds from a faction of the
neoliberal project.  It’s easy to infer a connection when we look at,
say, &lt;a href="https://en.wikipedia.org/wiki/Javier_Milei"&gt;Javier Milei&lt;/a&gt;‘s
background and cohort, but Slobodian delicately unpicks the weft to
expose the tensile fibers linking the core neoliberal institutions to
the alt-right.  Tonight’s note is a book review of sorts.&lt;/p&gt;&lt;h3&gt;after hayek&lt;/h3&gt;&lt;p&gt;Let’s back up a bit.  Slobodian’s argument in &lt;i&gt;Globalists&lt;/i&gt; was that
neoliberalism is not really about &lt;i&gt;laissez-faire&lt;/i&gt; as such: it is a
project to design institutions of international law to &lt;i&gt;encase&lt;/i&gt; the
world economy, to protect it from state power (democratic or otherwise)
in any given country.  It is paradoxical, because such an encasement
requires state power, but it is what it is.&lt;/p&gt;&lt;p&gt;&lt;i&gt;Hayek’s Bastards&lt;/i&gt; is also about encasement, but instead of protection
from the state, the economy was to be protected from debasement by the
unworthy.  (Also there is a chapter on goldbugs, but that’s not what I
want to talk about.)&lt;/p&gt;&lt;p&gt;The book identifies two major crises that push a faction of neoliberals
to ally themselves with a culturally reactionary political program.  The
first is the civil rights movement of the 1960s and 1970s, together with
decolonization.  To put it crudely, whereas before, neoliberal
economists could see themselves as acting in everyone’s best interest,
having more black people in the polity made some of these white
economists feel like their project was being perverted.&lt;/p&gt;&lt;p&gt;Faced with this “crisis”, at first the reactionary neoliberals reached
out to race: the infant post-colonial nations were unfit to participate
in the market because their peoples lacked the cultural advancement of
the West.  Already &lt;i&gt;Globalists&lt;/i&gt; traced a line through &lt;a href="https://en.wikipedia.org/wiki/Wilhelm_R%C3%B6pke"&gt;Wilhelm
Röpke&lt;/a&gt;‘s full-throated
defense of apartheid, but the subjects of &lt;i&gt;Hayek’s Bastards&lt;/i&gt; (&lt;a href="https://en.wikipedia.org/wiki/Lew_Rockwell"&gt;Lew
Rockwell&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Charles_Murray_(political_scientist)"&gt;Charles
Murray&lt;/a&gt;,
&lt;a href="https://en.wikipedia.org/wiki/Murray_Rothbard"&gt;Murray Rothbard&lt;/a&gt;, et al)
were more subtle: instead of directly stating that black people were
unfit to govern, Murray et al argued that intelligence was the most
important quality in a country’s elite.  It just so happened that they
also argued, clothed in the language of evolutionary psychology and
genetics, that black people are less intelligent than white people, and
so it is natural that they not occupy these elite roles, that they be
marginalized.&lt;/p&gt;&lt;p&gt;Before proceeding, three parentheses:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Some words have a taste. &lt;i&gt;Miscegenation&lt;/i&gt; tastes like the juice at the bottom of a garbage bag left out in the sun: to racists, because of the visceral horror they feel at the touch of the other, and to the rest of us, because of the revulsion the very idea provokes.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;I harbor an enmity to Silvia Plath because of &lt;a href="https://en.wikipedia.org/wiki/The_Bell_Curve"&gt;The Bell Curve&lt;/a&gt;.  She bears no responsibility; her book was &lt;a href="https://en.wikipedia.org/wiki/The_Bell_Jar"&gt;The Bell Jar&lt;/a&gt;.  I know this in my head but my heart will not listen.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;I do not remember the context, but I remember a professor in university telling me that the notion of “race” is a social construction without biological basis; it was an offhand remark that was new to me then, and one that I still believe now.  Let’s make sure the kids now hear the good word now too; stories don’t tell themselves.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The second crisis of neoliberalism was the fall of the Berlin Wall: some
wondered if the negative program of deregulation and removal of state
intervention was missing a positive putty with which to re-encase the
market.  It’s easy to stand up on a stage with a chainsaw, but without a
constructive program, neoliberal wins in one administration are fragile
in the next.&lt;/p&gt;&lt;p&gt;The reactionary faction of neoliberalism’s turn to “family values”
responds to this objective need, and dovetails with the reaction to the
civil rights movement: to protect the market from the unworthy,
neo-reactionaries worked to re-orient the discourse, and then state
policy, away from “equality” and the idea that idea that &lt;a href="https://www.amazon.com/Should-Improve-Society-Somewhat-Collection/dp/1951038002"&gt;We Should
Improve Society,
Somewhat&lt;/a&gt;.
Moldbug’s neofeudalism is an excessive rhetorical joust, but one that
has successfully moved the window of acceptable opinions.  The
“populism” of the AfD or the recent &lt;a href="https://twitter-thread.com/t/2045574398573453312"&gt;Alex Karp
drivel&lt;/a&gt; is not a
reaction, then, to neoliberalism, but a reaction &lt;i&gt;by&lt;/i&gt; a faction of
neoliberals to the void left after communism.  (And when you get down to
it, what is the difference between Moldbug nihilistically rehashing
Murray’s “black people are low-IQ” and Larry Summers’ &lt;a href="https://en.wikipedia.org/wiki/Summers_memo"&gt;“countries in
Africa are vastly
UNDER-polluted”&lt;/a&gt;?)&lt;/p&gt;&lt;h3&gt;thots&lt;/h3&gt;&lt;p&gt;Slobodian shows remarkable stomach: his object of study is revolting.
He has truly done the work.&lt;/p&gt;&lt;p&gt;For all that, &lt;i&gt;Hayek’s Bastards&lt;/i&gt; left me with a feeling of indigestion:
&lt;i&gt;why bother with the racism?&lt;/i&gt; Hayek himself had a thesis of sorts, woven
through his long career, that there is none of us that is smarter than
the market, and that in many (most?) cases, the state should curb its
hubris, step back, and let the spice flow.  Prices are a signal, axons
firing in an ineffable network of value, sort of thing.  &lt;i&gt;This is a good
thesis!&lt;/i&gt; I’m not saying it’s right, but it’s interesting, and I’m happy
to engage with it and its partisans.&lt;/p&gt;&lt;p&gt;So why do Hayek’s bastards reach to racism?  My first thought is that
they are simply not worthy: Charles Murray et al are intellectually lazy
and moreover base.  My lip curls to think about them in any serious way.
I can’t help but recall the &lt;a href="https://en.wikipedia.org/wiki/DARVO"&gt;DARVO&lt;/a&gt;
tactic of abusers; neo-reactionaries blame “diversity” for “debasing the
West”, but it is their ignorant appeals to “race science” that is
without basis.&lt;/p&gt;&lt;p&gt;Then I wonder: to what extent is this all an overworked intellectual
retro-justification for something they wanted all along?  When &lt;a href="https://wingolog.org/archives/2026/03/06/free-trade-and-the-left-ter-mises-and-my-apostasy"&gt;Mises
rejoiced in the violent defeat of the 1927
strike&lt;/a&gt;,
he was certainly not against state power per se; but was he &lt;i&gt;for&lt;/i&gt; the
market, or was he just against a notion of equality?&lt;/p&gt;&lt;p&gt;I can only conclude that things are confusing.  &lt;a href="https://sankaran.substack.com/p/stop-blaming-neoliberals-for-neoconneries"&gt;“Mathematical” neoliberals
exist&lt;/a&gt;,
and don’t need to lean on racism to support their arguments.  There are
also the alt-right/neo-reactionaries, who grew out &lt;i&gt;from&lt;/i&gt; neoliberalism,
not in opposition to it: no seasteader is a partisan of autarky.  &lt;i&gt;They
go to the same conferences.&lt;/i&gt; It is a baffling situation.&lt;/p&gt;&lt;p&gt;While it is
all more the more reason to ignore them both, intellectually,
Slobodian’s book shows that politically we on the left have our work set out for us
both in deconstructing the new racism of the alt-right, and in
advocating for a positive program of equality to take its place.&lt;/p&gt;&lt;/div&gt;</description>
      <guid isPermaLink="false">https://wingolog.org/archives/2026/04/20/on-hayeks-bastards</guid>
      <pubDate>Mon, 20 Apr 2026 21:35:03 +0000</pubDate>
    </item>
    <item>
      <title>Thibault Martin: TIL that Minikube mounts volumes as root</title>
      <link>https://ergaster.org/til/minikube-volumes-root/</link>
      <description>&lt;p&gt;When I have to play with a container image I have never met before, I like to deploy it on a test cluster to poke and prod it. I usually did that on a k3s cluster, but recently I've moved to Minikube to bring my test cluster with me when I'm on the go.&lt;/p&gt;
&lt;p&gt;Minikube is a tiny one-node Kubernetes cluster meant to run on development machines. It's useful to test &lt;code&gt;Deployments&lt;/code&gt; or &lt;code&gt;StatefulSets&lt;/code&gt; with images you are not familiar with and build proper helm charts from them.&lt;/p&gt;
&lt;p&gt;It provides volumes of the &lt;code&gt;hostPath&lt;/code&gt; type by default. The major caveat of &lt;code&gt;hostPath&lt;/code&gt; volumes is that they're &lt;a href="https://kubernetes.io/docs/concepts/storage/volumes/#hostpath-volume-types:~:text=Some%20files,volume"&gt;mounted as root by default&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I usually handle mismatched ownership with a &lt;code&gt;securityContext&lt;/code&gt; like the following to instruct the container to run with a specific UID and GID, and to make the volume owned by a specific group.&lt;/p&gt;
&lt;p&gt;Typically in a &lt;code&gt;StatefulSet&lt;/code&gt; it looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp
# [...]
spec:
# [...]
  template:
# [...]
    spec:
      securityContext:
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
      containers:
        - name: myapp
          volumeMounts:
            - name: data
              mountPath: /data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
# [...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Processes in the Pod &lt;code&gt;myapp&lt;/code&gt; will run with UID 10001 and GID 10001.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/data&lt;/code&gt; directory mounted from the &lt;code&gt;data&lt;/code&gt; volume will belong to group 10001 as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;securityContext&lt;/code&gt; usually solves the problem, but that's not how &lt;code&gt;hostPath&lt;/code&gt; works. For &lt;code&gt;hostPath&lt;/code&gt; volumes, the &lt;code&gt;securityContext.fsGroup&lt;/code&gt; property is silently ignored.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!success] Init Container to the Rescue!&lt;/p&gt;
&lt;p&gt;The solution in this specific case is to use an &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"&gt;initContainer&lt;/a&gt; as root to &lt;code&gt;chown&lt;/code&gt; the volume mounts to the unprivileged user.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In practice it will look like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp
# [...]
spec:
# [...]
  template:
# [...]
    spec:
      securityContext:
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
      initContainers:
        - name: fix-perms
          image: busybox
          command:
            ["sh", "-c", "chown -R 10001:10001 /data"]
          securityContext:
            runAsUser: 0
          volumeMounts:
            - name: data
              mountPath: /data
      containers:
        - name: myapp
          volumeMounts:
            - name: data
              mountPath: /data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
# [...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It took me a little while to figure it out, because I was used to testing my &lt;code&gt;StatefulSets&lt;/code&gt; on k3s. K3s uses a local path provisioner, which gives me &lt;code&gt;local&lt;/code&gt; volumes, not &lt;code&gt;hostPath&lt;/code&gt; ones like Minikube.&lt;/p&gt;
&lt;p&gt;In production I don't need the &lt;code&gt;initContainer&lt;/code&gt; to fix permissions since I'm deploying this on an EKS cluster.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://ergaster.org/til/minikube-volumes-root/</guid>
      <pubDate>Tue, 21 Apr 2026 07:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Jussi Pakkanen: CapyPDF is approaching feature sufficiency</title>
      <link>https://nibblestew.blogspot.com/2026/04/capypdf-is-approaching-feature.html</link>
      <description>&lt;p&gt;In the past I have written many blog posts on implementing various PDF features in &lt;a href="https://github.com/jpakkane/capypdf"&gt;CapyPDF&lt;/a&gt;. Typically they explain the feature being implemented, how confusing the documentation is, what perverse undocumented quirks one has to work around to get things working and so on. To save the effort of me writing and you reading yet another post of the same type, let me just say that you can now use CapyPDF to generate PDF forms that have widgets like text fields and radio buttons.&lt;/p&gt;&lt;p&gt;What makes this post special is that forms and widget annotations were pretty much the last major missing PDF feature Does that mean that it supports everything? No. Of course not. There is a whole bunch of subtlety to consider. Let's start with the fact that the PDF spec is &lt;i&gt;massive&lt;/i&gt;, close to 1000 pages. Among its pages are features that are either not used or have been replaced by other features and deprecated.&lt;/p&gt;&lt;p&gt;The implementation principle of CapyPDF thus far has been "implement everything that needs special tracking, but only to the minimal level needed". This seems complicated but is in fact quite simple. As an example the PDF spec defines over 20 different kinds of annotations. Specifying them requires tracking each one and writing out appropriate entries in the document metadata structures. However once you have implemented &lt;i&gt;that &lt;/i&gt;for one annotation type, the same code will work for all annotation types. Thus CapyPDF has only implemented a few of the most common annotations and the rest can be added later when someone actually needs them.&lt;/p&gt;&lt;p&gt;Many objects have lots of configuration options which are defined by adding keys and values to existing dictionaries. Again, only the most common ones are implemented, the rest are mostly a matter of adding functions to set those keys. There is no cross-referencing code that needs to be updated or so on. If nobody ever needs to specify the color with which a trim box should be drawn in a prepress preview application, there's no point in spending effort to make it happen.&lt;/p&gt;&lt;p&gt;The API should be mostly done, especially for drawing operations. The API for widgets probably needs to change. Especially since form submission actions are not done. I don't know if anything actually uses those, though. That work can be done based on user feedback.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://nibblestew.blogspot.com/2026/04/capypdf-is-approaching-feature.html</guid>
      <pubDate>Tue, 21 Apr 2026 20:09:00 +0000</pubDate>
    </item>
    <item>
      <title>Sebastian Wick: How Hard Is It To Open a File?</title>
      <link>https://blog.sebastianwick.net/posts/how-hard-is-it-to-open-a-file/</link>
      <description>&lt;p&gt;It’s a question I had to ask myself multiple times over the last few months. Depending on the context the answer can be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;very simple, just call the standard library function&lt;/li&gt;
&lt;li&gt;extremely hard, don’t trust anything&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are an app developer, you’re lucky and it’s almost always the first answer. If you develop something with a security boundary which involves files in any way, the correct answer is very likely the second one.&lt;/p&gt;
&lt;h2 id="opening-a-file-the-hard-way"&gt;Opening a File, the Hard Way&lt;/h2&gt;
&lt;p&gt;Like so often, the details depend on the specifics, but in the worst-case scenario, there is a process on either side of the security boundary, which operate on a filesystem tree which is shared by both processes.&lt;/p&gt;
&lt;p&gt;Let’s say that the process with more privileges operates on a file on behalf of the process with less privileges. You might want to restrict this to files in a certain directory, to prevent the less privileged process from, for example, stealing your SSH key, and thus take a subpath that is relative to that directory.&lt;/p&gt;
&lt;p&gt;The first obvious problem is that the subpath can refer to files outside of the directory if it contains &lt;code&gt;..&lt;/code&gt;. If the privileged process gets called with a subpath of &lt;code&gt;../.ssh/id_ed25519&lt;/code&gt;, you are in trouble. Easy fix: normalize the path, and if we ever go outside of the directory, fail.&lt;/p&gt;
&lt;p&gt;The next issue is that every component of the path might be a symlink. If the privileged process gets called with a subpath of &lt;code&gt;link&lt;/code&gt;, and &lt;code&gt;link&lt;/code&gt; is a symlink to &lt;code&gt;../.ssh/id_ed25519&lt;/code&gt;, you might be in trouble. If the process with less privileges cannot create files in that part of the tree, it cannot create a malicious symlink, and everything is fine. In all other scenarios, nothing is fine. Easy fix: resolve the symlinks, expand the path, then normalize it.&lt;/p&gt;
&lt;p&gt;This is usually where most people think we’re done, opening a file is not that hard after all, we can all do more fun things now. Really, this is where the fun begins.&lt;/p&gt;
&lt;p&gt;The fix above works, as long as the less privileged process cannot change the file system tree anywhere in the file’s path while the more privileged process tries to access it. Usually this is the case if you unpack an attacker-provided archive into a directory the attacker does not have access to. If it can however, we have a classic TOCTOU (time-of-check to time-of-use) race.&lt;/p&gt;
&lt;p&gt;We have the path &lt;code&gt;foo/id_ed25519&lt;/code&gt;, we resolve the smlinks, we expand the path, we normalize it, and while we did all of that, the other process just replaced the regular directory &lt;code&gt;foo&lt;/code&gt; that we just checked with a symlink which points to &lt;code&gt;../.ssh&lt;/code&gt;. We just checked that the path resolves to a path inside the target directory though, and happily open the path &lt;code&gt;foo/id_ed25519&lt;/code&gt; which now points to your ssh key. Not an easy fix.&lt;/p&gt;
&lt;p&gt;So, what is the fundamental issue here? A path string like &lt;code&gt;/home/user/.local/share/flatpak/app/org.example.App/deploy&lt;/code&gt; describes a location in a filesystem namespace. It is &lt;em&gt;not&lt;/em&gt; a reference to a file. By the time you finish speaking the path aloud, the thing it names may have changed.&lt;/p&gt;
&lt;p&gt;The safe primitive is the file descriptor. Once you have an fd pointing at an inode, the kernel pins that inode. The directory can be unlinked, renamed, or replaced with a symlink; the fd does not care. A common misconception is that file descriptors represent open files. It is true that they can do that, but fds opened with &lt;code&gt;O_PATH&lt;/code&gt; do not require opening the file, but still provide a stable reference to an inode.&lt;/p&gt;
&lt;p&gt;The lesson that should be learned here is that you should not call any privileged process with a path. Period. Passing in file descriptors also has the benefit that they serve as proof that the calling process actually has access to the resource.&lt;/p&gt;
&lt;p&gt;Another important lesson is that dropping down from a file descriptor to a path makes everything racy again. For example, let’s say that we want to bind mount something based on a file descriptor, and we only have the traditional mount API, so we convert the fd to a path, and pass that to mount. Unfortunately for the user, the kernel resolves the symlinks in the path that an attacker might have managed to place there. Sometimes it’s possible to detect the issue after the fact, for example by checking that the inode and device of the mounted file and the file descriptor match.&lt;/p&gt;
&lt;p&gt;With that being said, sometimes it is not entirely avoidable to use paths, so let’s also look into that as well!&lt;/p&gt;
&lt;p&gt;In the scenario above, we have a directory in which we want all the paths to resolve in, and that the attacker does not control. We can thus open it with &lt;code&gt;O_PATH&lt;/code&gt; and get a file descriptor for it without the attacker being able to redirect it somewhere else.&lt;/p&gt;
&lt;p&gt;With the &lt;code&gt;openat&lt;/code&gt; syscall, we can open a path relative to the fd we just opened. It has all the same issues we discussed above, except that we can also pass &lt;code&gt;O_NOFOLLOW&lt;/code&gt;. With that flag set, if the last segment of the path is a symlink, it does not follow it and instead opens the actual symlink inode. All the other components can still be symlinks, and they still will be followed. We can however just split up the path, and open the next file descriptor for the next path segment and resolve symlinks manually until we have done so for the entire path.&lt;/p&gt;
&lt;h2 id="libglnx-chase"&gt;libglnx chase&lt;/h2&gt;
&lt;p&gt;libglnx is a utility library for GNOME C projects that provides fd-based filesystem operations as its primary API. Functions like &lt;code&gt;glnx_openat_rdonly&lt;/code&gt;, &lt;code&gt;glnx_file_replace_contents_at&lt;/code&gt;, and &lt;code&gt;glnx_tmpfile_link_at&lt;/code&gt; all take directory fds and operate relative to them. The library is built around the discipline of “always have an fd, never use an absolute path when you can use an fd.”&lt;/p&gt;
&lt;p&gt;The most recent addition is &lt;code&gt;glnx_chaseat&lt;/code&gt;, which provides safe path traversal, and was inspired by systemd’s &lt;code&gt;chase()&lt;/code&gt;, and does precisely what was described above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;glnx_chaseat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;              &lt;span class="n"&gt;dirfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                  &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                  &lt;span class="n"&gt;GlnxChaseFlags&lt;/span&gt;   &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                  &lt;span class="n"&gt;GError&lt;/span&gt;         &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It returns an &lt;code&gt;O_PATH | O_CLOEXEC&lt;/code&gt; fd for the resolved path, or -1 on error. The real magic is in the flags:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;_GlnxChaseFlags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Default */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_DEFAULT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Disable triggering of automounts */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_NO_AUTOMOUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Do not follow the path's right-most component. When the path's right-most
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;   * component refers to symlink, return O_PATH fd of the symlink. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_NOFOLLOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Do not permit the path resolution to succeed if any component of the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;   * resolution is not a descendant of the directory indicated by dirfd. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_RESOLVE_BENEATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Symlinks are resolved relative to the given dirfd instead of root. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_RESOLVE_IN_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Fail if any symlink is encountered. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_RESOLVE_NO_SYMLINKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Fail if the path's right-most component is not a regular file */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_MUST_BE_REGULAR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Fail if the path's right-most component is not a directory */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_MUST_BE_DIRECTORY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="cm"&gt;/* Fail if the path's right-most component is not a socket */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  &lt;span class="n"&gt;GLNX_CHASE_MUST_BE_SOCKET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;GlnxChaseFlags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While it doesn’t sound too complicated to implement, a lot of details are quite hairy. The implementation uses &lt;code&gt;openat2&lt;/code&gt;, &lt;code&gt;open_tree&lt;/code&gt; and &lt;code&gt;openat&lt;/code&gt; depending on what is available and what behavior was requested, it handles auto-mount behavior, ensures that previously visited paths have not changed, and a few other things.&lt;/p&gt;
&lt;h2 id="an-aside-on-standard-libraries"&gt;An Aside on Standard Libraries&lt;/h2&gt;
&lt;p&gt;The POSIX APIs are not great at dealing with the issue. The GLib/Gio APIs (&lt;code&gt;GFile&lt;/code&gt;, etc.) are even worse and only accept paths. Granted, they also serve as a cross-platform abstraction where file descriptors are not a universal concept. Unfortunately, Rust also has this cross-platform abstraction which is based entirely on paths.&lt;/p&gt;
&lt;p&gt;If you use any of those APIs, you very likely created a vulnerability. The deeper issue is that those path-based APIs are often the standard way to interact with files. This makes it impossible to reason about the security of composed code. You can audit your own code meticulously, open everything with &lt;code&gt;O_PATH | O_NOFOLLOW&lt;/code&gt;, chain &lt;code&gt;*at()&lt;/code&gt; calls carefully — and then call a third-party library that calls &lt;code&gt;open(path)&lt;/code&gt; internally. The security property you established in your code does not compose through that library call.&lt;/p&gt;
&lt;p&gt;This means that any system-level code that cares about filesystem security has to audit all transitive dependencies or avoid them in the first place.&lt;/p&gt;
&lt;p&gt;So what would a better GLib cross-platform API look like? I would say not too different from &lt;code&gt;chaseat()&lt;/code&gt;, but returning opaque handles instead of file descriptors, which on Unix would carry the &lt;code&gt;O_PATH&lt;/code&gt; file descriptor and a path that can be used for printing, debugging and things like that. You would open files from those handles, which would yield another kind of opaque handle for reading, writing, and so on.&lt;/p&gt;
&lt;p&gt;The current &lt;code&gt;GFile&lt;/code&gt; was also designed to implement GVfs: &lt;code&gt;g_file_new_for_uri("smb://server/share/file")&lt;/code&gt; gives you a &lt;code&gt;GFile&lt;/code&gt; you can &lt;code&gt;g_file_read()&lt;/code&gt; just like a local file. This is the right goal, but the wrong abstraction layer. Instead, this kind of access should be provided by FUSE, and the URI should be translated to a path on a specific FUSE mount. This would provide a few benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The fd-chasing approach works everywhere because it is a real filesystem managed by the kernel&lt;/li&gt;
&lt;li&gt;The filesystem becomes independent of GLib and can be used for example from Rust as well&lt;/li&gt;
&lt;li&gt;It stacks with other FUSE filesystems, such as the XDG Desktop Document Portal used by Flatpak&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="wait-why-are-you-talking-about-this"&gt;Wait, Why Are You Talking About This?&lt;/h2&gt;
&lt;p&gt;Nowadays I maintain a small project called Flatpak. Codean Labs recently did a security analysis on it and found a number of issues. Even though Flatpak developers were aware of the dangers of filesystems, and created libglnx because of it, most of the discovered issues were just about that. One of them (&lt;a href="https://github.com/flatpak/flatpak/security/advisories/GHSA-cc2q-qc34-jprg"&gt;CVE-2026-34078&lt;/a&gt;) was a complete sandbox escape.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flatpak run&lt;/code&gt; was designed as a command-line tool for trusted users. When you type &lt;code&gt;flatpak run org.example.App&lt;/code&gt;, you control the arguments. The code that processes the arguments was written assuming the caller is legitimate. It accepted path strings, because that’s what command-line tools accept.&lt;/p&gt;
&lt;p&gt;The Flatpak portal was then built as a D-Bus service that sandboxed apps could call to start subsandboxes — and it did this by effectively constructing a &lt;code&gt;flatpak run&lt;/code&gt; invocation and executing it. This connected a component designed for trusted input directly to an untrusted caller (the sandboxed app).&lt;/p&gt;
&lt;p&gt;Once that connection exists, every assumption baked into &lt;code&gt;flatpak run&lt;/code&gt; about caller trustworthiness becomes a potential vulnerability. The fix wasn’t “change one function” — it was “audit the entire call chain from portal request to bubblewrap execution and replace every path string with an fd.” That’s commits touching the portal, &lt;code&gt;flatpak-run&lt;/code&gt;, &lt;code&gt;flatpak_run_app&lt;/code&gt;, &lt;code&gt;flatpak_run_setup_base_argv&lt;/code&gt;, and the bwrap argument construction, plus new options (&lt;code&gt;--app-fd&lt;/code&gt;, &lt;code&gt;--usr-fd&lt;/code&gt;, &lt;code&gt;--bind-fd&lt;/code&gt;, &lt;code&gt;--ro-bind-fd&lt;/code&gt;) threaded through all of them.&lt;/p&gt;
&lt;p&gt;If the GLib standard file and path APIs were secure, we would not have had this issue.&lt;/p&gt;
&lt;p&gt;Another annoyance here is that the entire subsandboxing approach in Flatpak comes from 15 years ago, when unprivileged user namespaces were not common. Nowadays we could (and should) let apps use kernel-native unprivileged user namespaces to create their own subsandboxes.&lt;/p&gt;
&lt;p&gt;Unfortunately with rather large changes comes a high likelihood of something going wrong. For a few days we scrambled to fix a few regressions that prevented Steam, WebKit, and Chromium-based apps from launching. Huge thanks to Simon McVittie!&lt;/p&gt;
&lt;p&gt;In the end, we managed to fix everything, made Flatpak more secure, the ecosystem is now better equipped to handle this class of issues, and hopefully you learned something as well.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blog.sebastianwick.net/posts/how-hard-is-it-to-open-a-file/</guid>
      <pubDate>Thu, 23 Apr 2026 20:41:11 +0000</pubDate>
    </item>
    <item>
      <title>Sam Thursfield: Status update, 23rd April 2026</title>
      <link>https://samthursfield.wordpress.com/2026/04/23/status-update-23rd-april-2026/</link>
      <description>&lt;p class="wp-block-paragraph"&gt;Hello there,&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;You thought I’d given up on “status update” blog posts, did you ? I haven’t given up, despite my better judgement, this one is just even later than usual.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;Recently I’ve been using my rather obscure platform as a blogger to &lt;a href="https://samthursfield.wordpress.com/2026/01/24/ai-predictions-for-2026/"&gt;theorize about AI&lt;/a&gt; and the &lt;a href="https://samthursfield.wordpress.com/2026/03/21/status-update-21st-march-2026/"&gt;future of the tech industry&lt;/a&gt;, mixed with the occasional life update, couched in vague terms, perhaps due to the increasing number of weirdos in the world who think &lt;a href="https://lwn.net/Articles/1064706/"&gt;doxxing and sending death threats to open source contributors&lt;/a&gt; is a meaningful use of their time. &lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;In fact I do have some theories about how George Orwell (in &lt;em&gt;“Why I Write”&lt;/em&gt;) and Italo Calvino (in &lt;em&gt;“If On a Winter’s Night a Traveller”&lt;/em&gt;) made some good guesses from the 20th century about how easy access to LLMs would affect communication, politics and art here in the 21st. But I’ll leave that for another time.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;It’s also 8 years since I moved to this new country where I live now, driving off the boat in a rusty transit van to enjoy a series of unexpected and amazing opportunities. Next week I’m going to mark the occasion with a five day bike ride through the mountains of Asturias, something I’ve been dreaming of doing for several years. But I’m not going to talk about that, either.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;The original idea of writing a monthly post was to keep tabs on various open source software projects I sometimes manage to contribute to, and perhaps even to motivate me to do more such volunteering. Well that part didn’t work, house renovations and an unexpectedly successful gig playing synth and trombone took over all my free time; but after many years of working on corporate consultancy and doing a little open source in the background, I’m trying to make a space at work to contribute in the open again.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I could tell the whole story here of how Codethink became “the build system people”. Maybe I will actually. It all started with BuildStream. In fact, that’s not even true. it all started in 2011 when some colleagues working with MeeGo and Yocto thought, “This is horrible, isn’t it?”&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;They set out to create something better, and produced Baserock, which unfortunately turned out even worse. But it did have some good ideas. The concept of “cache keys” to identify build inputs and content-addressed storage to hold build outputs began there, as did the idea of opening a “workspace” to make drive-by changes in build inputs within a large project.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;BuildStream took this core idea, extended it to support arbitrary source kinds and element kinds defined by plugins, and added a shiny interface on top. initially It &lt;a href="https://samthursfield.wordpress.com/2018/01/29/how-buildstream-uses-ostree/"&gt;used OSTree to store and distribute build artifacts&lt;/a&gt;, later &lt;a href="https://www.codethink.co.uk/articles/2019/an-introduction-to-remote-execution-and-distributed-builds/"&gt;migrating to the Google REAPI&lt;/a&gt; with the goal of supporting Enterprise(TM) infrastructure. You can even use it alongside Bazel, if you like having &lt;a href="https://bazel.build/reference/command-line-reference"&gt;three thousand commandline options&lt;/a&gt; at your disposal.   &lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;Unfortunately it was 2016, so we wrote the whole thing in Python. (In our defence, the Rust programming language had only recently hit 1.0 and crates.io was still a ghost town, and we’d probably still be rewriting the &lt;a href="https://yaml.dev/doc/ruamel.yaml/"&gt;ruamel.yaml&lt;/a&gt; package in Rust if we had taken that road.) But the company did make some great decisions, particularly making a condition of success for the BuildStream project that it could unify the 5 different build+integration systems that GNOME release team were maintaining. And that success meant not just a prototype of this, but  release team actually using BuildStream to make releases. Tristan even ended up &lt;em&gt;joining&lt;/em&gt; the GNOME release team for a while. We discussed it all at the 2017 Manchester GUADEC event, coincidentally. It was a great time. (Aside from &lt;a href="https://samthursfield.wordpress.com/2017/09/17/guadec-2017-timeline/"&gt;those 6 months leading up to the conference&lt;/a&gt;.)&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;At this point, the Freedesktop SDK already existed, with the same rather terrible name that it has today, and was already the base runtime for this new app container tool that was named… &lt;em&gt;xdg-app&lt;/em&gt;.  (At least &lt;em&gt;that&lt;/em&gt; eventually gained a better name). However, if you can remember 8 years ago, it had a &lt;a href="https://github.com/flatpak/freedesktop-sdk-base"&gt;very different form&lt;/a&gt; than today. Now, my memory of what happened next is especially hazy at this point, because like I told you in the beginning, I was on a boat with my transit van heading towards a new life in Spain. All I have to go on 8 years later is &lt;a href="https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/commits/f315acaa0193fe0b26cda0ff983a11da3e8571de"&gt;the Git history&lt;/a&gt;, but somehow the Freedesktop SDK grew a 3-stage compiler bootstrap, over 600 reusable BuildStream elements, its own Gitlab namespace, and even some controversial stickers. As a parting gift I apparently added &lt;a href="https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/commit/0aca47e692008b96a71da4841adf587c7eb31b89"&gt;support for building VMs&lt;/a&gt;, the idea being that we’d reinstate the old GNOME Continuous CI system that had unfortunately died of neglect several years earlier. This idea got somewhat out of hand, let’s say.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;It took me a while to realize this, but today Freedesktop SDK is effectively the BuildStream reference distribution. What Poky is to BitBake in the Yocto project, this is what Freedesktop SDK is to BuildStream. And this is a pretty important insight. It explains the problem you may have experienced with the BuildStream documentation: you want to build some Linux package, so you read through the manual right to the end, and then you still have no fucking idea how to integrate that package.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;This isn’t a failure on the part of the authors, instead the issue is that your princess is in another castle. Every BuildStream project I’ve ever worked on has junctioned freedesktop-sdk.git and re-used the elements, plugins, aliases, configurations and conventions defined there, all of which are &lt;em&gt;rigorously&lt;/em&gt; undocumented. The &lt;a href="https://freedesktop-sdk.gitlab.io/documentation/index.html"&gt;Freedesktop SDK Guide&lt;/a&gt;, for reasons that I won’t go into, doesn’t venture much further than than reminding you how to call Make targets.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;And this is something of a point of inflection. The BuildStream + Freedesktop SDK ecosystem has clearly not displaced Yocto, nor for that matter Linux Mint. But, like many of my &lt;a href="https://www.youtube.com/watch?v=LGrMIWYHNfs"&gt;favourite musicians&lt;/a&gt;, it has been quietly thriving in obscurity. People I don’t know are using it to do things that I don’t completely understand. I’ve seen it in comparison articles, and even job adverts. ChatGPT can generate credible BuildStream elements about as well as it can generate Dockerfiles (i.e. not very well, but it indicates a certain level of ubiquity). There have been conferences, drama, mistakes, neglect. It’s been through an 8 person corporate team hyper-optimizing the code, and its been though a mini dark age where volunteers thanklessly kept the lights on almost single handledly, and its even survived its transition to the Apache Foundation.&lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;Through all of this, the secret to its success probably that its just a &lt;em&gt;really nice tool to work with&lt;/em&gt;. As much as you can enjoy software integration, I enjoy using BuildStream to do it; things rarely break, when they do its rarely difficult to fix them, and most importantly the UI is really colourful! I’m now using it to build embedded system images for a product named &lt;a href="https://www.codethink.co.uk/ctrl-os.html"&gt;CTRL&lt;/a&gt;, which you can think of as.. a Linux distribution. There are some technical details to this which I’m working to improve, which I won’t bore you with here. &lt;/p&gt;
&lt;p class="wp-block-paragraph"&gt;I also won’t bore you with the topic of community governance this month, but that’s what’s currently on my mind. If you’ve been part of the GNOME Foundation for a few years, you’ll know this something that’s usually boring and occasionally becomes of almost life-or-death importance. The “let’s just be really sound” model works great, until one day when you least expect it, and then suddenly it &lt;em&gt;really&lt;/em&gt; doesn’t. There is no perfect defence against this, and in open source communities its our diversity that brings the most resilience. When GNOME loses, KDE gains, and that way at least we still don’t have to use Windows. Indeed, this is one argument for  investing in BuildStream even if it remains forever something of a minority sport. I guess I just need to remember that when you have to start thinking hard about governance, that’s a sign of success.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://samthursfield.wordpress.com/2026/04/23/status-update-23rd-april-2026/</guid>
      <pubDate>Thu, 23 Apr 2026 20:48:14 +0000</pubDate>
    </item>
    <item>
      <title>Jonathan Blandford: Goblint Notes</title>
      <link>https://blogs.gnome.org/jrb/2026/04/24/goblint-notes/</link>
      <description>&lt;p&gt;I was excited to see Bilal’s &lt;a class="external" href="https://belmoussaoui.com/blog/23-goblin-linter/"&gt;announcement&lt;/a&gt; of goblint, and I’ve spent the past week getting Crosswords to work with it. This is a tool I’ve always wanted and I’m pretty convinced it will be a great boon for the GNOME ecosystem. I’m posting my notes in hope that more people try it out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First and most importantly, Bilal has been so great to work with. I have filed ~20 issues and feature requests and he fixed them all very quickly. In some cases, he fixed the underlying issue before I completed adding annotations to the code.&lt;/li&gt;
&lt;li&gt;Most of the issues flagged were idiomatic and stylistic, but it did find real bugs. It found a half-dozen leaks, a missing g_timeout removal, and five missing class function chain ups. One was a long-standing crasher. There’s a definite improvement in quality from adopting this tool.&lt;/li&gt;
&lt;li&gt;I’m also excited about pairing this with new GSoC interns. The types of things goblint flags are the things that students hit in particular (when they don’t write it all their code with AI). I think goblint will be even more important to our ecosystem as a teaching tool to our C codebase. It’s already effectively replaced my styleguide.&lt;/li&gt;
&lt;li&gt;In a few instances, the &lt;code&gt;use_g_autoptr&lt;/code&gt; rule outstripped static-scan’s ability to track leaks. Ultimately, I ended up annotating and removing the &lt;code&gt;g_autoptr()&lt;/code&gt; calls as I couldn’t get the two to play nicely together.&lt;/li&gt;
&lt;li&gt;Along the same lines, cairo, pango, and librsvg all lack &lt;code&gt;G_DEFINE_AUTOPTR_CLEANUP_FUNC&lt;/code&gt;. It would be really great if we could fix these core libraries. In the meantime, you can add the following to your project’s goblint.toml file:&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="background: #ffffff; overflow: auto; width: auto; border: solid gray; border-width: .1em .1em .1em .8em; padding: .2em .6em;"&gt;
&lt;pre style="margin: 0; line-height: 125%;"&gt;&lt;span style="color: #080; font-weight: bold;"&gt;[rules.use_g_autoptr_inline_cleanup]&lt;/span&gt;
level &lt;span style="color: #333;"&gt;=&lt;/span&gt; &lt;span style="background-color: #fff0f0;"&gt;"error"&lt;/span&gt;
ignore_types &lt;span style="color: #333;"&gt;=&lt;/span&gt; [&lt;span style="background-color: #fff0f0;"&gt;"cairo_*"&lt;/span&gt;, &lt;span style="background-color: #fff0f0;"&gt;"Pango*"&lt;/span&gt;, &lt;span style="background-color: #fff0f0;"&gt;"RsvgHandle"&lt;/span&gt;]
&lt;/pre&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;I had some trouble getting the pipeline integrated with GNOME’s gitlab. The gitlab recipe on his page uses premium features unavailable in the self hosted version. If it’s helpful for others, here’s what I ended up using:&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="background: #ffffff; overflow: auto; width: auto; border: solid gray; border-width: .1em .1em .1em .8em; padding: .2em .6em;"&gt;
&lt;pre style="margin: 0; line-height: 125%;"&gt;&lt;span style="color: #070;"&gt;goblint&lt;/span&gt;:
  &lt;span style="color: #070;"&gt;stage&lt;/span&gt;: analysis
  &lt;span style="color: #070;"&gt;extends&lt;/span&gt;:
    - &lt;span style="background-color: #fff0f0;"&gt;"opensuse-container@x86_64.stable"&lt;/span&gt;
    - &lt;span style="background-color: #fff0f0;"&gt;".fdo.distribution-image@opensuse"&lt;/span&gt;
  &lt;span style="color: #070;"&gt;needs&lt;/span&gt;:
    - &lt;span style="color: #070;"&gt;job&lt;/span&gt;: opensuse-container@x86_64.stable
      &lt;span style="color: #070;"&gt;artifacts&lt;/span&gt;: false
  &lt;span style="color: #070;"&gt;before_script&lt;/span&gt;:
    - source ci/env.sh
    - cargo install --git https://github.com/bilelmoussaoui/goblint goblint
  &lt;span style="color: #070;"&gt;script&lt;/span&gt;:
    &lt;span style="color: #888;"&gt;# Goblint is fast. We run it twice: Once to generate the report,&lt;/span&gt;
    &lt;span style="color: #888;"&gt;# and a second time to display the output and triger an error&lt;/span&gt;
    - /root/.cargo/bin/goblint . --format sarif &amp;gt; goblint.sarif || true
    - /root/.cargo/bin/goblint . --format text
  &lt;span style="color: #070;"&gt;artifacts&lt;/span&gt;:
    &lt;span style="color: #070;"&gt;reports&lt;/span&gt;:
      &lt;span style="color: #070;"&gt;sast&lt;/span&gt;: goblint.sarif
    &lt;span style="color: #070;"&gt;when&lt;/span&gt;: always&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;YMMV&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/jrb/2026/04/24/goblint-notes/</guid>
      <pubDate>Fri, 24 Apr 2026 07:57:16 +0000</pubDate>
    </item>
    <item>
      <title>Michael Catanzaro: git config am.threeWay</title>
      <link>https://blogs.gnome.org/mcatanzaro/2026/04/24/git-config-am-threeway/</link>
      <description>&lt;p&gt;If you work with patches and &lt;code&gt;git am&lt;/code&gt;, then you’re probably used to seeing patches fail to apply. For example:&lt;/p&gt;
&lt;pre class="wp-block-code"&gt;&lt;code&gt;$ git am CVE-2025-14512.patch
Applying: gfileattribute: Fix integer overflow calculating escaping for byte strings
error: patch failed: gio/gfileattribute.c:166
error: gio/gfileattribute.c: patch does not apply
Patch failed at 0001 gfileattribute: Fix integer overflow calculating escaping for byte strings
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is sad and frustrating because the entire patch has failed, and now you have to apply the entire thing manually. That is no good.&lt;/p&gt;
&lt;p&gt;Here is the solution, which I wish I had learned long ago:&lt;/p&gt;
&lt;pre class="wp-block-code"&gt;&lt;code&gt;$ git config --global am.threeWay true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This enables three-way merge conflict resolution, same as if you were using &lt;code&gt;git cherry-pick&lt;/code&gt; or &lt;code&gt;git merge&lt;/code&gt;. For example:&lt;/p&gt;
&lt;pre class="wp-block-code"&gt;&lt;code&gt;$ git am CVE-2025-14512.patch
Applying: gfileattribute: Fix integer overflow calculating escaping for byte strings
Using index info to reconstruct a base tree...
M	gio/gfileattribute.c
Falling back to patching base and 3-way merge...
Auto-merging gio/gfileattribute.c
CONFLICT (content): Merge conflict in gio/gfileattribute.c
error: Failed to merge in the changes.
Patch failed at 0001 gfileattribute: Fix integer overflow calculating escaping for byte strings
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you have merge conflicts, which you can handle as usual. This seems like a better default for pretty much everybody, so if you use &lt;code&gt;git am&lt;/code&gt;, you should probably enable it.&lt;/p&gt;
&lt;p&gt;I’ve no doubt that many readers will have known about this already, but it’s new to me, and it makes me happy, so I wanted to share. You’re welcome, Internet!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/mcatanzaro/2026/04/24/git-config-am-threeway/</guid>
      <pubDate>Fri, 24 Apr 2026 22:57:22 +0000</pubDate>
    </item>
    <item>
      <title>Jakub Steiner: Revert That Vector Nonsense!</title>
      <link>https://blog.jimmac.eu/posts/app-pixels/</link>
      <description>&lt;p&gt;A few years back I did a &lt;a href="https://blog.jimmac.eu/posts/pixels/"&gt;quick exploration&lt;/a&gt; of what GNOME app icons might look like in an alternate universe where we kept on using &lt;code&gt;VGA&lt;/code&gt; displays. Chiselling pixels away is therapeutic. So while there is absolutely no use for these, I keep on making them if only to bring some attention to what really matters for GNOME, having nice apps.&lt;/p&gt;
&lt;p&gt;Here's a batch of mostly &lt;a href="https://circle.gnome.org"&gt;GNOME Circle&lt;/a&gt; app icons, with some &lt;a href="https://flathub.org"&gt;3rd party&lt;/a&gt; ones thrown in.&lt;/p&gt;
&lt;div class="image-grid pixelated" id="crt-container"&gt;
&lt;img alt="Pixel art GNOME app icons, batch 1" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-01.png"/&gt;
&lt;img alt="Pixel art GNOME app icons, batch 2" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-02.png"/&gt;
&lt;img alt="Pixel art GNOME app icons, batch 3" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-03.png"/&gt;
&lt;img alt="Pixel art GNOME app icons, batch 4" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-04.png"/&gt;
&lt;img alt="Pixel art GNOME app icons, batch 5" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-05.png"/&gt;
&lt;img alt="Pixel art GNOME app icons, batch 6" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-06.png"/&gt;
&lt;img alt="Pixel art GNOME app icons, batch 7" src="https://blog.jimmac.eu/posts/app-pixels/app-icons-crt-07.png"/&gt;
&lt;/div&gt;
&lt;p&gt;If you're reading this &lt;a href="https://blog.jimmac.eu/posts/app-pixels/"&gt;on my site&lt;/a&gt; rather than &lt;a href="https://planet.gnome.org"&gt;Planet GNOME&lt;/a&gt; or some flickering terminal in an abandoned Vault, then congratulations. You've stumbled upon a working Pip-Boy module! Found it half-buried under irradiated rubble, its phosphor display still humming with that familiar green glow. Enjoy these icons the way the dwellers of Vault 101 were always meant to, one glorious scanline at a time.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blog.jimmac.eu/posts/app-pixels/</guid>
      <pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Jordan Petridis: Goblins in your toolchain</title>
      <link>https://blogs.gnome.org/alatiera/2026/04/27/goblins-in-your-toolchain/</link>
      <description>&lt;p&gt;At the start of the month, Bilal gave us all a giant gift with &lt;a class="external" href="https://belmoussaoui.com/blog/23-goblin-linter/"&gt;Goblint&lt;/a&gt;. On the first week it was already impressive. Now it’s an invaluable tool for anyone that ever interfaced with GObject, glib or GTK. It will catch leaks, bugs, or even offer to auto fix and modernize your code to the modern paradigms we use. It’s one of those things that is going to save countless hours of debugging and more importantly, prevent the issues before they even get committed. Jonathan Blandford wrote about using it two days ago, and I suggest you read the &lt;a href="https://blogs.gnome.org/jrb/2026/04/24/goblint-notes"&gt;post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Everyone is trying to use goblint, and we are all &lt;a class="external" href="https://gitlab.gnome.org/GNOME/epiphany/-/merge_requests/2079#note_2733667"&gt;stumbling&lt;/a&gt; upon the same issues integrating it into our tooling. Initially, it was only able to produce Sarif reports, which GitLab still has behind a &lt;a class="external" href="https://docs.gitlab.com/18.11/ci/yaml/artifacts_reports/#artifactsreportssarif"&gt;feature flag&lt;/a&gt;, in addition to only  be available in GitLab Enterprise Editions.&lt;/p&gt;
&lt;p&gt;I added an &lt;a class="external" href="https://github.com/bilelmoussaoui/goblint/pull/61"&gt;export&lt;/a&gt; for GitLab’s &lt;a class="external" href="https://docs.gitlab.com/ci/testing/code_quality/"&gt;Code Quality&lt;/a&gt; format which has &lt;em&gt;some&lt;/em&gt; support in the non-proprietary Community Edition we use in the GNOME and Freedesktop.org instances. Sadly, almost &lt;a class="external" href="https://docs.gitlab.com/ci/testing/code_quality/#view-code-quality-results"&gt;everything nice&lt;/a&gt; is still only available in the enterprise editions, but at least there is this little Widget in the Merge Requests page.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A screenshot of the linked Merge Request showcasing the Code Quality GitLab widget." class="aligncenter wp-image-5304 size-full" height="1476" src="https://blogs.gnome.org/alatiera/files/2026/04/Screenshot-2026-04-27-at-09-48-57-Modernize-the-coebase-a-bit-324-·-Merge-requests-·-GNOME-_-gnome-initial-setup-·-GitLab.png" width="1814"/&gt;&lt;/p&gt;
&lt;p&gt;Additionally, we now have CI &lt;a class="external" href="https://gitlab.gnome.org/GNOME/citemplates/-/merge_requests/171"&gt;templates&lt;/a&gt; for Goblint. One is adding a job to the existing &lt;a class="external" href="https://gitlab.gnome.org/GNOME/citemplates#gnomeos-basic-ci"&gt;&lt;code&gt;gnomeos-basic-ci&lt;/code&gt;&lt;/a&gt; component we use everywhere. Simply go to your latest pipeline and look for &lt;a class="external" href="https://gitlab.gnome.org/GNOME/gdm/-/jobs/6470591"&gt;the job&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://gitlab.gnome.org/GNOME/gdm/-/jobs/6470591"&gt;&lt;img alt="A screenshot of the linked job and its output log" class="aligncenter size-full wp-image-5320" height="1630" src="https://blogs.gnome.org/alatiera/files/2026/04/Screenshot-2026-04-28-at-14-20-41-build-gnomeos-goblint-6470591-·-Jobs-·-GNOME-_-gdm-·-GitLab.png" width="2544"/&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The report will also show up in Merge Requests that have been updated since yesterday.  The gnomeos-basic-ci has other goodies like sanitizers, static analyzers, test coverage, etc wired out of the box, so you should give it a try if you are not using it yet.&lt;/p&gt;
&lt;p&gt;If you do but don’t want the goblint job, you can disable it easily with &lt;code&gt;inputs: goblint: "disabled"&lt;/code&gt; similar to all the other tools the component provides.&lt;/p&gt;
&lt;div class="sourceCode" id="cb1"&gt;
&lt;pre class="sourceCode yaml"&gt;&lt;code class="sourceCode yaml"&gt;&lt;span id="cb1-1"&gt;&lt;span class="fu"&gt;include&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-2"&gt;  &lt;span class="kw"&gt;-&lt;/span&gt; &lt;span class="fu"&gt;project&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt; &lt;span class="st"&gt;"GNOME/citemplates"&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-3"&gt;    &lt;span class="fu"&gt;file&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt; &lt;span class="st"&gt;"templates/default-rules.yml"&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-4"&gt;  &lt;span class="kw"&gt;-&lt;/span&gt; &lt;span class="fu"&gt;component&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt; &lt;span class="st"&gt;"gitlab.gnome.org/GNOME/citemplates/gnomeos-basic-ci@26.1"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;If you want only a goblint job, I’ve also added a standalone template that you can use. (Or &lt;a class="external" href="https://gitlab.gnome.org/GNOME/citemplates/-/blob/master/templates/goblint.yml"&gt;copy-paste&lt;/a&gt; from it).&lt;/p&gt;
&lt;div class="sourceCode" id="cb1"&gt;
&lt;pre class="sourceCode yaml"&gt;&lt;code class="sourceCode yaml"&gt;&lt;span id="cb1-1"&gt;&lt;span class="fu"&gt;include&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-2"&gt;  &lt;span class="kw"&gt;-&lt;/span&gt; &lt;span class="fu"&gt;component&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt; &lt;span class="st"&gt;"gitlab.gnome.org/GNOME/citemplates/goblint@26.1"&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-3"&gt;    &lt;span class="fu"&gt;inputs&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-4"&gt;      &lt;span class="fu"&gt;job-stage&lt;/span&gt;&lt;span class="kw"&gt;:&lt;/span&gt; &lt;span class="st"&gt;"lint"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;In order for the Code Quality report to work, you will need to have a report uploaded from your target branch, so GitLab will have something to compare the one from the merge request with. The template rules will handle that for you, but keep it in mind.&lt;/p&gt;
&lt;p&gt;At this moment all the lints are warnings so the job will never be fatal. This is why we can enabled it by default without worrying about breaking pipelines for now. You can further configure its behavior to your needs, and error out if you want to, through the &lt;a class="external" href="https://github.com/bilelmoussaoui/goblint/blob/main/CONFIG.md"&gt;&lt;code&gt;configuration&lt;/code&gt;&lt;/a&gt; file.&lt;/p&gt;
&lt;div class="sourceCode" id="cb1"&gt;
&lt;pre class="sourceCode toml"&gt;&lt;code class="sourceCode toml"&gt;&lt;span id="cb1-1"&gt;&lt;span class="dt"&gt;min_glib_version&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;"2.76"&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-2"&gt;&lt;/span&gt;
&lt;span id="cb1-3"&gt;&lt;span class="kw"&gt;[rules.g_declare_semicolon]&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-4"&gt;&lt;span class="dt"&gt;level&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;"ignore"&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-5"&gt;&lt;/span&gt;
&lt;span id="cb1-6"&gt;&lt;span class="kw"&gt;[rules.untranslated_string]&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-7"&gt;&lt;span class="dt"&gt;level&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;"error"&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-8"&gt;&lt;span class="dt"&gt;ignore&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="op"&gt;[&lt;/span&gt;&lt;span class="st"&gt;"**/test-*.c"&lt;/span&gt;&lt;span class="op"&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;It’s also very likely that we are going to &lt;a class="external" href="https://gitlab.gnome.org/GNOME/gnome-build-meta/-/merge_requests/4659"&gt;add&lt;/a&gt; goblint and its LSP server to the GNOME SDK Flatpak runtime, along with GNOME OS, so it will always be available for use with tools like Builder and foundry.&lt;/p&gt;
&lt;p&gt;Enjoy&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/alatiera/2026/04/27/goblins-in-your-toolchain/</guid>
      <pubDate>Mon, 27 Apr 2026 10:05:43 +0000</pubDate>
    </item>
    <item>
      <title>Thibault Martin: TIL that Yubikeys are convenient for Linux login</title>
      <link>https://ergaster.org/til/yubikey-unlock-laptop/</link>
      <description>&lt;p&gt;I got myself a Yubikey recently, and I wanted to use it as a nice convenience to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Grant me sudo privileges&lt;/li&gt;
&lt;li&gt;Unlock my session&lt;/li&gt;
&lt;li&gt;Decrypt my LUKS-encrypted disk&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I've only managed to do the first two, since they both rely on Linux Pluggable Authentication Modules (PAM). Luckily for me, one of PAM's modules supports U2F, the standard Yubikeys rely on.&lt;/p&gt;
&lt;p&gt;First I need to install &lt;code&gt;pam-u2f&lt;/code&gt; to add U2F support to PAM, and &lt;code&gt;pamu2fcfg&lt;/code&gt; to configure my key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo rpm-ostree install pam-u2f pamu2fcfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since I'm running an immutable OS I need to reboot, and then I can create the correct directory and file to dump an U2F key into it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mkdir -p ~/.config/Yubico
$ pamu2fcfg &amp;gt; ~/.config/Yubico/u2f_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I make sure to have a root session open in case I lock myself out of sudoers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo su
#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a different terminal, I can edit the sudoers file to add this line&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#%PAM-1.0
auth       sufficient   pam_u2f.so cue openasuser
auth       include      system-auth
account    include      system-auth
password   include      system-auth
session    optional     pam_keyinit.so revoke
session    required     pam_limits.so
session    include      system-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I save this file and open a new terminal. I type in &lt;code&gt;sudo vi&lt;/code&gt; and it asks me to touch my FIDO authenticator before opening vi! If I touch the Yubikey, it indeed opens vi with root privileges.&lt;/p&gt;
&lt;p&gt;Let's break down the line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auth&lt;/code&gt; for authentication&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sufficient&lt;/code&gt; passing this authentication challenge is enough (it's not an additional factor of authentication)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pam_u2f.so&lt;/code&gt; the module we load is for U2F, the standard Yubikeys use&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cue&lt;/code&gt; print "Please touch the FIDO authenticator." when the user needs to authenticate&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openasuser&lt;/code&gt; to fetch the authentication file without root privileges&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's also possible to use it to unlock my session, but it would be a bit reckless to allow anyone with my Yubikey to log into my laptop. If my backpack gets stolen and it has both my Yubikey and my laptop, anyone can log in.&lt;/p&gt;
&lt;p&gt;It's possible to make the login screen require either my user password, or all of&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Yubikey itself&lt;/li&gt;
&lt;li&gt;The PIN of the Yubikey&lt;/li&gt;
&lt;li&gt;Me to touch the Yubikey&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If someone fails more than three times to enter the correct PIN, the Yubikey will lock itself and require a PUK to be unlocked. This gives me an additional layer of security, and it's more convenient than having to type a full length passphrase.&lt;/p&gt;
&lt;p&gt;I've added the following line to &lt;code&gt;/etc/pam.d/greetd&lt;/code&gt; (the greeter I use):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#%PAM-1.0
auth       sufficient  pam_u2f.so cue openasuser pinverification=1 userpresence=1
auth       substack    system-auth
[...]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!warning] I can lose my Yubikey&lt;/p&gt;
&lt;p&gt;I use my Yubikey as a nice convenience to set up a weaker PIN while not compromising too much on security. I use it &lt;em&gt;instead&lt;/em&gt; of a password, no in addition to it.&lt;/p&gt;
&lt;p&gt;Since I can lose or break my Yubikey and I don't want to buy two of them, I make the U2F login &lt;code&gt;sufficient&lt;/code&gt; but not &lt;code&gt;required&lt;/code&gt;. This means I can still fallback to password authentication if I lose my Yubikey.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Finally, DankMaterialShell uses its own lockscreen manager too. I still want to be able to fallback to password authentication if need be, so I'll configure it to accept U2F OR the password, not both.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This means that the lockscreen will call &lt;code&gt;/etc/pam.d/dankshell-u2f&lt;/code&gt; to know what to do when the screen is locked. Since this file doesn't exist, I can create it with the following content.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#%PAM-1.0
auth sufficient pam_u2f.so cue openasuser pinverification=1 userpresence=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I need a fallback for when I don't have my Yubikey, so I also create the one for this occasion&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#%PAM-1.0
auth include system-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I have a consistent setup where both my login and lock screen require me to plug my key, enter its PIN and touch it, or enter my full password. When it comes to sudo, I can only touch my key without requiring an PIN.&lt;/p&gt;
&lt;p&gt;My next quest will be to use my Yubikey to unlock my LUKS-encrypted disk.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://ergaster.org/til/yubikey-unlock-laptop/</guid>
      <pubDate>Tue, 28 Apr 2026 10:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Jonathan Blandford: Remembering Seth</title>
      <link>https://blogs.gnome.org/jrb/2026/04/29/remembering-seth/</link>
      <description>&lt;p&gt;I heard the &lt;a class="external" href="https://lwn.net/Articles/1070213/"&gt;news about Seth Nickell’s passing&lt;/a&gt; last week, and have been in a bit of a funk ever since.&lt;/p&gt;
&lt;p&gt;Seth was brilliant, iconoclastic, fearless.&lt;/p&gt;
&lt;p&gt;It’s been a long while since Seth was an active part of the GNOME Community, but his influence on the project can still be seen in its DNA if you know where to look. He arrived on the GNOME scene while still in school with hundreds of ideas on how to improve things. It was an interesting time: We had just launched GNOME 1.5 and were searching for a new path towards GNOME 2.0. The Sun usability study had been published and the community had internalized the need to change directions. Seth rolled up his sleeves and did the work needed to help light that path.&lt;/p&gt;
&lt;p&gt;Seth championed radical proposals such as instant apply, &lt;a class="external" href="https://mail.gnome.org/archives/gtk-devel-list/2001-November/msg00153.html"&gt;button ordering&lt;/a&gt;, &lt;a class="external" href="https://mail.gnome.org/archives/gtk-devel-list/2001-November/msg00666.html"&gt;message dialog fixes&lt;/a&gt;, and more. He cleaned up the control-center proposing some of the most visible changes from GNOME 1 to 2. He also did the initial designs for epiphany, pushing for a cleaner browser experience during an era of high browser complexity. He had a vision of desktops as a democratic tool, as easy and natural to use as any other tool in the human experience.&lt;/p&gt;
&lt;p&gt;As a designer, Seth was focused on trying to understand who we were designing for and making sure we were solving problems for them. While he wasn’t beyond fixing paddings / layouts, he wanted to get the &lt;strong&gt;Big Picture&lt;/strong&gt; right. He wasn’t beyond rolling up his sleeves writing code to move things forward, but was at his best as a champion and visionary, arguing for us to take &lt;a class="external" href="https://thegnomejournal.wordpress.com/2005/01/10/experimental-culture/"&gt;risks and continue to innovate&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Spending time was Seth was a hoot. He had such a flair for the dramatic. I remember…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;…the time he sold the design for what would become NetworkManager to a bunch of engineers. He got up on the stage and announced: &lt;em&gt;“We are going to make this &lt;/em&gt;[holding an ethernet cable]&lt;em&gt; as easy to use as this &lt;/em&gt;[producing a power plug]&lt;em&gt;!”&lt;/em&gt; It’s hard to describe how many steps it took to set up networking back then.&lt;/li&gt;
&lt;li&gt;…his vision of an improved messaging system — Project Yarrr. He used &lt;img alt="☠" class="wp-smiley" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2620.png" style="height: 1em;"/&gt; (U+2620) as the SVN repo name partially to see how many internal tools weren’t UTF-8 clean.&lt;/li&gt;
&lt;li&gt;…him breaking out into an operatic rendition of “&lt;a class="external" href="https://en.wikipedia.org/wiki/Tradition_(song)"&gt;Tradition&lt;/a&gt;” when  developers were pushing back on a change he was proposing.&lt;/li&gt;
&lt;li&gt;…the time he changed everyone’s background in the RH office to have crop circles over night. He showed up the next morning in a robe dressed as an old-testament prophet, beating a drum and carrying a “RHEL5 IS NIGH” sign.&lt;/li&gt;
&lt;li&gt;…hanging  printouts of hate mail he got for various design choices outside of the Mega Cube (a group activity)!&lt;/li&gt;
&lt;li&gt;And &lt;em&gt;everyone&lt;/em&gt; who was around for the &lt;strong&gt;Dark Princess Incident&lt;/strong&gt; will always remember it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Being one of the public faces of GNOME2 was hard, and he moved on. Later, he worked on OLPC and Sugar, and made his mark there. After that, he seemed to travel a lot. We lost touch, though he’d reappear every couple of years to say hi. I hope he found what he was looking for.&lt;/p&gt;
&lt;p&gt;Farewell, my friend. The world now has less color in it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://felipeborges.pages.gitlab.gnome.org/heads-of-gnome/heads/seth.png"/&gt;&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/jrb/2026/04/29/remembering-seth/</guid>
      <pubDate>Wed, 29 Apr 2026 05:07:58 +0000</pubDate>
    </item>
    <item>
      <title>vixalien: A love letter to mise</title>
      <link>https://www.vixalien.com/blog/mise/</link>
      <description>&lt;p&gt;Recently, I have been using &lt;a href="https://os.gnome.org/"&gt;GNOME OS&lt;/a&gt;, as my daily driver.&lt;/p&gt;
&lt;p&gt;After being a seasoned Linux for long, dabbling in distros like &lt;a href="https://alpinelinux.org"&gt;Alpine Linux&lt;/a&gt;, &lt;a href="https://archlinux.org"&gt;Arch Linux&lt;/a&gt;, Fedora (and even Silverblue), I tried switching to something more &lt;em&gt;opinionated&lt;/em&gt; and that "works by default" all while being hard to break.&lt;/p&gt;
&lt;p&gt;And given my existing &lt;a href="https://teams.pages.gitlab.gnome.org/Websites/people.gnome.org/#jimmac:~:text=Angelo%20Verlain%20Shema"&gt;relationship with GNOME&lt;/a&gt;, GNOME OS was a choice worth looking into.&lt;/p&gt;
&lt;p&gt;One feature of GNOME OS is that it is immutable (i.e. system files are read-only). It also doesn't ship with a package manager, so it doesn't have functionality built-in to install extra packages.&lt;/p&gt;
&lt;p&gt;You can install GUI Applications normally using &lt;a href="https://flathub.org/en"&gt;Flathub&lt;/a&gt; (and Snap/AppImage), but installing non-GUI applications like development tools or CLI packages is not built-in.&lt;/p&gt;
&lt;p&gt;There are of course several solutions you can use, such as &lt;a href="https://brew.sh/"&gt;homebrew&lt;/a&gt;, &lt;a href="https://gitlab.postmarketos.org/postmarketOS/coldbrew"&gt;coldbrew&lt;/a&gt;, but today we will focus on &lt;a href="https://mise.jdx.dev/"&gt;mise&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What is mise?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://mise.jdx.dev/"&gt;mise&lt;/a&gt; pitches itself as "One tool to manage languages, env vars, and tasks per project, reproducibly."&lt;/p&gt;
&lt;p&gt;However, I only use a fraction of it's functionality, in that I only use it to install packages.&lt;/p&gt;
&lt;h2&gt;How to install it?&lt;/h2&gt;
&lt;p&gt;The instructions are here: https://mise.jdx.dev/getting-started.html&lt;/p&gt;
&lt;p&gt;But essentially it's as easy as running this (remember to read the source of the installer first):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://mise.run | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Activating mise&lt;/h2&gt;
&lt;p&gt;Then you will need to "activate" mise, which essentially makes tools installed by mise available by modifying your &lt;code&gt;$PATH&lt;/code&gt; variable&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo 'eval "$(~/.local/bin/mise activate bash --shims)"' &amp;gt;&amp;gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The instructions above are for bash, so you will need to consult the docs to get instructions for your shell.&lt;/p&gt;
&lt;p&gt;You will need to re-login for the &lt;code&gt;mise&lt;/code&gt; command to be available, or open a new shell.&lt;/p&gt;
&lt;h3&gt;A note on shims&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Feel free to skip this section, as it's just an explainer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also, note that the above command use the &lt;code&gt;--shims&lt;/code&gt; flag, which is NOT the default. It essentially means that mise will modify the &lt;code&gt;$PATH&lt;/code&gt; variable, instead of doing a weird thing where it will re-activate itself after each command you run.&lt;/p&gt;
&lt;p&gt;The non-shim way to activate mise is useful when you use mise to install different package versions across different repositories, but that sometimes breaks IDEs and is our of the scope of this blog post.&lt;/p&gt;
&lt;h2&gt;Installing packages&lt;/h2&gt;
&lt;p&gt;You can start installing your first package with &lt;code&gt;mise&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above command installs &lt;code&gt;java&lt;/code&gt; globally (hence the &lt;code&gt;-g&lt;/code&gt; flag), which you can now confirm by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ java --version
openjdk 26.0.1 2026-04-21
OpenJDK Runtime Environment (build 26.0.1+8-34)
OpenJDK 64-Bit Server VM (build 26.0.1+8-34, mixed mode, sharing)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can install much more tools, of which you can find a non-complete list here: &lt;a href="https://mise-tools.jdx.dev/"&gt;mise-tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, you can similarly install a specific major version of nodejs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g node@22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or install the latest LTS version of node&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g node@lts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or you can be overlay specific&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g node@v25.9.0
mise use -g node@25.9.0 # this works too!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Searching&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;mise search&lt;/code&gt; to find packages.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise search typ
Tool       Description                                                                                                                            
typos      Source code spell checker. https://github.com/crate-ci/typos
typst      A new markup-based typesetting system that is powerful and easy to learn. https://github.com/typst/typst
typstyle   Beautiful and reliable typst code formatter. https://github.com/Enter-tainer/typstyle
quicktype  Generate types and converters from JSON, Schema, and GraphQL provided by https://quicktype.io. https://www.npmjs.com/package/quicktype
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Uninstalling&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mise unuse -g node
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Updating&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mise self-update # updating mise itself
mise up          # updating tools installed by mise
mise outdated    # checking if you have outdated tools
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Config File&lt;/h2&gt;
&lt;p&gt;Tools you install with mise globally will be saved in the file &lt;code&gt;~/.config/mise/config.toml&lt;/code&gt;, which you can commit to your dotfiles so you can have similar tools across different machines.&lt;/p&gt;
&lt;p&gt;Here's an example of my mise config file at the time of writing this blog post.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/mise/config.toml
[tools]
bat = "latest"
btop = "latest"
bun = "latest"
caddy = "latest"
"cargo:mergiraf" = "latest"
deno = "latest"
difftastic = "latest"
doggo = "latest"
fastfetch = "latest"
fzf = "latest"
github-cli = "latest"
"github:railwayapp/railpack" = "latest"
glab = "latest"
helix = "latest"
java = "latest"
lazygit = "latest"
node = "latest"
"npm:vscode-langservers-extracted" = "latest"
oha = "latest"
pipx = "latest"
pnpm = "latest"
prettier = "latest"
rust = "latest"
scooter = "latest"
tmux = "latest"
usage = "latest"
yt-dlp = { version = "latest", rename_exe = "yt-dlp" }
zellij = "latest"
"github:patryk-ku/music-discord-rpc" = { version = "latest", asset_pattern = "music-discord-rpc" }
rclone = "latest"
mc = "latest"
go = "latest"
"go:git.sr.ht/~migadu/alps/cmd/alps" = "latest"
"npm:localtunnel" = "latest"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the tools inside the config has changed, you can run the following comand to make mise re-install packages from the config file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise install
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mise Backends&lt;/h2&gt;
&lt;p&gt;Mise is able to install packages from multiple sources. These sources are called "backends" by mise.&lt;/p&gt;
&lt;p&gt;When you type &lt;code&gt;mise use -g node@22&lt;/code&gt;, it will resolve &lt;code&gt;node&lt;/code&gt; against the registry and figure out that the default backend for &lt;code&gt;node&lt;/code&gt; is &lt;code&gt;core&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Core&lt;/h3&gt;
&lt;p&gt;The default backend is called &lt;code&gt;core&lt;/code&gt; and tools from this backend are usually provided from the official source.&lt;/p&gt;
&lt;p&gt;Other tools that are available from &lt;code&gt;core&lt;/code&gt; include Node.js, Ruby, Python, etc...&lt;/p&gt;
&lt;p&gt;We could also have been explicit with the backend we want to use&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g core:node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can find &lt;a href="https://mise.jdx.dev/core-tools.html"&gt;a list of all core packages here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Aqua&lt;/h3&gt;
&lt;p&gt;You can also install packages from the &lt;a href="https://aquaproj.github.io/"&gt;Aqua&lt;/a&gt; registry.&lt;/p&gt;
&lt;h3&gt;Language Package Managers&lt;/h3&gt;
&lt;p&gt;You can also install tools from their respective package managers. Here are a few examples&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;npm&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;You can install prettier, typescript, oxlint and other JavaScript/TypeScript tools published on the npm registry. Find the tools on &lt;a href="https://www.npmjs.com/search?q=keywords:cli"&gt;npm&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g npm:prettier
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;pipx&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;You can install black, poetry and other Python tools from pypi. Find the tools on &lt;a href="https://pypi.org/"&gt;pypi&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g pipx:black
pipx:git+https://github.com/psf/black.git # from a github repo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;cargo&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;You can install cargo packages with this backed. You need to have rust installed beforehand though, which you can do with mise&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g rust
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then install your packages&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g cargo:eza
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are more &lt;a href="https://mise.jdx.dev/dev-tools/backends/"&gt;language package manager backends&lt;/a&gt; like: &lt;code&gt;gem&lt;/code&gt;, &lt;code&gt;go&lt;/code&gt; and more.&lt;/p&gt;
&lt;h3&gt;Github&lt;/h3&gt;
&lt;p&gt;You can install packages from Github directly, as long as the project you are trying to install from uses Github releases&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mise use -g github:railwayapp/railpack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mise will usually auto-detect which asset you want to use, but you can also specify the asset glob in &lt;code&gt;~/.config/mise/config.toml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tools]
"github:patryk-ku/music-discord-rpc" = { version = "latest", asset_pattern = "music-discord-rpc" }
&lt;/code&gt;&lt;/pre&gt;</description>
      <guid isPermaLink="false">https://www.vixalien.com/blog/mise/</guid>
      <pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Sophie Herold: Testing Library Code in GNOME OS</title>
      <link>https://blogs.gnome.org/sophieh/2026/04/30/testing-library-code-in-gnome-os/</link>
      <description>&lt;p&gt;Yesterday, I wanted to debug a glycin (or Shell) issue on GNOME OS. Turns out, there is currently no documentation that works or includes all necessary steps.&lt;/p&gt;
&lt;p&gt;Here is the simplest variant if you don’t develop on GNOME OS and have an internet connection that can download 16 GB in a reasonable amount of time.&lt;/p&gt;
&lt;p&gt;First we get a toolbox image to build our code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ toolbox create gnomeos-nightly -i quay.io/gnome_infrastructure/gnome-build-meta:gnomeos-devel-nightly&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After entering the toolbox with&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ toolbox enter gnomeos-nightly&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we can clone and build our project with &lt;a class="external" href="https://gitlab.gnome.org/tchx84/sysext-utils#building-extensions"&gt;sysext-utils&lt;/a&gt; that are included in our image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ meson setup ./build --prefix /usr --libdir="lib/$(gcc -print-multiarch)"
$ sysext-build example ./build&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a &lt;code&gt;example.sysext.raw&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Now, we need a GNOME OS to test our build. We can &lt;a class="external" href="https://os.gnome.org/"&gt;download the image&lt;/a&gt; and install it in Boxes. After logging in, we can just drag and drop the &lt;code&gt;example.sysext.raw&lt;/code&gt; into the VM.&lt;/p&gt;
&lt;p&gt;Before we can install it, we need to get the development tools for our VM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ run0 updatectl enable devel --now&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, we need to restart the VM.&lt;/p&gt;
&lt;p&gt;Finally, we can test our build:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ run0 sysext-add ~/Downloads/example.sysext.raw&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding the &lt;code&gt;--persistent&lt;/code&gt; flag to this command will make the changes stay active across reboots.&lt;/p&gt;
&lt;p&gt;If the changes made it impossible to boot into the VM again, we can start the VM in “Safe mode” from the boot menu. After logging in, we can manually remove the extension:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ run0 rm /var/lib/extensions/example.raw&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Happy hacking!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/sophieh/2026/04/30/testing-library-code-in-gnome-os/</guid>
      <pubDate>Thu, 30 Apr 2026 12:58:20 +0000</pubDate>
    </item>
    <item>
      <title>Felipe Borges: Let’s Welcome Our Google Summer of Code 2026 Contributors!</title>
      <link>https://blogs.gnome.org/feborges/gsoc-2026-contributors-announcement/</link>
      <description>&lt;p&gt;GNOME is once again participating in &lt;a class="external" href="https://summerofcode.withgoogle.com/"&gt;GSoC&lt;/a&gt;. This year, we have 6 contributors working on adding Debug Adapter Protocol support to GJS, incorporating vocab-style puzzles into GNOME Crosswords, creating a native GTK4/Rust rewrite of the Pitivi timeline ruler, porting gitg to GTK4, implementing app uninstallation in the GNOME Shell app grid, and enabling recovery from GPU resets.&lt;/p&gt;
&lt;p&gt;As we onboard the contributors, we will be adding them to &lt;a class="external" href="https://planet.gnome.org/"&gt;Planet GNOME&lt;/a&gt;, where you can get to know them better and follow their project updates.&lt;/p&gt;
&lt;p&gt;GSoC is a great opportunity to welcome new people into our project. Please help them get started and make them feel at home in our community!&lt;/p&gt;
&lt;p&gt;Special thanks to our community mentors, who are donating their time and energy to help welcome and guide our new contributors: Philip Chimento, Jonathan Blandford, Yatin, Alex Băluț, Alberto Fanjul,  Adrian Vovk, Jonas Ådahl, and Robert Mader.&lt;/p&gt;
&lt;p&gt;For more information, visit &lt;a class="external" href="https://summerofcode.withgoogle.com/programs/2026/organizations/gnome-foundation"&gt;https://summerofcode.withgoogle.com/programs/2026/organizations/gnome-foundation&lt;/a&gt;&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/feborges/gsoc-2026-contributors-announcement/</guid>
      <pubDate>Thu, 30 Apr 2026 21:05:07 +0000</pubDate>
    </item>
    <item>
      <title>This Week in GNOME: #247 International Workers' Day</title>
      <link>https://thisweek.gnome.org/posts/2026/05/twig-247/</link>
      <description>&lt;p&gt;Update on what happened across the GNOME project in the week from April 24 to May 01.&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id="gnome-circle-apps-and-libraries"&gt;GNOME Circle Apps and Libraries&lt;/h2&gt;
&lt;h3 id="newsflash-feed-reader-"&gt;NewsFlash feed reader &lt;a href="https://gitlab.com/news-flash/news_flash_gtk"&gt;↗&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Follow your favorite blogs &amp;amp; news sites.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/6a980031a26d87453f03a74d5423c26fc88007d29a4ccd0523a7e973406bc24c"&gt;Jan Lukas&lt;/a&gt; announces&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hi TWIG. Newsflash can now swipe between articles. This closes off one of the oldest still standing feature requests. And hopefully makes all the mobile users happy.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;video controls="controls"&gt;&lt;source src="https://thisweek.gnome.org/posts/2026/05/twig-247/TWIG Newsflash Swipe.webm" type="video/webm"/&gt;&lt;/video&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="third-party-projects"&gt;Third Party Projects&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/981c6b65a9d91b1e4764b504aa374aa6e2ded1f40c9ac1eb8cc04c30f2b3751b"&gt;xjuan&lt;/a&gt; reports&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Casilda 1.2.4 Released!&lt;/p&gt;
&lt;p&gt;I am very happy to announce a new version of Casilda!&lt;/p&gt;
&lt;p&gt;A simple Wayland compositor widget for Gtk  4 and GNOME&lt;/p&gt;
&lt;p&gt;This release comes with several new features like fractional scaling support, bug fixes and extra polish that it is making it start to feel like a proper compositor.
You can read more about it at &lt;a href="https://blogs.gnome.org/xjuan/2026/04/19/casilda-1-2-4-released/"&gt;https://blogs.gnome.org/xjuan/2026/04/19/casilda-1-2-4-released/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="996" src="https://thisweek.gnome.org/_astro/casilda_broken_fractional_scale.ZKWovAtO_11rfhf.webp" width="1315"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/729728ceeb003695c86ccf7fed21c023bd29c2bbae9cb8eb7c63fceeee0e9d9f"&gt;Anton Isaiev&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RustConn (connection manager for SSH, RDP, VNC, SPICE, Telnet, Serial, Kubernetes, MOSH, and Zero Trust protocols)&lt;/p&gt;
&lt;p&gt;Versions 0.11.0–0.12.7 bring the three biggest features since the project started, plus a mountain of polish driven by community feedback.&lt;/p&gt;
&lt;p&gt;Cloud Sync landed. You can now synchronize connection configurations between devices and team members through any shared directory - Google Drive, Syncthing, Nextcloud, Dropbox, or even a USB stick. Two modes: Group Sync (per-group .rcn files with Master/Import access) and Simple Sync (single-file bidirectional merge). A file watcher auto-imports changes, and the new Cloud Sync settings page shows sync status, synced groups, and available files. CLI got &lt;code&gt;sync status&lt;/code&gt;, &lt;code&gt;sync list&lt;/code&gt;, &lt;code&gt;sync export&lt;/code&gt;, &lt;code&gt;sync import&lt;/code&gt;, and &lt;code&gt;sync now&lt;/code&gt; commands.&lt;/p&gt;
&lt;p&gt;SSH Tunnel Manager is a standalone window for managing headless SSH port-forwarding tunnels without terminal sessions - Local, Remote, and Dynamic forwards with auto-start on launch and auto-reconnect. SSH jump host support was extended to RDP, VNC, and SPICE connections, so you can tunnel graphical sessions through a bastion host. Ctrl+T opens the tunnel manager.&lt;/p&gt;
&lt;p&gt;Tab management was completely reworked around AdwTabView. Tab Overview (Ctrl+Shift+O) gives a GNOME Web-style grid of all open tabs. Tab Pinning keeps important tabs at the left edge. A tab switcher in the Command Palette (% prefix) provides fuzzy search across open tabs. Right-click context menu gained Close Others / Left / Right / All / Ungrouped actions.&lt;/p&gt;
&lt;p&gt;Other highlights: custom terminal color themes with full 16-color ANSI palette editor; terminal scrollbar; font zoom (Ctrl+Scroll); copy-on-select; SSH Keep-Alive and verbose mode; Hoop.dev as the 11th Zero Trust provider; custom SSH agent socket override (fixes KeePassXC/Bitwarden agent in Flatpak); RDP mouse jiggler; terminal activity/silence monitor; host online check with auto-connect; highlight rules now render with actual colors via Cairo overlay; connection dialog rebuilt with adw:: widgets following GNOME HIG.&lt;/p&gt;
&lt;p&gt;Packaging grew significantly. RustConn is now available as Flatpak on Flathub, Snap with strict confinement, AppImage, native .deb and .rpm packages via OBS repositories (Debian 13, Ubuntu 24.04/26.04, Fedora 43/44, openSUSE Tumbleweed/Slowroll/Leap 16.0), plus ARM64 builds. A huge thank you to the community maintainers: the AUR package for Arch Linux, the FreeBSD port, and there is an open request to include RustConn in Debian proper.&lt;/p&gt;
&lt;p&gt;Thank you to everyone who reported issues, contributed translations, and tested pre-releases - your feedback shaped every one of these 25 releases. Special thanks to GaaChun for the complete Simplified Chinese translation, and to Phil Dodd and Todor Todorov for the support.&lt;/p&gt;
&lt;p&gt;Project: &lt;a href="https://github.com/totoshko88/RustConn"&gt;https://github.com/totoshko88/RustConn&lt;/a&gt;
Flatpak: &lt;a href="https://flathub.org/en/apps/io.github.totoshko88.RustConn"&gt;https://flathub.org/en/apps/io.github.totoshko88.RustConn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="1250" src="https://thisweek.gnome.org/_astro/rustconn_CloudSync.BxupfSm0_1uQkG6.webp" width="700"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="1100" src="https://thisweek.gnome.org/_astro/rustconn_ssh-tunnels.B2WAUh0K_Z1hlwGT.webp" width="1300"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="2378" src="https://thisweek.gnome.org/_astro/rustconn_TabView.Dnv9BDQJ_Z1ONH2b.webp" width="3668"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="2378" src="https://thisweek.gnome.org/_astro/rustconn_Settings.C6pzEfPn_ZvwbKN.webp" width="3668"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/39766c7b39dcf27cff85865f3304fe1ba10d2c217dc96f63b3c5e30ddd103280"&gt;Capypara&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Field Monitor 50.0&lt;/p&gt;
&lt;p&gt;Field Monitor - the remote desktop viewer focused on accessing VMs - has been updated to version 50.0.&lt;/p&gt;
&lt;p&gt;Some highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for multiple monitors for SPICE connections.&lt;/li&gt;
&lt;li&gt;Support for sharing USB devices with SPICE sessions using the XDG USB Portal (even with the Flatpak).&lt;/li&gt;
&lt;li&gt;KVM/QEMU VMs can now be accessed with hardware accelerated GPU rendering - if enabled.&lt;/li&gt;
&lt;li&gt;Field Monitor now validates server certificates and asks you for your trust if a certificate isn’t automatically trusted by your system.&lt;/li&gt;
&lt;li&gt;Several bugfixes to RDP and SPICE sessions, such as cursor rendering issues and overall performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Field Monitor is available via Flathub: &lt;a href="https://flathub.org/apps/de.capypara.FieldMonitor"&gt;https://flathub.org/apps/de.capypara.FieldMonitor&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/51e6f3a671be54d4022a5c37fdd52351d8cfe42d93457d4d38a7093c12f1f2f9"&gt;Christian&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The first public release of Gitte is out!&lt;/p&gt;
&lt;p&gt;Gitte is a GTK4/libadwaita git GUI written in Rust, built on Relm4 and git2 (no shelling out to the git binary).&lt;/p&gt;
&lt;p&gt;What’s in the initial release:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browse repositories with a saved repositories start screen&lt;/li&gt;
&lt;li&gt;View the working copy, stage and unstage changes, commit them, amend commits&lt;/li&gt;
&lt;li&gt;Read the commit log and inspect diffs file by file&lt;/li&gt;
&lt;li&gt;Manage branches, tags, remotes, and stashes&lt;/li&gt;
&lt;li&gt;Push from and pull to remotes, auto-fetching remotes in the background&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s early days, so expect rough edges. Bug reports and feedback are very welcome.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://flathub.org/apps/de.wwwtech.gitte"&gt;Get it on Flathub&lt;/a&gt; or &lt;a href="https://codeberg.org/ckruse/Gitte"&gt;check the source code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img height="563" src="https://thisweek.gnome.org/_astro/07-gitte-repo-list-view.Kj2zZIRu_vQooQ.webp" width="1035"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="1385" src="https://thisweek.gnome.org/_astro/01-gitte-working-copy-view.Ccf6vbtV_Z1JX65W.webp" width="2247"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="1385" src="https://thisweek.gnome.org/_astro/02-gitte-commit-log-view.LmF1BZD4_Z1Qev2c.webp" width="2247"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="1385" src="https://thisweek.gnome.org/_astro/05-gitte-pull-view.BEWgLyvT_3Pn2z.webp" width="2247"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="parabolic-"&gt;Parabolic &lt;a href="https://flathub.org/apps/details/org.nickvision.tubeconverter"&gt;↗&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Download web video and audio.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/b77e70357e23d9014c0bf6590a1f80bae500e21461bfab9eabc65fd8567d6dd7"&gt;Nick&lt;/a&gt; reports&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Parabolic &lt;a href="https://github.com/NickvisionApps/Parabolic/releases/tag/2026.4.1"&gt;V2026.4.1&lt;/a&gt; is here with plenty of bug fixes!&lt;/p&gt;
&lt;p&gt;Here’s the full changelog:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed an issue where some settings would not save correctly&lt;/li&gt;
&lt;li&gt;Fixed an issue where playlist downloads with a resolution limit had no audio&lt;/li&gt;
&lt;li&gt;Fixed an issue where portrait/vertical videos in playlists downloaded at incorrect resolutions&lt;/li&gt;
&lt;li&gt;Fixed an issue where downloads from sites with muxed-only streams would fail&lt;/li&gt;
&lt;li&gt;Fixed an issue where downloading a time frame clip from a long video produced an incomplete result&lt;/li&gt;
&lt;li&gt;Fixed an issue where downloading a time frame clip from a long video could hang indefinitely with aria2c enabled&lt;/li&gt;
&lt;li&gt;Fixed an issue where X/Twitter quoted downloads could produce the same video twice&lt;/li&gt;
&lt;li&gt;Fixed an issue where deno was unable to be updated in-app on Linux&lt;/li&gt;
&lt;li&gt;Fixed an issue where browser cookies could not be found when running via Flatpak on Linux&lt;/li&gt;
&lt;li&gt;Fixed an issue where Parabolic would not start on KDE desktops&lt;/li&gt;
&lt;li&gt;Fixed an issue where Parabolic did not open links from browser extension on Windows&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id="thats-all-for-this-week"&gt;That’s all for this week!&lt;/h2&gt;
&lt;p&gt;See you next week, and be sure to stop by &lt;a href="https://matrix.to/#/#thisweek:gnome.org"&gt;#thisweek:gnome.org&lt;/a&gt; with updates on your own projects!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://thisweek.gnome.org/posts/2026/05/twig-247/</guid>
      <pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Allan Day: GNOME Foundation Update, 2026-05-01</title>
      <link>https://blogs.gnome.org/aday/2026/05/01/gnome-foundation-update-2026-05-01/</link>
      <description>&lt;p&gt;It’s the first day of May, and it’s time for another update on what’s been happening at the GNOME Foundation. It’s been two weeks since my last post, and this update covers highlights of what we’ve been doing since then. &lt;/p&gt;
&lt;h2&gt;Remembering Seth Nickell&lt;/h2&gt;
&lt;p&gt;This week we received the very sad news of the death of Seth Nickell. It’s been a long time since Seth was active in the GNOME project, so many of our members won’t be familiar with him or his work. However, Seth played an important part in GNOME’s history, and was a special and unique character.&lt;/p&gt;
&lt;p&gt;Jonathan wrote &lt;a href="https://blogs.gnome.org/jrb/2026/04/29/remembering-seth/"&gt;a wonderful post about Seth&lt;/a&gt;, with some great stories. Federico migrated the &lt;a class="external" href="https://handbook.gnome.org/foundation/memorial.html"&gt;memorial page&lt;/a&gt; from the old wiki to the handbook, and added Seth there (work is currently ongoing to develop that page). Seth’s death has also been &lt;a class="external" href="https://lwn.net/Articles/1070213/"&gt;covered by LWN&lt;/a&gt;, which includes dedications from GNOME contributors.&lt;/p&gt;
&lt;p&gt;Whether you knew Seth or came to GNOME after his time, I think we can all appreciate the contributions that he made, which live on in the project and wider ecosystem to this day. &lt;/p&gt;
&lt;h2&gt;GNOME Fellowship&lt;/h2&gt;
&lt;p&gt;Applications for the first round of the new &lt;a class="external" href="https://fellowship.gnome.org/"&gt;GNOME Fellowship program&lt;/a&gt; closed last week, on 20th April. We had a great response and received some excellent proposals, and now we have the tough job of deciding who is going to receive support through the program.&lt;/p&gt;
&lt;p&gt;To that end, the &lt;a class="external" href="https://handbook.gnome.org/foundation/committees/fellowship.html"&gt;Fellowship Committee&lt;/a&gt; met this week to review the proposals and begin the selection process. We have identified a shortlist of candidates, and will be meeting again next week to narrow the selection further.&lt;/p&gt;
&lt;p&gt;Since this is the first round of the Fellowship, we are establishing the selection process as we go. Hopefully we’ll get to put this to use again in future Fellowship rounds!&lt;/p&gt;
&lt;h2&gt;Conferences&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://linuxappsummit.org/"&gt;Linux App Summit (LAS)&lt;/a&gt; will be held in Berlin on 16-17 May – that’s in a little over two weeks! The &lt;a class="external" href="https://conf.linuxappsummit.org/event/9/timetable/"&gt;schedule has been finalized&lt;/a&gt; and looks great, and this year’s LAS is shaping up to be a fantastic event. Please do consider going, and please do register!&lt;/p&gt;
&lt;p&gt;Due to high demand, the organizing team have decided to stream the talks from this year, so look out for details about remote participation.&lt;/p&gt;
&lt;p&gt;Aside from LAS, preparations for &lt;a class="external" href="https://guadec.org/"&gt;July’s GUADEC conference&lt;/a&gt; continue to be worked on. &lt;a class="external" href="https://handbook.gnome.org/events/travel.html"&gt;Travel sponsorship&lt;/a&gt; is still available if you need assistance in order to attend, so do consider applying for that.&lt;/p&gt;
&lt;h2&gt;Office transitions ongoing&lt;/h2&gt;
&lt;p&gt;Work to update many of our backoffice systems and processes has continued at a steady pace over the past fortnight. Many of the big moves are done (new payments system, email accounts, mailing system, accounting procedures, credit card platform), and we are now firmly in the final stages, making sure that our new address is used everywhere, emails are going to the right places, recurring payments are transferred over to new credit cards, and vendors are setup on the new payments system.&lt;/p&gt;
&lt;p&gt;The value of this work is already showing, with smoother accounting procedures, more up to date finance reports, and better tracking of incoming queries.&lt;/p&gt;
&lt;p&gt;That’s it for this update. Thanks for reading, and take care.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/aday/2026/05/01/gnome-foundation-update-2026-05-01/</guid>
      <pubDate>Fri, 01 May 2026 10:34:10 +0000</pubDate>
    </item>
    <item>
      <title>Andrea Veri: SELinux MCS challenges with GitLab Runners</title>
      <link>https://www.dragonsreach.it/2026/05/01/selinux-mcs-challenges-gitlab-runners/</link>
      <description>&lt;h2 id="table-of-contents"&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#the-mcs-problem"&gt;The MCS problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#the-test-script"&gt;The test script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#gitlabs-official-suggestion-and-why-it-falls-short"&gt;GitLab’s official suggestion and why it falls short&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#how-gnome-currently-handles-this"&gt;How GNOME currently handles this&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#exploring-libkrun"&gt;Exploring libkrun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#firecracker-and-the-custom-executor-path"&gt;Firecracker and the custom executor path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dragonsreach.it/categories/planet-gnome/index.xml#what-comes-next"&gt;What comes next&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;GNOME’s GitLab runners use Podman as the container runtime with SELinux in Enforcing mode on Fedora. The GitLab Runner Docker/Podman executor spawns multiple containers per job: a &lt;strong&gt;helper&lt;/strong&gt; container that clones the repository and handles artifacts, and a &lt;strong&gt;build&lt;/strong&gt; container that runs the actual CI script. Both containers need to share a &lt;code&gt;/builds&lt;/code&gt; volume — and this is where SELinux’s &lt;strong&gt;Multi-Category Security (MCS)&lt;/strong&gt; becomes a problem.&lt;/p&gt;
&lt;h2 id="the-mcs-problem"&gt;The MCS problem&lt;/h2&gt;
&lt;p&gt;An SELinux label has four fields: &lt;code&gt;user:role:type:level&lt;/code&gt;. For containers the interesting part is the &lt;strong&gt;level&lt;/strong&gt;, also called the MCS field. A level looks like &lt;code&gt;s0:c123,c456&lt;/code&gt; — &lt;code&gt;s0&lt;/code&gt; is the sensitivity (always &lt;code&gt;s0&lt;/code&gt; in targeted policy), and &lt;code&gt;c123,c456&lt;/code&gt; are the &lt;strong&gt;categories&lt;/strong&gt;. A process or file can carry up to two categories.&lt;/p&gt;
&lt;p&gt;MCS access is based on &lt;strong&gt;dominance&lt;/strong&gt;. A subject’s label dominates an object’s label if the subject’s categories are a superset of (or equal to) the object’s categories:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subject&lt;/th&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Access?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Exact match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s0:c100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Subject’s categories are a superset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c300&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Subject lacks &lt;code&gt;c300&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s0:c0.c1023&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Full range dominates everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s0:c100,c200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No categories can’t dominate any&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Both have no categories&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;How this applies to the runners:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container A runs as &lt;code&gt;container_t:s0:c100,c100&lt;/code&gt; — it can only access objects labeled &lt;code&gt;s0:c100,c100&lt;/code&gt; (or &lt;code&gt;s0:c100&lt;/code&gt;, or &lt;code&gt;s0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Container B runs as &lt;code&gt;container_t:s0:c200,c200&lt;/code&gt; — it can only access objects labeled &lt;code&gt;s0:c200,c200&lt;/code&gt; (or &lt;code&gt;s0:c200&lt;/code&gt;, or &lt;code&gt;s0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Container A cannot access Container B’s files — &lt;code&gt;c100,c100&lt;/code&gt; doesn’t dominate &lt;code&gt;c200,c200&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Overlay layers labeled &lt;code&gt;s0&lt;/code&gt; (no categories) — accessible by all containers since every category set dominates the empty set&lt;/li&gt;
&lt;li&gt;Podman at &lt;code&gt;container_runtime_t:s0-s0:c0.c1023&lt;/code&gt; — the full range means it dominates every possible category combination, so it can manage all containers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;strong&gt;range syntax&lt;/strong&gt; (&lt;code&gt;s0-s0:c0.c1023&lt;/code&gt;) is used for processes that need to operate across multiple levels. It means “my low clearance is &lt;code&gt;s0&lt;/code&gt; and my high clearance is &lt;code&gt;s0:c0.c1023&lt;/code&gt;.” The process can read objects at any level within that range and create objects at any level within it. This is why Podman needs the full range — it creates containers with different MCS labels and needs to access all of them.&lt;/p&gt;
&lt;p&gt;When Podman starts a container, it picks a random pair of categories (e.g., &lt;code&gt;s0:c512,c768&lt;/code&gt;) from within its allowed range and assigns that as the container’s process label. Files created by the container inherit that label. Another container gets a different random pair (e.g., &lt;code&gt;s0:c33,c901&lt;/code&gt;). Since &lt;code&gt;c512,c768&lt;/code&gt; and &lt;code&gt;c33,c901&lt;/code&gt; do not match — neither is a superset of the other — SELinux denies cross-container file access. This is the isolation mechanism, and the root cause of the problem with GitLab Runner’s multi-container-per-job architecture.&lt;/p&gt;
&lt;p&gt;The helper container gets one random MCS pair, writes the cloned repo to &lt;code&gt;/builds&lt;/code&gt; labeled with that pair, and the build container gets a different pair. The build container cannot read or write those files. The &lt;code&gt;:Z&lt;/code&gt; volume flag (exclusive relabel) relabels the volume to the mounting container’s category, but that only helps the first container — the second one still has a different label.&lt;/p&gt;
&lt;h2 id="the-test-script"&gt;The test script&lt;/h2&gt;
&lt;p&gt;I wrote a script that demonstrates the problem with both standard containers (crun) and microVMs (libkrun). The script creates two containers per test — a helper that writes a file to a shared &lt;code&gt;/builds&lt;/code&gt; volume, and a build container that tries to read it — simulating the GitLab Runner workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Description: SELinux MCS Diagnostic (crun vs krun)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;getenforce&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enforcing"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: SELinux is not in Enforcing mode. This test requires Enforcing mode."&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TEST_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/gitlab-runner-mcs-test"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CRUN_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;/crun-builds"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;KRUN_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;/krun-builds"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Cleanup from previous runs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"======================================================="&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" TEST 1: Standard Container Isolation (crun)"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"======================================================="&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. CREATE Helper&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman create --name crun-helper -v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;:/builds:Z"&lt;/span&gt; fedora bash -c &lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; echo '[crun] -&amp;gt; Helper Process Context (Inside):'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; cat /proc/self/attr/current
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; echo 'crun-data' &amp;gt; /builds/artifact.txt
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; echo '[crun] -&amp;gt; File Label INSIDE Helper:'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; ls -Z /builds/artifact.txt
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[crun] Starting Helper Container (applying :Z relabel)..."&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;HELPER_HOST_LABEL_CRUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;podman inspect -f &lt;span class="s1"&gt;'{{.ProcessLabel}}'&lt;/span&gt; crun-helper&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[crun] -&amp;gt; HOST METADATA: Podman assigned process label: &lt;/span&gt;&lt;span class="nv"&gt;$HELPER_HOST_LABEL_CRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman start -a crun-helper
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[crun] -&amp;gt; File Label ON HOST (Notice the specific MCS category):"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -Z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/artifact.txt"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. CREATE Build Container (The Victim)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman create --name crun-build -v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;:/builds"&lt;/span&gt; fedora bash -c &lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; echo ' [Build-Internal] Process Context:'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; cat /proc/self/attr/current 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; echo ' [Build-Internal] Executing ls -laZ /builds :'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; ls -laZ /builds 2&amp;gt;&amp;amp;1 | sed 's/^/ /'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; echo ' [Build-Internal] Executing cat /builds/artifact.txt :'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; cat /builds/artifact.txt 2&amp;gt;&amp;amp;1 | sed 's/^/ /'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[crun] Starting Build Container to inspect shared volume..."&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BUILD_HOST_LABEL_CRUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;podman inspect -f &lt;span class="s1"&gt;'{{.ProcessLabel}}'&lt;/span&gt; crun-build&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[crun] -&amp;gt; HOST METADATA: Podman assigned process label: &lt;/span&gt;&lt;span class="nv"&gt;$BUILD_HOST_LABEL_CRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman start -a crun-build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman rm -f crun-helper crun-build &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"======================================================="&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" TEST 2: MicroVM Isolation (libkrun / virtio-fs) FIXED"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"======================================================="&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# --- Write the execution scripts to the host to avoid parsing errors ---&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &lt;span class="s"&gt;&amp;lt;&amp;lt; 'EOF' &amp;gt; "$TEST_BASE/krun_helper.sh"
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;echo '[krun] -&amp;gt; Helper Process Context (Inside VM):'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cat /proc/self/attr/current 2&amp;gt;/dev/null || echo ' (SELinux disabled/unavailable in guest kernel)'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;echo 'krun-data' &amp;gt; /builds/artifact.txt
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;echo '[krun] -&amp;gt; File Label INSIDE Helper VM (Blindspot):'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;ls -laZ /builds/artifact.txt 2&amp;gt;&amp;amp;1 | sed 's/^/ /'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &lt;span class="s"&gt;&amp;lt;&amp;lt; 'EOF' &amp;gt; "$TEST_BASE/krun_build.sh"
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;echo ' [Build-Internal] Process Context (Inside VM):'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cat /proc/self/attr/current 2&amp;gt;/dev/null || echo ' (SELinux disabled/unavailable in guest kernel)'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;echo ' [Build-Internal] Executing ls -laZ /builds :'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;ls -laZ /builds 2&amp;gt;&amp;amp;1 | sed 's/^/ /'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;echo ' [Build-Internal] Executing cat /builds/artifact.txt :'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cat /builds/artifact.txt 2&amp;gt;&amp;amp;1 | sed 's/^/ /'
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;/krun_helper.sh"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;/krun_build.sh"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ---------------------------------------------------------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. CREATE Helper MicroVM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman create --name krun-helper --runtime krun --memory&lt;span class="o"&gt;=&lt;/span&gt;1024m &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; -v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;:/builds:Z"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; -v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;/krun_helper.sh:/script.sh:ro,Z"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; fedora /script.sh &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[krun] Starting Helper MicroVM (applying :Z relabel)..."&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;HELPER_HOST_LABEL_KRUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;podman inspect -f &lt;span class="s1"&gt;'{{.ProcessLabel}}'&lt;/span&gt; krun-helper&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[krun] -&amp;gt; HOST METADATA: Podman assigned process label: &lt;/span&gt;&lt;span class="nv"&gt;$HELPER_HOST_LABEL_KRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman start -a krun-helper
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[krun] -&amp;gt; File Label ON HOST (Podman applied the helper's MCS category via :Z):"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -Z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/artifact.txt"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. CREATE Build MicroVM (The Victim)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman create --name krun-build --runtime krun --memory&lt;span class="o"&gt;=&lt;/span&gt;1024m &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; -v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KRUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;:/builds"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; -v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_BASE&lt;/span&gt;&lt;span class="s2"&gt;/krun_build.sh:/script.sh:ro,Z"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; fedora /script.sh &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[krun] Starting Build MicroVM to inspect shared volume..."&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BUILD_HOST_LABEL_KRUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;podman inspect -f &lt;span class="s1"&gt;'{{.ProcessLabel}}'&lt;/span&gt; krun-build&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[krun] -&amp;gt; HOST METADATA: Podman assigned process label: &lt;/span&gt;&lt;span class="nv"&gt;$BUILD_HOST_LABEL_KRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" *** THE virtiofsd DAEMON ON THE HOST IS TRAPPED IN THIS CONTEXT ***"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman start -a krun-build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Cleanup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman rm -f krun-helper krun-build &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"======================================================="&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" Test Complete."&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Test 1 (crun)&lt;/strong&gt; creates a helper container that mounts the builds directory with &lt;code&gt;:Z&lt;/code&gt; (exclusive relabel) and writes &lt;code&gt;artifact.txt&lt;/code&gt;. Podman assigns it a random MCS label — in this run it was &lt;code&gt;s0:c20,c540&lt;/code&gt;. The file on disk inherits that label. Then a second container (the build container) mounts the same path without &lt;code&gt;:Z&lt;/code&gt; and gets a different random label (&lt;code&gt;s0:c46,c331&lt;/code&gt;). Since &lt;code&gt;c46,c331&lt;/code&gt; does not dominate &lt;code&gt;c20,c540&lt;/code&gt;, the build container is denied access to the file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test 2 (krun)&lt;/strong&gt; runs the same scenario but with &lt;code&gt;--runtime krun&lt;/code&gt;, which boots each container inside a lightweight microVM via &lt;a href="https://github.com/containers/libkrun"&gt;libkrun&lt;/a&gt;. The helper VM gets &lt;code&gt;container_kvm_t:s0:c823,c999&lt;/code&gt; and the build VM gets &lt;code&gt;container_kvm_t:s0:c309,c405&lt;/code&gt; — same MCS mismatch, same denial. The type changes from &lt;code&gt;container_t&lt;/code&gt; to &lt;code&gt;container_kvm_t&lt;/code&gt;, but the MCS mechanism is identical. On the host side, &lt;code&gt;virtiofsd&lt;/code&gt; — the daemon that serves the volume into the VM via virtio-fs — runs under the MCS label Podman assigned to the VM. The build VM’s &lt;code&gt;virtiofsd&lt;/code&gt; is trapped in &lt;code&gt;s0:c309,c405&lt;/code&gt; and cannot access files labeled &lt;code&gt;s0:c823,c999&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;An interesting detail: inside the libkrun VMs, &lt;code&gt;cat /proc/self/attr/current&lt;/code&gt; returns just &lt;code&gt;kernel&lt;/code&gt; — SELinux is not available in the guest. The VM thinks it has no mandatory access control, but the host-side &lt;code&gt;virtiofsd&lt;/code&gt; is still fully subject to MCS enforcement. This is a blindspot worth being aware of.&lt;/p&gt;
&lt;p&gt;The output from a run on Fedora with SELinux Enforcing and Podman 5.8.2:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;=======================================================
TEST 1: Standard Container Isolation (crun)
=======================================================
[crun] Starting Helper Container (applying :Z relabel)...
[crun] -&amp;gt; HOST METADATA: Podman assigned process label: system_u:system_r:container_t:s0:c20,c540
[crun] -&amp;gt; Helper Process Context (Inside):
system_u:system_r:container_t:s0:c20,c540 [crun] -&amp;gt; File Label INSIDE Helper:
system_u:object_r:container_file_t:s0:c20,c540 /builds/artifact.txt
[crun] -&amp;gt; File Label ON HOST (Notice the specific MCS category):
system_u:object_r:container_file_t:s0:c20,c540 /tmp/gitlab-runner-mcs-test/crun-builds/artifact.txt
[crun] Starting Build Container to inspect shared volume...
[crun] -&amp;gt; HOST METADATA: Podman assigned process label: system_u:system_r:container_t:s0:c46,c331
*** COMPARE THE cXXX,cYYY ABOVE TO THE FILE LABEL. THIS MISMATCH CAUSES THE DENIAL ***
[Build-Internal] Process Context:
system_u:system_r:container_t:s0:c46,c331 [Build-Internal] Executing ls -laZ /builds :
ls: cannot open directory '/builds': Permission denied
[Build-Internal] Executing cat /builds/artifact.txt :
cat: /builds/artifact.txt: Permission denied
=======================================================
TEST 2: MicroVM Isolation (libkrun / virtio-fs) FIXED
=======================================================
[krun] Starting Helper MicroVM (applying :Z relabel)...
[krun] -&amp;gt; HOST METADATA: Podman assigned process label: system_u:system_r:container_kvm_t:s0:c823,c999
[krun] -&amp;gt; Helper Process Context (Inside VM):
kernel [krun] -&amp;gt; File Label INSIDE Helper VM (Blindspot):
-rw-r--r--. 1 root root system_u:object_r:container_file_t:s0:c823,c999 10 May 2 2026 /builds/artifact.txt
[krun] -&amp;gt; File Label ON HOST (Podman applied the helper's MCS category via :Z):
system_u:object_r:container_file_t:s0:c823,c999 /tmp/gitlab-runner-mcs-test/krun-builds/artifact.txt
[krun] Starting Build MicroVM to inspect shared volume...
[krun] -&amp;gt; HOST METADATA: Podman assigned process label: system_u:system_r:container_kvm_t:s0:c309,c405
*** THE virtiofsd DAEMON ON THE HOST IS TRAPPED IN THIS CONTEXT ***
[Build-Internal] Process Context (Inside VM):
kernel [Build-Internal] Executing ls -laZ /builds :
ls: /builds: Permission denied
ls: cannot open directory '/builds': Permission denied
[Build-Internal] Executing cat /builds/artifact.txt :
cat: /builds/artifact.txt: Permission denied
=======================================================
Test Complete.
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="gitlabs-official-suggestion-and-why-it-falls-short"&gt;GitLab’s official suggestion and why it falls short&lt;/h2&gt;
&lt;p&gt;GitLab’s documentation on &lt;a href="https://docs.gitlab.com/runner/executors/docker/#configure-selinux-mcs"&gt;configuring SELinux MCS&lt;/a&gt; suggests applying the same MCS label to all containers launched by a runner:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;security_opt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"label=level:s0:c1000,c1000"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This works — all containers get the same category pair, so the helper and build containers can share files. But it collapses MCS isolation between all concurrent jobs on that runner. With &lt;code&gt;concurrent = 4&lt;/code&gt;, four simultaneous jobs all run as &lt;code&gt;s0:c1000,c1000&lt;/code&gt; and can read each other’s &lt;code&gt;/builds&lt;/code&gt; content — cloned source code, build artifacts, cached dependencies. On a shared or multi-tenant runner, this is a security regression: it trades MCS isolation for functionality.&lt;/p&gt;
&lt;p&gt;For runners with &lt;code&gt;concurrent = 1&lt;/code&gt; or dedicated single-tenant runners this is an acceptable tradeoff, but it does not generalize to shared infrastructure where multiple untrusted projects run side by side.&lt;/p&gt;
&lt;h2 id="how-gnome-currently-handles-this"&gt;How GNOME currently handles this&lt;/h2&gt;
&lt;p&gt;GNOME’s runners are managed via an &lt;a href="https://gitlab.gnome.org/GNOME/ansible/-/tree/master/roles/gitlab-runner"&gt;Ansible role&lt;/a&gt; that enforces SELinux in Enforcing mode, installs rootless Podman running as a dedicated &lt;code&gt;podman&lt;/code&gt; system user with linger enabled, and deploys custom SELinux policy modules. The Podman service runs under &lt;code&gt;SELinuxContext=system_u:system_r:container_runtime_t:s0-s0:c0.c1023&lt;/code&gt; via a systemd override — the full MCS range (&lt;code&gt;s0-s0:c0.c1023&lt;/code&gt;) gives the container runtime the ability to spawn containers at any MCS level and relabel volumes accordingly, as explained in the dominance rules above.&lt;/p&gt;
&lt;p&gt;Four custom SELinux &lt;code&gt;.te&lt;/code&gt; modules are compiled and loaded on every runner host: &lt;code&gt;pydocuum&lt;/code&gt; (allows the image cleanup daemon to talk to the Podman socket), &lt;code&gt;podman&lt;/code&gt; (grants &lt;code&gt;user_namespace create&lt;/code&gt; and &lt;code&gt;/dev/null&lt;/code&gt; mapping), &lt;code&gt;flatpak&lt;/code&gt; (permits the filesystem mounts flatpak builds need), and &lt;code&gt;gnome_runner&lt;/code&gt; (covers &lt;code&gt;binfmt_misc&lt;/code&gt; access, device nodes, and other permissions GNOME OS builds require).&lt;/p&gt;
&lt;p&gt;For the MCS problem specifically, the runner &lt;code&gt;config.toml&lt;/code&gt; — rendered from a Jinja2 template via per-host Ansible variables — sets a fixed MCS label per runner type. Here’s a representative snippet from one of the runner hosts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="chroma" tabindex="0"&gt;&lt;code class="language-toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"a15948139c78"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;executor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"quay.io/fedora/fedora:latest"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;privileged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;security_opt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"label=level:s0:c100,c100"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/dev/kvm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/dev/udmabuf"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cap_add&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SYS_PTRACE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"SYS_CHROOT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"a15948139c78-flatpak"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;executor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"quay.io/gnome_infrastructure/gnome-runtime-images:gnome-master"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;privileged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;security_opt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"seccomp:/home/podman/gitlab-runner/flatpak.seccomp.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"label=level:s0:c200,c200"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cap_drop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the same approach GitLab’s documentation suggests, with one refinement: we use &lt;strong&gt;different fixed categories per runner type&lt;/strong&gt; — &lt;code&gt;c100,c100&lt;/code&gt; for untagged runners and &lt;code&gt;c200,c200&lt;/code&gt; for flatpak runners — so that flatpak builds and regular builds remain MCS-isolated from each other, even though builds of the same type share a category.&lt;/p&gt;
&lt;p&gt;This is a pragmatic compromise, not an ideal solution. All concurrent jobs on the same runner type share the same MCS category. With &lt;code&gt;concurrent: 4&lt;/code&gt; on our Hetzner runners, four simultaneous untagged jobs can read each other’s &lt;code&gt;/builds&lt;/code&gt; content. For GNOME’s use case — a community CI infrastructure where the runners are shared by GNOME project maintainers — this is an acceptable tradeoff. The alternative, leaving MCS labels random, would break every single job. But it is precisely this tradeoff that motivates exploring per-job VM isolation via microVMs.&lt;/p&gt;
&lt;h2 id="exploring-libkrun"&gt;Exploring libkrun&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/containers/libkrun"&gt;libkrun&lt;/a&gt; is a lightweight Virtual Machine Monitor (VMM) that integrates with Podman via &lt;code&gt;--runtime krun&lt;/code&gt;, running each container inside a microVM with its own lightweight kernel. The appeal is strong: per-container VM isolation would give each job its own kernel and address space, making the MCS cross-container problem irrelevant inside the VM.&lt;/p&gt;
&lt;p&gt;I tested libkrun on a Fedora system and hit an immediate blocker: &lt;code&gt;Fatal glibc error: rseq registration failed&lt;/code&gt;. The &lt;strong&gt;rseq&lt;/strong&gt; (Restartable Sequences) syscall was introduced in Linux kernel 5.3 and is required by glibc &amp;gt;= 2.35. libkrun uses a custom minimal kernel that does not expose &lt;code&gt;rseq&lt;/code&gt; support. Since the guest images — Fedora in our case — ship modern glibc that expects &lt;code&gt;rseq&lt;/code&gt; to be available, the process aborts at startup before any user code runs.&lt;/p&gt;
&lt;p&gt;The libkrun kernel is compiled into the library itself and cannot be modified or replaced by the user. This is not a configuration issue but a fundamental limitation of the current libkrun release.&lt;/p&gt;
&lt;p&gt;Even if the &lt;code&gt;rseq&lt;/code&gt; issue were resolved, the MCS challenge would still be there — as the test script demonstrates in Test 2. On the host side, Podman assigns MCS labels to the &lt;code&gt;virtiofsd&lt;/code&gt; process that serves the volume into the VM via virtio-fs. Different VMs get different host-side MCS labels, meaning the same &lt;code&gt;:Z&lt;/code&gt; relabel / cross-container access denial applies. The mechanism changes from overlay mounts to virtio-fs, but the SELinux enforcement is identical: &lt;code&gt;virtiofsd&lt;/code&gt; for the build VM runs at &lt;code&gt;container_kvm_t:s0:c309,c405&lt;/code&gt; and cannot access files labeled &lt;code&gt;s0:c823,c999&lt;/code&gt; by the helper VM’s &lt;code&gt;virtiofsd&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="firecracker-and-the-custom-executor-path"&gt;Firecracker and the custom executor path&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://firecracker-microvm.github.io/"&gt;Firecracker&lt;/a&gt; is another microVM technology, the one behind AWS Lambda and Fly.io, that could provide strong per-job isolation. However, there is no native GitLab Runner executor for Firecracker. The only integration path is the &lt;a href="https://docs.gitlab.com/runner/executors/custom.html"&gt;Custom Executor&lt;/a&gt;, which requires implementing &lt;code&gt;prepare&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, and &lt;code&gt;cleanup&lt;/code&gt; scripts from scratch.&lt;/p&gt;
&lt;p&gt;The job image is exposed via &lt;code&gt;CUSTOM_ENV_CI_JOB_IMAGE&lt;/code&gt;, but everything else is on the operator: pulling the OCI image, extracting a rootfs, booting a Firecracker VM with the right kernel and network configuration, injecting the build script, mounting or copying the cloned repository into the VM, collecting artifacts and cache after the job finishes, and tearing the VM down. GitLab provides an &lt;a href="https://docs.gitlab.com/runner/executors/custom_examples/lxd/"&gt;LXD-based example&lt;/a&gt; that shows the pattern — &lt;code&gt;prepare&lt;/code&gt; creates a container and installs dependencies, &lt;code&gt;run&lt;/code&gt; pipes the job script into it, &lt;code&gt;cleanup&lt;/code&gt; destroys it — but adapting that to microVMs adds the complexity of VM lifecycle management, kernel and rootfs preparation, networking, and storage. This is a significant engineering effort, essentially rebuilding the entire Docker executor workflow from scratch.&lt;/p&gt;
&lt;h2 id="what-comes-next"&gt;What comes next&lt;/h2&gt;
&lt;p&gt;MCS is a core SELinux feature. Type enforcement (TE) already confines processes by type — &lt;code&gt;container_t&lt;/code&gt; can only access &lt;code&gt;container_file_t&lt;/code&gt;, not &lt;code&gt;user_home_t&lt;/code&gt; or &lt;code&gt;httpd_sys_content_t&lt;/code&gt; — but TE alone cannot distinguish one &lt;code&gt;container_t&lt;/code&gt; process from another. MCS adds that layer: by assigning each container a unique category pair, the kernel enforces isolation &lt;em&gt;between&lt;/em&gt; processes that share the same type. Container A at &lt;code&gt;s0:c100,c100&lt;/code&gt; and Container B at &lt;code&gt;s0:c200,c200&lt;/code&gt; are both &lt;code&gt;container_t&lt;/code&gt;, but MCS ensures they cannot touch each other’s files. The conflict with GitLab Runner’s multi-container-per-job architecture is that two containers that &lt;em&gt;need&lt;/em&gt; to share a volume are given different categories by default. The workarounds we deploy today, including the fixed MCS labels on GNOME’s runners, trade that inter-container isolation for functionality.&lt;/p&gt;
&lt;p&gt;The most promising direction I’ve found so far is the combination of &lt;a href="https://www.cloudhypervisor.org/"&gt;Cloud Hypervisor&lt;/a&gt; and the &lt;a href="https://github.com/helmholtzcloud/fleeting-plugin-fleetingd"&gt;fleeting-plugin-fleetingd&lt;/a&gt; plugin. Cloud Hypervisor is built on Intel’s Rust-VMM crate and is essentially a more capable sibling of Firecracker — it supports CPU and memory hotplugging, VFIO device passthrough, and virtio-fs, features that are often necessary for complex CI tasks like building large binaries or running UI tests and that Firecracker’s minimalist design deliberately omits. The fleeting-plugin-fleetingd is a community plugin for GitLab’s &lt;strong&gt;Instance Executor&lt;/strong&gt; (the modern evolution of the Custom Executor) that automates the full VM lifecycle: downloading cloud images, creating Copy-on-Write disks, launching Cloud Hypervisor VMs with direct kernel boot, provisioning them via cloud-init, and tearing them down after each build. Each job gets a fresh disposable VM, which is exactly the per-job isolation model we need. The plugin already handles networking via TAP interfaces and nftables SNAT, and supports customization of the VM image through cloud-init commands — so preinstalling Podman or other build tools is straightforward.&lt;/p&gt;
&lt;p&gt;Beyond that, I’ll also keep evaluating libkrun (promising Red Hat technology), Firecracker with a hand-rolled custom executor, and QEMU’s microvm machine type. The common denominator across all of these — except for the fleeting-plugin-fleetingd path — is that none of them have an existing GitLab Runner integration. Regardless of which microVM technology we settle on, the path forward involves either building a workflow from scratch using the &lt;a href="https://docs.gitlab.com/runner/executors/custom.html"&gt;Custom Executor&lt;/a&gt; and its &lt;code&gt;prepare&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;cleanup&lt;/code&gt; hooks, or leveraging the fleeting plugin ecosystem that GitLab has been building around the Instance and Docker Autoscaler executors.&lt;/p&gt;
&lt;h3 id="cve-2026-31431"&gt;CVE-2026-31431&lt;/h3&gt;
&lt;p&gt;The urgency of per-job VM isolation was underscored by &lt;a href="https://www.helpnetsecurity.com/2026/04/30/copyfail-linux-lpe-vulnerability-cve-2026-31431/"&gt;CVE-2026-31431&lt;/a&gt; (“Copy Fail”), a nine-year-old logic bug in the kernel’s &lt;code&gt;algif_aead&lt;/code&gt; cryptographic module disclosed at the end of April. The flaw lets an unprivileged local user write four controlled bytes into the page cache of any readable file — enough to patch a setuid binary like &lt;code&gt;/usr/bin/su&lt;/code&gt; and escalate to root. Unlike Dirty Cow or Dirty Pipe, Copy Fail requires no race condition: the exploit is deterministic, leaves no trace on disk, and — critically — can break out of container isolation. In a shared-runner CI environment, any project that can execute arbitrary code in a job already has exactly the access the exploit needs. Separately, &lt;a href="https://www.computing.co.uk/analysis/2026/claude-mythos-how-ai-broke-out-of-its-sandbox"&gt;Claude Mythos&lt;/a&gt; — an Anthropic model trained for cybersecurity research that escaped its own sandbox during a red-team exercise in April — demonstrated that AI-assisted vulnerability discovery and exploitation is no longer theoretical; models can now autonomously find and chain bugs that would take human researchers weeks to exploit. The combination of a reliable, public kernel LPE and AI-augmented offensive tooling makes the case for ephemeral microVMs compelling: when every CI job boots a fresh, disposable VM with its own kernel, a vulnerability like Copy Fail becomes a local-root inside a throwaway guest that is destroyed seconds later, not a stepping stone to the host or adjacent jobs.&lt;/p&gt;
&lt;p&gt;That should be all for today, stay tuned!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://www.dragonsreach.it/2026/05/01/selinux-mcs-challenges-gitlab-runners/</guid>
      <pubDate>Sat, 02 May 2026 01:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Nick Richards: WhatCable, Framework, and USB-C</title>
      <link>https://www.nedrichards.com/2026/05/whatcable-framework-and-usb-c/</link>
      <description>&lt;p&gt;USB-C is excellent, provided you don’t look too closely.&lt;/p&gt;
&lt;p&gt;I’ve been seeing a drum beat of interest in the internals of USB-C. &lt;a href="https://github.com/darrylmorley/whatcable"&gt;Darryl Morley’s macOS WhatCable&lt;/a&gt;, &lt;a href="https://nelsonslog.wordpress.com/2026/01/05/chromebooks-identify-usb-c-details/"&gt;Chromebooks exposing lots of lovely info about emarkers&lt;/a&gt;, &lt;a href="https://www.amazon.co.uk/dp/B0DYJ4T37X/"&gt;USB cable testers&lt;/a&gt; and a bit more. Very &lt;a href="https://infrastructureclub.org/"&gt;infrastructure club&lt;/a&gt; topics. So I made a small GTK app also called &lt;a href="https://github.com/nedrichards/whatcable-linux"&gt;WhatCable&lt;/a&gt; which is intended to show what Linux knows about your USB ports, cables, chargers and devices, but written as a GNOME/libadwaita app and using the interfaces Linux exposes through sysfs.&lt;/p&gt;
&lt;p&gt;The hope was fairly straightforward: plug things into my Framework 13, ask Linux what is going on, and present the answer in a way that doesn’t require remembering which bit of &lt;code&gt;/sys&lt;/code&gt; to poke. In particular I wanted cable identity and e-marker details. These are the useful little facts that tell you whether a cable is what it claims to be, or at least what it claims to be electronically. Given the number of USB-C cables in the house whose origin story is “came in a box with something”, this felt like a public service, or at least a satisfying evening.&lt;/p&gt;
&lt;p&gt;The first bit is pleasantly sensible. Linux has standard-ish places for this information:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-text"&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;/sys/bus/usb/devices
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;/sys/class/typec
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;/sys/class/usb_power_delivery
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;/sys/bus/thunderbolt/devices
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When those are populated, a normal unprivileged app can learn quite a lot. It can show USB devices, Type-C ports, partners, cables, roles, power data, Thunderbolt and USB4 domains. That’s exactly the sort of thing a small Flatpak app should be good at: read some public kernel state, translate it into something at least moderately human friendly and then depart.&lt;/p&gt;
&lt;p&gt;On my Framework 13, the USB device and Thunderbolt sides were useful. The Type-C side was not. &lt;code&gt;/sys/class/typec&lt;/code&gt; existed but had no ports. &lt;code&gt;/sys/class/usb_power_delivery&lt;/code&gt; existed but was empty. This is a slightly annoying result, because it means the nice standard API is present as a signpost rather than a destination.&lt;/p&gt;
&lt;p&gt;The next clue was that the machine clearly does have USB-C machinery, and not just because I could look at the side of the device. It is a Framework 13 with the embedded controller and Cypress CCG power delivery controllers doing real work. The relevant kernel modules were loaded, including UCSI and Chrome EC pieces. There was also an ACPI UCSI device at:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-text"&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;/sys/bus/acpi/devices/USBC000:00
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;but &lt;code&gt;ucsi_acpi&lt;/code&gt; did not appear to bind to it and create the Type-C class ports. So the hardware and firmware know things, but they were not arriving in the standard Linux userspace shape.&lt;/p&gt;
&lt;p&gt;Framework’s own tooling gives another route in. I built &lt;code&gt;framework_tool&lt;/code&gt; from &lt;a href="https://github.com/FrameworkComputer/framework-system"&gt;FrameworkComputer/framework-system&lt;/a&gt; and asked the EC what it could see. The Framework-specific PD port command did not work on this firmware:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-text"&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;USB-C Port 0:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;[ERROR] EC Response Code: InvalidCommand
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and similarly for the other ports. That’s not very poetic, but it is at least clear.&lt;/p&gt;
&lt;p&gt;The Chromebook-style power command was more useful. With a charger connected it reported, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-text"&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;USB-C Port 0 (Right Back):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Role:          Sink
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Charging Type: PD
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Voltage Now:   19.776 V, Max: 20.0 V
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Current Lim:   2250 mA, Max: 2250 mA
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Dual Role:     Charger
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Max Power:     45.0 W
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s good information. It’s not cable identity, but it is the kind of port state people actually want when they are trying to work out why a laptop is charging slowly, or not charging, or doing something else mildly USB-C shaped.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;framework_tool --pd-info&lt;/code&gt; could also talk through the EC to the Cypress controllers and report their firmware details:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-text"&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;Right / Ports 01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Silicon ID:     0x2100
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Mode:           MainFw
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Ports Enabled:  0, 1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  FW2 (Main)   Version: Base: 3.4.0.A10,  App: 3.8.00
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;Left / Ports 23
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Silicon ID:     0x2100
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Mode:           MainFw
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  Ports Enabled:  0, 1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;  FW2 (Main)   Version: Base: 3.4.0.A10,  App: 3.8.00
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, useful. Again, not the cable.&lt;/p&gt;
&lt;p&gt;Much of this investigation and app code was written with AI tools in the loop. That was useful for chasing down boring plumbing and generating probes. The decisive test was asking the Chrome EC for the newer Type-C discovery data directly. The EC advertised USB PD support, but not the newer Type-C command set. &lt;code&gt;EC_CMD_TYPEC_STATUS&lt;/code&gt; and &lt;code&gt;EC_CMD_TYPEC_DISCOVERY&lt;/code&gt; both came back as invalid commands on all four ports.&lt;/p&gt;
&lt;p&gt;That means that on this Framework 13 firmware path I cannot get Discover Identity results, SOP/SOP’ discovery data, SVIDs, mode lists or e-marker details through Chrome EC host commands. The cable may well be telling the PD controller interesting things, but those things are not exposed through a stable unprivileged interface I can sensibly use in a desktop app.&lt;/p&gt;
&lt;p&gt;This is the main lesson from the whole exercise: USB-C inspection on Linux is not one API. It is a set of possible stories. Sometimes the kernel Type-C class tells you lots of things. Sometimes Thunderbolt sysfs tells you a different useful slice. Sometimes a vendor EC can tell you power state, but only as root. Sometimes the information exists below you somewhere, but not in a form you should build an app around.&lt;/p&gt;
&lt;p&gt;So WhatCable needs to be honest. It should show the sources it can read, and it should say when a source is unavailable rather than pretending absence means certainty. “No cable identity exposed on this machine” is a very different statement from “this cable has no identity”. The former is boring but true. The latter is how you end up lying with an icon (it is not a nice icon).&lt;/p&gt;
&lt;p&gt;The current shape I think is right is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use USB, Type-C, USB PD and Thunderbolt sysfs whenever they are available;&lt;/li&gt;
&lt;li&gt;show raw values as well as friendly summaries;&lt;/li&gt;
&lt;li&gt;explain missing sources in diagnostics;&lt;/li&gt;
&lt;li&gt;treat Framework EC data as an optional extra, not a default dependency;&lt;/li&gt;
&lt;li&gt;if EC access is added, put it behind a narrow read-only helper rather than teaching a Flatpak app to fling arbitrary commands at &lt;code&gt;/dev/cros_ec&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point matters. On the host &lt;code&gt;/dev/cros_ec&lt;/code&gt; exists, but it is root-only. Making a normal app require broad device access would be a poor bargain. A small privileged helper that answers a few known-safe questions might be acceptable. A graphical app with arbitrary EC command execution would be exciting in the wrong way.&lt;/p&gt;
&lt;p&gt;This is not quite the result I wanted when I started. I wanted to show a friendly “this is a 100W e-marked cable” label and feel very clever about it. What I have instead is a more modest app and a better understanding of where the bodies are buried. That’s still useful. A tool that tells you what your machine actually exposes is better than one that implies the USB-C universe is more orderly than it is. Given this, I’m not going to be sharing this one more widely, but fork away if you wish, or come back with a better idea.&lt;/p&gt;
&lt;p&gt;It’s very easy to run with GNOME Builder, so just check out the source and ‘press play’ or get an artifact out of the Github Actions. If you run &lt;a href="https://github.com/nedrichards/whatcable-linux"&gt;WhatCable&lt;/a&gt; on a different laptop and see rich Type-C data, lovely. If you run it on a Framework 13 like mine and mostly see USB devices, Thunderbolt controllers and a note that Type-C data is missing, that is also information. Not as glamorous as catching a suspicious cable in the act, but much more likely to be true.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://www.nedrichards.com/2026/05/whatcable-framework-and-usb-c/</guid>
      <pubDate>Sun, 03 May 2026 20:10:00 +0000</pubDate>
    </item>
    <item>
      <title>Michael Meeks: 2026-05-04 Monday</title>
      <link>https://meeksfamily.uk/~michael/blog/2026-05-04.html</link>
      <description>&lt;ul&gt; &lt;!-- lmj --&gt;
&lt;li&gt;
		A day off - about time. Early partner call.
	&lt;/li&gt;
&lt;li&gt;
		Helped J. put up stainless wire for rose
	training in the garden. Plugged away at garage tidying
	with more good progress.
	&lt;/li&gt;
&lt;li&gt;
		Lunch with the family outside in the sun; tidied my
	office for the first time in a while; got the ladder moved
	into J's garden shed.
	&lt;/li&gt;
&lt;li&gt;
		Made a wooden spatula with H. in the evening,
	turning plus band-sawing action; fun. Left it in tung-oil
	overnight.
	&lt;/li&gt;
&lt;/ul&gt;</description>
      <guid isPermaLink="false">https://meeksfamily.uk/~michael/blog/2026-05-04.html</guid>
      <pubDate>Mon, 04 May 2026 21:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Steven Deobald: Apologies</title>
      <link>https://blogs.gnome.org/steven/2026/05/06/apologies/</link>
      <description>&lt;p&gt;I believe accountability can be a challenge in a nonprofit, which only makes it all the more important. In this post, I am holding myself accountable. For the avoidance of doubt, nothing that follows has anything to do with my exit from the GNOME Foundation last August.&lt;/p&gt;
&lt;p&gt;I owe a few folks some apologies from my time as Executive Director. I have apologized to most of them individually already, where I could. But I believe that public accountability is the antidote to public frustration and I hope this contributes, in a small way, to the GNOME community moving forward.&lt;/p&gt;
&lt;p&gt;First off, I sincerely apologize to Jehan Pagès and Christian Hergert. I was curt with both of you last summer and neither of you deserved it. From July 23rd to August 29th I was dealing with significant sleep deprivation but that’s no excuse for the way I spoke to either of you. I’m sorry.&lt;/p&gt;
&lt;p&gt;Next, I apologize to the former Executive Directors and active community members who raised concerns to me. Holly, you warned me. Twice. Many other people tried to share their perspectives. I was too focused on the Foundation’s financial situation, and I did not take the time to fully understand what I was hearing from you all. I regret that.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;Sonny&lt;/h2&gt;
&lt;p&gt;To Sonny Piers: I am sorry. I had a long call with you last June. You told me your complicated story. You seemed hurt — but I didn’t believe you. My understanding was incomplete and I did not approach the situation with the care it deserved.&lt;/p&gt;
&lt;p&gt;I’m sorry I didn’t do more to support you.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;Tobias&lt;/h2&gt;
&lt;p&gt;More than anyone, I want to apologize to Tobias Bernard. Tobias, I am sorry. You gave me many hours of your time, patience, and thoughtfulness. You shared your ideas openly and in good faith, and I didn’t always meet that with the same level of openness.&lt;/p&gt;
&lt;p&gt;In particular, when we discussed Sonny’s situation, I did not listen as carefully as I should have. I was too focused on my existing understanding, and I failed to engage with what you were trying to convey. You deserved better from me.&lt;/p&gt;
&lt;p&gt;Sonny is lucky to have a friend like you.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;Meta&lt;/h2&gt;
&lt;p&gt;This post reflects only my personal experiences and perspectives. It is not intended to make allegations or factual claims about the conduct of any individual or organization.&lt;/p&gt;
&lt;p&gt;Until Microsoft goes out of business, a permanent copy of this apology can be found &lt;a class="external" href="https://gist.github.com/deobald/6eeb2de20e2566b93bb5f8ddf5735c38"&gt;in this gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/steven/2026/05/06/apologies/</guid>
      <pubDate>Wed, 06 May 2026 03:37:00 +0000</pubDate>
    </item>
    <item>
      <title>Richard Hughes: LVFS Sponsorship Announcement</title>
      <link>https://blogs.gnome.org/hughsie/2026/05/06/lvfs-sponsorship-announcement/</link>
      <description>&lt;p&gt;&lt;strong&gt;Some great news&lt;/strong&gt;: I’m pleased to announce that both &lt;a class="external" href="https://www.dell.com/"&gt;Dell&lt;/a&gt; and &lt;a class="external" href="https://www.lenovo.com/"&gt;Lenovo&lt;/a&gt; have agreed to be premier sponsors for the &lt;a class="external" href="https://fwupd.org/"&gt;Linux Vendor Firmware Service (LVFS)&lt;/a&gt; as part of our new &lt;a class="external" href="https://docs.google.com/presentation/d/1l_Rmkn2-UwOkS0XK5d6u9pi29TZpmG-upa4Lu5HuEy4/edit?usp=sharing"&gt;sustainability effort&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blogs.gnome.org/hughsie/files/2026/05/Screenshot-2026-05-06-at-14-10-52-LVFS-Home.png"&gt;&lt;img alt="" class="aligncenter size-large wp-image-10034" height="282" src="https://blogs.gnome.org/hughsie/files/2026/05/Screenshot-2026-05-06-at-14-10-52-LVFS-Home-1024x438.png" width="660"/&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Over 145 million firmware updates have been deployed now, from over a hundred different vendors to millions of different Linux devices.&lt;br/&gt;
With the huge industry support from Lenovo and Dell (and our existing sponsors of &lt;a class="external" href="https://frame.work/"&gt;Framework&lt;/a&gt;, &lt;a class="external" href="https://opensourcefirmware.foundation/"&gt;OSFF&lt;/a&gt;, and of course both the &lt;a class="external" href="https://www.linuxfoundation.org/"&gt;Linux Foundation&lt;/a&gt; and &lt;a class="external" href="https://www.redhat.com/en"&gt;Red Hat&lt;/a&gt;) &lt;strong&gt;we can build this ecosystem stronger and higher than before; we can continue the great work we’ve done long into the future&lt;/strong&gt;.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/hughsie/2026/05/06/lvfs-sponsorship-announcement/</guid>
      <pubDate>Wed, 06 May 2026 12:13:21 +0000</pubDate>
    </item>
    <item>
      <title>This Week in GNOME: #248 Tracking Performance</title>
      <link>https://thisweek.gnome.org/posts/2026/05/twig-248/</link>
      <description>&lt;p&gt;Update on what happened across the GNOME project in the week from May 01 to May 08.&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id="gnome-core-apps-and-libraries"&gt;GNOME Core Apps and Libraries&lt;/h2&gt;
&lt;h3 id="glycin-"&gt;Glycin &lt;a href="https://gitlab.gnome.org/GNOME/glycin"&gt;↗&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Sandboxed and extendable image loading and editing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/78a55022fd487dd708fbc8b46a274879ddd95a53c11137fde5bd519ebf5699ab"&gt;Sophie (she/her)&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Automatically running tests on GitLab has now been a standard for a while. But tracking performance metrics is much less common. &lt;a href="https://gitlab.gnome.org/GNOME/glycin"&gt;Glycin&lt;/a&gt; now started running &lt;a href="https://bencher.dev/perf/glycin/plots"&gt;basic performance tests&lt;/a&gt; on &lt;a href="https://bencher.dev/"&gt;bencher.dev&lt;/a&gt;’s bare metal runners, which will hopefully provide comparable results.&lt;/p&gt;
&lt;p&gt;As of now, the benchmarks are only covering the overhead of the loader stack, by loading a 1px PNG, and the binary file sizes for glycin loaders and the thumbnailer. But the tests should be easy to expand. The benchmarks are always run for commits in the main branch, and can be manually started for merge requests. This way it will be possible to track performance improvements and catch regressions early.&lt;/p&gt;
&lt;p&gt;&lt;img height="639" src="https://thisweek.gnome.org/_astro/glycin-loader-overhead.BcL8QYC1_2qw0Ad.webp" width="653"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="third-party-projects"&gt;Third Party Projects&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/51e6f3a671be54d4022a5c37fdd52351d8cfe42d93457d4d38a7093c12f1f2f9"&gt;Christian&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🎉 Gitte 0.2.0 is out!&lt;/p&gt;
&lt;p&gt;This week, Gitte 0.2.0 was released with a big focus on interactive rebasing and polishing everyday Git workflows.&lt;/p&gt;
&lt;p&gt;The biggest addition is interactive rebasing directly from the commit log. Commits can now be reordered via drag &amp;amp; drop, dropped, reworded, edited during a paused rebase, or squashed and fixuped without leaving the GUI.&lt;/p&gt;
&lt;p&gt;Remote operations like push, pull, fetch and clone now use the Git CLI internally, improving credential handling and protocol support. The diff view font is now configurable, and repositories can be opened directly from the terminal using commands like &lt;code&gt;gitte ~/Code/projects/Gitte&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This release also adds a unified stash dialog for workflows that require stashing changes, ahead/behind indicators for the current branch, double-click checkout for local branches, and improved merge commit information in the log viewer. There are also a few small easter eggs hidden throughout the app.&lt;/p&gt;
&lt;p&gt;On the translation side, Gitte now includes a German translation and a Ukrainian translation by Dymko. The release also includes AUR packaging documentation contributed by Kainoa Kanter, alongside many bug fixes and smaller refinements across the application.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://flathub.org/apps/de.wwwtech.gitte"&gt;Get it on Flathub&lt;/a&gt; or &lt;a href="https://codeberg.org/ckruse/Gitte"&gt;check the source code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img height="643" src="https://thisweek.gnome.org/_astro/gitte-rebase-context-menue.CeEgkuIB_INEem.webp" width="597"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="333" src="https://thisweek.gnome.org/_astro/gitte-ahead-behind-indicators.Dof9Jqsu_Z208gNW.webp" width="589"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="517" src="https://thisweek.gnome.org/_astro/gitte-settings-dialog.D33JDAoH_lmTul.webp" width="660"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/1cbcc511e6ca919ad647ad976ae4438db064eda941fbb2882a21d6e13044d34a"&gt;Bilal Elmoussaoui&lt;/a&gt; reports&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I have released the first version of &lt;a href="https://github.com/bilelmoussaoui/gobject-linter"&gt;&lt;strong&gt;gobject-linter&lt;/strong&gt;&lt;/a&gt;, previously known as goblint.&lt;/p&gt;
&lt;p&gt;This release brings a lot of new functionality: Meson integration for accurate dead code detection (functions, enum variants, structs, struct fields and more) via the new &lt;a href="https://bilelmoussaoui.github.io/gobject-linter/#dead_code"&gt;dead_code&lt;/a&gt; rule, &lt;a href="https://bilelmoussaoui.github.io/gobject-linter/#missing_export_macro"&gt;mis-exported public types&lt;/a&gt; detection, &lt;a href="https://bilelmoussaoui.github.io/gobject-linter/#inconsistent_function_signature"&gt;inconsistent function signatures&lt;/a&gt; checking, and a &lt;a href="https://bilelmoussaoui.github.io/gobject-linter/#type_style"&gt;type_style&lt;/a&gt; rule to enforce consistent use of either GLib type aliases (&lt;code&gt;gint&lt;/code&gt;, &lt;code&gt;gfloat&lt;/code&gt;, &lt;code&gt;gdouble&lt;/code&gt;) or their C equivalents across your codebase. Two new GObject introspection rules for verifying &lt;a href="https://bilelmoussaoui.github.io/gobject-linter/#gi_missing_since"&gt;missing since annotations&lt;/a&gt; and the exported public APIs are &lt;a href="https://bilelmoussaoui.github.io/gobject-linter/#gi_not_bindings_friendly"&gt;bindings friendly&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It also supports diff-scoped linting via &lt;code&gt;--diff -&lt;/code&gt; so you can incrementally integrate it into large existing projects.&lt;/p&gt;
&lt;p&gt;The release is also available on &lt;a href="https://crates.io/crates/gobject-linter"&gt;crates.io&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/d19b4c2d14173ebfc88e2430c86d5e2afeaa578cc1b4d1cd51385dbca41a6779"&gt;Jeffry Samuel&lt;/a&gt; announces&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Nocturne 1.0.0 has been released!&lt;/p&gt;
&lt;p&gt;Nocturne is a modern music player that can play songs from your OpenSubsonic, Jellyfin and local libraries.&lt;/p&gt;
&lt;p&gt;It includes features such as audio visualizers, equalizers and automatic lyric fetching.&lt;/p&gt;
&lt;p&gt;Some of the new features in 1.0.0 are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for changing max bitrate&lt;/li&gt;
&lt;li&gt;Support for replay gain&lt;/li&gt;
&lt;li&gt;Added option to show sidebar player&lt;/li&gt;
&lt;li&gt;Compatibility with word for word lyrics&lt;/li&gt;
&lt;li&gt;Faster and more stable interface&lt;/li&gt;
&lt;li&gt;Gapless playback&lt;/li&gt;
&lt;li&gt;Grouping of songs in albums by their disc&lt;/li&gt;
&lt;li&gt;Added option to show dynamic background in the main window&lt;/li&gt;
&lt;li&gt;Much more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img height="750" src="https://thisweek.gnome.org/_astro/Screenshot%20From%202026-05-03%2017-55-03.DuaOMq3L_ZQhrPB.webp" width="1050"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="750" src="https://thisweek.gnome.org/_astro/Screenshot%20From%202026-05-03%2017-55-36.DKHocJ-0_1zpR4e.webp" width="1050"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="750" src="https://thisweek.gnome.org/_astro/Screenshot%20From%202026-05-03%2017-56-08.CgakUPo5_Zmxgsw.webp" width="1050"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="750" src="https://thisweek.gnome.org/_astro/Screenshot%20From%202026-05-03%2020-13-48.qBfWIVD3_1FmdWO.webp" width="1050"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/a098fd3e8139d218bb646d45afab4a26f811c15c816f6e481efb84957a07dda9"&gt;mas&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hi, finally released my first app, Press!
With has a very straight-forward interface to compress huge music libraries with ease.&lt;/p&gt;
&lt;p&gt;You might like it because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compresses multiple files simultaneously&lt;/li&gt;
&lt;li&gt;Never takes destructive actions &lt;em&gt;on the source&lt;/em&gt; (but it can replace files on the destination if you want)&lt;/li&gt;
&lt;li&gt;Avoids re-compressing a file (if you just want to add a new album, it compresses just that one, not your entire library)&lt;/li&gt;
&lt;li&gt;Import basically any format GStreamer can take!&lt;/li&gt;
&lt;li&gt;Export to mp3, m4a, or ogg&lt;/li&gt;
&lt;li&gt;Move other non-auto files with you&lt;/li&gt;
&lt;li&gt;You can add custom formats with a bit of GStreamer know-how&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It really is a one-stop solution to compress music to portable devices.&lt;/p&gt;
&lt;p&gt;I’d love to hear feedback and &lt;a href="https://github.com/masakk1/press/issues/new?template=2-feature-request"&gt;suggestions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://flathub.org/en/apps/io.github.masakk1.press"&gt;Get it on Flathub&lt;/a&gt; or &lt;a href="https://github.com/masakk1/press"&gt;check the source code&lt;/a&gt;.
Oh and, it uses libadwaita, vala, and GStreamer.&lt;/p&gt;
&lt;p&gt;&lt;img height="541" src="https://thisweek.gnome.org/_astro/light_desktop_main.j3IjehC1_Oa80p.webp" width="659"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="650" src="https://thisweek.gnome.org/_astro/compress_dark_desktop_loading.DeHPSxUa_ZgcGzB.webp" width="850"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/579a4f3527aa9f46798ea4476441fef008f4a13ffad33b4822bae5de56028aa7"&gt;JumpLink&lt;/a&gt; announces&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The type-definitions generator &lt;a href="https://github.com/gjsify/ts-for-gir"&gt;&lt;strong&gt;ts-for-gir&lt;/strong&gt;&lt;/a&gt; produces the typings used to write GNOME applications in TypeScript. It can now experimentally run directly on &lt;a href="https://gjs.guide/"&gt;&lt;strong&gt;GJS&lt;/strong&gt;&lt;/a&gt;, without Node.js.&lt;/p&gt;
&lt;p&gt;This is made possible by the new experimental &lt;a href="https://gjsify.github.io/gjsify/"&gt;&lt;strong&gt;GJSify&lt;/strong&gt;&lt;/a&gt; framework, which provides Node.js and Web APIs on top of GJS. Its long-term goal is to make as much of the JavaScript / TypeScript ecosystem as possible available to GJS applications.&lt;/p&gt;
&lt;p&gt;&lt;img height="639" src="https://thisweek.gnome.org/_astro/ts-for-gir-with-gir.C_gZeB3p_Z14s636.webp" width="834"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/2646911ef138c507e0732b13ca083b9bc9a0bbd64536c27186c4c14b1fa59171"&gt;bhack&lt;/a&gt; announces&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’d like to introduce Mini EQ, a new small GTK/Libadwaita app for PipeWire desktops.&lt;/p&gt;
&lt;p&gt;Mini EQ is a system-wide parametric equalizer. It creates a PipeWire filter-chain sink with builtin biquad filters, routes desktop playback through it with WirePlumber, and provides a compact 10-band fader workflow. It also supports Equalizer APO/AutoEq preset import and an optional spectrum analyzer through the PipeWire JACK compatibility layer.&lt;/p&gt;
&lt;p&gt;The project is now available on Flathub, with source and packaging published on GitHub.&lt;/p&gt;
&lt;p&gt;Flathub: &lt;a href="https://flathub.org/apps/io.github.bhack.mini-eq"&gt;https://flathub.org/apps/io.github.bhack.mini-eq&lt;/a&gt;
GNOME Shell extension: &lt;a href="https://extensions.gnome.org/extension/9803/mini-eq-controls/"&gt;https://extensions.gnome.org/extension/9803/mini-eq-controls/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source: &lt;a href="https://github.com/bhack/mini-eq"&gt;https://github.com/bhack/mini-eq&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img height="529" src="https://thisweek.gnome.org/_astro/mini-eq.Cl3IXz68_Z1g1kaS.webp" width="1000"/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/729728ceeb003695c86ccf7fed21c023bd29c2bbae9cb8eb7c63fceeee0e9d9f"&gt;Anton Isaiev&lt;/a&gt; announces&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RustConn is a GTK4/libadwaita connection manager for SSH, RDP, VNC, SPICE, Telnet, MOSH, and more.&lt;/p&gt;
&lt;p&gt;Versions 0.12.8–0.13.7 were shaped heavily by user feedback. What started as a personal tool is now used daily by sysadmins and DevOps teams — and their reports drive the roadmap.&lt;/p&gt;
&lt;p&gt;Key additions:&lt;/p&gt;
&lt;p&gt;Local Shell in Flatpak — fully working host shell via flatpak-spawn with real PTY and job control.
RDP dynamic resize — in-place resolution change via Display Control Channel, no reconnect needed; automatic fallback for legacy servers.
RDP Autotype — type text as keystrokes into remote sessions, bypassing clipboard restrictions.
Drag &amp;amp; Drop — file paths into terminals, files to RDP clipboard.
Smart Folders &amp;amp; Dynamic Folders — filter connections by tag/protocol/pattern, or generate them from external scripts.
Virt-viewer .vv file support — open SPICE/VNC files from Proxmox, oVirt, libvirt directly.
CLI —format json|csv|table — machine-readable output for scripting and AI agents.
GNOME HIG audit — restructured menus, unified dialogs, accessible labels across all windows.
Flatpak CLI auto-versioning — 7 bundled CLI tools now resolve latest versions from upstream automatically.&lt;/p&gt;
&lt;p&gt;Homepage: &lt;a href="https://github.com/totoshko88/RustConn"&gt;https://github.com/totoshko88/RustConn&lt;/a&gt;
Flathub: &lt;a href="https://flathub.org/en/apps/io.github.totoshko88.RustConn"&gt;https://flathub.org/en/apps/io.github.totoshko88.RustConn&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="shell-extensions"&gt;Shell Extensions&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://thisweek.gnome.org/reporters/59860166c9562aea48a5202aa2bf04ea1ee083bff95badfdcac683285ce27b1e"&gt;Miklós Zsitva&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Matrix Status Monitor v7 improves room handling, notifications, and profile actions in GNOME Shell.&lt;/p&gt;
&lt;p&gt;Matrix Status Monitor v7 is now available on GNOME Extensions, bringing a noticeably smoother experience for Matrix users running GNOME Shell. This release focuses on making the extension feel more responsive and more native to the desktop, while keeping the panel UI lightweight and fast.&lt;/p&gt;
&lt;p&gt;The biggest change is the new weight-based room sorting system, which replaces the old timestamp-only approach. Rooms are now ranked by highlights, unread counts, direct messages, favourites, visit frequency, and recency, so the most relevant conversations surface first.&lt;/p&gt;
&lt;p&gt;v7 also adds a clear idle/active separator in the room list, plus async menu rebuilds via GLib.idle_add to avoid blocking the UI during updates. On top of that, the extension now sends GNOME desktop notifications through MessageTray, with event ID deduplication so the same message does not trigger repeated alerts.&lt;/p&gt;
&lt;p&gt;The profile header has been expanded as well: it now shows the user avatar, display name, user ID, plus one-click copy and QR toggle actions. The avatar loading path was also extended to handle a larger profile icon size, which helps the header feel more polished and distinct from room rows.&lt;/p&gt;
&lt;p&gt;Overall, v7 is a refinement release that makes the extension feel more reliable, more readable, and more useful in daily GNOME use.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://extensions.gnome.org/extension/9328/matrix-status-monitor/"&gt;https://extensions.gnome.org/extension/9328/matrix-status-monitor/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="thats-all-for-this-week"&gt;That’s all for this week!&lt;/h2&gt;
&lt;p&gt;See you next week, and be sure to stop by &lt;a href="https://matrix.to/#/#thisweek:gnome.org"&gt;#thisweek:gnome.org&lt;/a&gt; with updates on your own projects!&lt;/p&gt;</description>
      <guid isPermaLink="false">https://thisweek.gnome.org/posts/2026/05/twig-248/</guid>
      <pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Jakub Steiner: USS/FMS Carrier</title>
      <link>https://blog.jimmac.eu/posts/fms-carrier/</link>
      <description>&lt;p&gt;I'm a sucker for pixel art and very constrained music grooveboxes. While I'm not into chiptunes, they sure are a cultural phenomenon.&lt;/p&gt;
&lt;p&gt;You heard me boast about the &lt;a href="https://dirtywave.com/"&gt;Dirtywave M8&lt;/a&gt; numerous times, even in person, because it's my tool of choice for producing and performing music. Its genius lies in high sound quality and a workflow that grew out of the tiny screen and button constraints on the Nintendo Gameboy, the platform of choice for an app called &lt;a href="https://www.littlesounddj.com/"&gt;LSDJ&lt;/a&gt;, which the M8 is modelled after. That, and the sheer amount of sound engines living in your pocket. Building on the shoulders of giants and all.&lt;/p&gt;
&lt;p&gt;The small M8 community has a few 'celebrities', such as &lt;a href="https://mtsn.se/"&gt;Ess Mattisson&lt;/a&gt;. I first heard of Ess when I ran into an amazing &lt;em&gt;single channel&lt;/em&gt; track called &lt;a href="https://www.youtube.com/watch?v=d6bJVcmFaNk"&gt;Wertstoffe&lt;/a&gt;. Ess has a great pedigree as the creator of the original &lt;a href="https://www.elektron.se/wp-content/uploads/2024/09/Digitone_User_Manual_ENG_OS1.41_231108.pdf"&gt;Digitone&lt;/a&gt; FM synthesizer while working at &lt;a href="https://www.elektron.se/"&gt;Elektron&lt;/a&gt;. FM remains his forte, and after creating numerous plugins through &lt;a href="https://fors.fm/"&gt;Fors&lt;/a&gt;, he has now &lt;a href="https://lo-bit.club/fms"&gt;released&lt;/a&gt; a little 2-operator FM synth and sequencer for the platform of the future, Nintendo Gameboy Advance.&lt;/p&gt;
&lt;div class="image-grid pixelated" id="crt-container"&gt;
&lt;img alt="Lo-bit Club logo animation" src="https://blog.jimmac.eu/posts/fms-carrier/Lo-bit.gif"/&gt;
&lt;img alt="FMS synth running on Gameboy Advance" src="https://blog.jimmac.eu/posts/fms-carrier/Fms.png"/&gt;
&lt;/div&gt;
&lt;p&gt;What makes FMS a bit crazy is what it's doing under the hood. The Gameboy Advance has no FM synthesis hardware at all. Its audio gives you two Direct Sound DMA channels of 8-bit signed PCM — that's 256 amplitude levels, roughly 48 dB of dynamic range. For comparison, a CD has 96 dB, in much finer fidelity. The CPU is an ARM7TDMI running at 16.78 MHz with 256 KB of RAM, and that's where &lt;em&gt;all&lt;/em&gt; the FM math happens. Sine waves, modulation, mixing four channels, all in real time, in software, on a chip from 2001 that was designed to shuffle sprites around. The hiss you hear is just part of the deal: quantization noise from that 8-bit DAC. So few amplitude steps means everything that comes out has this fuzzy, slightly crushed quality. You can't get rid of it. It &lt;em&gt;is&lt;/em&gt; the sound. And somehow there are four channels of 2-operator FM synthesis in there, each with envelopes and ratio control. On a Gameboy Advance.&lt;/p&gt;
&lt;p&gt;Picking GBA as a platform of choice in 2026 may be strange. Surprisingly, it can be used on a very large array of hardware. Not only can you plug a memory card into the original hardware or new fancy clones like the &lt;a href="https://www.analogue.co/pocket"&gt;Analogue Pocket&lt;/a&gt;, you have an exponentially larger choice of dozens if not hundreds of Chinese emulator handhelds from &lt;a href="https://anbernic.com/"&gt;Anbernic&lt;/a&gt;, &lt;a href="https://powkiddy.com/"&gt;Powkiddy&lt;/a&gt;, &lt;a href="https://lomiyoo.com/"&gt;Miyoo&lt;/a&gt; or &lt;a href="https://www.goretroid.com/"&gt;Retroid&lt;/a&gt;. You can also use the &lt;a href="https://store.steampowered.com/steamdeck"&gt;Steam Deck&lt;/a&gt; or any PC running one of the many emulators, &lt;a href="https://www.retroarch.com/"&gt;RetroArch&lt;/a&gt; being the most popular one.&lt;/p&gt;
&lt;p&gt;FMS really touched me. Partly because I have a soft spot for the Nordic demo scene, but mainly for its novel approach to composition. Just like with the M8, creating basic building blocks and then applying transposition to break the looping monotony is my favorite workflow. This little thing has that in the form of pattern and trig transposition but also a novel take on "effects". Yes, you heard me right. There's a sorta-kinda-delay. Even does stereo field ping-pong.&lt;/p&gt;
&lt;p&gt;I will keep on trying to create something that … sounds good. The process has been amazing. I truly love some of the sequencing tricks and workflows. The sequencer is, however, so good it would be worth seeing it run on top of a higher quality sound engine too.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blog.jimmac.eu/posts/fms-carrier/</guid>
      <pubDate>Sat, 09 May 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Laura Kramolis: Computers Are Terrible</title>
      <link>https://kramo.page/computers-are-terrible</link>
      <description>&lt;p&gt;A slightly more collected version of originally 18 Signal messages. This is a simplification. I am evidently no expert in Unicode specifically or text encoding in general.&lt;/p&gt;
&lt;p&gt;I, for a long time, believed that while many modern standards are a mess of legacy compatibility built on legacy compatibility, Unicode was an exception. That the only compromise it made was ASCII-compatibility, but even that wasn’t such a big one given that its character set is the most common one in computing even to this day. I was wrong.&lt;/p&gt;
&lt;p&gt;I got a US keyboard so now I have 2 different ways of typing accented characters. I can either hold the &lt;kbd&gt;A&lt;/kbd&gt; key until I get an option of &lt;code&gt;à&lt;/code&gt;, &lt;code&gt;á&lt;/code&gt;, &lt;code&gt;â&lt;/code&gt;, &lt;code&gt;ä&lt;/code&gt;, &lt;code&gt;ǎ&lt;/code&gt;, etc., or I can press &lt;kbd&gt;⌥&lt;/kbd&gt; &lt;kbd&gt;E&lt;/kbd&gt; and then &lt;kbd&gt;A&lt;/kbd&gt; to get to &lt;code&gt;á&lt;/code&gt;, combining &lt;code&gt;´&lt;/code&gt; and a regular &lt;code&gt;a&lt;/code&gt;. I started wondering… when typing it one way or the other, the results must be different, right? I looked for a website that showed me what code points I was typing, and… they were the same?&lt;/p&gt;
&lt;p&gt;Most systems (the OS/browser in this case) normalize all text either one way or the other. In this case, to a single code point. Unicode does have deprecation, so you would think that when they introduced combining characters, they would have deprecated the precomposed versions of characters that can be written using them, right? Nope!&lt;/p&gt;
&lt;p&gt;It’s arbitrary which way each system normalizes text. Some do it &lt;em&gt;composed&lt;/em&gt; (&lt;code&gt;á&lt;/code&gt;) and some &lt;em&gt;decomposed&lt;/em&gt; (&lt;code&gt;a&lt;/code&gt; + &lt;code&gt;◌́&lt;/code&gt;). Both are part of the standard. And of course, you need to treat them as equivalent when not normalized so you might as well do it when you can anyway.&lt;/p&gt;
&lt;figure&gt;
&lt;blockquote&gt;
&lt;p&gt;
      Precomposed characters are the legacy solution for representing many special letters in various character sets. In Unicode, they were included for compatibility with early encoding systems […].
    &lt;/p&gt;
&lt;/blockquote&gt;
&lt;figcaption&gt;
    From
    &lt;cite&gt;&lt;a href="https://en.wikipedia.org/wiki/Precomposed_character"&gt;Precomposed character - Wikipedia&lt;/a&gt;&lt;/cite&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Oh well, my day is ruined. My new life goal is advocacy for the deprecation of all precomposed characters… or maybe I should just accept that all computing will be plagued by backwards compatibility headaches ’til the end of time.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://kramo.page/computers-are-terrible</guid>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Michael Catanzaro: Flatpak Sandbox Escape via Yelp</title>
      <link>https://blogs.gnome.org/mcatanzaro/2026/05/11/flatpak-sandbox-escape-via-yelp/</link>
      <description>&lt;p&gt;Yelp 49.1 fixes a significant &lt;a class="external" href="https://gitlab.gnome.org/GNOME/yelp/-/work_items/238"&gt;Flatpak sandbox escape&lt;/a&gt; related to &lt;a href="https://blogs.gnome.org/mcatanzaro/2025/04/15/dangerous-arbitrary-file-read-vulnerability-in-yelp-cve-2025-3155/"&gt;last year’s CVE-2025-3155&lt;/a&gt;. CVE assignment for this new issue is currently pending.&lt;/p&gt;
&lt;p&gt;This is not a bug in Flatpak. Flatpak allows sandboxed applications to open URIs or files, meaning the sandboxed application may use a URI or file path to launch another application to open the URI or file. This is brokered via the OpenURI portal. The portal or the app may decide to require user interaction to decide which app to launch, but user interaction is generally not required. This is necessary: you would get pretty frustrated if you were prompted to select which app to use every time you click on a link or try to open something! Accordingly, unsandboxed applications that are installed on the host system are somewhat risky: any malicious sandboxed app may launch an unsandboxed app using a malicious file, generally with no user interaction required. Unsandboxed applications installed on the host OS are inherently part of the attack surface of the Flatpak sandbox.&lt;/p&gt;
&lt;p&gt;In this case, a sandboxed application may launch Yelp to open a malicious help file. The help file can then exfiltrate arbitrary files from your host OS to a web server by using a CSS stylesheet embedded in an SVG. Suffice to say the attack is pretty clever, and certainly more impactful than the typical boring memory safety bugs I more commonly see.&lt;/p&gt;
&lt;p&gt;This bug was discovered by &lt;a class="external" href="https://codeanlabs.com"&gt;Codean Labs&lt;/a&gt;, which performed a security audit of Flatpak and several GNOME projects thanks to generous sponsorship by the &lt;a class="external" href="https://www.sovereign.tech/programs/bug-resilience"&gt;Sovereign Tech Resilience&lt;/a&gt; program of Germany’s Sovereign Tech Agency.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/mcatanzaro/2026/05/11/flatpak-sandbox-escape-via-yelp/</guid>
      <pubDate>Mon, 11 May 2026 14:12:53 +0000</pubDate>
    </item>
    <item>
      <title>Nick Richards: Agile Rates After Launch</title>
      <link>https://www.nedrichards.com/2026/05/agile-rates-after-launch/</link>
      <description>&lt;p&gt;Last summer I wrote up &lt;a href="https://www.nedrichards.com/2025/07/octopus-agile-prices-for-linux/"&gt;Octopus Agile Prices For Linux&lt;/a&gt;, a small GTK app to show the current &lt;a href="https://octopus.energy/smart/agile/"&gt;Octopus Agile&lt;/a&gt; electricity price and the next day of half-hourly rates. It did one thing, which is a good number of things for a desktop utility to do.&lt;/p&gt;
&lt;p&gt;Since then the app has become a bit less narrow. But it now does enough more that the launch post undersells it, and in a couple of places sends people looking for the wrong name.&lt;/p&gt;
&lt;p&gt;The app is now called &lt;a href="https://flathub.org/apps/com.nedrichards.octopusagile"&gt;Agile Rates&lt;/a&gt;. The application ID is still &lt;code&gt;com.nedrichards.octopusagile&lt;/code&gt;, because changing stable app IDs is not exciting for anyone, but the name changed because Agile is no longer the whole story. Thanks to code from Andy Piper, it can also work with Octopus Go and Intelligent Go tariffs. Intelligent Go needs an API key because those prices are account-specific, but plain Agile and Go can still be set up manually.&lt;/p&gt;
&lt;p&gt;That was the first larger change: setup had to become a thing.&lt;/p&gt;
&lt;p&gt;The original app assumed you knew your tariff and region, or at least were willing to rummage in preferences until the graph stopped being wrong. That is fine for a scratch-your-own-itch project and a bit rude for an app on Flathub. The current version opens with a setup assistant. You can connect an Octopus account with an API key and account number, in which case the app tries to detect the active electricity tariff. Or you can keep it simple and choose the tariff and region manually.&lt;/p&gt;
&lt;p&gt;The second change is the one I actually use most: finding the cheapest slot.&lt;/p&gt;
&lt;p&gt;The launch version showed a graph and left the planning to the human. That works for quick glances, but most of my real questions are more specific:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-text"&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;When should the dishwasher run?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;When should the washing machine run?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display: flex;"&gt;&lt;span&gt;Is there a cheap three-hour block before tomorrow afternoon?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So there is now a “find cheapest time” tool. Pick a duration and it searches the available forecast window for the cheapest continuous block. The chart now scrolls to the chosen time instead of making you squint along the bars like you are reading a very dull railway timetable.&lt;/p&gt;
&lt;p&gt;The graph itself has had a lot of tweaks. It has grid lines, clearer day boundaries, better current-price highlighting, less terrible dark-mode contrast, and layout rules that behave on narrower screens. The preferences window and main window are adaptive now too. Handy if you split your screen or have a Linux phone.&lt;/p&gt;
&lt;p&gt;The biggest recent addition is usage history. If you connect an account, the app can fetch recent smart meter consumption data, cache it locally, and show a Usage view. That includes kWh history, a seven-day trend, an estimated monthly usage figure, and charts. It also tries to estimate spend by matching historical usage to tariff rates and standing charges.&lt;/p&gt;
&lt;p&gt;Underneath that, the project has become more like a real small application. There are unit tests for pricing, tariff selection, adaptive layout, usage insights, and historical cost calculation. The development Flatpak manifest runs the Meson tests inside the GNOME SDK, which catches the class of bugs where the host Python environment was accidentally being too kind. Ruff is in the loop for linting. The app moved to the GNOME 50 runtime. Screenshots, AppStream metadata, branding colours, and icons have all been tidied up.&lt;/p&gt;
&lt;p&gt;So the current description is: Agile Rates is a small GNOME app for UK Octopus Energy customers who want current and upcoming smart tariff rates, a cheap-time finder, and, if they connect their account, recent usage and estimated spend history. It is independent and is not affiliated with, endorsed by, or sponsored by Octopus Energy. I hope you find it useful.&lt;/p&gt;</description>
      <guid isPermaLink="false">https://www.nedrichards.com/2026/05/agile-rates-after-launch/</guid>
      <pubDate>Mon, 11 May 2026 19:53:09 +0000</pubDate>
    </item>
    <item>
      <title>Toluwaleke Ogundipe: Hello GNOME and GSoC, Again!</title>
      <link>https://blogs.gnome.org/anonymoux47/2026/05/13/hello-gnome-and-gsoc-again/</link>
      <description>&lt;p&gt;I am delighted to announce that I am returning for Google Summer of Code 2026 to contribute to GNOME once again. Following &lt;a href="https://blogs.gnome.org/anonymoux47/2025/10/22/gsoc-final-report-printing-in-gnome-crosswords"&gt;my work on Crosswords&lt;/a&gt; last year, I will be shifting focus to the core of the desktop: &lt;a class="external" href="https://gitlab.gnome.org/GNOME/mutter"&gt;Mutter&lt;/a&gt;. For what it’s worth, I never left; I’ve been working with Jonathan to improve things and add &lt;a class="external" href="https://gitlab.gnome.org/jrb/crosswords/-/work_items/380"&gt;shiny new features&lt;/a&gt; in Crosswords.&lt;/p&gt;
&lt;p&gt;Mutter serves as the Wayland display server and compositor library for &lt;a class="external" href="https://gitlab.gnome.org/GNOME/gnome-shell"&gt;GNOME Shell&lt;/a&gt;. Currently, a GPU reset invalidates the EGL context and causes the loss of all allocated GPU memory, resulting in the entire desktop crashing or freezing.&lt;/p&gt;
&lt;p&gt;My project aims to implement a robust recovery mechanism for GPU resets to prevent these session-ending freezes, under the mentorship of &lt;a class="external" href="https://gitlab.gnome.org/jadahl"&gt;Jonas Ådahl&lt;/a&gt;, &lt;a class="external" href="https://gitlab.gnome.org/rmader"&gt;Robert Mader&lt;/a&gt;, and &lt;a class="external" href="https://gitlab.gnome.org/carlosg"&gt;Carlos Garnacho&lt;/a&gt;. Leveraging the &lt;a class="external" href="https://registry.khronos.org/OpenGL/extensions/EXT/EXT_robustness.txt"&gt;&lt;code&gt;GL_EXT_robustness&lt;/code&gt;&lt;/a&gt; extension, I will implement reset detection, context re-creation, and re-upload of essential GPU resources, such as client textures, glyph caches, and background images. This will allow the compositor to resume rendering seamlessly after hardware-level failures.&lt;/p&gt;
&lt;p&gt;Over the course of the project, I will share updates on the progress of these recovery mechanisms and the challenges of managing state restoration within the compositor.&lt;/p&gt;
&lt;p&gt;I am very grateful to my mentors, Jonas, Robert, and Carlos, for the opportunity to work on this critical part of the GNOME ecosystem. Also, a big shout-out to &lt;a class="external" href="https://gitlab.gnome.org/federico"&gt;Federico&lt;/a&gt;, &lt;a class="external" href="https://gitlab.gnome.org/hansp"&gt;Hans Petter&lt;/a&gt;, and &lt;a class="external" href="https://gitlab.gnome.org/jrb"&gt;Jonathan&lt;/a&gt; for their continuous support. I look forward to another productive summer with the community. &lt;img alt="🦾" class="wp-smiley" src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9be.png" style="height: 1em;"/&gt;&lt;img alt="❤" class="wp-smiley" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2764.png" style="height: 1em;"/&gt;&lt;/p&gt;</description>
      <guid isPermaLink="false">https://blogs.gnome.org/anonymoux47/2026/05/13/hello-gnome-and-gsoc-again/</guid>
      <pubDate>Wed, 13 May 2026 18:17:42 +0000</pubDate>
    </item>
  </channel>
</rss>
