26 May 2026
Planet Grep
Dries Buytaert: Why Drupal CMS matters
Last week at Drupal South, Pamela Barone delivered a keynote on Drupal CMS. Her talk is one of the clearest articulations I've seen of what Drupal CMS is, why it exists, and where it's headed. That shouldn't come as a surprise because Pam is the Product Lead for Drupal CMS.
Pam quoted a familiar Drupal saying: Drupal makes hard things possible, but it also makes easy things hard.
. The room laughed because it's true.
Her keynote was about how Drupal CMS is helping to fix that. Drupal CMS is making Drupal easier to learn, easier to use, and easier to sell, without removing any of Drupal's power and flexibility. It brings visual page editing, a smoother path for new developers, and better project economics.
And these improvements are not just interesting for smaller projects. Universities, governments, and large enterprises want the same benefits. That is why Drupal CMS matters at every scale.
Pam also explains how Drupal CMS sits on top of Drupal Core, why it is not a Drupal distribution, how it gives digital agencies leverage, what site templates unlock, and how Drupal Canvas reshapes the page building experience.
If you watch one Drupal video this week, make it Pam's!
26 May 2026 5:09pm GMT
Dries Buytaert: The gap between Drupal and its reputation

I saw two thoughtful posts in my LinkedIn feed over the last week that I wanted to reshare here before the LinkedIn feed buried them. Both were spot on, honest, and deserve a longer shelf life.
The first was from Hynek Naceradsky:
I'm pissed.
Not at Drupal. At the people confidently hating on it without ever having understood what it actually does.
"Drupal is outdated." "Drupal is too complex." "Nobody uses Drupal anymore."
Tell that to the EU institutions, governments, universities, and enterprises quietly running mission-critical platforms on it.
Here is what actually gets me though: the Drupal community lets this narrative win.
I am guilty of this too.
We literally have thousands of contributed modules, maintained for free, by people who owe you absolutely nothing. The security team responds faster than most paid vendors. The community has been showing up for 20+ years.
And yet we're somehow losing the PR war to frameworks that can't handle a proper content workflow without three paid plugins and a prayer.
Drupal people: talk louder. Write the posts. Go to the meetups. Tell the stories, fight for Drupal.
Because the Drupal community is honestly the best thing in Open Source, and both it and Drupal deserve way better than silence.
The second was from Thomas Scola, writing from a Drupal AI event in New York (lightly trimmed):
I overheard a couple people say, "Drupal? Is that still around?"
Hell yes it is.
And not only is it still around, I'd argue pretty heavily that Drupal is uniquely positioned for what comes next with the agentic web.
API-first before API-first was cool and trendy. Structured content that actually makes sense. Mature permissions, workflows, governance, integrations.
A lot of platforms are now scrambling to figure out how AI fits into what they already built.
Drupal doesn't have to force it. The architecture has been there.
But honestly, the tech is only part of it. The community is what always gets me. The people, passion and innovation. [...]
What comes next? Who knows.
But if I'm betting on a community to adapt, build, and help define that future, I'm putting my money on this one, and on what we've all built together.
For a platform people love to ask if it's "still around", it feels more relevant than ever.
I could not agree more with both posts. Drupal is one of the strongest Open Source platforms out there right now, but too few people realize it. The Drupal community has been modernizing the platform faster than its reputation evolves.
If the loudest narrative about Drupal is that it is outdated, people will keep repeating it, even when it is wrong. AI systems will too, because they absorb the same narratives, blog posts, forum threads, and social media the rest of the industry does.
The danger is not just that Drupal is misunderstood today. It's that the gap between perception and reality may be growing, not shrinking.
The narratives we reinforce today become part of how AI describes Drupal tomorrow. The Drupal community's silence today becomes tomorrow's AI consensus.
So if you're in the Drupal community, take Hynek's advice and help set the record straight. Not for AI, but for people. Write about the great work happening in Drupal: share the case studies, the technical breakthroughs, the AI innovation, the shared learnings, and the hard problems being solved every day.
We need to spend a lot more time explaining where Drupal fits, the kinds of problems it solves well, and why so many organizations believe in Open Source and the Drupal community.
I know many people in Open Source dislike marketing or self-promotion. I do too, sometimes. But if we don't document what is great about Drupal, others will define Drupal for us.
Every accurate case study, technical blog post, demo, presentation, or community success story helps future developers, evaluators, and AI systems understand what Drupal actually is.
Drupal does not need hype. It needs a better public record.
26 May 2026 5:09pm GMT
Dries Buytaert: Grow the ecosystem, not just yourself

In Open Source software, competition works differently than in proprietary software.
Companies compete through their own products and services, but they all depend on the same commons: the software, the community, the project's reputation, and the shared work that helps people trust and adopt it.
That shared foundation creates a different kind of responsibility: sharing a commons means sharing the work of keeping it strong.
The Open Source companies I admire most show up in two ways. They compete on the merits of their own products: features, support, and price. And they help sustain the commons: through code, documentation, security, marketing, events, education, sponsorships, and more.
Judge companies by what they do
Over the past year, Pantheon, one of Acquia's competitors in the Drupal market, has focused much of its messaging on attacking Acquia, including making our private equity ownership part of its story.
I have no quarrel with Pantheon's products or the people who build them. Competition is healthy. My concern is with marketing that attacks another Drupal company, often with misleading or unwarranted messaging.
I've spent nearly twenty years building Acquia through different stages and ownership models. Acquia has grown from a startup into a company backed first by venture capital and later by private equity. Every ownership model creates different pressures, but ownership determines far from everything.
Customers don't choose a platform because of an ownership model. They choose it because it works, because they can get help, and because they trust the platform will keep getting better. In Open Source, that trust depends on the health of the commons behind it.
Customers, partners, community members, and end users are not helped by vendor attacks. They are helped when companies build better products, contribute to Drupal, and help more people adopt it.
License permits, stewardship grows
For an Open Source company, the test is not only what they build for themselves. It is what they help build for everyone.
An Open Source license defines what companies are allowed to do. It sets the floor. Contribution is not required.
Above that floor is a social contract. No one enforces it, but every healthy Open Source ecosystem depends on it.
Stewardship is what companies choose to do beyond the license: contribute code, fund security work, support maintainers, improve documentation, sponsor events, promote adoption, and more.
Drupal thrives because people and organizations honor the social contract and choose to do more than the license requires.
Contribution is one measure of stewardship
Drupal.org credit is one public signal of that commitment. Acquia is the largest single corporate contributor to Drupal, but the wider community contributes far more than any one company.
In the past year, Acquia engineers earned 2,955 weighted credits on Drupal issues, plus 164 from the Drupal Security Team.
These contributions are good for Acquia, for Drupal, and for every organization that builds on Drupal, including our competitors.
In the same period, Pantheon earned 30 issue credits and 2 security credits. Credits don't capture every form of contribution, and Pantheon contributes in other ways too. Even so, the gap is substantial.
What we let pass becomes the social contract
I don't usually write publicly about competitors. It's not how I want to spend my voice.
Before writing this, I asked myself a simple question: if a major company contributing to Drupal were under sustained attack from another major Drupal company, would I feel a responsibility as Drupal's founder and project lead to speak up?
I would.
The fact that Acquia is the company being attacked made me slower to respond, but it doesn't change the answer.
When companies built on Drupal spend their energy attacking each other instead of growing the project, it bothers me. It's not good for Drupal.
I'm not writing this believing it will change anyone's marketing and sales tactics. I'm writing it because what we let pass now will shape what is acceptable in Drupal years from now.
Communities like ours evolve their social contract through moments like this, when we say in public what we expect of each other. If this post contributes to a healthier social contract taking hold, I'm happy.
Compete on merit, but grow the commons
Every company that builds on Drupal depends on the same commons. Every company has a choice about whether to help sustain it, and how much. Drupal gets stronger when more of us invest in it.
My invitation to every company that builds on Drupal is simple: let's compete on the merits of our products and services, not by attacking each other. Let's serve customers well, contribute where we can, and put our energy into helping more organizations choose Drupal in the first place.
That is the social contract I'd like all of us to live by. I want Acquia to be judged by that same standard: what we ship, how well we serve customers, how much we contribute, and whether Drupal is stronger because of our work.
Not by who owns us. Not by claims made about us. By whether we keep building, contributing, and helping the ecosystem grow.
I have said what I wanted to say, and I won't turn this into an ongoing debate or respond to social media comments on this. My focus is on building and contributing.
26 May 2026 5:09pm GMT
Planet Lisp
Joe Marshall: CLRHack: Multiple return values
Multiple Return Value Implementation in CLRHack
The CLRHack compiler implements Multiple Return Values (MRV) by extending the single-value limitation of the .NET Common Intermediate Language (CIL) stack through a thread-local side-channel. This allows Lisp forms to communicate multiple values (up to 64) across function boundaries.
1. The Side-Channel Storage
Because a CIL method can only return a single object on the stack, CLRHack utilizes a static class [LispBase]Lisp.Values. This class contains [ThreadStatic] fields that act as a secondary communication channel:
- Primary Value: Always resides on the CIL evaluation stack.
ReturnCount: Anint32field indicating the total number of values returned (including the primary one).Value1throughValue63: Object fields that store the second through sixty-fourth return values.
2. Producing Multiple Values (The Staging Logic)
To prevent corruption during evaluation, the values form uses a Stage-and-Commit strategy. This is necessary because the side-channel is global to the thread; if a sub-expression inside a values form itself returns multiple values, it would overwrite the global fields before the outer values form is finished.
The compilation process for (values form1 form2 ... formN) follows these steps:
- Evaluation: Each form is evaluated in order.
- Local Staging: The result of
form1is kept on the stack. The results ofform2throughformNare immediately stored into method-local variables (temporaries). This ensures that ifform3calls a function that returns multiple values, the result ofform2is safely tucked away in a local variable and cannot be overwritten. - Commitment: After all forms are evaluated, the compiler generates code to move the values from the local temporaries into the global
Value1...ValueNfields. - Finalization: The
ReturnCountis set toN.
3. Preservation across Control Flow
Certain Lisp constructs must evaluate sub-forms without allowing those sub-forms to interfere with the return values of the primary form. This is handled by a Save-Restore pattern.
Multiple-Value-Prog1
The multiple-value-prog1 form evaluates its first form, then saves the entire side-channel state (the primary value, the ReturnCount, and all ValueN fields) into local variables. It then evaluates the subsequent forms. After they finish, it restores the side-channel state from its locals, ensuring the values of the first form are what the caller receives.
Unwind-Protect
In unwind-protect, the protected form is evaluated and its primary result is stored in a local variable. Crucially, the finally block (cleanup) must not destroy the side-channel state produced by the protected form. The compiler generates code at the start of the finally block to save ReturnCount and Value1...63 into locals. Once the cleanup forms complete, the state is restored from these locals before the method returns.
4. Nested Multiple Values (The Re-entrancy Problem)
The fundamental problem with a global side-channel is re-entrancy. If the compiler were to store form2 directly into the global Value1 field, and then form3 involved a function call like (some-func), that function might execute its own (values ...) logic. This would overwrite the global Value1 that was just set for the outer form.
By enforcing the use of method-local temporaries during the production of values, CLRHack ensures that the global side-channel is only updated at the last possible moment ("atomically" relative to the Lisp expression), effectively shielding the return values from being corrupted by nested evaluations.
26 May 2026 7:00am GMT
Planet Debian
Russ Allbery: Review: The Keeper of Magical Things
Review: The Keeper of Magical Things, by Julie Leong
| Publisher: | Ace |
| Copyright: | 2025 |
| ISBN: | 0-593-81593-9 |
| Format: | Kindle |
| Pages: | 353 |
The Keeper of Magical Things is a cozy fantasy novel. It is set in the same universe as The Teller of Small Fortunes, but it doesn't share any characters or plot, they're not marketed as a series, and so far as I can remember neither book would spoil the other. It is Julie Leong's second novel.
Certainty Bulrush is a novice mage with one reliable magical ability: She can talk to objects and occasionally convince them to do small things. This ability is clearly magical, which means Certainty is indeed a mage, but this appears to be all that her magic can do. The Guild has requirements for the level of magical ability required to become a full mage that go beyond talking stained quilts into unstaining themselves, which is why Certainty has been a novice for six years.
This by itself is a problem, since Certainty's cohort keeps passing her by. Worse, though, is that she was counting on the wages of a full mage to pay for her brother's training to become an apothecary. The thought of failing him is extremely upsetting. Certainty therefore jumps at an offered mission to take a cartload of excess magical objects that are causing a dangerous build-up of energies in the Guildtower to safe storage in the small and very unmagical village of Shpelling. Successful completion of that mission will earn Certainty a promotion to Deputy Keeper and therefore to a full mage.
This is the opportunity she didn't know to hope for. The only drawback is that she will have to work with Mage Aurelia, the famously off-putting farspeaker and magical scholar the other novices refer to as the ice witch.
Aurelia is every bit as icy, formal, and condescending as Certainty was afraid she would be, Shpelling grows nothing but garlic, and the inhabitants are suspicious and hostile. The mission could be a disaster if it weren't for Certainty's stubborn good nature.
It's arguably a spoiler to say that there's an enemies to lovers romance, but it's hinted at on the cover, mentioned in the publisher's blurb and, honestly, if you aren't expecting an enemies to lovers romance by a few chapters in, you probably haven't read many books of this sort.
I found The Keeper of Magical Things quietly enjoyable but extremely predictable. If you're in the mood for what it's offering, the predictability may not be a problem, but it was the kind of book where the direction the plot was headed was so obvious that I got a bit bored waiting for it to arrive. Certainty has a good heart, humble origins, limited but specialized magical ability, and a self-esteem problem, and if you've read much fantasy, you've probably read two or three or a dozen other books with variations of this protagonist. You know how they generally turn out, and that is indeed what you're going to get after the obligatory setbacks and tragedies and looming catastrophes.
Aurelia, similarly, is a variation on a character you've probably met before. Certainty discovers, not long into the book, that the brilliant over-achieving mage wears a necklace (supposedly to help her focus) that constantly whispers to her how inadequate she is and how much harder she needs to work. The necklace was given to her by her parents. This book is not exactly subtle.
That said, there's nothing wrong with the characterization. Both Certainty and Aurelia are interesting characters with rounded-out personalities, although it takes a while before Certainty (or the reader) is allowed to see Aurelia's. Their interactions with the inhabitants of Shpelling are fun to watch in the same way that it can be fun to watch people play PowerWash Simulator. You're not in overwhelming suspense about what's going to happen, but the details are amusing and it is satisfying to watch people with good intentions slowly fix things. There is a plot, and a villain, and a not-subtle message about how everyone deserves acknowledgment and respect, and the hours I spent reading about these characters were enjoyable.
The problem with this book isn't that there's anything wrong with it, but that it may not give you more enjoyment than another book you could have been reading. I quite liked The Teller of Small Fortunes in part because it surprised me in a few places and the main character felt a bit different than the typical fantasy protagonist. The Keeper of Magical Things felt less original and a bit more obvious and predictable. It was still quietly good-hearted and occasionally charming, and I think I'll still remember Certainty in a few months, but I'm not feeling the urge to push it into anyone's hands.
If you're in the mood for a gentle fantasy about finding solutions to people's problems and waiting out the prickliness of people who desperately need a friend, you may enjoy this a great deal. Just don't expect unpredictable twists and turns or a surprising plot structure.
An apparent third book in this loose series, The Isle of Lonely Monsters, is currently scheduled for publication in 2027.
Rating: 6 out of 10
26 May 2026 2:50am GMT
25 May 2026
Planet Lisp
Tim Bradshaw: Measuring slot access cost in Common Lisp
I've been interested in how slow CLOS slot access is in Common Lisp. Here's how I measured it.
I wanted to compare the cost of access to fields of various objects in Common Lisp. In particular I wanted to get a feel for the difference between a slot in a class defined with defclass, so an instance of a subclass of standard-object, and a field in a class defined with defstruct, so an instance of a subclass of structure-object.
A naïve model of the access cost
I measured forms like
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (slot-value a 'a)))
(incf s (the fixnum (slot-value a 'b)))
...))
For \(N\) iterations you might think the time \(T\) should be
\[T = N(c_l + n(c_i + c_s)) + c_c\]
where \(c_l\) is the per-step loop overhead, \(n\) is the number of slots, \(c_i\) is the time to increment a variable, \(c_s\) is the slot read cost (the thing we want), and \(c_c\) is the overhead of calling the function to do all this. \(N\gtrapprox 10^9\), so it's safe to treat \(c_c\) as zero. This is linear in \(n\), and \(T\) is a thing we can measure, so we can differentiate and get an expression for \(c_s\) which is what we want.
\[c_s = \frac{1}{N}\frac{dT}{dn} - c_i\]
In fact, everything works in terms of the per-step time \(t\doteq T/N\) as \(N\) varies for different classes and numbers of slots to keep the runtimes reasonable, and then \(c_s = dt/dn - c_i\).
A better model
Well, this turns out to be wrong. In particular if you estimate \(c_i\) (see below on how I did this) and use it in the above expression you will end up calculating values of \(c_s\) for structures which are either absurdly tiny (\(\sim 10^{-11}\)s for a machine with a cycle time \(\sim 10^{-9}\)s) or even negative. The reason is pretty obviously that the increment and the access are largely overlapped.
So in what follows I simply treat \(c_i\) as zero. This may overestimate \(c_s\) somewhat. But a result of that overestimation is that the factor by which slot access is slower than structure field access will be underestimated, which will make CLOS seem faster than it is, since if \(a \gt b \gt c \gt 0\) then \((a - c)/(b - c) \gt a/b\). That's good, because what I'm trying to demonstrate is that it's really slow, so an underestimation is safe.
The new model expression for \(c_s\) is then just \(c_s = dt/dn\).
What I did
I measured slot access time in the same way for a class with 10 slots, measuring 2, 4, 6, 8 and 10 slots, and did the same thing for a structure with 10 fields.
Because the access times and numbers of accesses per step vary widely I adjusted the number of iterations to keep the run-times sane: more than 10 seconds per test but ideally less than 60.
Each measurement was repeated 4 times.
I then fitted a linear function to the data for each class (least-squares fit), and used its gradient and the estimated variable-increment cost to estimate \(c_s\) for each type.
All the measurements were done on an M1 MacBook Air, using caffeinate to prevent it sleeping. I measured LispWorks 8.1.2 and SBCL 2.6.4. Total run times were somewhat over an hour for each implementation.
Results
SBCL slot access data and best fit
LispWorks slot access data and best fit
SBCL structure field access data and best fit
LispWorks structure field access data and best fit
From these you can see that the results are consistent between runs and the best fit is pretty good.
The per-slot cost is then the slope of the best fit curve, or perhaps slightly less.
Structure field access cost estimate
- SBCL: \(c_s \approx 3.2\times 10^{-10}\)s.
- LispWorks: \(c_s \approx 3.1\times 10^{-10}\)s.
Note that these are both almost certainly a single cycle up to rounding.
standard-instance subclass slot access cost estimate
- SBCL: \(c_s \approx 1.2\times 10^{-8}\)s.
- LispWorks: \(c_s \approx 1.0\times 10^{-8}\)s.
Ratios
The ratios between these two values for each implementation are then about 38 for SBCL and about 32 for LispWorks: this is how much slower CLOS slot access is than structure field access. In fact it is probably an underestimate of how much slower it is.
What is obvious1
CLOS slot access is really slow.
What is less obvious but almost certainly true2
This is not because multiple inheritance is inherently slow: it's because the design of CLOS, especially if you want to take the AMOP MOP seriously, implies crappy performance.
Can this be fixed? Yes, I think so, with well-defined tradeoffs. Will it be? Up to implementors. So, probably not, sadly.
Notes
To get an estimate of the time to increment a variable, \(c_i\), first measure a large number of iterations of an empty loop and then a loop which increments a variable 100 times for each step. Both of the implementations I measured do not optimize empty loops away, intentionally I think. This estimate is now not used (see above), but if it's not about a clock cycle (about \(3.3\times 10^{-10}\)s on M1) then probably something is wrong.
Code
This is the CL code I used.
;;;; Some slot-value benchmarks
;;;
;;; None of this code is general-purpose.
;;;
(in-package :cl-user)
(define-condition too-short (simple-error)
((seconds :initform 0 :initarg :seconds :reader too-short-seconds)))
(defmacro noting-too-short (&body forms)
`(handler-bind ((too-short (lambda (e)
(format *debug-io* "~&Too short: ~,2Fs when minimum was ~Ds~%"
(too-short-seconds e)
*minimum-seconds*)
(continue e))))
,@forms))
(defvar *minimum-seconds* 10) ;how long it must run for
(defmacro ticks (&body forms)
`(let ((start (get-internal-real-time))
(end (progn
,@forms
(get-internal-real-time))))
(let* ((elapsed-ticks (- end start))
(elapsed-seconds (/ elapsed-ticks internal-time-units-per-second)))
(when (< elapsed-seconds *minimum-seconds*)
(cerror "just return ~D (~,2F seconds)"
(make-condition
'too-short
:format-control "~D ticks (~,2F seconds) is not long enough"
:format-arguments (list elapsed-ticks (float elapsed-seconds))
:seconds (float elapsed-seconds))
elapsed-ticks (float elapsed-seconds)))
elapsed-ticks)))
(defun seconds (ticks &optional (divider 1))
(/ ticks internal-time-units-per-second divider))
(defun note (control &rest args)
(format *debug-io* "~&[~?]~%" control args)
(force-output *debug-io*))
(defmacro noting ((&rest notes) &body forms)
;; Single value only, but this is all we need
`(progn
(format *debug-io* "~&[~@{~A~^ ~}" ,@notes)
(force-output *debug-io*)
(let ((r (progn ,@forms)))
(format *debug-io* " -> ~A]~%" r)
(force-output *debug-io*)
r)))
(defun inc-n (n incs)
(declare (type fixnum n incs)
(optimize speed (safety 0)))
(case incs
(0
(dotimes (i n 0)))
(100
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s)
(incf s))))
(otherwise
(error "what even is this"))))
(defun estimate-increment-time (&key
(exponent 11)
&aux
(n (round (expt 10 exponent)))
(n/100 (round (expt 10 (- exponent 2)))))
(declare (type fixnum n n/100))
(/ (- (seconds (noting (100 n/100) (ticks (inc-n n/100 100))) n/100)
(seconds (noting (0 n) (ticks (inc-n n 0))) n))
100))
(defclass a ()
((a :initform 0 :reader a-a)
(b :initform 0 :reader a-b)
(c :initform 0 :reader a-c)
(d :initform 0 :reader a-d)
(e :initform 0 :reader a-e)
(f :initform 0 :reader a-f)
(g :initform 0 :reader a-g)
(h :initform 0 :reader a-h)
(i :initform 0 :reader a-i)
(j :initform 0 :reader a-j)))
(defstruct b
(a 0)
(b 0)
(c 0)
(d 0)
(e 0)
(f 0)
(g 0)
(h 0)
(i 0)
(j 0))
(defgeneric svn (o n count &key)
(declare (optimize speed)))
(defmethod svn ((a a) n count &key (reader nil))
(declare (type fixnum n count)
(optimize speed (safety 0)))
(if reader
(case count
(2
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (a-a a)))
(incf s (the fixnum (a-b a))))))
(4
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (a-a a)))
(incf s (the fixnum (a-b a)))
(incf s (the fixnum (a-c a)))
(incf s (the fixnum (a-d a))))))
(6
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (a-a a)))
(incf s (the fixnum (a-b a)))
(incf s (the fixnum (a-c a)))
(incf s (the fixnum (a-d a)))
(incf s (the fixnum (a-e a)))
(incf s (the fixnum (a-f a))))))
(8
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (a-a a)))
(incf s (the fixnum (a-b a)))
(incf s (the fixnum (a-c a)))
(incf s (the fixnum (a-d a)))
(incf s (the fixnum (a-e a)))
(incf s (the fixnum (a-f a)))
(incf s (the fixnum (a-g a)))
(incf s (the fixnum (a-h a))))))
(10
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (a-a a)))
(incf s (the fixnum (a-b a)))
(incf s (the fixnum (a-c a)))
(incf s (the fixnum (a-d a)))
(incf s (the fixnum (a-e a)))
(incf s (the fixnum (a-f a)))
(incf s (the fixnum (a-g a)))
(incf s (the fixnum (a-h a)))
(incf s (the fixnum (a-i a)))
(incf s (the fixnum (a-j a))))))
(otherwise
(error "what even is this")))
(case count
(2
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (slot-value a 'a)))
(incf s (the fixnum (slot-value a 'b))))))
(4
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (slot-value a 'a)))
(incf s (the fixnum (slot-value a 'b)))
(incf s (the fixnum (slot-value a 'c)))
(incf s (the fixnum (slot-value a 'd))))))
(6
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (slot-value a 'a)))
(incf s (the fixnum (slot-value a 'b)))
(incf s (the fixnum (slot-value a 'c)))
(incf s (the fixnum (slot-value a 'd)))
(incf s (the fixnum (slot-value a 'e)))
(incf s (the fixnum (slot-value a 'f))))))
(8
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (slot-value a 'a)))
(incf s (the fixnum (slot-value a 'b)))
(incf s (the fixnum (slot-value a 'c)))
(incf s (the fixnum (slot-value a 'd)))
(incf s (the fixnum (slot-value a 'e)))
(incf s (the fixnum (slot-value a 'f)))
(incf s (the fixnum (slot-value a 'g)))
(incf s (the fixnum (slot-value a 'h))))))
(10
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (slot-value a 'a)))
(incf s (the fixnum (slot-value a 'b)))
(incf s (the fixnum (slot-value a 'c)))
(incf s (the fixnum (slot-value a 'd)))
(incf s (the fixnum (slot-value a 'e)))
(incf s (the fixnum (slot-value a 'f)))
(incf s (the fixnum (slot-value a 'g)))
(incf s (the fixnum (slot-value a 'h)))
(incf s (the fixnum (slot-value a 'i)))
(incf s (the fixnum (slot-value a 'j))))))
(otherwise
(error "what even is this")))))
(defmethod svn ((b b) n count &key)
(declare (type fixnum n)
(optimize speed (safety 0)))
(case count
(2
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (b-a b)))
(incf s (the fixnum (b-b b))))))
(4
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (b-a b)))
(incf s (the fixnum (b-b b)))
(incf s (the fixnum (b-c b)))
(incf s (the fixnum (b-d b))))))
(6
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (b-a b)))
(incf s (the fixnum (b-b b)))
(incf s (the fixnum (b-c b)))
(incf s (the fixnum (b-d b)))
(incf s (the fixnum (b-e b)))
(incf s (the fixnum (b-f b))))))
(8
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (b-a b)))
(incf s (the fixnum (b-b b)))
(incf s (the fixnum (b-c b)))
(incf s (the fixnum (b-d b)))
(incf s (the fixnum (b-e b)))
(incf s (the fixnum (b-f b)))
(incf s (the fixnum (b-g b)))
(incf s (the fixnum (b-h b))))))
(10
(let ((s 0))
(declare (type fixnum s))
(dotimes (i n s)
(incf s (the fixnum (b-a b)))
(incf s (the fixnum (b-b b)))
(incf s (the fixnum (b-c b)))
(incf s (the fixnum (b-d b)))
(incf s (the fixnum (b-e b)))
(incf s (the fixnum (b-f b)))
(incf s (the fixnum (b-g b)))
(incf s (the fixnum (b-h b)))
(incf s (the fixnum (b-i b)))
(incf s (the fixnum (b-j b))))))
(otherwise
(error "what even is this"))))
(defun measure-thing (thing &key
(exponent 11)
(specs
'((2 -0.2)
(4 -0.4)
(6 -0.6)
(8 -0.8)
(10 -1)))
(sleep 0)
&aux (cn (class-name (class-of thing))))
(mapcar (lambda (spec)
(destructuring-bind (count delta &rest kws &key) spec
(let ((iterations (round (expt 10 (+ exponent delta)))))
(let ((per-step (float
(seconds (noting (cn count iterations)
(ticks (apply #'svn thing
iterations count kws)))
iterations))))
(note "~S ~D elapsed ~Ds per-step ~Ds"
cn count (* per-step iterations) per-step)
(when (> sleep 0)
(noting ("sleep" sleep)
(sleep sleep)))
(list cn count per-step)))))
specs))
(defun measure-things (&key
(things-and-exponents `((,(make-b) 11)
(,(make-instance 'a) 10)))
(log-file "thing-times.ldat")
(tries 4)
(sleep 5))
;; Dump measurements to a log file
(with-standard-io-syntax
(with-open-file (log log-file :direction :output
:if-exists :supersede)
(noting-too-short
(let ((increment-time (float (estimate-increment-time))))
(note "increment time ~Ds" increment-time)
(pprint increment-time log)
(force-output log))
(dolist (thing-and-exponent things-and-exponents)
(destructuring-bind (thing exponent) thing-and-exponent
(note "~S exponent ~D"
(class-name (class-of thing))
exponent)
(dotimes (try tries)
(pprint
(measure-thing thing
:exponent exponent
:sleep sleep)
log)
(force-output log)))))))
log-file)
This is the Racket code which plotted the data and computed the fit & cost.
#lang racket
;;;; Fit data from tsv
;;;
(require simple-polynomial
plot)
(define (snarf from)
;; Stolen from warranted (wcs.rkt): just read all the forms, safely
(call-with-default-reading-parameterization
(thunk
(parameterize ([read-accept-lang #f]
[read-accept-reader #f])
(call-with-input-file from
(λ (p)
(for/list ([form (in-port read p)])
form)))))))
(define (classify file-data)
;; The data is an increment time, followed by lists of (class-name
;; slot-count seconds) Return a hash table mapping from class names
;; and the imcrememt time
(match-let ([(cons increment-time records) file-data])
(define cmap (make-hasheqv))
(for* ([record (in-list records)]
[single (in-list record)])
(match-let ([(list class-name count seconds) single])
(hash-update! cmap class-name
(λ (c)
(cons (list count seconds) c))
'())))
(values cmap increment-time)))
(define (linear-fit class-name cmap)
(points->best-fit-polynomial (hash-ref cmap class-name) 1))
(define (slot-cost class-name cmap (increment-time 0.0))
(- (first (polynomial-terms (linear-fit class-name cmap)))
increment-time))
(define (file-slot-cost class-name file
#:use-increment-time (use-increment-time #f))
(let-values ([(cmap increment-time)
(classify (snarf file))])
(slot-cost class-name cmap (if use-increment-time
increment-time
0.0))))
(define (file-A/B-ratio file #:use-increment-time (use-increment-time #f))
(/ (file-slot-cost 'A file #:use-increment-time use-increment-time)
(file-slot-cost 'B file #:use-increment-time use-increment-time)))
(define (plot-linear-fit class-name cmap
#:to-file (to-file #f)
#:title (title #f))
(parameterize ([plot-font-family 'modern]
[plot-width 560]
[plot-x-far-axis? #f]
[plot-y-far-axis? #f]
[plot-x-ticks (linear-ticks #:number 5)])
(define pts (hash-ref cmap class-name))
((if to-file
(curryr plot-file to-file)
plot)
(list
(points pts #:sym 'plus #:label (format "~A data" class-name))
(function (points->best-fit-polynomial pts 1) #:label "linear fit"))
#:x-min 0
#:x-max 10.5
#:x-label "count"
#:y-label "seconds/step"
#:title title)))
(define (file-plot-linear-fit class-name file
#:to-file (to-file #f)
#:title (title #f))
(let-values ([(cmap _) (classify (snarf file))])
(plot-linear-fit class-name cmap
#:to-file to-file
#:title title)))
25 May 2026 1:46pm GMT
Joe Marshall: CLRHack: Tail Recursion
Tail-Call Handling in CLRHack
I decided to make proper tail recursion a fundamental requirement in CLRHack. This prevents stack overflow errors during standard recursive patterns and ensures the runtime remains stable regardless of recursion depth. Technically, Common Lisp isn't required to be tail recursive, but I want mine to be.
1. Tail Position Identification
The compiler performs a structural analysis of the Abstract Syntax Tree (AST) to identify "tail positions." An expression is in a tail position if its value is the final result of the function, meaning no further work remains to be done in the current frame after the call returns. The generate-step2 walker propagates a tail-p flag through the following logic:
- Functions/Lambdas: The final expression in the body is in the tail position.
- Conditionals (IF): Both the "then" and "else" branches are in the tail position.
- Sequences (PROGN/LET): Only the very last form in the sequence is in the tail position.
- Blocks: The last form of a
BLOCKis in the tail position, provided the block is not the target of aRETURN-FROM.
2. CIL Instruction Emission
To implement proper tail-call semantics, the compiler utilizes the native tail. prefix in the Common Intermediate Language (CIL). When a function call is detected in a tail position, the compiler applies the following mandatory transformation:
- The Prefix: It prepends the
tail.opcode to thecallorcallvirtinstruction. - The Return: It immediately follows the call with a
ret(return) instruction.
The tail. prefix instructs the .NET Just-In-Time (JIT) compiler to discard the current method's stack frame before jumping to the target function. This ensures that the call consumes zero additional stack space, turning the recursive call into a semantic jump.
3. Safety and Context Constraints
The implementation of tail-calls is subject to specific safety rules imposed by the Common Language Runtime (CLR) to maintain execution integrity:
- Protected Regions: The CLR prohibits
tail.calls insidetry,catch, orfinallyblocks. Because Lisp constructs such asunwind-protectandhandler-caserely on these CIL features, tail-call elimination is suspended within these specific scopes to ensure cleanup handlers and error recovery mechanisms function correctly. - Frame Cleanup: The compiler ensures that all local resources are in a valid state before the
tail.prefix is issued, allowing the CLR to safely deallocate the current frame.
Example CIL Output
Consider a recursive counter that must be able to run indefinitely:
(defun count-down (n)
(if (= n 0)
"Done"
(count-down (- n 1))))
The compiled CIL for the recursive branch is transformed to ensure stack neutrality:
; ... code to calculate (- n 1) ...
tail.
call object Program::'COUNT-DOWN'(object)
ret
By strictly enforcing this pattern, CLRHack guarantees that recursive programs can execute with constant stack space, fulfilling my core requirement of tail recursion.
25 May 2026 7:00am GMT
24 May 2026
Planet Debian
Russell Coker: Debian SE Linux and PinTheft
We have a new Linux exploit called PinTheft [1]. I did some tests of it with Debian kernel 6.12.74+deb13+1-amd64.
user_t
When I run the exploit as user_t I see the following in the audit log:
type=PROCTITLE msg=audit(1779615031.043:15540): proctitle="./exp"
type=AVC msg=audit(1779615031.043:15541): avc: denied { create } for pid=1360 comm="exp" scontext=user_u:user_r:user_t:s0 tcontext=user_u:user_r:user_t:s0 tclass=rds_socket permissive=0
type=SYSCALL msg=audit(1779615031.043:15541): arch=c000003e syscall=41 success=no exit=-13 a0=15 a1=5 a2=0 a3=0 items=0 ppid=879 pid=1360 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts0 ses=1 comm="exp" exe="/home/test/b/pocs/pintheft/exp" subj=user_u:user_r:user_t:s0 key=(null)ARCH=x86_64 SYSCALL=socket AUID="test" UID="test" GID="test" EUID="test" SUID="test" FSUID="test" EGID="test" SGID="test" FSGID="test"
The last of the output of running the exploit is the following:
[-] only stole 0/1024 refs - may not be enough [-] too few stolen refs, aborting [-] attempt 5 failed, retrying... [-] all 5 attempts failed
unconfined_t
When I run it as unconfined_t it gave the same output and stracing it had many of the following:
socket(AF_RDS, SOCK_SEQPACKET, 0) = -1 EAFNOSUPPORT (Address family not supported by protocol)
After I ran "modprobe rds" the exploit worked as unconfined_t with the following output:
[*] verifying page cache overwrite... [*] page cache page 0 AFTER overwrite (our shellcode) (129 bytes): 0000: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 0010: 03 00 3e 00 01 00 00 00 68 00 00 00 00 00 00 00 |..>.....h.......| 0020: 38 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |8...............| 0030: 00 00 00 00 40 00 38 00 01 00 00 00 05 00 00 00 |....@.8.........| 0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0050: 2f 62 69 6e 2f 73 68 00 81 00 00 00 00 00 00 00 |/bin/sh.........| 0060: 81 00 00 00 00 00 00 00 31 ff b0 69 0f 05 48 8d |........1..i..H.| 0070: 3d db ff ff ff 6a 00 57 48 89 e6 31 d2 b0 3b 0f |=....j.WH..1..;.| 0080: 05 |.| [+] verification PASSED - page cache overwritten with SHELL_ELF [+] executing /usr/bin/su (now contains setuid(0) + execve /bin/sh)... === RESTORE: sudo cp /tmp/.backup_su_13294 /usr/bin/su && sudo chmod u+s /usr/bin/su === #
Conclusion
SE Linux in a "strict" configuration stops this exploit.
The test VM is running Debian/Testing, I haven't bothered investigating whether it's a default setting for Debian to not load the rds module or whether it was some change that I made either directly or indirectly. Security via SE Linux is of more interest to me than security via controlling module load.
24 May 2026 10:32am GMT
Vincent Bernat: Scaling Akvorado BMP RIB with sharding
To associate routing information-like AS paths or BGP communities-to flows, Akvorado can import routes through the BGP Monitoring Protocol (BMP). As the Internet routing table contains more than 1 million routes, Akvorado needs to scale to tens of millions of routes.1 This has been a long-standing challenge,2 but I expect this issue is now fixed by using RIB sharding, a method that splits the routing database into several parts to enable concurrent updates.
Previous implementation
Akvorado connects 2 elements to build its RIB:
- a prefix tree, and
- a list of routes attached to each prefix.
In the diagram above, the RIB stores five IPv4 prefixes and two IPv6 prefixes. One of them, 2001:db8:1::/48, contains three routes:
- from peer 3, next hop
2001:db8::3:1, AS 65402, AS path65402, community65402:31, - from peer 4, next hop
2001:db8::4:1, same ASN, AS path, and community, - from peer 5, next hop
2001:db8::5:1, AS 65402, AS path65401 65402, community65402:31.
The rib structure is defined in Go as follows:
type rib struct { tree *bart.Table[prefixIndex] routes map[routeKey]route nlris *intern.Pool[nlri] nextHops *intern.Pool[nextHop] rtas *intern.Pool[routeAttributes] nextPrefixID prefixIndex freePrefixIDs []prefixIndex }
The prefix tree uses the bart package, an adaptation of Donald Knuth's ART algorithm. The benchmarks demonstrate it outperforms other packages for lookups, insertions, and memory usage.3 Plus, the author is quite helpful.
Storing routes in a map
The list of routes for each prefix is not stored directly in the prefix tree: it would put too much pressure on the garbage collector by allocating per-prefix arrays.
Instead, the RIB assigns a unique 32-bit prefix identifier for each prefix, either by picking the last available prefix identifier from the freePrefixIDs array if any, or using the nextPrefixID value before incrementing it. Then, the routes are stored in the routes map, leveraging the optimized Swiss table in Go. To retrieve routes attached to a prefix, we look them up one by one in the routes map with a 64-bit key combining the 32-bit prefix index with a 32-bit route index matching the position of the route in the list. Akvorado scans routes from the first to the last to find the best one.4 It knows there is no more route if the route key returns no result.
type prefixIndex uint32 type routeIndex uint32 type routeKey uint64
Interning routes
A route contains a BGP peer identifier, a partial NLRI5, the next hop, and the attributes.
type route struct { peer uint32 nlri intern.Reference[nlri] nextHop intern.Reference[nextHop] attributes intern.Reference[routeAttributes] prefixLen uint8 } type nlri struct { family bgp.Family path uint32 rd RD } type nextHop netip.Addr type routeAttributes struct { asn uint32 asPath []uint32 communities []uint32 largeCommunities []bgp.LargeCommunity }
To save memory and allocations, NLRI, next hops, and route attributes are "interned:" a 32-bit integer replaces the real value. The mechanism predates the unique package introduced in Go 1.23. We keep it because it has different trade-offs:
- It uses explicit reference counting instead of relying on weak pointers.
- It works with non-comparable values implementing
Hash()andEqual()methods.6 - It uses explicit pool instances. This will be useful for sharding.
- It has better performance. See for example this benchmark.
- It consumes half the memory thanks to unsigned 32-bit references instead of pointers.
- But it is not safe for concurrent use.
Why does it not scale?
Note
At AS 12322, we don't use BMP yet.7 But Gerhard Bogner had the patience, availability, and technical skills to help me debug this issue.
The global read/write lock is a bottleneck in this implementation. But how? There are several users of the RIB, each with its own set of constraints:
-
The Kafka workers look up the RIB to enrich flows with routing information. They are bound by the number of Kafka partitions.8 Akvorado also adjusts their number to ensure efficient batching to ClickHouse. On our setup, the number of workers oscillates between 8 and 16. As we want to observe the latest data, we cannot afford for the Kafka workers to lag too much.
-
The monitored routers send route updates through the BMP protocol. When connecting, they can send millions of routes.9 After the initial synchronization, updates are sent continuously and may spike from time to time. The router detects a stuck BMP station when its TCP window is full and resets the session in this case. While Akvorado implements a large incoming buffer, it still needs to update the received routes with the write lock held fast enough to avoid being detected as stuck.
-
When a remote BGP peer goes down, Akvorado flushes the associated routes by walking the RIB with the write lock held. When a monitored router goes down, Akvorado waits a bit but eventually flushes all the associated routes.
In short: on a busy setup, lock contention is high for both readers and writers, and neither can lag too much behind.
RIB sharding
First step: basic sharding
To remove the global lock, the RIB is split into several "shards," each one handling a subset of the prefixes:
The prefix tree stays global and is protected by a single lock. Each shard gets its read/write lock, its route map, and its intern pools to store NLRIs, next hops, and route attributes, which would not have been possible with Go's unique package. The prefix indexes are also sharded: the 8 most significant bits are the shard index and the 24 remaining bits are the local prefix index.
Gerhard confirmed that after this blind change, the BMP receiver chugged steadily. 🎉
Later, I wrote a concurrent benchmark over half a million synthetic but plausible routes10 partitioned over 0 to 8 writers, churning routes as fast as possible, while 1 to 16 readers continuously look up a set of 10,000 routes. I don't know if this benchmark is realistic, but it confirms the improvements for both read and write latencies:
It also shows that a high number of writers degrades read latency.
Second step: lock-free reads
The single read/write lock protecting the prefix tree is the next target. The bart package provides alternative mutation methods returning an updated tree using copy-on-write. Readers don't need the global lock any more, leaving it only to synchronize writers. The prefix tree is boxed in an atomic pointer.
Without a lock, readers can now fetch a stale prefix index when walking their copy of the tree if a concurrent writer removes the last route attached to this prefix index and recycles it for another prefix. To avoid this issue, we combine the prefix index with a generation number and store them in the tree:
type generation uint32 type prefixRef struct { idx prefixIndex gen generation } type rib struct { mu sync.Mutex tree atomic.Pointer[bart.Table[prefixRef]] shards []*ribShard }
Each shard stores the generation number for each local prefix index. The generation number increases by one if the associated prefix index is freed. When looking up the routes attached to a prefix index, the reader checks if the generation number matches. Otherwise, it assumes the index was recycled and the list of routes is empty.11 You can see this case in the diagram above for prefix index 5, stored with a generation index of 3, while the current value in the []generations array is 4. The generation number could overflow, but it is not a problem as lookups are quick.
Running the concurrent benchmark against this new implementation shows the improvements for the read latency as soon as the cost of the copy-on-write prefix tree is amortized.
Among the multiple attempts to optimize the BMP component, RIB sharding is one of the more satisfying. Akvorado 2.2 implements the first step. PR #2433, drafted while writing this blog post, implements the second step and will be released with Akvorado 2.4. 🪓
-
Each router exporting flows doesn't need to send its routes. When Akvorado does not find a route from a specific device, it falls back to a route sent by another device. It is up to the operator to decide if this is a good enough approximation. ↩
-
I made many attempts to scale the BMP component. See for example PR #254, PR #255, PR #278, PR #2244, and PR #2245. Despite these efforts, this component remained problematic for some users. See discussion #2287 as the latest example. ↩
-
It keeps improving: bart 0.28.0 features a new implementation that trades a bit of memory for greater lookup performance. I did not test it yet, as I have been preparing this blog post for a couple of months already. ↩
-
Akvorado prefers the route matching the exact next hop. Otherwise, it falls back to any other route. This is an approximation. An alternative would be to have one prefix tree for each BGP peer but it would require configuring all routers to export their routes. pmacct's BMP daemon implements this approach. ↩
-
If we consider the BGP RIB as a database, the Network Layer Reachability Information (NLRI) is the primary key. Its content depends on the BGP family. With IPv4 or IPv6 unicast, this is the prefix. For VPNv4 and VPNv6 families, it includes the route distinguisher. If you enable the ADD-PATH extension, the NLRI also contains a path identifier.
In our implementation, we don't store the prefix as we get it from the looked-up IP address using the separately-stored prefix length. ↩
-
The
Hash()methods rely on thehash/maphashpackage and on theunsafepackage to avoid memory copies. See for example theHash()function for thenlristructure. ↩ -
Despite being an author or co-author of the first BMP-related RFCs since 2016 (RFC 7854, RFC 8671, RFC 9069), Cisco did not implement it in a usable way in IOS XR until version 24.2.1. We still need to upgrade a few routers to enable this feature. ↩
-
KIP-932 introduces, in Kafka 4.2, the concept of share groups to enable cooperative consumption on the same partition. This is not supported in Akvorado yet. ↩
-
You can configure BMP to send routes for each BGP peer before or after applying the incoming policies. In this case, you can get more than one million routes for each transit peer. You can also tell BMP to send the local RIB, which only contains the best path for each prefix. ↩
-
The prefixes are random, but the prefix size distribution and the AS path length distribution follow the data provided by Geoff Huston. ↩
-
Alternatively, we could retry the lookup, but it would be pointless: the RIB is an eventually consistent database, and an empty list was a correct answer at some point in the recent past. ↩
24 May 2026 7:50am GMT
25 Apr 2026
FOSDEM 2026
All FOSDEM 2026 videos are online
All video recordings from FOSDEM 2026 that are worth publishing have been processed and released. Videos are linked from the individual schedule pages for the talks and the full schedule page. They are also available, organised by room, at video.fosdem.org/2026. While all released videos have been reviewed by a human, it remains possible that one or more issues fell through the cracks. If you notice any problem with a video you care about, please let us know as soon as possible so we can look into it before the video-processing infrastructure is shut down for this edition. To report any舰
25 Apr 2026 10:00pm GMT
29 Jan 2026
FOSDEM 2026
Join the FOSDEM Treasure Hunt!
Are you ready for another challenge? We're excited to host the second yearly edition of our treasure hunt at FOSDEM! Participants must solve five sequential challenges to uncover the final answer. Update: the treasure hunt has been successfully solved by multiple participants, and the main prizes have now been claimed. But the fun doesn't stop here. If you still manage to find the correct final answer and go to Infodesk K, you will receive a small consolation prize as a reward for your effort. If you're still looking for a challenge, the 2025 treasure hunt is still unsolved, so舰
29 Jan 2026 11:00pm GMT
26 Jan 2026
FOSDEM 2026
Call for volunteers
With FOSDEM just a few days away, it is time for us to enlist your help. Every year, an enthusiastic band of volunteers make FOSDEM happen and make it a fun and safe place for all our attendees. We could not do this without you. This year we again need as many hands as possible, especially for heralding during the conference, during the buildup (starting Friday at noon) and teardown (Sunday evening). No need to worry about missing lunch at the weekend, food will be provided. Would you like to be part of the team that makes FOSDEM tick?舰
26 Jan 2026 11:00pm GMT