Patch the patch – things you do when you miss the update train

In my last post about Yocto I wrote about dealing with disappearing dependencies and how one should try to keep his layers up to date.

“Next day I started by updating all the layers properly”

Those were my words at the end. And I really wish things were so simple.

Kirkstone reached end of support in May 2026. Not an unexpected event. What matters is that once it happens, you’re essentially left with two options:

  • migrate to a newer release, or
  • maintain your existing codebase yourself.

For me, the first option simply wasn’t realistic. Too much work, too many decisions, not enough resources.

And that’s exactly where you never want to find yourself with a production project.

If you do end up there (like I did), having a local archive of dependencies becomes invaluable. Unfortunately for me, I also had to get the project building on a completely clean system, so relying on cached artifacts wasn’t an option.

Fortunately, there are still a few engineering tricks that can keep an unsupported Yocto project alive long enough to buy you time.

One of those tricks turned out to be… patching a patch.

The problem

A security fix for libpam was backported to the Kirkstone branch as a patch in the Poky layer. This patch accidentally removed a single header line and caused the whole build to fail.

One removed line was enough to turn into several rather unpleasant ones in the build log:

pam_namespace.c: In function 'parse_config_file':
pam_namespace.c:944:17: warning: implicit declaration of function 'setlocale'
pam_namespace.c:944:27: error: 'LC_COLLATE' undeclared (first use in this function)

My guess is that the the submitter’s build environment pulled <locale.h> in indirectly from another header, so the problem never surfaced there. I wasn’t that lucky.

As I mentioned earlier, once a Yocto release reaches end of support, the problem becomes yours to solve.

The fix? Patch the patch.

Instead of spending hours trying to understand why the upstream change was breaking the build, I took a more pragmatic approach: restore the missing include removed by the security patch.

One option is simply to revert the problematic part of the security patch and apply your own corrective patch before the build starts. There are several ways to integrate such a fix into an automated build pipeline. If you’re using kas, you can even add it directly in the repos.patches section of the configuration, making the workaround completely automatic (big thanks @Mathieu for teaching me this).

In my case, the patch itself was almost trivial. It simply restored the removed #include line. Had the fix required any deeper changes, I probably wouldn’t have dared to modify a security patch at all. But for a one-line correction, the risk was justified.

diff --git a/meta/recipes-extended/pam/libpam/CVE-2025-6020-01.patch b/meta/recipes-extended/pam/libpam/CVE-2025-6020-01.patch
index 53ae2bd2ee..47154f95ae 100644
--- a/meta/recipes-extended/pam/libpam/CVE-2025-6020-01.patch
+++ b/meta/recipes-extended/pam/libpam/CVE-2025-6020-01.patch
@@ -1528,7 +1528,7 @@ diff --git a/modules/pam_namespace/pam_namespace.h b/modules/pam_namespace/pam_n
index b51f284..abd570d 100644
--- a/modules/pam_namespace/pam_namespace.h
+++ b/modules/pam_namespace/pam_namespace.h
-@@ -44,21 +44,17 @@
+@@ -44,21 +44,18 @@
#include <stdlib.h>
#include <errno.h>
#include <syslog.h>
@@ -1546,7 +1546,7 @@ index b51f284..abd570d 100644
#include <fcntl.h>
#include <sched.h>
#include <glob.h>
--#include <locale.h>
+ #include <locale.h>
#include "security/pam_modules.h"
#include "security/pam_modutil.h"
#include "security/pam_ext.h"

The final step was simply to tell kas to apply the patch during repository checkout by adding it to the repos.patches section of the configuration:

repos:
poky:
#kirkstone-4.0.35
commit: 393064579dfdd0ed2d4ed4c27d238d7d2292c08b
patches:
fix-libpam:
repo: my-layer-name
path: patches/poky/0001-fix-cve-2025-6020-01-patch.patch

Build went green — and with a bit of luck, this should keep the project running for at least another month.

Why not just add a .bbappend in our own layer?

You certainly could. But the whole idea behind layers and recipes is to provide a flexible way to extend or customize the default behavior—not to fix bugs in a specific Poky revision. Another option would be to maintain your own mirror of Poky with the fix applied, but for such a small change, that seemed like overkill.

Is this good engineering?

Absolutely not!

You won’t find techniques like this in any Yocto best-practices guide. Every month an unsupported branch ages, another dependency may break, another CVE may appear, and another workaround may become necessary.

In a perfect world, we would already have migrated to a newer Yocto release.

But software isn’t built in a perfect world.

Sometimes the best you can do is keep the system alive long enough to catch the next passing train.


Software Architecture and a Cosmic Whale

Has Anyone Seen My Architecture?

There are countless definitions of software architecture.
Some emphasize decisions, others structures, others “the important stuff,” or whatever is hardest to change. Read enough of them and architecture begins to feel like something that slips through every classification—a creature everyone describes differently, yet no one seems to have seen.

And yet, this creature clearly exists. No one doubts that.
We recognize it by its effects: slow delivery, bugs that refuse to die, changes that feel far riskier than they should, systems that push back against even the smallest improvement.

The Mysterious Creature

One might try to exercise the imagination—to picture something that lives partly in code and partly in our heads. A multidimensional entity, not bound to a single moment in time, but stretched across the full span of its existence. Shaped by past decisions and external forces, while simultaneously guiding—and constraining—what changes are possible next. With enough effort, one might even convince oneself of having seen it.

But that is not the point.

We are software developers. Our job is not to chase mystical creatures, but to solve problems. We have deadlines. Features. Things that must work. We have bugs that reliably appear at 3 a.m.

What actually matters are the long-term consequences of change:

  • Whether, given what we have today, we can meet business requirements tomorrow.
  • Where to look when things begin to break apart.
  • Whether deleting a piece of code is safe—or the first step toward disaster.

Chop It!

To reason about architecture, we do what physicists do with spacetime—a similarly ungraspable monstrosity. If you are still holding on to some animal-like mental picture of architecture, now is the time to let it go. Things are about to get drastic.

We are going to slice it.

The axis we choose depends on what we want to understand, and which trade-offs we want to bring into the light.

Boundary axis (Context diagram)
What is inside the system, what is outside, and who depends on whom.

Time axis (Architecture Decision Records)
How the system arrived at its current shape.
Which decisions were made under which constraints—and which alternatives were rejected.

Runtime behavior axis (Sequence diagram)
How work flows through the system while it is running.
Who calls whom, in what order, and where latency or failure can occur.

Infrastructure axis (Deployment diagram)
How the system maps onto physical or virtual resources.
What runs where, what can be deployed independently—and what cannot.

Change axis (Module or service diagram)
How the system tends to evolve over time.
What changes together, what should not, and where change is expensive.

There are many more possible slices.

But the important thing is this: none of these projections is the architecture.
They are views—showing relationships, revealing trade-offs, and giving your brain something it can actually navigate.

The End Game

The goal of the architecture game is not to catch the mysterious whale.
Those who try usually end up with piles of documents that age faster than the code—and quickly become useless.

The goal is to deliver. To know which axes to use at any given moment.
To move comfortably across different projections, and to predict the consequences of change—whether we introduce it deliberately or it is forced upon us. To prepare for disasters and to minimize the impact radius when they arrive.

One who knows how to play the game can deliberately evolve the system.
One who does not will eventually be eaten by code-degradation crabs.

Journey with Rust Part 4: First boss fight – fat pointer

Human asked: how can raw pointer be 16 bytes – that makes no sense. It should be just a normal pointer no?
Toaster thought for 20s and replied: Yeah, this is one of those “Rust is doing what?!” moments…

Intro

For a C++ programmer, learning Rust is as much fun as learning to ride a bicycle* – once you understand that assignment means move, everything starts rolling smoothly. Until one day when you encounter a Box inside a Box:

let inner: Box<dyn Debug> = Box::new(42);
let outer: Box<Box<dyn Debug>> = Box::new(inner);

You might think that winning a few battles against the compiler made you understand the language. Well, it didn’t. This is the moment you realize how much you don’t know, and that skipping all those pages of the user manual may not have been the best idea after all.

Congratulations: you’ve reached the point where the Rust journey starts to be really interesting… and dangerous. Now, let’s climb back inside the box.

The Simple View: Box as Dynamic Allocation

A Box is described as a way to store data on the heap – and for a long time that’s exactly how I treated it. Something like memory allocation (new in C++) combined with unique pointer in one single concept. Meaning this:

let boxed_int = Box::new(42);

Is equivalent to this:

auto ptr = std::make_unique<uint32_t>(42);

In both cases, you create an object that owns a pointer to a heap-allocated integer.

Simple.

But there is more in a Box…

Because Box can do more than simply allocate memory. What it stores depends on the type you put inside it. To keep things simple, let’s focus on one use case: dynamic polymorphism, aka trait objects in Rust.

We all know how this works in C++. Everyone has heard of the vtable (and if not, here’s a good explanation: vtable-and-vptr). Whenever a class uses virtual functions, the compiler generates a table of function pointers and places it somewhere in the binary. Each instance carries a hidden vptr pointing to that table. All invisible thanks to compiler magic.

Rust takes a slightly different approach. The vtable still exists, but the pointer to it does not live inside the object itself. Rust follows the “don’t pay for what you don’t use” principle: plain data stays plain and carries no hidden fields. As a result, when we use dynamic dispatch, Rust builds a special kind of pointer – a fat pointer – that contains both the data address and the vtable pointer. You can see this clearly if you inspect one:


And that explains why we sometimes end up with a Box inside a Box.

Because a Box<dyn Trait> is itself a fat pointer, and when we want to pass something that looks like a single thin pointer (for example to C code), we need to heap-allocate the inner trait object so the outer Box can remain thin. One Box holds the data; the other holds the fat pointer describing how to use it.

And that leads us straight to the next topic.

Fat pointers can be dangerous

Why? Because it’s very easy to accidentally destroy the metadata that makes them work.

Consider this code:

// Create trait object
let trait_object: Box<dyn Drinkable> = Box::new(Beer::new("IPC", 4.5));
println!("Size of trait_object: {}", std::mem::size_of_val(&trait_object));

// So far so good - we can drink our beer
trait_object.drink();

// Convert trait object to raw pointer
let beer_ptr = Box::into_raw(trait_object);
println!("Size of beer_ptr: {}", std::mem::size_of_val(&beer_ptr));

// Store the raw pointer as a void pointer (not good)
let c_ptr = beer_ptr as *mut ::std::os::raw::c_void;
println!("Size of c_ptr: {}", std::mem::size_of_val(&c_ptr));

// ... part below might sit megabytes of code away

// Cast the void pointer back to a trait object pointer (function expects thin pointer)
let bad_beer = unsafe { Box::from_raw(c_ptr as *mut Box<dyn Drinkable>) };
println!("Size of beer_ptr_2: {}", std::mem::size_of_val(&bad_beer));

bad_beer.drink();

Not good. Drinking last beer crashes the whole universe.

A Box<dyn Drinkable> is represented as a fat pointer(16 bytes on a 64-bit machine) that holds both a data pointer and a vtable pointer. When we call Box::into_raw, we get a raw pointer of type *mut dyn Drinkable which is still fat (16 bytes) and not just single memory address as one could expect.

The moment we cast it to *mut c_void, we throw away half of that information: the vtable pointer is gone, and only the data address remains. The compiler and Clippy are both fine with this – the cast is legal – but there is no magic that keeps the vtable ptr alive somewhere.

And when we later try to use that thin pointer as if it were still a fat one, very bad things happen.

Happy ending

There sits a big, fat lie in the example above. When we cast the C pointer back to a Box, we do this as if the original fat pointer had been wrapped inside a thin one – that’s why we cast to *mut Box.

The good news is that Rust will not let us cast directly to *mut dyn Drinkable. The compiler knows you can’t magically recreate a fat pointer out of 8 bytes (ask your toaster for std::mem::transmute if you want to see proper way to do this). In other words: Rust refuses to fabricate the missing vtable pointer. So we are partially saved.

Partially – because once everything “looks fine”, someone might decide that a Box inside a Box is one Box too many (“raw pointers are just pointers, right?”). One box removed, one universe destroyed.

The happy part? In 99% of real-world Rust code, nobody deals with these problems.
And if someone does… well, they knew what they signed up for.

Toaster last words

“Rust will protect you from yourself…
until you insist otherwise.
After that, it politely steps aside and lets physics handle the rest.”

Now that we’ve learned the secret art of shooting ourselves in the foot, we can ‘safely’ move on with our Rust adventure. The journey continues…

* ok – its like pedaling uphill on a bumpy road with ducks wandering in front of you every 10 seconds. No one ever said riding a bike was pure pleasure.