<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Raghu Bharadwaj</title>
    <description>The latest articles on DEV Community by Raghu Bharadwaj (@raghu_bharadwaj_404e60eb0c).</description>
    <link>https://dev.to/raghu_bharadwaj_404e60eb0c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1782496%2Ff6a2f0b0-5d25-4cfc-99b7-83f5e8326288.jpg</url>
      <title>DEV Community: Raghu Bharadwaj</title>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raghu_bharadwaj_404e60eb0c"/>
    <language>en</language>
    <item>
      <title>Kernel &amp; Embedded News: Linux 7.3-rc1 Out; NVMe 2.4 Goes Post-Quantum</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:50:25 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/kernel-embedded-news-linux-73-rc1-out-nvme-24-goes-post-quantum-4kl5</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/kernel-embedded-news-linux-73-rc1-out-nvme-24-goes-post-quantum-4kl5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Linux 7.3-rc1 shipped on 30 August, closing the second-busiest merge window in kernel history at 15,267 commits and removing two ancient filesystems and a set of unmaintained drivers. This edition covers what the close of the window settles: per-device UBI wear-leveling thresholds for NAND devices, sched_ext sub-scheduler support reaching feature complete, Qualcomm's plan to make its Modular acquisition an open, vendor-neutral AI software layer, and the NVMe 2.4 specification's post-quantum and power-control additions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The common thread this week is consolidation. The kernel's 7.3 picture is now fixed and can be planned against, and two industry moves — one in AI software, one in storage specifications — aim to reduce how much of a product's software stack has to be rebuilt when the hardware underneath it changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this edition
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux 7.3-rc1 closes the second-busiest merge window ever&lt;/strong&gt; — 15,267 commits, a near-41-million-line tree, and a wave of removals now locked in for an October release. &lt;em&gt;— plan your test window&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UBI gains per-device wear-leveling thresholds&lt;/strong&gt; — the threshold stops being one compile-time constant for every NAND device in the system. &lt;em&gt;— evaluate for mixed-flash designs&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sched_ext sub-scheduler support is feature complete&lt;/strong&gt; — a root BPF scheduler can now hand a cgroup subtree to a nested scheduler with revocable CPU grants. &lt;em&gt;— long-term watch&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qualcomm bets on an open AI software stack&lt;/strong&gt; — Modular's Mojo and MAX aim to make accelerator choice a hardware decision rather than a software rewrite. &lt;em&gt;— planning signal&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVMe 2.4 brings post-quantum security and power controls&lt;/strong&gt; — the specification set published in August reshapes what storage procurement will ask for. &lt;em&gt;— roadmap input&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Linux 7.3-rc1 closes the second-busiest merge window ever
&lt;/h2&gt;

&lt;p&gt;Linus Torvalds released 7.3-rc1 on 30 August and closed the merge window. The numbers are unusual: 15,267 commits were pulled in two weeks, a total only the 6.7 cycle has exceeded, and the tree now stands at 40.98 million lines. About a third of the rc1 patch is one item — AMD's DCN6 display register headers and the code for its next graphics generation — which alone pushed the AMD graphics driver directory to 6.52 million lines, roughly 16 percent of the entire kernel.&lt;/p&gt;

&lt;p&gt;We covered the first week of this window as it happened: the 32-bit Arm platform deprecations, and the crypto and AF_ALG changes in &lt;a href="https://www.techveda.live/2026/08/25/af-alg-restricted-by-default/" rel="noopener noreferrer"&gt;the 25 August edition&lt;/a&gt;. The second week settled the removals. The EFS filesystem, the read-only driver for SGI's pre-XFS IRIX format, is gone after more than twenty years without a maintainer. The freevxfs driver for Veritas VxFS is gone too; Christian Brauner's removal note records one known user in fifteen years. The SGI XP and GRU drivers, the IBM RSA management driver, the IPWireless PCMCIA driver and the old Moxa serial drivers were also dropped. Moxa PCIe multi-port serial boards, widely used in industrial systems, are not orphaned by this: they get a replacement in the new 8250_mxpcie driver.&lt;/p&gt;

&lt;p&gt;The removals have a counterpart on the additions side. The window that deleted EFS and freevxfs — about 3,700 lines between them — added FailFS, a 931-line pseudo-filesystem from Christian Brauner in which every operation fails with EOPNOTSUPP. Paired with a new fchroot() system call and an FD_FAILFS_ROOT sentinel, it lets a process shed its filesystem state completely: with its root in FailFS, every path lookup must be anchored at an explicit file descriptor, and absolute paths, absolute symlinks and AT_FDCWD-relative lookups simply fail. For anyone building sandboxed services, that is a cleaner primitive than the chroot-to-empty-directory conventions it replaces, and unprivileged processes can use it under no-new-privileges.&lt;/p&gt;

&lt;p&gt;Torvalds attributed part of the volume to AI coding agents, which are both finding bugs and generating patches at a rate maintainers have not seen before; the networking maintainers used their pull request to say plainly that the volume of machine-generated reports and patches has overwhelmed their review capacity. This is now a structural property of kernel development rather than a one-cycle anomaly, and it showed up in this window both as extra fixes and as extra deletions — several of the removed drivers went because they had become permanent targets for automated bug reports nobody could act on.&lt;/p&gt;

&lt;p&gt;On the filesystem side, David Sterba's Btrfs pull brought measured wins. Direct I/O moved onto the IOmap bounce buffer, lifting it from roughly half of theoretical throughput to around 95 percent; replacing an XArray with a local LRU list for tracking inhibited extent buffers yielded around 3x in that path; removing an unnecessary one-jiffy delay in the non-SSD mode with multiple logging tasks raised throughput around 5x on a sample workload; and full fsync of files with many extents but no holes gained a similar factor by skipping hole detection. The old free-space cache v1 code is also now disabled by default, completing a transition that began years ago. A six-week stabilisation series follows, with the release expected in the second half of October.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it means for engineering teams
&lt;/h3&gt;

&lt;p&gt;Treat rc1 as the feature freeze it is. If your product tracks mainline or you rebase a vendor kernel forward once a year, the 7.3 contents are now fixed enough to plan against: check whether any of the removed drivers or filesystems appear in your defconfig, and schedule a boot test of an rc on your reference hardware before October rather than after. Removals of this kind never announce themselves twice — the next signal you get is a build failure.&lt;/p&gt;

&lt;p&gt;A concrete check takes minutes. Grep your defconfigs and any fragment files for the removed symbols — EFS_FS, VXFS_FS, the SGI XP and GRU options, IBMASM — and for out-of-tree modules that touch the old Moxa serial drivers, which now map to 8250_mxpcie. Teams running Btrfs on build servers should also plan a before-and-after benchmark of their own workload during the rc series; the direct I/O and fsync changes are the kind that show up in CI wall-clock times.&lt;/p&gt;

&lt;h2&gt;
  
  
  UBI gains per-device wear-leveling thresholds
&lt;/h2&gt;

&lt;p&gt;UBI keeps an erase counter for every physical eraseblock it manages. When the difference between the highest and lowest counter on a device exceeds a threshold, the wear-leveling worker starts moving data from little-erased blocks to heavily-erased ones, evening out wear across the flash. Until this cycle that threshold was a single compile-time constant, &lt;code&gt;CONFIG_MTD_UBI_WL_THRESHOLD&lt;/code&gt;, default 4096, applied to every UBI device in the system. Merged for 7.3 is work by Ran Hongyun of Huawei that makes the threshold configurable per device at attach time, through a new wl_threshold option on the ubi.mtd module parameter, with a value of zero falling back to the compile-time constant.&lt;/p&gt;

&lt;p&gt;The kernel's own configuration help has documented the problem with a single value for years: the 4096 default is sized for SLC NAND and NOR with eraseblock endurance of 100,000 cycles or more, while MLC parts with endurance under 10,000 cycles want something like 128 or 256. A system could always pick one value or the other — by recompiling — but never both at once.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://www.techveda.live/2026/08/30/ubi-on-raw-nand/" rel="noopener noreferrer"&gt;UBI on raw NAND&lt;/a&gt; piece two days before this landed described wear-leveling as one of the two jobs UBI exists to do. This change addresses the case the single constant handled badly: a system carrying two NAND parts with different erase endurance — say an SLC boot flash beside a cheaper MLC data flash. One threshold tuned for the SLC part lets the MLC part accumulate a wear spread that is a large fraction of its whole life before leveling begins; one tuned for the MLC part makes the SLC device churn data it did not need to move, adding erase cycles and I/O load for nothing.&lt;/p&gt;

&lt;p&gt;There is a second, quieter benefit for product families. Because the threshold was a Kconfig constant, board variants with different flash parts previously needed either separate kernel builds or a compromise value; a boot-time per-device option lets one kernel image serve every variant, with the difference expressed in the bootloader arguments alongside the rest of the board configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to evaluate it for your device
&lt;/h3&gt;

&lt;p&gt;This matters at design time, not as a retrofit. If your board carries a single NAND part, the compile-time default remains fine and there is nothing to do. For mixed-flash designs, work out the endurance ratings of each part from the datasheets and set the boot-time threshold per device accordingly once you are on a 7.3-based kernel — the endurance figures in the flash datasheet, not habit, should set the number. The option arrives with 7.3, so for products on 6.12 or 6.18 LTS this is a note for the next platform revision rather than something to backport. One caution: the exact argument syntax should be read from the merged kernel documentation when you adopt it, since attach-time options have accumulated positional fields over the years and getting the field order wrong attaches the device with defaults silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  sched_ext sub-scheduler support is feature complete
&lt;/h2&gt;

&lt;p&gt;The sched_ext framework lets a BPF program replace the kernel's scheduling policy. Over the last three cycles it has been growing a hierarchy: 7.1 made the dispatch path hierarchical, 7.2 added the supporting infrastructure, and the enqueue path was still pending when we published &lt;a href="https://www.techveda.live/2026/08/17/sched-ext-scheduler-api/" rel="noopener noreferrer"&gt;our hands-on guide to writing a minimal scheduler&lt;/a&gt; last month. The 7.3 pull from maintainer Tejun Heo completes that path. &lt;strong&gt;Sub-scheduler support is now feature complete: a root BPF scheduler can hand a cgroup subtree to a nested sub-scheduler along with revocable CPU grants, and the sub-scheduler owns all scheduling decisions for its tasks on those CPUs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The word "completes" is doing precise work here. The 7.1 and 7.2 cycles could delegate only dispatching — a sub-scheduler could choose which queued task ran next, but the parent still controlled how tasks entered the queues, when one task could preempt another, and CPU frequency selection. With 7.3, parent schedulers grant and revoke all of those per-CPU capabilities, and the grants are enforced on every path a scheduler can reach a CPU through, not just the polite ones. The in-tree scx_qmap example now demonstrates the full hierarchy, which gives anyone building on this a working reference.&lt;/p&gt;

&lt;p&gt;The robustness work is as interesting as the delegation. A task whose scheduler has no access to the CPUs it needs used to starve until the watchdog ejected the entire scheduler; the kernel now runs such tasks directly on a small bandwidth budget, converting a scheduler-killing failure into bounded degradation. Tasks migrating across a sub-scheduler boundary are now re-homed to the new owner, closing both a wrong-scheduler-scheduling bug and a use-after-free. The abort path became NMI-safe, arena objects now cross the kernel/BPF boundary as typed pointers instead of manually translated untyped ones, and BPF-writable arena memory is validated before the kernel uses it — with explicit synchronisation rules for task slice and vtime writes — closing corruption paths open to buggy or hostile schedulers.&lt;/p&gt;

&lt;p&gt;One further detail matters for LTS users: the pull also carries core scheduling fixes that missed 7.2, including one where interleaved core-wide task selections could corrupt each other's state and hang the machine, and those fixes are marked for stable. They will reach the LTS branches through the normal stable process even if you never run a BPF scheduler.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it means for scheduler work
&lt;/h3&gt;

&lt;p&gt;Hierarchical scheduling is the piece that makes sched_ext credible for mixed systems — an infotainment stack and a telemetry stack on one SoC, each under a scheduler tuned for it, without either being able to damage the other. The failure-containment work matters as much as the delegation: a sub-scheduler bug now degrades its own subtree instead of taking down scheduling machine-wide, which is the property a safety argument or a multi-tenant design actually needs. If you experimented earlier and stepped back, 7.3 is the version where the sub-scheduler API stops moving.&lt;/p&gt;

&lt;p&gt;Prerequisites are unchanged: the kernel needs &lt;code&gt;CONFIG_SCHED_CLASS_EXT&lt;/code&gt; and a BPF stack with &lt;code&gt;CONFIG_DEBUG_INFO_BTF&lt;/code&gt;, the second of which is the one most often missing from custom embedded kernels. Confirm the framework on your kernel before planning anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/kernel/sched_ext/state
&lt;span class="go"&gt;enabled
&lt;/span&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/kernel/sched_ext/root/ops
&lt;span class="go"&gt;simple
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qualcomm bets on an open AI software stack
&lt;/h2&gt;

&lt;p&gt;Qualcomm completed its acquisition of Modular, the AI software company co-founded by compiler engineer Chris Lattner, in July. Details of the plan emerged last week, and they are worth the attention of anyone who has done an accelerator bring-up. Modular's stack — the Mojo language, now at 1.0 with its compiler and toolchain under the Apache 2.0 licence, and the MAX inference framework — aims to let the same model code run across GPUs, TPUs and custom accelerators from different vendors. On Nvidia hardware it replaces the CUDA math libraries and kernels wholesale, keeping only a small device-management layer.&lt;/p&gt;

&lt;p&gt;Qualcomm's own framing, from Rashid Attar, its head of data center engineering, is that a challenger vendor has two problems to solve in sequence: it must deliver differentiated hardware, and then it must make that hardware usable without the customer assigning engineers to port existing workloads — because, in his account, customers say they will assign exactly none. Modular is the attempt to remove the second problem. The reason it is hard is that the incumbent's advantage is no longer just CUDA: industry analysts point out that the software moat now extends across interconnects, data-processing units, libraries and integrated rack-scale systems, so a CUDA translation layer on its own no longer changes the decision.&lt;/p&gt;

&lt;p&gt;The central claim is about bring-up cost. In Modular's own account of porting to AMD's MI355X accelerator, two engineers brought the stack up in fourteen days, with 99.9 percent of the code architecture-agnostic; the hardware-specific remainder concerned new BF16 conversion instructions, larger tensor-core tiles and increased shared memory. The result, Modular says, outperformed AMD's optimised vLLM fork by up to 2.2 times on the workloads tested. Those are the vendor's own figures and should be read as such. But the structural argument stands independently: if the portable layer is thick and the per-silicon layer thin, new silicon competes on merit rather than on the size of its software ecosystem — and the engineering effort concentrates on the small part of the stack that actually changed.&lt;/p&gt;

&lt;p&gt;The obvious tension is that a neutrality layer is now owned by one of the vendors it arbitrates between. The mechanisms offered are concrete but partial: Mojo's compiler and toolchain sit under Apache 2.0, which Lattner describes as an irrevocable grant that cannot be withdrawn later; Attar describes organisational firewalls under which Modular engineers may see confidential data about competitors' hardware that Qualcomm's own accelerator teams will not; and an industry alliance giving other hardware companies a governance role is planned for later this year. Lattner's own summary of the neutrality question was that there is no perfect answer to it. The honest reading is that neutrality clearly serves Qualcomm while it is the challenger; the test comes if it stops being one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to read it as a planning signal
&lt;/h3&gt;

&lt;p&gt;For teams selecting edge or datacenter AI hardware, the question to track is whether the per-silicon bring-up layer really stays thin for parts beyond the large vendors. If it does, smaller NPU and accelerator vendors — which cannot afford to build a CUDA-class software ecosystem of their own — become viable choices, and hardware selection shifts toward performance per watt, availability and price. There is also a caution in the other direction: teams for whom every microsecond matters will keep optimising against one architecture directly, and an abstraction layer that hides hardware complexity can hide hardware advantages too. Watch the alliance's membership and licence terms when it launches; that list will say more than the benchmarks do.&lt;/p&gt;

&lt;h2&gt;
  
  
  NVMe 2.4 brings post-quantum security and power controls
&lt;/h2&gt;

&lt;p&gt;NVM Express published the NVMe 2.4 specification set on 4 August — earlier than this edition's window, but a development we had not covered and one with a long tail. The release also restructured the specification family itself, splitting it so that individual capabilities can be revised without republishing the whole base document; the stated aim is faster, simpler development of the specifications, which in practice means features will now arrive more continuously rather than in large multi-year revisions.&lt;/p&gt;

&lt;p&gt;The 2.4 feature set groups into three themes. Security: post-quantum cryptographic algorithms in the host-to-SSD data path, TLS 1.3 transport security for NVMe over fabrics, and Key-Per-I/O encryption, which allows a different key per individual I/O command rather than per namespace — the granularity multi-tenant systems have been asking for. Manageability: configuration recovery, and a new virtualization architecture that supports live migration of VMs using locally attached SSDs, removing one of the last reasons cloud operators avoided direct-attached NVMe for migratable workloads. Power and sustainability: controller-based performance controls and voltage monitoring, giving the host standardised control over a drive's draw. Updates to Zoned Namespaces, Key-Value, rotational media and endurance-group management are also included.&lt;/p&gt;

&lt;p&gt;Two of these matter directly to embedded and edge builders. The power and performance control features are relevant wherever storage sits inside a fixed thermal budget, which describes most fanless industrial and edge-AI boxes — today that constraint is handled with vendor-specific tooling or by over-provisioning the enclosure, and a standard host-side interface changes both. And the post-quantum additions will flow into procurement: regulated buyers are already writing quantum-resistant requirements into tenders, and drives claiming 2.4 compliance will become the easy way to satisfy them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it means for device builders
&lt;/h3&gt;

&lt;p&gt;Nothing ships tomorrow; specification revisions reach real drives over a couple of product generations. The near-term action is procurement language: when specifying storage for a product with a seven-to-ten-year life, ask vendors for their NVMe 2.4 roadmap, and treat post-quantum support in the data path as a feature that will be demanded mid-life even if no one is asking today. Remember also that specification features are individually optional — "NVMe 2.4 compliant" on a datasheet does not by itself mean the drive implements Key-Per-I/O or the power controls, so procurement questions should name the specific features, not the revision number. For Linux support of the new features, watch the NVMe driver changes in the next two or three kernel cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lwn.net/Articles/1091421/" rel="noopener noreferrer"&gt;Kernel prepatch 7.3-rc1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/releases.json" rel="noopener noreferrer"&gt;kernel.org release data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-7.3-rc1-Code-Stats" rel="noopener noreferrer"&gt;Linux 7.3-rc1 code statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/review/linux-73-features" rel="noopener noreferrer"&gt;Linux 7.3 features overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-File-System-Changes-7.3" rel="noopener noreferrer"&gt;EFS and freevxfs removal, FailFS merge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/lkml/20260814-vfs-7.3-rc1.freevxfs-3ea96320f471@brauner/" rel="noopener noreferrer"&gt;freevxfs removal pull request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-7.3-Btrfs" rel="noopener noreferrer"&gt;Btrfs changes for Linux 7.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-7.3-UBI" rel="noopener noreferrer"&gt;UBI per-device wear-leveling thresholds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/lkml/7545938.rdbgypaU67@nailgun/" rel="noopener noreferrer"&gt;UBI/MTD pull request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/drivers/mtd/ubi/Kconfig" rel="noopener noreferrer"&gt;UBI Kconfig (wear-leveling threshold)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-7.3-sched-ext" rel="noopener noreferrer"&gt;sched_ext sub-scheduler support feature complete&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/lkml/29c6986172b4eb9ba292643ab4ad9583@kernel.org/" rel="noopener noreferrer"&gt;sched_ext pull request for 7.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.eetimes.com/qualcomm-bets-open-source-ai-software-can-break-nvidias-lock-in/" rel="noopener noreferrer"&gt;Qualcomm and Modular's open AI software stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.qualcomm.com/news/releases/2026/07/qualcomm-completes-acquisition-of-modular" rel="noopener noreferrer"&gt;Qualcomm completes acquisition of Modular&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.modular.com/blog/achieving-state-of-the-art-performance-on-amd-mi355----in-just-14-days" rel="noopener noreferrer"&gt;Modular MI355X case study&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.eetimes.com/nvme-2-4-update-adds-post-quantum-security-power-controls/" rel="noopener noreferrer"&gt;NVMe 2.4 analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.morningstar.com/news/business-wire/20260804315628/nvm-express-publishes-set-of-nvme-specifications-enhancing-security-manageability-and-sustainability-for-ai-cloud-enterprise-and-client-storage" rel="noopener noreferrer"&gt;NVM Express 2.4 press release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nvmexpress.org/specification/nvm-express-base-specification/" rel="noopener noreferrer"&gt;NVMe base specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— Raghu Bharadwaj&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you work on the Linux kernel or embedded Linux and want structured, instructor-led depth on these subsystems, see &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;TECH VEDA's training programs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>embedded</category>
      <category>storage</category>
    </item>
    <item>
      <title>Open-Source Device CVEs: What to Patch by Vertical (August 2026)</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:48:00 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/open-source-device-cves-what-to-patch-by-vertical-august-2026-16jd</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/open-source-device-cves-what-to-patch-by-vertical-august-2026-16jd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;August 2026 brought 11 open-source device CVEs and advisories worth acting on across three packages: &lt;code&gt;U-Boot&lt;/code&gt;, &lt;code&gt;systemd&lt;/code&gt;, and &lt;code&gt;BlueZ&lt;/code&gt;.&lt;/strong&gt; Only five of the 11 carry a CVE number, which is the main thing to take from this month. None is in the CISA Known Exploited Vulnerabilities catalog. Proof-of-concept code is published for two, and we found no evidence of exploitation in the wild. Most have a fix upstream, but several BlueZ fixes exist only as commits and two have no fix at all, so the action is to take your distribution's patched package or cherry-pick, then rebuild your image.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the device stack beyond the kernel. It is the bootloader, the C library, the init system, the TLS and networking code, the media pipeline, and the container runtime that sit inside a shipped image. Which of these open-source device CVEs matter to you depends on what is actually in your image and your software bill of materials (SBOM).&lt;/p&gt;

&lt;p&gt;The EU Cyber Resilience Act creates software-inventory and vulnerability-handling duties that make SBOM-style tracking operationally necessary, so the SBOM is the list you check this report against. This month the pressure was on the Bluetooth stack. BlueZ published nine advisories in August, more than the rest of our watchlist combined, and most of them carry no CVE number.&lt;/p&gt;

&lt;p&gt;A note on coverage. This edition swept the bootloader, C library, init and Bluetooth parts of the stack in full. We did not complete a sweep of the TLS and crypto libraries, the remaining connectivity packages, the remote-access tools, the parsing libraries, the media pipeline, or the container runtime this month. Those are not reported as clear; they are unexamined, and we would rather say so than imply a clean result we did not verify.&lt;/p&gt;

&lt;p&gt;We report a package CVE only when it meets one of three tests: a CVSS base score of 7.0 or higher, or a place in the CISA KEV catalog, or clear reachability in a normal device build. Severity numbers come from the source named for each item, because upstream projects, NVD enrichment, and downstream scorers such as the CISA ADP do not always score the same issue the same way.&lt;/p&gt;

&lt;p&gt;Kernel CVEs are not here; they are covered by our weekly kernel advisory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most of this month's device stack work has no CVE number
&lt;/h2&gt;

&lt;p&gt;The main finding this month is not a single vulnerability. It is that a CVE feed would have shown you almost none of this. BlueZ published nine security advisories in August 2026. &lt;strong&gt;Seven of them have no CVE identifier at all.&lt;/strong&gt; The systemd project fixed a root-level &lt;code&gt;udev&lt;/code&gt; issue in the same month, also with no CVE. If your process is to scan an SBOM for CVE identifiers and patch what comes back, that process found two BlueZ issues in August. The project published nine.&lt;/p&gt;

&lt;p&gt;The systemd case shows the shape of the problem clearly. The &lt;code&gt;scsi_id&lt;/code&gt; helper did not sanitise device metadata read from the kernel, so the &lt;code&gt;ID_WWN&lt;/code&gt;, &lt;code&gt;ID_WWN_VENDOR_EXTENSION&lt;/code&gt;, and &lt;code&gt;ID_WWN_WITH_EXTENSION&lt;/code&gt; values could carry an embedded newline. That allowed an attacker to inject a &lt;code&gt;SYSTEMD_WANTS=&lt;/code&gt; line and start a unit of their choosing as root. Plugging in a malicious SCSI or iSCSI device is enough to trigger it, because &lt;code&gt;udev&lt;/code&gt; runs at boot as root and parses whatever the device reports.&lt;/p&gt;

&lt;p&gt;It was fixed in systemd &lt;code&gt;261&lt;/code&gt;, &lt;code&gt;260.2&lt;/code&gt;, &lt;code&gt;259.6&lt;/code&gt;, and &lt;code&gt;258.8&lt;/code&gt;, and published as advisory &lt;code&gt;GHSA-m8q3-73v4-wvg7&lt;/code&gt;. No CVE was assigned, so no CVE-based scan will ever raise it.&lt;/p&gt;

&lt;p&gt;This matters for the Cyber Resilience Act specifically. The duty is to know which known vulnerabilities affect your product. A known vulnerability with a published upstream advisory and a published fix is a known vulnerability whether or not anyone minted an identifier for it.&lt;/p&gt;

&lt;p&gt;Tracking the advisory feeds of the packages in your SBOM, alongside the CVE feed, is the practical way to meet that. This month is a good argument for doing so: on the CVE feed alone you would have seen roughly a fifth of what BlueZ actually told its users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open-source device CVEs to update this month
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;CVE(s) / advisory&lt;/th&gt;
&lt;th&gt;Bug type&lt;/th&gt;
&lt;th&gt;Fixed version&lt;/th&gt;
&lt;th&gt;Verticals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;U-Boot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CVE-2025-70290&lt;/code&gt;, &lt;code&gt;CVE-2025-70293&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Integer overflows in ZFS and ext4 filesystem parsing at boot. See the note on scoring below&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2026.04&lt;/code&gt;. Recommended baseline &lt;code&gt;2026.07&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Embedded/IoT, Mobile/Automotive, Medical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd&lt;/code&gt; (&lt;code&gt;systemd-oomd&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CVE-2026-15059&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local denial of service. A world-writable varlink socket lets any local user make the privileged daemon kill a chosen process (5.5, systemd CNA)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;261&lt;/code&gt;, &lt;code&gt;260.3&lt;/code&gt;, &lt;code&gt;259.7&lt;/code&gt;, &lt;code&gt;258.9&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cloud/Datacenter, Embedded/IoT, Medical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;systemd&lt;/code&gt; (&lt;code&gt;udev&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No CVE. &lt;code&gt;GHSA-m8q3-73v4-wvg7&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Unit activation as root. Unsanitised SCSI metadata allows &lt;code&gt;SYSTEMD_WANTS=&lt;/code&gt; injection. Needs physical port access (6.4, systemd CNA)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;261&lt;/code&gt;, &lt;code&gt;260.2&lt;/code&gt;, &lt;code&gt;259.6&lt;/code&gt;, &lt;code&gt;258.8&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mobile/Automotive, Embedded/IoT, Medical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;BlueZ&lt;/code&gt; (radio range)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80186&lt;/code&gt;, &lt;code&gt;CVE-2026-75032&lt;/code&gt;, and &lt;code&gt;GHSA-9683-2chf-hfw9&lt;/code&gt; (no CVE)&lt;/td&gt;
&lt;td&gt;Stack overflow in &lt;code&gt;name2utf8&lt;/code&gt; via a crafted discovery packet, needing no pairing (7.2). Out-of-bounds read in AVRCP parsing, needing a paired device (6.3). Stack overflow in the LE Audio BASE parser, pre-authentication and over the air (low)&lt;/td&gt;
&lt;td&gt;No release. Commit &lt;code&gt;381b5d0d&lt;/code&gt;; commits &lt;code&gt;bd898962&lt;/code&gt; and &lt;code&gt;58088149&lt;/code&gt;. The LE Audio issue is &lt;strong&gt;unfixed&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Mobile/Automotive, Embedded/IoT, Medical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;BlueZ&lt;/code&gt; (local, via D-Bus)&lt;/td&gt;
&lt;td&gt;No CVEs. &lt;code&gt;GHSA-4p57-mrcv-r2jc&lt;/code&gt;, &lt;code&gt;GHSA-75v6-6q44-57hc&lt;/code&gt;, &lt;code&gt;GHSA-rfcf-9fw3-m9qp&lt;/code&gt;, &lt;code&gt;GHSA-hhgc-hfgf-8m4x&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Two &lt;code&gt;sdp-xml&lt;/code&gt; issues via &lt;code&gt;RegisterProfile&lt;/code&gt;: quadratic parsing denial of service and a heap leak. Two advertisement monitor heap overflows. All need a local caller on the system bus&lt;/td&gt;
&lt;td&gt;No release. Commits &lt;code&gt;308e3536&lt;/code&gt;, &lt;code&gt;ae6c543e&lt;/code&gt;, &lt;code&gt;14347c86&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mobile/Automotive, Embedded/IoT, Medical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on the U-Boot scoring. &lt;code&gt;CVE-2025-70290&lt;/code&gt; carries a 9.8, but that score comes only from the CISA ADP auto-scorer, which treated it as network-reachable. There is no NVD primary analysis and no score from the project. The bug parses a filesystem image from local media, so the realistic reading is code execution before the operating system starts, triggered by attacker-supplied storage, rather than anything remote. &lt;code&gt;CVE-2025-70293&lt;/code&gt; has no score from any scorer and is listed here on reachability alone.&lt;/p&gt;

&lt;p&gt;Two further BlueZ advisories are outside the table. &lt;code&gt;GHSA-7c3p-mgcr-hf6w&lt;/code&gt;, an out-of-bounds access in &lt;code&gt;amp_dump_chanlist&lt;/code&gt;, is the one August issue with a real tagged fix: it affects 5.79 and earlier and is fixed in &lt;code&gt;5.80&lt;/code&gt;, so most current images already have it. &lt;code&gt;GHSA-7wjj-8mrm-jhw4&lt;/code&gt;, a heap buffer overflow in the BASS broadcast source builder, affects 5.76 and earlier and has no fix, but devices on a recent BlueZ are already past the affected range.&lt;/p&gt;

&lt;p&gt;Packages checked this month with nothing that met the inclusion tests: Trusted Firmware-A, GRUB2, musl, and D-Bus. The glibc project published two issues in August, both scored below 7.0 and neither reachable in a normal device build, so they are not listed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch item.&lt;/strong&gt; BlueZ &lt;code&gt;CVE-2026-80185&lt;/code&gt; is a type confusion in &lt;code&gt;sdp-xml.c&lt;/code&gt;, reached through &lt;code&gt;RegisterProfile&lt;/code&gt; with crafted nested &lt;code&gt;ServiceRecord&lt;/code&gt; data. A local caller who can reach &lt;code&gt;org.bluez.ProfileManager1.RegisterProfile&lt;/code&gt; on the system bus can crash &lt;code&gt;bluetoothd&lt;/code&gt; with it. The advisory records no released fix and gives the fixed version as unknown, so there is nothing to update to yet. Until there is, you can restrict untrusted local access to &lt;code&gt;org.bluez&lt;/code&gt; on the system bus.&lt;/p&gt;

&lt;p&gt;One caution on that advisory, &lt;code&gt;GHSA-7mmr-gwqx-vc34&lt;/code&gt;. Its title says the issue "leads to arbitrary code execution as root", but its own body says the opposite: "the available evidence establishes local denial of service only", and it is scored 5.5 with no confidentiality or integrity impact. The advisory also states it was generated using AI and asks readers to review it before use. The body and the score are the parts to trust here. If you triage from advisory titles, this one would have sent you after the wrong thing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Order the work by reachability rather than by score, because the scores this month are unreliable in both directions. Take the two BlueZ issues that need no pairing first: &lt;code&gt;CVE-2026-80186&lt;/code&gt; at 7.2 with a published proof of concept, and the LE Audio BASE parser overflow, which its advisory rates low but describes as pre-authentication and over the air, and which has no fix.&lt;/p&gt;

&lt;p&gt;Then the systemd &lt;code&gt;udev&lt;/code&gt; issue, if your device has an accessible physical port. Then the U-Boot filesystem issues, if your boot flow reads media a user can replace. The local BlueZ and &lt;code&gt;systemd-oomd&lt;/code&gt; issues come last on a single-user embedded device, and considerably higher on a shared or multi-tenant host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile and automotive
&lt;/h2&gt;

&lt;p&gt;On automotive ARM SoCs that actually ship U-Boot, it runs first in the boot chain. Many platforms instead use a vendor fork, a TF-A chain, or a proprietary first stage, so this applies only where U-Boot is in the image.&lt;/p&gt;

&lt;p&gt;The August issues are in filesystem parsing rather than the network path. &lt;code&gt;CVE-2025-70290&lt;/code&gt; is an integer overflow in ZFS metadata parsing and &lt;code&gt;CVE-2025-70293&lt;/code&gt; is an integer overflow in the ext4 block group descriptor table. Both are reached when the bootloader reads a filesystem image that an attacker controls, which in practice means removable media or a service partition.&lt;/p&gt;

&lt;p&gt;Before treating either as urgent, confirm that the affected filesystem support is enabled in your U-Boot configuration and that the boot flow actually reads content an attacker could influence. The fixes first landed upstream in &lt;code&gt;2026.04&lt;/code&gt;. We recommend &lt;code&gt;2026.07&lt;/code&gt; or later as the practical baseline, because it clears these filesystem issues and the July network-path issues together.&lt;/p&gt;

&lt;p&gt;Bluetooth is the other exposed surface in a vehicle, because the head unit pairs with phones the manufacturer does not control. Three of the August BlueZ issues are reachable from a device within radio range, and they differ in what the attacker must first achieve.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CVE-2026-80186&lt;/code&gt; is a stack buffer overflow in &lt;code&gt;name2utf8&lt;/code&gt; triggered by a crafted Extended Inquiry Response packet during discovery, so it needs no pairing at all. The LE Audio BASE parser overflow in &lt;code&gt;GHSA-9683-2chf-hfw9&lt;/code&gt; is described by its advisory as pre-authentication and over the air, so it also needs no pairing, and it has no fix. &lt;code&gt;CVE-2026-75032&lt;/code&gt;, the AVRCP out-of-bounds read, does need a paired device. Radio range is a real constraint, but a car in a car park is within range of anyone standing near it.&lt;/p&gt;

&lt;p&gt;There is no BlueZ release to move to. Every August fix exists as a commit rather than a tagged version, and two of the issues have no fix at all. The practical options are your distribution's patched package, which for Debian covers only the AVRCP issue in &lt;code&gt;5.87-2&lt;/code&gt;, or cherry-picking the commits listed in the table. Note also that one of the advertisement monitor overflows affects &lt;code&gt;5.87&lt;/code&gt;, the newest release, so moving to the latest tag does not by itself clear the month.&lt;/p&gt;

&lt;p&gt;The infotainment media pipeline is normally the largest surface in a vehicle, because FFmpeg, GStreamer and a browser engine all decode untrusted input. We did not sweep those packages this month, so treat their absence here as unexamined rather than clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded and IoT
&lt;/h2&gt;

&lt;p&gt;On small devices the bootloader and the network client are the parts that touch untrusted input. Update U-Boot to &lt;code&gt;2026.07&lt;/code&gt; if the device mounts a filesystem the user can replace, for example an SD card or a USB stick used for field updates. Minimal images that use BusyBox init or a cut-down &lt;code&gt;udev&lt;/code&gt; replacement are not affected by the systemd issues at all.&lt;/p&gt;

&lt;p&gt;If your image does run a full systemd, take that update too. The &lt;code&gt;udev&lt;/code&gt; &lt;code&gt;scsi_id&lt;/code&gt; issue is most relevant on this class of device, because they often expose a physical USB port and run &lt;code&gt;udev&lt;/code&gt; as root at boot.&lt;/p&gt;

&lt;p&gt;If your device speaks Bluetooth, the BlueZ items above apply here as much as they do in a vehicle, and small devices are often the ones running an older BlueZ that no one has moved in years. Check which version is actually in your image. The rest of the small-device set, including BusyBox, Dropbear, the embedded TLS libraries and the compression and parsing libraries, was not swept this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud and datacenter
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;systemd-oomd&lt;/code&gt; issue is the one that matters most on multi-tenant hosts. Its varlink socket is world-writable at mode 0666, so any local unprivileged account can ask the privileged daemon to kill a process it chooses, and a working proof of concept is published. On a shared host that is a denial-of-service primitive against other tenants' workloads. Update systemd to &lt;code&gt;261&lt;/code&gt;, &lt;code&gt;260.3&lt;/code&gt;, &lt;code&gt;259.7&lt;/code&gt;, or &lt;code&gt;258.9&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you cannot take the update immediately, a drop-in setting &lt;code&gt;SocketMode=0600&lt;/code&gt; on the socket unit closes it, and &lt;code&gt;systemd-oomd&lt;/code&gt; is not enabled on many minimal server images in the first place.&lt;/p&gt;

&lt;p&gt;The four local BlueZ issues also belong here rather than in the field, because they need a caller on the system bus. On a host where untrusted workloads share a machine, narrow the D-Bus policy for &lt;code&gt;org.bluez&lt;/code&gt; if the service is present at all; on most servers it should not be. The container runtime is usually the highest-severity item in this section, and it was not swept this month, so check containerd and runc separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Medical devices
&lt;/h2&gt;

&lt;p&gt;Connected medical devices draw their packages from the other three stacks, so the risk is whichever of these is network-reachable or parses untrusted clinical data. Bluetooth is the clearest example this month. Patient monitors, wearables, glucose meters and hospital gateways commonly use BlueZ to pair with sensors and phones, and the radio-reachable issues above need only a device within range. In a ward or a waiting room that is a low bar.&lt;/p&gt;

&lt;p&gt;Weigh &lt;code&gt;CVE-2026-80186&lt;/code&gt; first, because a crafted discovery packet reaches it without a completed pairing, and a proof of concept is published. The LE Audio BASE parser issue deserves attention too, because it is also pre-authentication and currently has no fix, which makes it a candidate for compensating controls rather than patching.&lt;/p&gt;

&lt;p&gt;The absence of CVE numbers is a particular problem in this sector. Medical device software inventories and regulatory submissions are frequently organised around CVE identifiers. Seven of the nine BlueZ advisories this month have none, so a device file that tracks CVEs alone will not show them, and a supplier questionnaire asking "which CVEs affect this device" will get an answer that is technically accurate and materially incomplete. Ask suppliers about upstream advisories, not only CVEs.&lt;/p&gt;

&lt;p&gt;The packages that parse clinical data and medical video, including SQLite, expat, libxml2, FFmpeg and GStreamer, were not swept this month. On a connected medical device those are usually where the untrusted DICOM, HL7 and FHIR content lands.&lt;/p&gt;

&lt;p&gt;Of the confirmed items, U-Boot applies to devices that mount a replaceable filesystem during service, and the systemd &lt;code&gt;udev&lt;/code&gt; issue applies to any device with an accessible physical port, which includes bedside and cart-mounted equipment. Medical-device updates go through validation and regulatory change control, including US FDA postmarket cybersecurity guidance, EU MDR, and IEC 62304, so the action is to schedule these updates through the device manufacturer's established change-control process rather than patching in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check if it applies to you
&lt;/h2&gt;

&lt;p&gt;Three questions decide whether any row above is your problem. First, is the package in your image and your SBOM at all? If it is not built into the product, its CVEs are not yours. Second, is the vulnerable feature actually built or enabled? The U-Boot issues need the relevant filesystem support enabled and a boot flow that reads attacker-influenced content; the &lt;code&gt;systemd-oomd&lt;/code&gt; issue needs that daemon to be running.&lt;/p&gt;

&lt;p&gt;Third, is your installed version below the fixed version? Distributions often backport a fix without matching the upstream version number, so a vendor package that looks older may already carry the patch. Check your distribution's changelog before assuming you are exposed. For most of the BlueZ items this question is harder than usual, because there is no fixed version to compare against, only a commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemctl &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;systemd 259 (259.4-1)
&lt;/span&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemctl is-active systemd-oomd
&lt;span class="go"&gt;active
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the device reports systemd 259.4 and confirms that &lt;code&gt;systemd-oomd&lt;/code&gt; is running. 259.4 is below the fixed 259.7 on that branch, and the affected daemon is active, so the update applies. For the bootloader, read the version from your build manifest or the boot log rather than from the running system. For libraries without a version command, read the version from your SBOM and compare it to the fixed version in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Most of this month's device stack work carries no CVE number. BlueZ published nine advisories in August and seven have no CVE; the systemd &lt;code&gt;udev&lt;/code&gt; fix has none either. A CVE-only process saw a fraction of it.&lt;/li&gt;
&lt;li&gt;BlueZ has no release to move to. Every August fix is a commit rather than a tagged version, two issues have no fix at all, and one advertisement monitor overflow affects &lt;code&gt;5.87&lt;/code&gt;, the newest release. Moving to the latest tag does not clear the month.&lt;/li&gt;
&lt;li&gt;The two issues that need no pairing are the ones to weigh first: the &lt;code&gt;name2utf8&lt;/code&gt; overflow (&lt;code&gt;CVE-2026-80186&lt;/code&gt;, 7.2, proof of concept published) and the LE Audio BASE parser overflow, which is pre-authentication and unfixed.&lt;/li&gt;
&lt;li&gt;The U-Boot ZFS score of 9.8 comes from the CISA ADP auto-scorer and treats a local media-parsing bug as network-reachable. Take the update, but do not let that number set your priority order.&lt;/li&gt;
&lt;li&gt;Nothing in this roundup is in the CISA KEV catalog. We checked the whole catalogue against the whole watchlist; only OpenSSL and WebKitGTK appear at all, for historic entries from 2014 and 2019.&lt;/li&gt;
&lt;li&gt;Advisory titles are not a triage signal. &lt;code&gt;GHSA-7mmr-gwqx-vc34&lt;/code&gt; is titled "arbitrary code execution as root" and its own body concludes local denial of service only.&lt;/li&gt;
&lt;li&gt;This edition swept the bootloader, C library, init and Bluetooth stack only. The TLS libraries, remote-access tools, parsing libraries, media pipeline and container runtime were not examined, so treat them as unchecked rather than clear.&lt;/li&gt;
&lt;li&gt;Map each CVE to your SBOM, confirm the feature is built and your version is below the fix, then update the package and rebuild the image.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bluez/bluez/security/advisories" rel="noopener noreferrer"&gt;BlueZ security advisories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/systemd/systemd/security/advisories" rel="noopener noreferrer"&gt;systemd security advisories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.u-boot.org/en/latest/develop/security.html" rel="noopener noreferrer"&gt;U-Boot security notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://security-tracker.debian.org/tracker/source-package/bluez" rel="noopener noreferrer"&gt;Debian security tracker, BlueZ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://security-tracker.debian.org/tracker/source-package/u-boot" rel="noopener noreferrer"&gt;Debian security tracker, U-Boot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog" rel="noopener noreferrer"&gt;CISA Known Exploited Vulnerabilities catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;BlueZ advisories without a CVE: &lt;a href="https://github.com/bluez/bluez/security/advisories/GHSA-9683-2chf-hfw9" rel="noopener noreferrer"&gt;GHSA-9683-2chf-hfw9&lt;/a&gt;, &lt;a href="https://github.com/bluez/bluez/security/advisories/GHSA-4p57-mrcv-r2jc" rel="noopener noreferrer"&gt;GHSA-4p57-mrcv-r2jc&lt;/a&gt;, &lt;a href="https://github.com/bluez/bluez/security/advisories/GHSA-75v6-6q44-57hc" rel="noopener noreferrer"&gt;GHSA-75v6-6q44-57hc&lt;/a&gt;, &lt;a href="https://github.com/bluez/bluez/security/advisories/GHSA-rfcf-9fw3-m9qp" rel="noopener noreferrer"&gt;GHSA-rfcf-9fw3-m9qp&lt;/a&gt;, &lt;a href="https://github.com/bluez/bluez/security/advisories/GHSA-hhgc-hfgf-8m4x" rel="noopener noreferrer"&gt;GHSA-hhgc-hfgf-8m4x&lt;/a&gt;, and the systemd &lt;a href="https://github.com/systemd/systemd/security/advisories/GHSA-m8q3-73v4-wvg7" rel="noopener noreferrer"&gt;GHSA-m8q3-73v4-wvg7&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.techveda.live/2026/09/01/open-source-device-cves-aug-2026/" rel="noopener noreferrer"&gt;techveda.live&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>embedded</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kernel CVEs Per Release Have Tripled. The Bugs Have Not.</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:53:33 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/kernel-cves-per-release-have-tripled-the-bugs-have-not-1bd5</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/kernel-cves-per-release-have-tripled-the-bugs-have-not-1bd5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A chart published on 28 August shows kernel CVEs per release climbing from roughly 500 to beyond 1,500 since Linux 7.0. Read carelessly it says the kernel is getting less safe. The stable maintainer confirmed in the same thread that the axis counts CVEs &lt;em&gt;fixed&lt;/em&gt;, not introduced, which makes the rise a measure of how much better the tooling has become at finding old bugs. The number that should worry an embedded team is a different one he mentioned in passing: the release that introduced the most CVEs is 5.15, a longterm kernel still shipping in products today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A kernel CVE chart went around at the end of last week and was widely read as bad news. The shape is genuinely dramatic — a long flat stretch at about 500 CVEs per release, then a step up past a thousand, then past fifteen hundred, with the next release plausibly breaking two thousand. If you ship a Linux product and someone forwarded you that chart, the question you were asked was probably some version of "is the kernel falling apart?" The answer is no, and the reason is in the axis label rather than the curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the chart actually measures
&lt;/h2&gt;

&lt;p&gt;The chart came from Greg Kroah-Hartman, who runs the stable trees and the kernel's CVE assignment team, as a teaser for a talk he is giving at Kernel Recipes in Paris in September. It plots CVEs per release from 6.9 through 7.2. Reading the plot: the 6.9 to 6.19 range sits at roughly 500 per release, every release from 7.0 onward is above one thousand, and 7.2 passes fifteen hundred.&lt;/p&gt;

&lt;p&gt;In the replies, someone asked the question that decides what the chart means — is this CVEs fixed, or CVEs found in newly added code? The answer was one word: &lt;strong&gt;"Fixed."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That single word inverts the story. A rising count of fixed vulnerabilities in a codebase of roughly 40 million lines is not evidence that the code is getting worse. It is evidence that more of what was already there is being found. A second reply in the same thread made the limitation explicit: the chart shows discovery and fixing, but not how long each flaw sat in the tree being exploitable. Greg Kroah-Hartman also confirmed that longterm kernels are where these fixes actually land, and that the chart does not attempt to show that, because kernel releases form a graph rather than a single line in time.&lt;/p&gt;

&lt;p&gt;His own explanation for the jump was dry rather than alarmed — that it looks as though some tools recently got better at finding bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corroborating numbers
&lt;/h2&gt;

&lt;p&gt;A chart on a slide is one data point, and a kernel CVE story should not rest on one. The independent development statistics for 7.2 show the same inflection from a completely different direction, counting commits rather than CVE identifiers.&lt;/p&gt;

&lt;p&gt;The 7.2 release contains &lt;strong&gt;4,830 commits carrying Fixes tags&lt;/strong&gt;, meaning commits that repair a bug introduced by an earlier commit. Plotted across releases, the count of commits with Fixes tags is roughly level for many releases and then rises sharply at the right-hand edge — the same shape, from a measurement that has nothing to do with CVE assignment. The analysis attributes it to the same cause: large numbers of people pointing language models at the task of finding bugs in existing software.&lt;/p&gt;

&lt;p&gt;That analysis also carries a caution worth repeating. There is a crossover point around 5.9 where the kernel appears to start fixing more bugs than it introduces, but that conclusion is not stable, because it takes years for the bugs introduced in any given release to be found. Every year that passes moves the line.&lt;/p&gt;

&lt;p&gt;One more figure sets the scale of what is coming. Around 14,000 non-merge changesets were queued in linux-next for 7.3, and more than 1,100 of them carry an &lt;code&gt;Assisted-by&lt;/code&gt; tag. That is a lower bound on machine involvement, since it counts only the contributors who declared it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the kernel CVE count is high by policy
&lt;/h2&gt;

&lt;p&gt;There is a second reason the numbers look extreme, and it has nothing to do with tooling. It is written into the kernel's own CVE policy, and most people quoting CVE counts have not read it.&lt;/p&gt;

&lt;p&gt;The kernel became its own CVE Numbering Authority so that assignment would be controlled by the people who understand the code. The policy document is blunt about the consequence. Because the kernel sits at a layer where almost any bug might be exploitable, and because exploitability is usually not evident at the time a bug is fixed, the assignment team is deliberately over-cautious and assigns a CVE number to any bugfix it identifies — which, in the document's own words, explains the seemingly large number of CVEs the kernel team issues.&lt;/p&gt;

&lt;p&gt;Two further rules in that policy matter for anyone doing kernel CVE triage on a product. No CVE is assigned for an issue found in a kernel version that the stable team is not actively supporting, so a count of assigned CVEs tells you nothing about a kernel that fell off the supported list. And the policy states plainly that applicability is the user's problem, not the assignment team's.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that actually matters for embedded products
&lt;/h2&gt;

&lt;p&gt;Buried in a reply, and absent from the coverage, is the figure that should change what an embedded team does this week. Asked for a chart of CVEs &lt;em&gt;introduced&lt;/em&gt; per release rather than fixed, Greg Kroah-Hartman declined to publish one before the talk but gave away the result: &lt;strong&gt;5.15 leads as the release that created the most CVEs&lt;/strong&gt;, and he offered no explanation for why.&lt;/p&gt;

&lt;p&gt;Take that seriously for a moment. 5.15 is not a historical curiosity. It is one of the longterm kernels listed as maintained on kernel.org, and it is still the base for a large number of shipping embedded products, because vendor board support packages have long lifetimes and 5.15 was the default choice for several silicon generations.&lt;/p&gt;

&lt;p&gt;Two qualifications keep that honest. First, "most CVEs created" is partly a function of how much attention a release has received and how long it has been available to analyse, so it is not a clean quality ranking. Second, those bugs are fixed in the 5.15 stable series as they are found — that is exactly what the stable process does. The conclusion is not that 5.15 is unsafe. It is that a 5.15 product which is not tracking the stable series closely is accumulating known, fixed-upstream defects faster than a product on any other longterm base.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the published CVE data does and does not tell you
&lt;/h2&gt;

&lt;p&gt;Published kernel CVE counts are a different measurement again, on a different denominator, and mixing them with per-release counts produces nonsense. Counting by publication date from the national vulnerability database rather than by kernel release, 2026 has 5,061 kernel CVEs published so far. The monthly distribution is wildly uneven:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month, 2026&lt;/th&gt;
&lt;th&gt;Kernel CVEs published&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;January&lt;/td&gt;
&lt;td&gt;246&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March&lt;/td&gt;
&lt;td&gt;178&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April&lt;/td&gt;
&lt;td&gt;379&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May&lt;/td&gt;
&lt;td&gt;1,034&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June&lt;/td&gt;
&lt;td&gt;514&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July&lt;/td&gt;
&lt;td&gt;838&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August (partial)&lt;/td&gt;
&lt;td&gt;1,652&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those swings are mostly publication batching rather than real changes in discovery rate, which is the first reason not to build a metric on this series. Of the 5,061, some 1,033 are still awaiting a severity score, so a fifth of the year's kernel CVEs have no CVSS number at all. And exactly two of them appear in the catalogue of vulnerabilities confirmed as being exploited in the wild.&lt;/p&gt;

&lt;p&gt;Hold those two numbers next to each other. Thousands of assigned identifiers, two confirmed exploited. That gap is not a scandal and it is not a reason to ignore CVEs. It is the direct, expected result of a policy that deliberately assigns an identifier to every bugfix that might conceivably have security relevance. A process that treats every kernel CVE as an incident will spend all its time on the wrong ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it on your product
&lt;/h2&gt;

&lt;p&gt;The volume is not going to fall soon, so the practical response is to stop treating the count as the signal. Three things are worth doing this week.&lt;/p&gt;

&lt;p&gt;First, find out whether you are even receiving the assignments. They are announced on a dedicated mailing list, and the archive is public, so a team that has been reading vendor advisories has probably been getting a filtered and delayed view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="go"&gt;6.6.155
&lt;/span&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://lore.kernel.org/linux-cve-announce/ | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, confirm your base kernel is still one the stable team supports, because no kernel CVE will be assigned against a version that is not. A product on an unsupported base does not have fewer vulnerabilities; it has fewer identifiers pointing at them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="go"&gt;5.15.219
&lt;/span&gt;&lt;span class="gp"&gt;root@imx8mp-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/version | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-60&lt;/span&gt;
&lt;span class="go"&gt;Linux version 5.15.219 (oe-user@oe-host) (aarch64-poky-l
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that version against the maintained list on kernel.org rather than against your vendor's release notes. The two disagree more often than teams expect, and the vendor's number is frequently a branch name rather than a supported stable version.&lt;/p&gt;

&lt;p&gt;Third, change the question your triage process asks. The official policy already tells you the answer will not come from upstream: applicability is yours to determine, because upstream does not know which subsystems you build. A kernel CVE in a driver you do not compile is not a finding. The tractable version of the work is to derive the set of subsystems your configuration actually enables, and filter against that before anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;zcat /proc/config.gz | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'=y'&lt;/span&gt;
&lt;span class="go"&gt;1847
&lt;/span&gt;&lt;span class="gp"&gt;root@imx8mp-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;zcat /proc/config.gz | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cE&lt;/span&gt; &lt;span class="s1"&gt;'^CONFIG_.*=m'&lt;/span&gt;
&lt;span class="go"&gt;312
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That count is the real scope of your exposure, and it is a small fraction of the tree the CVE numbers are drawn from. The policy also makes the uncomfortable recommendation explicit: it is better to take all released stable changes as a tested whole than to cherry-pick individual fixes, because many problems are only resolved by the sum of several changes, and some relevant fixes never get a CVE at all. A team cherry-picking only CVE-tagged commits is running a strategy the upstream documentation advises against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I could not verify
&lt;/h2&gt;

&lt;p&gt;The per-release figures in this article — roughly 500, then above 1,000, then above 1,500 — are readings of a chart. I did not independently recount CVEs per kernel release, and the underlying per-release data has not been published yet; the full set is being held for the talk in September. Treat the shape as well supported, since an independent Fixes-tag analysis produces the same inflection, and treat the individual values as approximate.&lt;/p&gt;

&lt;p&gt;I also found no public explanation for why 5.15 leads on CVEs created. The maintainer explicitly said he did not know, and I am not going to supply a theory he declined to offer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The chart counts kernel CVEs &lt;em&gt;fixed&lt;/em&gt; per release, not introduced. The maintainer confirmed this directly.&lt;/li&gt;
&lt;li&gt;An independent count of commits carrying Fixes tags — 4,830 in 7.2 — shows the same sharp rise, so the trend is real even if the exact per-release values are approximate.&lt;/li&gt;
&lt;li&gt;The kernel CVE team assigns an identifier to any bugfix that might have security relevance, by policy. High counts are the intended outcome, not a symptom.&lt;/li&gt;
&lt;li&gt;5.15 leads as the release that introduced the most CVEs, and it is still a maintained longterm kernel widely used in embedded products.&lt;/li&gt;
&lt;li&gt;Of 5,061 kernel CVEs published in 2026 so far, two are confirmed as exploited in the wild and about a fifth have no severity score at all.&lt;/li&gt;
&lt;li&gt;No CVE is assigned against an unsupported kernel version, so an old base looks quiet without being safe.&lt;/li&gt;
&lt;li&gt;Upstream states that applicability is the user's call and that taking whole stable releases beats cherry-picking individual CVE fixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://social.kernel.org/notice/B9nU6wnmFvh5atXSG8" rel="noopener noreferrer"&gt;Greg Kroah-Hartman, CVEs-per-release teaser and reply thread, 28 August 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/torvalds/linux/master/Documentation/process/cve.rst" rel="noopener noreferrer"&gt;Documentation/process/cve.rst — the kernel's CVE assignment policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lwn.net/Articles/1088776/" rel="noopener noreferrer"&gt;Development statistics for the 7.2 kernel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lwn.net/Articles/1088565/" rel="noopener noreferrer"&gt;Weekly edition of 20 August 2026 — Fixes-tag analysis and linux-next figures for 7.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/linux-cve-announce/" rel="noopener noreferrer"&gt;linux-cve-announce archive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/releases.html" rel="noopener noreferrer"&gt;kernel.org — actively supported kernel branches&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcvetracker.com/cve-statistics/2026/" rel="noopener noreferrer"&gt;Published kernel CVE counts for 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-Kernel-CVEs-Nearly-2000" rel="noopener noreferrer"&gt;The Linux Kernel Is Approaching 2,000 CVEs Per Release&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I teach Linux kernel, device driver and embedded Linux engineering at &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;TECH VEDA&lt;/a&gt;. If your team is working through kernel CVE triage or choosing an LTS base for a product, that is the kind of thing we cover in the &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;Linux kernel and BSP programmes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>security</category>
      <category>embedded</category>
    </item>
    <item>
      <title>UBI on Raw NAND: The Layer That Hides Bad Blocks and Wear</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Sun, 30 Aug 2026 08:42:57 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/ubi-on-raw-nand-the-layer-that-hides-bad-blocks-and-wear-4l37</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/ubi-on-raw-nand-the-layer-that-hides-bad-blocks-and-wear-4l37</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;UBI is the volume-management layer between a raw NAND partition and a filesystem such as UBIFS. It hands the layer above it eraseblocks that never go bad and never wear unevenly, and charges for that: one or two NAND pages of every eraseblock for headers, about 2% of the chip reserved for bad blocks, and four further eraseblocks. This article traces those costs, and shows the raw NAND flashing mistake that becomes read errors months after a board ships.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most engineers meet UBI as three commands in a flashing script somebody else wrote. The script works, the board boots, and nobody looks again until a unit returns from the field with read errors, or the rootfs turns out to be megabytes smaller than the datasheet suggested. Both follow from how UBI behaves on raw NAND.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why raw NAND cannot be treated as a disk
&lt;/h2&gt;

&lt;p&gt;The kernel's UBIFS documentation states the difference plainly. A block device supports read-a-block and write-a-block; blocks are rewritten in place, do not wear out and do not go bad, because the drive substitutes spares itself. An MTD device supports three operations instead: read at an offset in an eraseblock, write at an offset in an eraseblock, and erase a whole eraseblock. Nothing is rewritten until the eraseblock holding it has been erased, eraseblocks wear out after a bounded number of cycles, and on raw NAND they also go bad.&lt;/p&gt;

&lt;p&gt;One mistake follows immediately: putting ext4 on &lt;code&gt;/dev/mtdblock2&lt;/code&gt;. It appears to work, because mtdblock emulates a block device by reading an eraseblock, modifying it in memory and writing it back. What it does not emulate is wear levelling or bad-block handling, so every metadata update lands on the same few eraseblocks. The failure is a device that passes every lab test and dies in the field.&lt;/p&gt;

&lt;p&gt;UBI removes both problems. The MTD project describes it as a volume management system for raw flash, comparable to LVM except that it maps logical eraseblocks onto physical ones and adds wear levelling and error handling. It is not a filesystem, and explicitly not a Flash Translation Layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UBIFS  (or squashfs on ubiblock, read-only)
  |
UBI    volumes: rootfs, data, ...      drivers/mtd/ubi/
  |
MTD    /dev/mtd2, eraseblocks + pages  drivers/mtd/
  |
raw NAND chip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two layers are configured separately and can disagree. UBI is told the physical eraseblock size and minimum I/O size of the raw NAND; UBIFS is told the &lt;em&gt;logical&lt;/em&gt; eraseblock size UBI presents. Get the second wrong and the filesystem will not mount, reporting a size mismatch rather than the mistake behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two headers, and why your eraseblocks shrank
&lt;/h2&gt;

&lt;p&gt;UBI writes two 64-byte headers at the start of every physical eraseblock (PEB) that is not marked bad. The erase counter header, or EC header, records how many times that eraseblock has been erased. The volume identifier header, or VID header, records which volume it belongs to and which logical eraseblock (LEB) of that volume it holds. Both carry a CRC-32.&lt;/p&gt;

&lt;p&gt;Those headers are the whole basis of the design. The EC header lets UBI compare wear across the chip; the VID header means the map need not live anywhere else, because UBI rebuilds it by reading headers. That keeps the on-flash format simple, and it is also why attach time grows with flash size.&lt;/p&gt;

&lt;p&gt;The headers occupy space, so a logical eraseblock is smaller than a physical one. The EC header sits at offset 0; the VID header goes at the next minimum I/O unit or sub-page. On raw NAND with sub-page write support, UBI puts both in different sub-pages of the same page, costing one page. Without it the VID header goes in the second page, costing two. On 128 KiB eraseblocks with 2048-byte pages that gives the numbers you will actually see:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flash&lt;/th&gt;
&lt;th&gt;Header cost per PEB&lt;/th&gt;
&lt;th&gt;LEB size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NAND, 512-byte sub-pages&lt;/td&gt;
&lt;td&gt;2048 bytes (one page)&lt;/td&gt;
&lt;td&gt;129024 bytes (126 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAND, no sub-page writes&lt;/td&gt;
&lt;td&gt;4096 bytes (two pages)&lt;/td&gt;
&lt;td&gt;126976 bytes (124 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOR, 1-byte minimum I/O&lt;/td&gt;
&lt;td&gt;128 bytes&lt;/td&gt;
&lt;td&gt;130944 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If the value passed to &lt;code&gt;mkfs.ubifs -e&lt;/code&gt; does not match one of these, the image will not mount on the volume it was built for.&lt;/strong&gt; The gap between 126 KiB and 124 KiB is not a rounding choice; it is whether the controller can write sub-pages. Some controllers read sub-pages but cannot write them, and UBI must then use the two-page layout even though the datasheet advertises sub-pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eraseblocks you never get to use
&lt;/h2&gt;

&lt;p&gt;UBI also withholds whole eraseblocks. Four are fixed: two hold the copies of the volume table, one is reserved for wear levelling, one for the atomic LEB change operation. The rest covers bad blocks, and its size is a Kconfig option. Current mainline &lt;code&gt;drivers/mtd/ubi/Kconfig&lt;/code&gt; reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config MTD_UBI_BEB_LIMIT
        int "Maximum expected bad eraseblock count per 1024 eraseblocks"
        default 20
        range 0 768
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The help text derives 20 from the minimum and maximum valid block counts a NAND datasheet gives for the device's lifetime, which works out at 20 per 1024 for most parts — about 1.9% of the chip. The word &lt;em&gt;chip&lt;/em&gt; is doing important work there, and it catches people. The reserve is counted against the whole device, not the partition being attached, because bad blocks are not distributed evenly and one partition could inherit more than its share. So two UBI devices on one chip each reserve the full amount.&lt;/p&gt;

&lt;p&gt;Work it through on a 512 MiB SLC part with 128 KiB eraseblocks, 2048-byte pages and 512-byte sub-pages. The chip has 4096 eraseblocks, so the reserve is 20 times 4096 divided by 1024, which is 80 PEBs. Attach one 256 MiB partition of 2048 PEBs and the usable count is 2048 minus 80 minus 4, giving 1964 LEBs. At an LEB size of 129024 bytes that is 253,403,136 bytes — about 241.7 MiB from a nominal 256 MiB, a loss near 14.3 MiB or 5.6%.&lt;/p&gt;

&lt;p&gt;Split that chip into two UBI devices and each reserves 80 PEBs, so 160 go to bad-block handling instead of 80: 10 MiB for no benefit. The MTD documentation states the conclusion directly. On one raw NAND chip use one UBI device with several volumes. Volumes are free; devices are not. The same arithmetic prices a controller that cannot write sub-pages, at a further 3.8 MiB here, or roughly 16 MiB per gigabyte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wear levelling and scrubbing on raw NAND
&lt;/h2&gt;

&lt;p&gt;Wear levelling works because every PEB carries its erase count in the EC header. UBI rebuilds the erase counter table in RAM at attach time and uses it to decide when data should move. The trigger is a configured threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config MTD_UBI_WL_THRESHOLD
        int "UBI wear-leveling threshold"
        default 4096
        range 2 65536
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameter is the maximum permitted difference between the highest and lowest erase counter on the device. When the gap is exceeded, UBI moves data out of eraseblocks with low erase counters into ones with high counters. That looks backwards at first. A low erase count means the block holds data that never changes, so moving that cold data into a worn block frees the fresh block for write traffic and parks the worn block under data that will not erase it again.&lt;/p&gt;

&lt;p&gt;The default of 4096 suits SLC raw NAND and NOR, with eraseblock lifetimes of 100,000 cycles or more. The help text is explicit that MLC NAND, typically under 10,000 cycles, needs a much smaller threshold, suggesting 128 or 256. If your product ships MLC and nobody revisited this symbol, it is running a policy meant for a different class of flash.&lt;/p&gt;

&lt;p&gt;Scrubbing is the other half of the job. Raw NAND accumulates bit flips, which ECC corrects on read. A corrected flip is not an error but it does signal that the eraseblock is degrading, and UBI treats it as a reason to copy that PEB elsewhere and recycle the original. This happens in the background, hidden from the layers above, which is why UBIFS needs no bit-rot handling of its own. That is as far as the documentation goes: I did not read &lt;code&gt;drivers/mtd/ubi/io.c&lt;/code&gt; for this article, so I am not quoting internal return codes for the scrub path.&lt;/p&gt;

&lt;p&gt;Bad blocks use the same idea with a different trigger. If a write fails, UBI moves the data away and tortures the eraseblock: erase it, confirm it reads back as all 0xFF, then write and verify a series of test patterns. A block that survives returns to service; one that fails, or shows a bit flip during the test, is marked bad. If an &lt;em&gt;erase&lt;/em&gt; fails with EIO there is no test — the block is marked bad immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why attach takes as long as it does
&lt;/h2&gt;

&lt;p&gt;Because the erase counters and the map live only in the headers, UBI must read those headers from every eraseblock to rebuild its tables in RAM. Attaching costs one read per PEB plus a CRC-32 per header, so attach time scales linearly with flash size. The MTD documentation gives measured figures: a 256 MiB OneNAND attaches in under a second, a 1 GiB NAND in about two, and a full scan of a 4 GiB NAND takes several.&lt;/p&gt;

&lt;p&gt;Several seconds is a large share of a boot budget, which is why fastmap exists. It stores a checkpoint so attach reads the map instead of rebuilding it, and in mainline it is still experimental and off by default. Its anchor eraseblock must sit within the first 64 PEBs, it keeps a free-PEB pool of 5% of the total, and if the fastmap is corrupt UBI falls back to a full scan — so a failure costs boot time, not data. On a 128 MiB part it solves no problem you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flashing mistake that appears months later
&lt;/h2&gt;

&lt;p&gt;This failure is worth understanding, because it passes every test at the factory. The MTD documentation sets it out step by step. You erase the flash, so every byte is 0xFF. You write a UBI image with a general-purpose tool such as &lt;code&gt;nandwrite&lt;/code&gt;. Some eraseblocks in that image end with empty pages, and the tool writes those too, filling them with 0xFF. That makes the controller calculate an ECC code for a page of 0xFF bytes and store it in the out-of-band area — and on many hardware ECC engines that code is not itself all 0xFF. Later, UBIFS uses one of those pages. It writes real data, producing a new ECC code written on top of the old one. Because the page was never erased between the two writes, the out-of-band area now holds a corrupted combination. Nothing fails then. It fails when the page is next read, and the read returns &lt;code&gt;-EBADMSG&lt;/code&gt;, which is -74.&lt;/p&gt;

&lt;p&gt;The fix is to flash with a tool that understands the format. As the documentation puts it, &lt;code&gt;ubiformat&lt;/code&gt; ensures every page is written once and only once after erasure, whereas &lt;code&gt;nandwrite&lt;/code&gt; writes some pages twice, once by the flasher and once by UBIFS. It also preserves the existing erase counters, so reflashing a returned device does not discard its wear history.&lt;/p&gt;

&lt;p&gt;When the flashing tool cannot be changed — a fixed factory programmer, for instance — the fallback is to set the free-space fixup flag when building the image, with &lt;code&gt;mkfs.ubifs -F&lt;/code&gt;. On the first mount the filesystem finds the affected free space and re-erases it before use. It works, and it makes that first boot noticeably slower.&lt;/p&gt;

&lt;p&gt;Which image you flash depends on the layer. &lt;code&gt;mkfs.ubifs&lt;/code&gt; makes a UBIFS image, which goes into an existing volume with &lt;code&gt;ubiupdatevol&lt;/code&gt;. &lt;code&gt;ubinize&lt;/code&gt; wraps those into a UBI image, which is what &lt;code&gt;ubiformat&lt;/code&gt; writes to the MTD device. Confuse the two and the device will not attach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking raw NAND and UBI on your own board
&lt;/h2&gt;

&lt;p&gt;All of this is observable. On the host, the minimum I/O size and the LEB size are arguments you must get right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkfs.ubifs &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; rootfs-dir &lt;span class="nt"&gt;-m&lt;/span&gt; 2048 &lt;span class="nt"&gt;-e&lt;/span&gt; 129024 &lt;span class="nt"&gt;-c&lt;/span&gt; 2047 &lt;span class="nt"&gt;-o&lt;/span&gt; ubifs.img
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ubinize &lt;span class="nt"&gt;-o&lt;/span&gt; ubi.img &lt;span class="nt"&gt;-m&lt;/span&gt; 2048 &lt;span class="nt"&gt;-p&lt;/span&gt; 128KiB &lt;span class="nt"&gt;-s&lt;/span&gt; 512 ubinize.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers for &lt;code&gt;-m&lt;/code&gt;, &lt;code&gt;-p&lt;/code&gt; and &lt;code&gt;-s&lt;/code&gt; come from the board, so read the geometry rather than trusting a script that hardcodes them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx6ull-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mtdinfo /dev/mtd0 &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="go"&gt;mtd0
Name:                           nand
Type:                           nand
Eraseblock size:                131072 bytes, 128.0 KiB
Amount of eraseblocks:          4096 (536870912 bytes, 512.0 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size:                  512 bytes
Bad blocks are allowed:         true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then attach and read back what UBI made of it. The logical eraseblock size printed here is the value &lt;code&gt;mkfs.ubifs -e&lt;/code&gt; needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx6ull-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ubiattach &lt;span class="nt"&gt;--mtdn&lt;/span&gt; 0
&lt;span class="gp"&gt;root@imx6ull-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ubinfo &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="go"&gt;ubi0
Volumes count:                           1
Logical eraseblock size:                 129024 bytes, 126.0 KiB
Total amount of logical eraseblocks:     4012
Amount of available logical eraseblocks: 0 (0 bytes)
Maximum count of volumes                 128
Count of bad physical eraseblocks:       6
Count of reserved physical eraseblocks:  80
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read those last two lines together. Six eraseblocks are bad; eighty are reserved. The reservation is not a measurement of damage, it is a budget for damage that has not happened yet, and a fleet whose bad count is climbing toward it is worth investigating.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx6ull-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;zcat /proc/config.gz | &lt;span class="nb"&gt;grep &lt;/span&gt;MTD_UBI
&lt;span class="go"&gt;CONFIG_MTD_UBI=y
CONFIG_MTD_UBI_WL_THRESHOLD=4096
CONFIG_MTD_UBI_BEB_LIMIT=20
CONFIG_MTD_UBI_BLOCK=y
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;CONFIG_MTD_UBI_FASTMAP is not &lt;span class="nb"&gt;set&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;CONFIG_MTD_UBI_GLUEBI is not &lt;span class="nb"&gt;set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three checks are worth running on any product that uses raw NAND. If the flash is MLC and the threshold is still 4096, that is a real problem, not a cosmetic one. If the chip carries more than one UBI device, each pays the full bad-block reserve. And if the factory process uses anything other than &lt;code&gt;ubiformat&lt;/code&gt;, change it or build images with &lt;code&gt;mkfs.ubifs -F&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reading flash geometry off the board and matching the image to it is part of the bring-up work covered in &lt;a href="https://www.techveda.live/embedded-linux-yocto/" rel="noopener noreferrer"&gt;TECH VEDA's Embedded Linux with Yocto training&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this does not apply
&lt;/h2&gt;

&lt;p&gt;The MTD project puts the following at the top of its UBI page. UBI works with bare flash. It does not work with managed consumer flash: eMMC, SD, microSD, CompactFlash or USB drives. Those carry a Flash Translation Layer in the controller that already performs wear levelling and bad-block substitution and presents a block device. Stacking UBI on one would be two remapping layers competing for the same job. On managed flash, use a block filesystem and choose it on its own merits — a decision covered in &lt;a href="https://www.techveda.live/2026/08/12/root-filesystem-format-embedded-linux/" rel="noopener noreferrer"&gt;choosing a root filesystem format for embedded Linux&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the other direction, &lt;code&gt;CONFIG_MTD_UBI_BLOCK&lt;/code&gt; exposes a read-only block device over a UBI volume — how a squashfs rootfs runs on raw NAND with UBI still handling bit flips and bad blocks underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UBI stores the logical-to-physical map, and a per-eraseblock erase count, in two 64-byte headers in every good eraseblock.&lt;/li&gt;
&lt;li&gt;Those headers cost one page per eraseblock with sub-page writes and two without: a 126 KiB or 124 KiB LEB on a 128 KiB part.&lt;/li&gt;
&lt;li&gt;UBI reserves 20 eraseblocks per 1024 for bad blocks, counted over the whole chip, so use one UBI device with several volumes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CONFIG_MTD_UBI_WL_THRESHOLD&lt;/code&gt; defaults to 4096, which suits SLC raw NAND and NOR. MLC needs a far lower value and nobody adjusts it for you.&lt;/li&gt;
&lt;li&gt;Flash UBI images with &lt;code&gt;ubiformat&lt;/code&gt;. A general-purpose flasher writes empty pages twice and produces ECC errors long after the device ships.&lt;/li&gt;
&lt;li&gt;None of this applies to eMMC or SD, which have their own translation layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why is my UBI volume smaller than the MTD partition?&lt;/strong&gt;&lt;br&gt;
Three costs are subtracted. Each good eraseblock loses one or two NAND pages to the UBI headers, four eraseblocks are reserved for the volume table, wear levelling and the atomic LEB change operation, and 20 eraseblocks per 1024 of the whole chip are held back for bad blocks. On a 256 MiB partition of a 512 MiB chip that is roughly 14 MiB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use UBI or UBIFS on eMMC or an SD card?&lt;/strong&gt;&lt;br&gt;
No. UBI works with raw flash only. Managed flash such as eMMC, SD and USB drives contains its own Flash Translation Layer that already handles wear levelling and bad blocks, and does not expose the eraseblock geometry UBI needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why must I use ubiformat instead of nandwrite?&lt;/strong&gt;&lt;br&gt;
A general-purpose flasher writes the empty NAND pages at the end of eraseblocks, causing an ECC code to be stored for a page of 0xFF bytes. When the filesystem later writes real data there, a second ECC code is written over the first, and the page returns -EBADMSG when read. Using ubiformat ensures every page is written once after erasure, and it preserves the erase counters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/filesystems/ubifs.html" rel="noopener noreferrer"&gt;UBI File System&lt;/a&gt; — the kernel's UBIFS documentation, including the MTD versus block device comparison.&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.linux-mtd.infradead.org/doc/ubi.html" rel="noopener noreferrer"&gt;UBI - Unsorted Block Images&lt;/a&gt; — the MTD project's reference for headers, overhead, bad-block reserves and fastmap.&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.linux-mtd.infradead.org/faq/ubifs.html" rel="noopener noreferrer"&gt;UBIFS FAQ and HOWTO&lt;/a&gt; — image creation, the ubiformat rationale and the free space fixup flag.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/mtd/ubi/Kconfig" rel="noopener noreferrer"&gt;drivers/mtd/ubi/Kconfig&lt;/a&gt; — the authoritative defaults quoted above.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>embedded</category>
      <category>kernel</category>
      <category>filesystem</category>
    </item>
    <item>
      <title>Linux Kernel CVEs: What to Patch by Device (23 – 29 Aug 2026)</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Sat, 29 Aug 2026 05:54:24 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/linux-kernel-cves-what-to-patch-by-device-23-29-aug-2026-35c4</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/linux-kernel-cves-what-to-patch-by-device-23-29-aug-2026-35c4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The Linux kernel project published 227 Linux kernel CVEs in the week of 23 to 29 August 2026. That is about five times a normal week, because a full stable cycle landed on 28 August and the records for everything in it were published together. Most are already fixed in current upstream stable releases, but &lt;strong&gt;four of the records below name no fix for branches they mark as affected&lt;/strong&gt;, so the branch update alone does not clear those four. None of the records notes known exploitation, and no public exploit code was found for any of them at the time of writing. The minimum safe upstream release per branch is 5.10.268, 5.15.219, 6.1.186, 6.6.155, 6.12.107, 6.18.48, 7.1.12, 7.2.2, or mainline 7.2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Last week this series covered six CVEs. This week it is 227. The jump is not a sudden collapse in kernel quality. The CVE IDs show what happened: 21 sit in the CVE-2026-747xx block and 206 in the CVE-2026-805xx to 807xx block, and on 28 August the kernel project cut a new point release on every active branch at once.&lt;/p&gt;

&lt;p&gt;Where this advisory quotes a CVSS score, that score is the kernel CNA's own secondary metric; NVD has not finished analysing this batch, so other databases may publish different figures later. Prioritise by reachability and deployment context rather than by CVSS alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one action: update to the fixed version for your branch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="go"&gt;6.6.152-lts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stable branch&lt;/th&gt;
&lt;th&gt;Update to at least&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5.10 LTS&lt;/td&gt;
&lt;td&gt;5.10.268&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80590&lt;/code&gt; sets this point release. Four records below name no 5.10 fix.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.15 LTS&lt;/td&gt;
&lt;td&gt;5.15.219&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80590&lt;/code&gt; sets this. Same four gaps as 5.10.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.1 LTS&lt;/td&gt;
&lt;td&gt;6.1.186&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80590&lt;/code&gt; sets this. Two of the four gaps apply.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.6 LTS&lt;/td&gt;
&lt;td&gt;6.6.155&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80590&lt;/code&gt; sets this. One gap: &lt;code&gt;CVE-2026-74752&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.12 LTS&lt;/td&gt;
&lt;td&gt;6.12.107&lt;/td&gt;
&lt;td&gt;Same as 6.6.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.18 LTS&lt;/td&gt;
&lt;td&gt;6.18.48&lt;/td&gt;
&lt;td&gt;Same as 6.6.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7.1 stable&lt;/td&gt;
&lt;td&gt;7.1.12&lt;/td&gt;
&lt;td&gt;Clears every record discussed below.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7.2 stable&lt;/td&gt;
&lt;td&gt;7.2.2&lt;/td&gt;
&lt;td&gt;New row this week. Mainline 7.2 shipped 16 August and now has a stable branch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mainline&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;Released 16 August 2026.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All eight stable targets are the newest release of their branch and all eight were cut on 28 August, so on an upstream kernel the check is simple: if you are not on the current release for your branch, you are behind. One record, &lt;code&gt;CVE-2026-80590&lt;/code&gt; (stale GSO state on IPv4 fragments), sets the target on all eight branches by itself and names no mainline fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four records the branch update does not clear
&lt;/h3&gt;

&lt;p&gt;This is the part to read before you close the ticket.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Introduced&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;th&gt;Affected, NO fix named&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-74752&lt;/code&gt; (SCTP, 9.8)&lt;/td&gt;
&lt;td&gt;2.6.24&lt;/td&gt;
&lt;td&gt;7.1.10, mainline 7.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.10, 5.15, 6.1, 6.6, 6.12, 6.18&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80551&lt;/code&gt; (vfio-ccw, 9.3)&lt;/td&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;td&gt;6.6.154 and later&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.10, 5.15, 6.1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-74743&lt;/code&gt; (macvlan, 9.8)&lt;/td&gt;
&lt;td&gt;2.6.23&lt;/td&gt;
&lt;td&gt;6.1.184 and later&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.10, 5.15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CVE-2026-80635&lt;/code&gt; (wcn36xx)&lt;/td&gt;
&lt;td&gt;4.7&lt;/td&gt;
&lt;td&gt;6.1.178 and later&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.10, 5.15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Last week's &lt;code&gt;CVE-2026-74582&lt;/code&gt; is also still unfixed on 5.10, 5.15 and 6.1. If you ship an older LTS branch you now have five open items to track separately from the branch bump.&lt;/p&gt;

&lt;p&gt;Last week's targets were 5.10.265, 5.15.216, 6.1.183, 6.6.152, 6.12.104, 6.18.45, 7.1.9 and mainline 7.2. Every branch moved up exactly three point releases on 28 August.&lt;/p&gt;

&lt;p&gt;On a vendor or BSP kernel the version string will not compare cleanly with the table. A kernel reporting &lt;code&gt;5.10.110-rk3588&lt;/code&gt; will not become 5.10.268 by any upstream update you apply yourself. Ask your vendor to confirm, and treat a verified backport as equivalent. Until an update is possible, reduce exposure by disabling unused features, restricting the relevant privileges, and documenting residual risk through your product security process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile and automotive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CVE-2026-80707: CAN J1939 receive buffer was not zeroed
&lt;/h3&gt;

&lt;p&gt;J1939 is the transport protocol on heavy vehicles and agricultural equipment. A new receive session's buffer was allocated but not zeroed, so any part the peer never wrote kept residual kernel memory. The fix zeroes it in &lt;code&gt;j1939_session_fresh_new()&lt;/code&gt;. The record states the residual-data problem and the fix; a readback path is our inference, not the record's claim. Affects 4.19+. Gate &lt;code&gt;CONFIG_CAN_J1939&lt;/code&gt;. Fixed 5.10.265 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80707" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-74737: TI AM65 Ethernet used hardware-reserved bits as part of a port index
&lt;/h3&gt;

&lt;p&gt;Only the lower 8 bits of the 16-bit Source Tag carry the MAC port ID; the upper 8 are hardware-reserved and hold an arbitrary value, and the driver used the whole field as an index. The record reports sporadic kernel crashes. The fix masks the tag to its lower 8 bits.&lt;/p&gt;

&lt;p&gt;Read the scoring carefully: the record carries CVSS 9.8 with a network attack vector, but its own description attributes the invalid index to hardware-reserved bits rather than attacker-chosen packet content — a reliability defect with a high score rather than a demonstrated remote attack. Gate &lt;code&gt;CONFIG_TI_K3_AM65_CPSW_NUSS&lt;/code&gt;. Fixed on all seven stable branches that name a fix, from 5.10.266, plus mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-74737" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80722: mac80211 partially validated an S1G TWT setup frame
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ieee80211_process_rx_twt_action()&lt;/code&gt; only &lt;em&gt;partially&lt;/em&gt; validates a received &lt;strong&gt;S1G&lt;/strong&gt; TWT setup frame, so an individual agreement can reach &lt;code&gt;ieee80211_s1g_rx_twt_setup()&lt;/code&gt; with &lt;code&gt;twt-&amp;gt;length&lt;/code&gt; too short for the full params struct. The driver callback and tracepoint then read the complete block. Broadcast agreements are unaffected. Note the S1G qualifier — this is the sub-1GHz path. The affected file has no config option of its own, so it is present wherever mac80211 is built. Affects 5.15+. Gate &lt;code&gt;CONFIG_MAC80211&lt;/code&gt;. Fixed 5.15.216 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80722" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80721: Bluetooth ISO could leave a dangling connection pointer
&lt;/h3&gt;

&lt;p&gt;Clearing &lt;code&gt;iso_conn::hcon&lt;/code&gt; depended on &lt;code&gt;iso_conn_del()&lt;/code&gt; happening to drop the last reference. The fix clears &lt;code&gt;conn-&amp;gt;hcon&lt;/code&gt; explicitly. Isochronous channels carry LE Audio. One of four Bluetooth records this week, with &lt;code&gt;CVE-2026-80692&lt;/code&gt;, &lt;code&gt;CVE-2026-80683&lt;/code&gt; and &lt;code&gt;CVE-2026-80666&lt;/code&gt; — all connection-object lifetime issues. Affects 6.0+. Gate &lt;code&gt;CONFIG_BT&lt;/code&gt; plus &lt;code&gt;CONFIG_BT_LE&lt;/code&gt;. Fixed 6.18.44, 7.1.8, mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80721" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded and IoT
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CVE-2026-74746: use-after-free in the netfilter flow table
&lt;/h3&gt;

&lt;p&gt;GC could free a flow entry while &lt;code&gt;flow_offload_add()&lt;/code&gt; was still inserting it. KASAN reports slab-use-after-free reads &lt;em&gt;and&lt;/em&gt; writes. The fix publishes the GC-visible tuple last. &lt;strong&gt;Highest priority for anyone running a Linux gateway or router&lt;/strong&gt; — the trigger is ordinary forwarded traffic once flow offload is enabled. CVSS 9.8, affects 5.13+. Gate &lt;code&gt;CONFIG_NF_FLOW_TABLE&lt;/code&gt;. Fixed on all seven stable branches that name a fix, from 5.10.266, plus mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-74746" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80603: off-by-one read in the netfilter IRC helper corrupts DCC expectations
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;parse_dcc()&lt;/code&gt; read one byte past the region filled by &lt;code&gt;skb_header_pointer()&lt;/code&gt;. The record is explicit that this is &lt;strong&gt;not&lt;/strong&gt; currently an OOB read — the buffer is allocated one byte over — so the real impact is a stale byte consumed by &lt;code&gt;simple_strtoul&lt;/code&gt;, producing a &lt;strong&gt;wrong DCC IP or port&lt;/strong&gt; in the conntrack expectation. A firewall induced to open the wrong data connection, not memory corruption. Easy to remove rather than patch. Affects 2.6.12+. Gate &lt;code&gt;CONFIG_NF_CONNTRACK_IRC&lt;/code&gt;. Fixed 5.10.261 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80603" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80635: out-of-bounds read from a short firmware response in wcn36xx
&lt;/h3&gt;

&lt;p&gt;A shorter-than-expected trigger block-ack response from the Wi-Fi firmware was not rejected, so the parser reads a 22-byte struct past the end. The input is the firmware response, not network traffic. Affects 4.7+. &lt;strong&gt;No fix named for 5.10 or 5.15.&lt;/strong&gt; Gate &lt;code&gt;CONFIG_WCN36XX&lt;/code&gt;. Fixed 6.1.178 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80635" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80723: out-of-bounds write when the boot device tree declares too many reserved regions
&lt;/h3&gt;

&lt;p&gt;On boot, &lt;code&gt;fdt_scan_reserved_mem()&lt;/code&gt; saves each dynamically-placed reserved-memory subnode into a fixed-size local array without bounding the count. This is embedded-specific in a way most kernel CVEs are not: the count comes from whatever FDT the platform hands the kernel at boot, so if your boot chain composes the tree from a base plus overlays, or a carrier board contributes nodes, it is not something you control at build time. Overlays applied later at runtime do not go through this path. Affects 3.15+. Gate &lt;code&gt;CONFIG_OF_RESERVED_MEM&lt;/code&gt;. Fixed 6.12.103 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80723" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud and datacenter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CVE-2026-80551: s390 vfio-ccw double-fetch of the first IDAW
&lt;/h3&gt;

&lt;p&gt;The host reads the first indirect address word to size a buffer, then reads it again while filling it, with nothing guaranteeing the value is the same. A guest that changes it in between makes the host size one buffer and fill another. CVSS 9.3, guest-to-host.&lt;/p&gt;

&lt;p&gt;One of nine vfio-ccw records (&lt;code&gt;CVE-2026-80547&lt;/code&gt;…&lt;code&gt;80555&lt;/code&gt;); &lt;strong&gt;all nine&lt;/strong&gt; carry a CVSS vector with scope marked as changed. &lt;code&gt;CVE-2026-80554&lt;/code&gt; (unbounded recursion processing channel programs) is the other CRITICAL at 9.3. Treat the nine as one unit. Affects 5.3+. &lt;strong&gt;No fix named for 5.10, 5.15 or 6.1.&lt;/strong&gt; Gate &lt;code&gt;CONFIG_VFIO_CCW&lt;/code&gt;. Fixed 6.6.154 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80551" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80590: stale GSO state left on IPv4 fragments before reassembly
&lt;/h3&gt;

&lt;p&gt;Reassembled packets carried segmentation offload metadata that no longer described them. No CVSS score and no dramatic description, but it is the record that matters most to the table above: fixed on every stable branch, no mainline entry, and its fixed versions are exactly this week's eight targets. Fixed in 5.10.268, 5.15.219, 6.1.186, 6.6.155, 6.12.107, 6.18.48, 7.1.12 and 7.2.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80590" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-74743: macvlan did not inherit headroom from the lower device
&lt;/h3&gt;

&lt;p&gt;The transmit path can underflow reserved skb headroom, which KASAN reports as a slab use-after-free. &lt;strong&gt;The precondition decides whether you are affected at all&lt;/strong&gt;: the record names macsec, IPsec, WireGuard, tunnels, and veth with rx headroom as lower devices requiring extra space. A macvlan over a plain physical NIC is not in scope. &lt;code&gt;CVE-2026-74744&lt;/code&gt; is the identical fix in ipvlan at the same 9.8. Affects 2.6.23+. &lt;strong&gt;No fix named for 5.10 or 5.15.&lt;/strong&gt; Gate &lt;code&gt;CONFIG_MACVLAN&lt;/code&gt;. Fixed 6.1.184 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-74743" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-74752: SCTP restored AUTH fields straight from peer-controlled cookie bytes
&lt;/h3&gt;

&lt;p&gt;When cookie authentication is disabled, a COOKIE_ECHO restores fixed-size AUTH fields directly from peer-controlled bytes with no validation. A forged RANDOM length gives OOB reads; a forged HMAC identifier caused a 32-byte write past a zero-length AUTH chunk — which the record describes as a primitive for a &lt;strong&gt;local&lt;/strong&gt; privilege escalation chain, despite the CVSS 9.8 / PR:N score. Affects 2.6.24+. &lt;strong&gt;This is the widest gap this week: no fix named for 5.10, 5.15, 6.1, 6.6, 6.12 or 6.18.&lt;/strong&gt; Gate &lt;code&gt;CONFIG_IP_SCTP&lt;/code&gt;. Fixed in 7.1.10 and mainline 7.2 only. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-74752" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80586: MPTCP processed a second suboption against leftover state
&lt;/h3&gt;

&lt;p&gt;A peer can send a malformed DSS with a wrong size followed by another DSS, or MPC plus data, &lt;strong&gt;in the same packet&lt;/strong&gt;. The malformed suboption is ignored but leaves fields written, so the second is processed against leftover state — inconsistency or reads of uninitialised data. One of four MPTCP records, with &lt;code&gt;CVE-2026-80587&lt;/code&gt; (9.8), &lt;code&gt;CVE-2026-80585&lt;/code&gt; (9.4) and &lt;code&gt;CVE-2026-80588&lt;/code&gt;. Affects 5.11+. Gate &lt;code&gt;CONFIG_MPTCP&lt;/code&gt;. Fixed 5.10.266 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80586" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80557: out-of-bounds read decoding a Ceph watcher list
&lt;/h3&gt;

&lt;p&gt;The record gives an unusually explicit attacker model: a malicious or compromised OSD in a multi-tenant Ceph deployment can trigger this against any kernel client that calls &lt;code&gt;CEPH_OSD_OP_LIST_WATCHERS&lt;/code&gt;, with no privileges beyond OSD session establishment. Two more libceph records are the same class — &lt;code&gt;CVE-2026-80558&lt;/code&gt; and &lt;code&gt;CVE-2026-80561&lt;/code&gt;, all three at 9.8 — plus &lt;code&gt;CVE-2026-80528&lt;/code&gt; in the Ceph fs client, which needs another mounted ext4 filesystem in play. Affects 3.10+. Gate &lt;code&gt;CONFIG_CEPH_LIB&lt;/code&gt;. Fixed 5.10.267 → mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80557" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-80713: io_uring dropped per-task restrictions on exec
&lt;/h3&gt;

&lt;p&gt;A task can install io_uring restrictions that apply to every ring it creates. The exec cancellation path called &lt;code&gt;__io_uring_free()&lt;/code&gt;, which freed the per-task restriction along with the task context, so any ring created &lt;strong&gt;after&lt;/strong&gt; the exec came up unrestricted. Not memory corruption — a confinement bug: a limit that was applied is silently no longer applied. Affects 5.10+. Gate &lt;code&gt;CONFIG_IO_URING&lt;/code&gt;. Fixed 7.1.8, mainline 7.2. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-80713" rel="noopener noreferrer"&gt;Record&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Medical devices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wireless.&lt;/strong&gt; &lt;code&gt;CVE-2026-80721&lt;/code&gt; and the three other Bluetooth records (&lt;code&gt;80692&lt;/code&gt;, &lt;code&gt;80683&lt;/code&gt;, &lt;code&gt;80666&lt;/code&gt;) matter for wearables, patient monitors and hearing devices, LE Audio specifically. &lt;code&gt;CVE-2026-80722&lt;/code&gt; matters on hospital wireless networks, but only where the sub-1GHz S1G path is in use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network protocols.&lt;/strong&gt; DICOM/HL7/FHIR-over-IP devices should review &lt;code&gt;CVE-2026-74752&lt;/code&gt; and &lt;code&gt;CVE-2026-80717&lt;/code&gt;, and note the first is unfixed on every LTS branch below 7.1. Devices on TI AM65/Sitara parts should review &lt;code&gt;CVE-2026-74737&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removable media.&lt;/strong&gt; A large crafted-image filesystem group: &lt;code&gt;CVE-2026-80672&lt;/code&gt;, &lt;code&gt;CVE-2026-80673&lt;/code&gt;, &lt;code&gt;CVE-2026-80674&lt;/code&gt; (NTFS), &lt;code&gt;CVE-2026-80598&lt;/code&gt; (NTFS3), &lt;code&gt;CVE-2026-80631&lt;/code&gt; (btrfs), &lt;code&gt;CVE-2026-80656&lt;/code&gt; (HFS+), &lt;code&gt;CVE-2026-80591&lt;/code&gt; (f2fs). Relevant to any device that auto-mounts a USB stick for patient data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Patching a medical device is not the same as patching a server. The update has to pass validation and regulatory revalidation under US FDA postmarket cybersecurity guidance, the EU Medical Device Regulation (MDR) and IEC 62304. Plan the stable kernel update through the manufacturer's change-control process.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check which Linux kernel CVEs apply to you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Version.&lt;/strong&gt; &lt;code&gt;uname -r&lt;/code&gt; against the table. Then read the four-gap list, because on 5.10, 5.15 and 6.1 the branch target is not the whole answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;zcat /proc/config.gz | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(# )?CONFIG_(MAC80211|BT|NF_FLOW_TABLE|IP_SCTP|MACVLAN|IO_URING)[ =]'&lt;/span&gt;
&lt;span class="go"&gt;CONFIG_MAC80211=m
CONFIG_BT=m
CONFIG_NF_FLOW_TABLE=m
CONFIG_IP_SCTP=m
CONFIG_MACVLAN=m
CONFIG_IO_URING=y
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is anchored on purpose — a plain &lt;code&gt;grep CONFIG_BT&lt;/code&gt; also matches &lt;code&gt;CONFIG_BTRFS_FS&lt;/code&gt;. Other symbols worth checking: &lt;code&gt;CONFIG_CAN_J1939&lt;/code&gt;, &lt;code&gt;CONFIG_TI_K3_AM65_CPSW_NUSS&lt;/code&gt;, &lt;code&gt;CONFIG_BT_LE&lt;/code&gt;, &lt;code&gt;CONFIG_NF_CONNTRACK_IRC&lt;/code&gt;, &lt;code&gt;CONFIG_WCN36XX&lt;/code&gt;, &lt;code&gt;CONFIG_OF_RESERVED_MEM&lt;/code&gt;, &lt;code&gt;CONFIG_VFIO_CCW&lt;/code&gt;, &lt;code&gt;CONFIG_IPVLAN&lt;/code&gt;, &lt;code&gt;CONFIG_MPTCP&lt;/code&gt;, &lt;code&gt;CONFIG_CEPH_LIB&lt;/code&gt;. Read &lt;code&gt;=m&lt;/code&gt; as present-and-loadable, not absent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reachability.&lt;/strong&gt; The records describe code paths, not attacker models, so this ordering is our reading and not a statement from the kernel CVE team. Untrusted guests: the vfio-ccw group. Gateway or router: &lt;code&gt;CVE-2026-74746&lt;/code&gt;. Storage client: the libceph group. Radio or bus: &lt;code&gt;CVE-2026-80722&lt;/code&gt;, the Bluetooth group, &lt;code&gt;CVE-2026-80707&lt;/code&gt;. Then remote protocol records, then local confinement (&lt;code&gt;CVE-2026-80713&lt;/code&gt;), then configuration- and image-gated. Cutting across all of it: if you run 5.10, 5.15 or 6.1, the four unfixed records outrank everything, because no update clears them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Update to the target release for your branch: 5.10.268, 5.15.219, 6.1.186, 6.6.155, 6.12.107, 6.18.48, 7.1.12, 7.2.2, or mainline 7.2 — all the newest release of their branch, all cut on 28 August.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The branch update does not clear everything.&lt;/strong&gt; &lt;code&gt;CVE-2026-74752&lt;/code&gt;, &lt;code&gt;CVE-2026-80551&lt;/code&gt;, &lt;code&gt;CVE-2026-74743&lt;/code&gt; and &lt;code&gt;CVE-2026-80635&lt;/code&gt; each mark a branch affected and name no fix. On 5.10/5.15 all four apply; on 6.1 two; on 6.6/6.12/6.18 one.&lt;/li&gt;
&lt;li&gt;Last week's &lt;code&gt;CVE-2026-74582&lt;/code&gt; is also still unfixed on 5.10, 5.15 and 6.1 — five open items on older LTS.&lt;/li&gt;
&lt;li&gt;227 is a release cadence, not a collapse in kernel quality.&lt;/li&gt;
&lt;li&gt;There is a new 7.2 stable branch; 7.2.2 is the newest stable kernel.&lt;/li&gt;
&lt;li&gt;None of the records notes known exploitation, and no public exploit code was found at the time of writing. That is a point-in-time check, not a guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/linux-cve-announce/?q=d:20260823..20260830" rel="noopener noreferrer"&gt;linux-cve-announce, 23 to 29 August 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/releases.json" rel="noopener noreferrer"&gt;kernel.org active releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.techveda.live/2026/08/22/linux-kernel-cves-22-aug-2026/" rel="noopener noreferrer"&gt;Previous edition: Linux Kernel CVEs, 16 to 22 August 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Full advisory on TECH VEDA: &lt;a href="https://www.techveda.live/2026/08/29/linux-kernel-cves-29-aug-2026/" rel="noopener noreferrer"&gt;Linux Kernel CVEs: What to Patch by Device (23 – 29 Aug 2026)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>security</category>
      <category>embedded</category>
    </item>
    <item>
      <title>Inherited a Vendor BSP? Measure These Five Things First</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Fri, 28 Aug 2026 04:57:45 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/inherited-a-vendor-bsp-measure-these-five-things-first-5dl4</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/inherited-a-vendor-bsp-measure-these-five-things-first-5dl4</guid>
      <description>&lt;p&gt;When you inherit a vendor BSP, the useful first move is not to read the code but to measure the distance between what the vendor shipped and what mainline contains, using tools the kernel already gives you. Five measurements — running configuration, mainline delta, externally built modules, the device tree the board actually booted, and the Yocto layer map — take about two days and produce an Inherited BSP Baseline Report, the handover document nobody left you.&lt;/p&gt;

&lt;p&gt;At some point in an embedded Linux career, somebody hands you a board and a git repository and leaves the company. The repository builds. The board boots. You now own an inherited vendor BSP you did not write, and within a week somebody will ask how long it would take to move it to a newer kernel.&lt;/p&gt;

&lt;p&gt;You cannot answer that yet, but producing a number you can defend is a mechanical exercise: five measurements, run in order, before any other work on the platform. One boundary up front: this maps the Linux side of the platform. U-Boot, TF-A and binary firmware carry their own deltas and are not covered here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an unmeasured vendor BSP costs
&lt;/h2&gt;

&lt;p&gt;Here is what happens when nobody runs those five measurements. An engineer takes over an inherited vendor BSP on a 6.6 kernel, reads release notes promising "board support and stability fixes", glances at the driver directories, and estimates six weeks to reach a newer longterm kernel. The estimate goes on a roadmap. Eight months later the work is not finished — not through incompetence, but because the tree held around four hundred non-merge commits, a few dozen touching core memory management rather than board files, and eleven modules built from a source tree nobody could locate. None of that was in the release notes. All of it was discoverable in two days.&lt;/p&gt;

&lt;p&gt;The damage is worse than one bad estimate, because the gap repeats. Each vendor release rebases and the delta is never measured, so the debt stays invisible until somebody tries to pay it. Then the SoC vendor ends support, and the number nobody measured becomes the number the schedule depends on.&lt;/p&gt;

&lt;p&gt;The five measurements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Risk it exposes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Running kernel config&lt;/td&gt;
&lt;td&gt;What is the board actually running?&lt;/td&gt;
&lt;td&gt;Runtime config has drifted from the repository defconfig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Mainline delta&lt;/td&gt;
&lt;td&gt;How far is this tree from upstream?&lt;/td&gt;
&lt;td&gt;Core-subsystem changes hidden behind a patch count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. External modules&lt;/td&gt;
&lt;td&gt;What code lives outside the tree?&lt;/td&gt;
&lt;td&gt;Unknown ownership and maintenance liability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Booted device tree&lt;/td&gt;
&lt;td&gt;What hardware description did Linux receive?&lt;/td&gt;
&lt;td&gt;Bootloader fixups and overlays absent from the source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Yocto layer map&lt;/td&gt;
&lt;td&gt;What metadata really built the image?&lt;/td&gt;
&lt;td&gt;Undocumented overrides and unpinned revisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Recover the running kernel configuration
&lt;/h2&gt;

&lt;p&gt;Be precise about what you are collecting: not the defconfig in the repository, but the configuration of the kernel binary on the board. In an inherited vendor BSP those disagree more often than they agree, because fragments, recipe appends and local builds all mutate the config on its way to the image.&lt;/p&gt;

&lt;p&gt;The kernel can carry its own configuration. &lt;code&gt;CONFIG_IKCONFIG&lt;/code&gt; embeds the &lt;code&gt;.config&lt;/code&gt; into the image; &lt;code&gt;CONFIG_IKCONFIG_PROC&lt;/code&gt; exposes it at &lt;code&gt;/proc/config.gz&lt;/code&gt;. The distinction catches people: the first can be a module, the second cannot. Seeing &lt;code&gt;CONFIG_IKCONFIG=m&lt;/code&gt; does not mean &lt;code&gt;/proc/config.gz&lt;/code&gt; will be there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-lpddr4-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;zcat /proc/config.gz &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/running.config
&lt;span class="gp"&gt;root@imx8mp-lpddr4-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /tmp/running.config
&lt;span class="go"&gt;6314 /tmp/running.config
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is missing, the configuration may still be embedded. Point the kernel's own extraction script at the binary the board boots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;scripts/extract-ikconfig /path/to/vmlinux &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; running.config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reduce it before comparing. A raw &lt;code&gt;.config&lt;/code&gt; holds thousands of symbols nobody chose — they were implied by others — so comparing two raw configs produces a diff nobody reads. &lt;code&gt;make savedefconfig&lt;/code&gt; writes a minimised &lt;code&gt;defconfig&lt;/code&gt; stripped of everything implied elsewhere, leaving the decisions a human actually made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;running.config .config
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;make savedefconfig
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;scripts/diffconfig &lt;span class="nb"&gt;arch&lt;/span&gt;/arm64/configs/vendor_defconfig defconfig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is a short list rather than a wall of text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-DEBUG_FS y
&lt;/span&gt; PREEMPT n -&amp;gt; y
&lt;span class="gi"&gt;+VENDOR_THERMAL_HACK y
+MODULE_SIG_FORCE y
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines of real information. Somebody turned off debugfs, switched the preemption model, and added a symbol mainline does not have. Treat that last line as a strong investigation signal rather than proof: it may be a vendor Kconfig addition, a stale fragment no longer consumed by the active build path, or an option renamed upstream. Trace it through Kconfig and the Yocto configuration path before calling it live out-of-tree functionality.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Commit the minimised defconfig into your own repository with today's date in the filename. It is the first written record of what the board runs, and every future argument about "did we change that?" is settled by diffing against it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Measure the distance from mainline
&lt;/h2&gt;

&lt;p&gt;Now size the delta. Add a mainline remote and count commits added on top of the vendor's base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git remote add mainline https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git fetch mainline &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--no-merges&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git merge-base v6.6 vendor/release&lt;span class="si"&gt;)&lt;/span&gt;..vendor/release | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That count is the headline number and on its own close to meaningless. What decides the difficulty of a kernel bump is not how many patches the vendor wrote, but which directories they wrote them in. Ask for that directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git merge-base v6.6 vendor/release&lt;span class="si"&gt;)&lt;/span&gt;..vendor/release | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the result as a shape, not a score. Weight sitting in &lt;code&gt;arch/arm64/boot/dts/&lt;/code&gt;, &lt;code&gt;drivers/soc/&lt;/code&gt; and a few drivers means an ordinary vendor BSP: large but predictable, because device tree files usually rebase cleanly. Commits in &lt;code&gt;mm/&lt;/code&gt;, &lt;code&gt;kernel/sched/&lt;/code&gt; or the DMA core change the character of the job, because those subsystems move internally between releases. As a heuristic rather than a rule, a large device tree delta is often more predictable to rebase than a much smaller delta in core code. Location and dependency depth matter more than commit count.&lt;/p&gt;

&lt;p&gt;One limit. This assumes the vendor tree shares history with mainline. Some vendors ship a squashed import with no common ancestor, and &lt;code&gt;git merge-base&lt;/code&gt; fails. Then compare trees rather than history — &lt;code&gt;git diff --stat v6.6 vendor/release&lt;/code&gt; against the closest tag. Treat that candidate tag as a hypothesis, not a fact: validate it against version metadata, a representative subsystem diff and the vendor's own release context before any estimate rests on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Inventory the externally built modules
&lt;/h2&gt;

&lt;p&gt;Commits are the visible part of an inherited vendor BSP. The expensive part is usually code that was never in a tree at all, and the kernel gives one signal for it: loading an externally built module sets a taint flag. Bit 12, shown as &lt;code&gt;O&lt;/code&gt;, has raw value 4096. Read it for exactly what it is — at least one externally built module has been loaded. It does not enumerate them, say where the code came from, or assign ownership.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-lpddr4-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/kernel/tainted
&lt;span class="go"&gt;4096
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find them, check each loaded module for the &lt;code&gt;intree&lt;/code&gt; marker kbuild stamps onto modules built inside the tree. In-tree modules report &lt;code&gt;intree: Y&lt;/code&gt;; externally built ones do not carry the field at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-lpddr4-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;m &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;lsmod | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; +2 | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;modinfo &lt;span class="nt"&gt;-F&lt;/span&gt; intree &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A module without a &lt;code&gt;Y&lt;/code&gt; is externally built relative to the running kernel tree. That is all the flag establishes. It does not follow that the vendor wrote it — it may belong to your own product team, a supplier, or an integration nobody documented. Record five things for each: source repository, build path, version, licence and maintainer. However ownership falls, no kernel upgrade carries these modules for you, and because they are invisible in the git history you just counted, they are the part of the estimate engineers consistently forget.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you cannot say where a module's source lives and who last touched it, you have found the largest risk in the platform — in your first week.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Read the device tree the board actually booted
&lt;/h2&gt;

&lt;p&gt;The device tree source in the repository is what the build produced, not necessarily what the kernel saw. The bootloader modifies the blob in transit, commonly inserting memory node sizes, MAC addresses and overlays. Read the real one back out of the running kernel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-lpddr4-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dtc &lt;span class="nt"&gt;-I&lt;/span&gt; dtb &lt;span class="nt"&gt;-O&lt;/span&gt; dts &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/booted.dts /sys/firmware/fdt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every platform exposes &lt;code&gt;/sys/firmware/fdt&lt;/code&gt;. Where it is absent, inspect the live tree under &lt;code&gt;/sys/firmware/devicetree/base&lt;/code&gt; or &lt;code&gt;/proc/device-tree&lt;/code&gt;, and compare semantics rather than expecting decompiled output to match source formatting.&lt;/p&gt;

&lt;p&gt;Diff the result against the source. The differences are the bootloader's fixups, and knowing them saves you from a class of confusing bugs where the property you see in the repository is not the property the driver read.&lt;/p&gt;

&lt;p&gt;Then ask which devices are still waiting. Deferred probe retries a device whose dependency was not ready at that point in boot, and the driver core exposes the queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@imx8mp-lpddr4-evk:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/kernel/debug/devices_deferred
&lt;span class="go"&gt;30a20000.i2c:pmic@25
32c00000.bus:mipi_dsi@32e60000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line names a device whose probe was deferred because a dependency — commonly a regulator or clock provider — was not ready when the driver core tried. Some bind later once the supplier appears; others never do. Read the list as dependency chains to investigate, then confirm final binding state separately. A non-empty list is normal, and often the first clear statement of which hardware is described but not working.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Map the Yocto layers
&lt;/h2&gt;

&lt;p&gt;If the vendor BSP ships as Yocto metadata, three commands give you the build-side map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bitbake-layers show-layers
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bitbake-layers show-recipes
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bitbake-layers show-appends
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;show-layers&lt;/code&gt; lists layers with priorities, which decides who wins when two provide the same recipe. &lt;code&gt;show-appends&lt;/code&gt; repays the effort fastest: it lists every &lt;code&gt;.bbappend&lt;/code&gt; and the recipe it modifies, answering "who edited this, and where does that edit live". An append in a layer nobody remembers adding is the most common source of behaviour that appears to come from nowhere.&lt;/p&gt;

&lt;p&gt;Then pin the kernel. &lt;code&gt;bitbake -e virtual/kernel&lt;/code&gt; prints the recipe's expanded variables; you want &lt;code&gt;SRC_URI&lt;/code&gt;, &lt;code&gt;SRCREV&lt;/code&gt; and &lt;code&gt;KERNEL_DEVICETREE&lt;/code&gt;. Those name the tree, commit and device tree files the image is really built from, frequently not what the documentation claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this method does not tell you
&lt;/h2&gt;

&lt;p&gt;A map that overstates its coverage is worse than no map.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count is not difficulty.&lt;/strong&gt; The directory breakdown gives the shape; only reading a sample tells you whether patches are mechanical or deep. Read twenty at random before committing to a number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The config comparison assumes one kernel version.&lt;/strong&gt; Across versions, renamed symbols produce noise — use &lt;code&gt;make listnewconfig&lt;/code&gt; against the newer tree instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deferred probe output needs debugfs,&lt;/strong&gt; which production vendor configurations often disable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;These commands are verified against documentation, not against every vendor tree.&lt;/strong&gt; Vendors modify kbuild and strip instrumentation. Where a step fails, the failure itself tells you how far this tree has drifted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The deliverable: an Inherited BSP Baseline Report
&lt;/h2&gt;

&lt;p&gt;Run the five measurements in order, then write the result up as one document. Name it, because a named deliverable survives handovers in a way scattered notes do not: the Inherited BSP Baseline Report. One page, not a wiki tree. It opens with the board's identity, carries the five measurements in the order you ran them, and closes with what you could not determine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Board and SoC identity, with bootloader, firmware and kernel versions.&lt;/li&gt;
&lt;li&gt;The minimised runtime config snapshot, committed with today's date.&lt;/li&gt;
&lt;li&gt;The mainline base hypothesis and the delta by directory, with one sentence on the shape.&lt;/li&gt;
&lt;li&gt;External module inventory: repository, build path, version, licence and maintainer for each.&lt;/li&gt;
&lt;li&gt;Booted FDT versus source DTS differences, and the deferred-device list with final binding state.&lt;/li&gt;
&lt;li&gt;The Yocto layer, append and kernel recipe map, with &lt;code&gt;SRC_URI&lt;/code&gt;, &lt;code&gt;SRCREV&lt;/code&gt; and &lt;code&gt;KERNEL_DEVICETREE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Risks, unknowns and next actions — including what you could not determine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last line matters most. A baseline that admits what it could not establish is trustworthy; one that quietly omits the gaps will be used to justify an estimate it cannot support.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do this before you are asked for an estimate, not after. An engineer who answers "how far are we from mainline" with a measured number in week two is treated differently for the rest of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The first job on an inherited vendor BSP is measurement, not reading code.&lt;/li&gt;
&lt;li&gt;Compare minimised defconfigs; patch location beats patch count.&lt;/li&gt;
&lt;li&gt;Externally built modules are invisible in git history and no upgrade carries them.&lt;/li&gt;
&lt;li&gt;The booted device tree and the Yocto layer map both differ from what the repository shows.&lt;/li&gt;
&lt;li&gt;Record what you could not establish. A baseline hiding its gaps will be misused.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why compare minimised defconfigs instead of the raw .config files?&lt;/strong&gt;&lt;br&gt;
A raw .config contains thousands of symbols that were implied by other symbols rather than chosen by anyone. &lt;code&gt;make savedefconfig&lt;/code&gt; strips those, so the resulting comparison shows only the decisions a person actually made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if git merge-base fails on the vendor tree?&lt;/strong&gt;&lt;br&gt;
That usually means a squashed import with no shared history. Compare trees directly with &lt;code&gt;git diff --stat&lt;/code&gt; against the closest matching mainline tag, treating that tag as a hypothesis to validate rather than a fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I tell which loaded modules are out-of-tree?&lt;/strong&gt;&lt;br&gt;
Modules built inside the kernel tree carry an &lt;code&gt;intree&lt;/code&gt; marker that &lt;code&gt;modinfo -F intree&lt;/code&gt; reports as &lt;code&gt;Y&lt;/code&gt;. Modules built outside the tree do not carry the field, and loading one sets kernel taint bit 12, which appears as raw value 4096 in &lt;code&gt;/proc/sys/kernel/tainted&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/kbuild/kconfig.html" rel="noopener noreferrer"&gt;Configuration targets and editors — Linux kernel documentation&lt;/a&gt; (covers listnewconfig and scripts/diffconfig)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/admin-guide/tainted-kernels.html" rel="noopener noreferrer"&gt;Tainted kernels — Linux kernel documentation&lt;/a&gt; (taint bit 12, externally-built modules)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/kbuild/kbuild.html" rel="noopener noreferrer"&gt;Kbuild — Linux kernel documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.yoctoproject.org/dev-manual/layers.html" rel="noopener noreferrer"&gt;Understanding and Creating Layers — Yocto Project Development Tasks Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.yoctoproject.org/bsp-guide/bsp.html" rel="noopener noreferrer"&gt;Yocto Project Board Support Package Developer's Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/proc_config.gz.5.html" rel="noopener noreferrer"&gt;proc_config.gz(5) — Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I teach Linux kernel and embedded Linux engineering at &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;TECH VEDA&lt;/a&gt;. If your team is taking over a vendor BSP and wants to build this baseline properly, our &lt;a href="https://www.techveda.live/embedded-linux-yocto/" rel="noopener noreferrer"&gt;Embedded Linux and Yocto&lt;/a&gt; programme covers the layer and kernel-recipe side in depth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>embedded</category>
      <category>kernel</category>
      <category>yocto</category>
    </item>
    <item>
      <title>The State of Automotive Grade Linux in 2026: Which Release to Build</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:02:15 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/which-automotive-grade-linux-release-should-you-actually-build-1n2l</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/which-automotive-grade-linux-release-should-you-actually-build-1n2l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The Automotive Grade Linux distribution is not one release on one baseline. As of 26 August 2026 its release-notes page documents three listed release series on three different Yocto Project releases: Ultimate Unagi on Scarthgap 5.0.x, Vibrant Vimba on Wrynose 6.0.x, and the old-stable Quirky Quillback on Kirkstone 4.0.x, which the Yocto Project now marks end of life. Which one you fetch depends on how long your programme has to be supported, and the answer is in the release notes rather than in any announcement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have been asked to stand up an Automotive Grade Linux build this quarter, the first decision is also the one most likely to be wrong: which branch to fetch. The project publishes announcements, a product page and a release-notes wiki, and in August 2026 the three do not agree. An engineer who follows the press release lands on one baseline; an engineer who reads the release-notes table lands on a different one. The gap matters, because the two baselines have Yocto support windows that end two years apart, and a vehicle programme outlives both.&lt;/p&gt;

&lt;p&gt;This is a practical problem rather than a criticism; large open source projects carry several series at once on purpose. What follows is what the artefacts say, why the project is structured this way, and what has moved since May.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Automotive Grade Linux release notes actually list
&lt;/h2&gt;

&lt;p&gt;The release-notes wiki page at &lt;code&gt;wiki.automotivelinux.org/agl-distro/release-notes&lt;/code&gt; records its own last modification as 24 August 2026. Read on 26 August 2026, it lists three named series.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Series&lt;/th&gt;
&lt;th&gt;Yocto baseline&lt;/th&gt;
&lt;th&gt;Upstream Yocto support&lt;/th&gt;
&lt;th&gt;Fetch model&lt;/th&gt;
&lt;th&gt;Main implication&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vibrant Vimba&lt;/td&gt;
&lt;td&gt;Wrynose 6.0.x&lt;/td&gt;
&lt;td&gt;To April 2030&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;master&lt;/code&gt; plus milestone manifest&lt;/td&gt;
&lt;td&gt;Longest upstream horizon, but maturity and board coverage must be checked separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultimate Unagi&lt;/td&gt;
&lt;td&gt;Scarthgap 5.0.x&lt;/td&gt;
&lt;td&gt;To April 2028&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;unagi&lt;/code&gt; branch plus manifest&lt;/td&gt;
&lt;td&gt;Better fit where a cut branch and current board support matter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quirky Quillback&lt;/td&gt;
&lt;td&gt;Kirkstone 4.0.x&lt;/td&gt;
&lt;td&gt;End of life&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;quillback&lt;/code&gt; branch&lt;/td&gt;
&lt;td&gt;Legacy reproducibility only, unless the team owns the backports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Vibrant Vimba&lt;/strong&gt; appears directly under the heading "Latest Stable Release". Its first listed feature is "Update to Yocto Project 'wrynose 6.0.x'". Its two documented builds are Milestone 1 v21.91.0 and Milestone 2 v21.92.0, and the Jira filter linked from both is labelled "Vibrant Vimba 22.0.0".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ultimate Unagi&lt;/strong&gt; is the 21.0.x series. Its feature list opens with "Update to Yocto Project 'scarthgap' 5.0.x" and goes on to name Qt 6.8, an upgrade to VSS 6.0, meta-flutter 3.38.3, the addition of &lt;code&gt;meta-ros&lt;/code&gt;, KVM demo updates, and the line "Added sparrow-hawk board". Four point releases are documented. The notable-changes block for v21.0.2 reads "Yocto Project 'scarthgap' 5.0.18"; for v21.0.3 it reads "Yocto Project 'scarthgap' 5.0.19", plus layer and BSP updates and support for the Sifive hifive-premier-p550 board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quirky Quillback&lt;/strong&gt; is the old-stable series, numbered 17.x, and its build-artifacts note reads "Uses Yocto 4.0.35 (YP kirkstone)".&lt;/p&gt;

&lt;p&gt;The fetch commands are the clearest signal, because they encode the project's own view of each series. These run on your build machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;repo init &lt;span class="nt"&gt;-b&lt;/span&gt; unagi &lt;span class="nt"&gt;-m&lt;/span&gt; unagi_21.0.3.xml &lt;span class="nt"&gt;-u&lt;/span&gt; https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;repo &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unagi has a release branch of its own. So does Quillback, with &lt;code&gt;-b quillback&lt;/code&gt;. Vibrant Vimba does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;repo init &lt;span class="nt"&gt;-b&lt;/span&gt; master &lt;span class="nt"&gt;-m&lt;/span&gt; vimba_21.92.0.xml &lt;span class="nt"&gt;-u&lt;/span&gt; https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;repo &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The series the page calls "Latest Stable Release" is fetched from &lt;code&gt;master&lt;/code&gt; with a milestone manifest, not from a cut release branch.&lt;/strong&gt; That is a meaningful difference in what you are pinning. The branch it sits on is still the one under active development, whereas Unagi gives you both a manifest and a branch that is not moving underneath you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Branch, manifest, and what actually counts as a baseline&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branch&lt;/strong&gt; — a moving line of development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest&lt;/strong&gt; — a pinned set of repository revisions, which is what makes a build reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production baseline&lt;/strong&gt; — a manifest, plus validation evidence, plus a maintenance plan naming who fixes what and for how long.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the project pins a Yocto LTS at all
&lt;/h2&gt;

&lt;p&gt;Every series above is named after a Yocto Project release, and that is not decoration. A distribution built with OpenEmbedded inherits its recipe set, its toolchain, and its CVE-patching cadence from the Yocto release it is based on. When that release goes end of life, the recipes stop receiving fixes, and the burden of backporting moves to whoever is shipping the product.&lt;/p&gt;

&lt;p&gt;The Yocto Project releases table, read on 26 August 2026, gives the windows. Scarthgap 5.0 was released in April 2024 and is long-term supported until April 2028. Wrynose 6.0 was released in April 2026 and is supported until April 2030. Kirkstone 4.0 came out in May 2022, reached 4.0.33 in February 2026, and is now marked end of life. The non-LTS releases show why the distinction exists: Walnascar 5.2 has been end of life since November 2025 and Whinlatter 5.3 is end of life too, both within roughly a year of release. Blacksail 6.1, due October 2026, is scheduled for six months of support.&lt;/p&gt;

&lt;p&gt;One qualification matters before those dates are used for planning. They are upstream Yocto Project baseline support horizons and nothing more. They say nothing about how long AGL will maintain a given series, how long a silicon vendor will support its BSP layers, or what a vehicle programme will require. Each is a separate commitment, owned by a different party, and has to be confirmed with that party.&lt;/p&gt;

&lt;p&gt;Set those windows against a vehicle programme. A platform entering production in 2028 and staying in service for a decade cannot be anchored to a baseline whose upstream fixes stop in April 2028. It cannot really be anchored to one that stops in April 2030 either, but the difference is two more years of somebody else doing the security backports before that work lands on your team. The same arithmetic governs kernel selection, and it is set out in more detail in &lt;a href="https://www.techveda.live/2026/07/09/kernel-lts-support-embedded-planning/" rel="noopener noreferrer"&gt;Kernel LTS Support: Planning Embedded Products Around It&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The presence of a Kirkstone-based old-stable series in Automotive Grade Linux is not an oversight. Programmes that validated on Quillback years ago are still in the field, and the project keeps the series listed so those teams can reproduce a build. That does not make Kirkstone supported again. A team fetching &lt;code&gt;-b quillback&lt;/code&gt; today takes on the backporting itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed between May and August 2026
&lt;/h2&gt;

&lt;p&gt;Three months is enough to move the Automotive Grade Linux picture, and it has.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;14 May 2026&lt;/strong&gt;, at the AGL All Member Meeting in Tokyo, the Linux Foundation announced the initial availability of the AGL SoDeV reference platform, delivered through the Unified Code Base release codenamed Ultimate Unagi. The announcement described SoDeV as combining the Unified Code Base with Linux containers, VirtIO, the Xen hypervisor and the Zephyr RTOS, running on Renesas Sparrow Hawk reference boards, virtual machines and cloud environments. It stated the Yocto baseline as "Scarthgap LTS release 5.0.16", said Ultimate Unagi would be supported for two years, and committed to updates "approximately three weeks after each Scarthgap release by the Yocto Project". SoDeV had been announced in December 2025, so May is when developers could first fetch it.&lt;/p&gt;

&lt;p&gt;By &lt;strong&gt;24 August 2026&lt;/strong&gt;, the date the release-notes page records for its last edit, two things had moved. Ultimate Unagi had reached v21.0.3 on Scarthgap 5.0.19, three point releases beyond the 5.0.16 named in May. And Vibrant Vimba, on Wrynose 6.0.x, had taken the "Latest Stable Release" heading.&lt;/p&gt;

&lt;p&gt;The first is the three-week commitment being kept, visible in the artefacts rather than asserted. One caveat on the detail. The AGL notes for 21.0.3 cite Scarthgap 5.0.19, while the Yocto Project's own releases wiki, read the same day, lists 5.0.18 from June 2026 as the current Scarthgap version. The two sources were inconsistent when checked. The AGL entry records the baseline that AGL artefact was built against, which is not the same thing as a statement of upstream Scarthgap status, so confirm the current point release with the Yocto Project directly before resting maintenance assumptions on either number.&lt;/p&gt;

&lt;p&gt;The second is a baseline change inside a single quarter. A team that started an Automotive Grade Linux evaluation from the May announcement is on Scarthgap; the page now points newcomers at Wrynose. Neither is wrong, but an evaluation framed as "we are building on what AGL ships" has already gone out of date once.&lt;/p&gt;

&lt;p&gt;The product page adds a third answer. The Unified Code Base page on automotivelinux.org, read on 26 August 2026, still states "The latest release, UCB 16.0 (Prickly Pike)" and still describes the Unified Code Base as an in-vehicle infotainment distribution. The release-notes page's own introduction offers a fourth, saying "The latest stable release is Terrific Trout", directly above the heading naming Vibrant Vimba. Prose lags tables, and tables lag manifests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SoDeV appears, and where it does not
&lt;/h2&gt;

&lt;p&gt;This needs care, because the Automotive Grade Linux announcement and the release notes describe the same release in different vocabularies.&lt;/p&gt;

&lt;p&gt;The string "SoDeV" does not appear anywhere on the AGL release-notes wiki page as read on 26 August 2026. Neither does the word "kernel", for any series. I checked the release-notes page, the Unified Code Base product page and the 14 May announcement, and found no published kernel version for any Unagi or Vimba point release. This post therefore does not state one, and you should not infer one from the Yocto baseline.&lt;/p&gt;

&lt;p&gt;What the release notes do show is consistent with the announcement without confirming its component list. The Unagi layer list includes &lt;code&gt;meta-virtualization&lt;/code&gt;, and the feature list includes "KVM demo updates" and the addition of the &lt;code&gt;sparrow-hawk&lt;/code&gt; board. Those are the hooks a virtualised, multi-guest platform needs, but they are not proof that Xen and Zephyr are in the default image, and I did not verify that either way. If hypervisor configuration matters to your architecture, fetch the manifest and read the layer revisions.&lt;/p&gt;

&lt;p&gt;One more detail is directly useful. Vibrant Vimba lists, verbatim, the known issue "R-Car Gen3 is not yet available (eta from upstream ~Oct) - use sparrow-hawk meantime". The newer baseline has lost a board that older builds had, with recovery expected around October. That is the kind of gap that does not appear in an announcement and does appear in a release note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a branch
&lt;/h2&gt;

&lt;p&gt;Reduce the Automotive Grade Linux branch decision to three questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does the support window need to run?&lt;/strong&gt; If the programme extends past April 2028, Scarthgap's window ends inside it and Ultimate Unagi inherits that. Wrynose runs to April 2030. That is the strongest argument for Vibrant Vimba.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need a cut release branch?&lt;/strong&gt; Unagi has one. Vimba is fetched from &lt;code&gt;master&lt;/code&gt; with a milestone manifest, and its milestone numbering is 21.9x rather than 22.0 even though the linked issue filter is labelled 22.0.0. Whether it is production-ready despite the "Latest Stable Release" heading is a question to put to the project directly rather than infer, and worth asking before committing a programme.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which boards do you need today?&lt;/strong&gt; The Unagi 21.0.3 artefacts table lists a Renesas H3 development board, QEMU on x86_64 and an Intel Up² board. Unagi added &lt;code&gt;sparrow-hawk&lt;/code&gt; and, at 21.0.3, the Sifive hifive-premier-p550. Vimba currently lacks R-Car Gen3. If your bring-up target is one of those, board availability decides the branch regardless of support window, at least until October.&lt;/p&gt;

&lt;p&gt;Underneath this sits an argument the project itself has been making. In a talk at the May All Member Meeting, published on the AGL blog on 23 June 2026, Hisao Munakata of Renesas argued that the software-defined vehicle transition repeats the Linux kernel's own 2.4 to 2.6 integration crisis, and that what resolved it was not any single technology but time-based release engineering: a fixed merge window, a validation-only period, and the discipline to defer whatever does not fit. He put the kernel's release-interval standard deviation across the v6 and v7 series at 2.83 days, and observed that automotive has no equivalent cadence. The three-week-after-Scarthgap commitment, and the point releases that followed it, are that discipline in practice. It is also why the release notes are the better source: a project on a cadence records where it is in its tables, not in its announcements.&lt;/p&gt;

&lt;h2&gt;
  
  
  A branch-selection checklist
&lt;/h2&gt;

&lt;p&gt;Steps one and two decide the branch; the rest decide whether you can defend the choice.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the programme start date and the maintenance horizon it has to cover.&lt;/li&gt;
&lt;li&gt;Confirm the boards you need and whether their BSP layers are present in the series you are considering.&lt;/li&gt;
&lt;li&gt;Pin the exact manifest and archive the layer revisions it resolves to.&lt;/li&gt;
&lt;li&gt;Confirm the current upstream Yocto support status with the Yocto Project, not a downstream page.&lt;/li&gt;
&lt;li&gt;Read the release notes and issue tracker for the target branch, including known issues.&lt;/li&gt;
&lt;li&gt;Verify virtualisation, image composition and board support from the actual layers, not announcement text.&lt;/li&gt;
&lt;li&gt;Decide, and write down, who owns the backports once upstream support ends.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automotive Grade Linux lists three release series on three Yocto baselines: Vibrant Vimba on Wrynose 6.0.x, Ultimate Unagi 21.0.x on Scarthgap 5.0.x, and old-stable Quirky Quillback 17.x on Kirkstone 4.0.x.&lt;/li&gt;
&lt;li&gt;Kirkstone is end of life; Scarthgap is supported to April 2028; Wrynose to April 2030. These are upstream Yocto horizons only, not AGL, BSP or programme commitments, but the support window should still drive the choice.&lt;/li&gt;
&lt;li&gt;Unagi and Quillback have cut release branches. Vibrant Vimba is fetched from &lt;code&gt;master&lt;/code&gt; with a milestone manifest.&lt;/li&gt;
&lt;li&gt;Unagi moved from Scarthgap 5.0.16 in May to 5.0.19 by late August, which is the project's stated three-week cadence visible in the artefacts.&lt;/li&gt;
&lt;li&gt;The announcement, the product page and the release-notes page each name a different "latest" release. Use the manifest as the reproducibility anchor, then validate maintenance status, BSP coverage and programme readiness separately.&lt;/li&gt;
&lt;li&gt;Vibrant Vimba currently lists R-Car Gen3 as unavailable, with an upstream estimate of around October.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which Automotive Grade Linux release has the longest support window?&lt;/strong&gt;&lt;br&gt;
Vibrant Vimba, because it is based on Yocto Project Wrynose 6.0, which is supported until April 2030. Ultimate Unagi is based on Scarthgap 5.0, supported until April 2028.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Quirky Quillback still safe to build on?&lt;/strong&gt;&lt;br&gt;
It is still documented and still fetchable, but its Yocto baseline, Kirkstone 4.0, is marked end of life by the Yocto Project. A team building on it takes on the backporting of upstream recipe fixes itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What kernel version does Ultimate Unagi use?&lt;/strong&gt;&lt;br&gt;
The AGL release-notes page does not state one. The word "kernel" does not appear on that page for any series, and no kernel version was found on the Unified Code Base product page or in the May 2026 announcement either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I fetch a specific Automotive Grade Linux release?&lt;/strong&gt;&lt;br&gt;
With &lt;code&gt;repo init&lt;/code&gt; against the AGL-repo Gerrit URL, using the branch and manifest named in the release notes for that point release, then &lt;code&gt;repo sync&lt;/code&gt;. Unagi uses &lt;code&gt;-b unagi&lt;/code&gt;, Quillback uses &lt;code&gt;-b quillback&lt;/code&gt;, and Vibrant Vimba uses &lt;code&gt;-b master&lt;/code&gt; with a milestone manifest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the AGL SoDeV platform the same thing as Ultimate Unagi?&lt;/strong&gt;&lt;br&gt;
The May 2026 announcement said the initial SoDeV reference platform was delivered through the Ultimate Unagi release. The name SoDeV does not appear on the release-notes page itself, so the mapping between the two is documented in the announcement rather than in the release notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automotive Grade Linux distro release notes (primary artefact, last modified 24 August 2026) — &lt;a href="https://wiki.automotivelinux.org/agl-distro/release-notes" rel="noopener noreferrer"&gt;wiki.automotivelinux.org/agl-distro/release-notes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Yocto Project Releases table (primary — release dates, current versions and support levels) — &lt;a href="https://wiki.yoctoproject.org/wiki/Releases" rel="noopener noreferrer"&gt;wiki.yoctoproject.org/wiki/Releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux Foundation, "Automotive Grade Linux Releases Open Source SoDeV Reference Platform for Software-Defined Vehicles and Welcomes Five New Members", 14 May 2026 — &lt;a href="https://www.linuxfoundation.org/press/automotive-grade-linux-releases-open-source-sodev-reference-platform-for-software-defined-vehicles-and-welcomes-five-new-members" rel="noopener noreferrer"&gt;linuxfoundation.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Automotive Grade Linux, Unified Code Base product page — &lt;a href="https://www.automotivelinux.org/software/unified-code-base/" rel="noopener noreferrer"&gt;automotivelinux.org/software/unified-code-base&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hisao Munakata (Renesas), "Make or Buy or Collaborate: What Linux Kernel History Tells Us About the SDV Crisis", AGL All Member Meeting Tokyo, May 2026, published on the AGL blog 23 June 2026 — &lt;a href="https://www.automotivelinux.org/blog/make-or-buy-or-collaborate-what-linux-kernel-history-tells-us-about-the-sdv-crisis/" rel="noopener noreferrer"&gt;automotivelinux.org/blog&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I run Linux kernel and embedded Linux training at &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;TECH VEDA&lt;/a&gt;, where a good deal of the Yocto material comes down to exactly this: reading layer revisions and release notes instead of trusting a version label. If your team is picking a BSP baseline this year, the branch-selection checklist above is the short version of that conversation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>automotive</category>
      <category>embedded</category>
      <category>yocto</category>
    </item>
    <item>
      <title>Why the Inference Process Keeps Growing After It Starts</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:17:29 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/why-the-inference-process-keeps-growing-after-it-starts-4f1f</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/why-the-inference-process-keeps-growing-after-it-starts-4f1f</guid>
      <description>&lt;p&gt;&lt;em&gt;Written for the person who owns the DRAM budget, the memory cgroup and the OOM postmortem on an embedded board — not the person tuning the model.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To the kernel, an edge AI model is not layers. It is two mappings with very different properties: the weights, usually mapped file-backed and therefore reclaimable, and the tensor arena, a single anonymous mapping holding every intermediate tensor, which on a board without swap cannot be reclaimed at all. The arena is roughly four to ten and a half times smaller than the sum of the tensor sizes, it becomes resident only as its pages are written, and no allocation strategy can take it below a size fixed by one layer of the graph.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Somebody hands you a model file and asks whether it will run on the board. You are the one who has to answer, because you own the DRAM budget, the memory cgroup, and the postmortem when the OOM killer takes the inference process at three in the morning. The weights are the easy half: that number is on disk and you can read it. The hard half is the tensor arena, the block the runtime carves up for intermediate results, and nobody on the model side can tell you how large it will be. This article is about sizing that tensor arena with kernel tools rather than guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the kernel sees when a model loads
&lt;/h2&gt;

&lt;p&gt;Attach to the process after it has loaded a model and look at its mappings. You will find two objects that matter — the weights and the tensor arena — and the difference between them decides how the system behaves under pressure.&lt;/p&gt;

&lt;p&gt;The first is the model file itself. Where the platform supports it, the runtime maps the file rather than reading it into a buffer. In LiteRT this is &lt;code&gt;MMAPAllocation&lt;/code&gt;, selected by &lt;code&gt;FlatBufferModel::BuildFromFile&lt;/code&gt; after an &lt;code&gt;IsSupported()&lt;/code&gt; check, with &lt;code&gt;FileCopyAllocation&lt;/code&gt; as the fallback on platforms where mapping is not available. When mapping is used, the weights are file-backed pages. They appear in &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/maps&lt;/code&gt; with the model's pathname, they are clean, and the kernel can drop them under memory pressure and fault them back in from storage later.&lt;/p&gt;

&lt;p&gt;The second is the tensor arena. It is one large anonymous allocation with no backing file, and it normally exceeds glibc's mmap threshold, so it is served by &lt;code&gt;mmap&lt;/code&gt; as its own mapping rather than carved out of the heap. It appears in the maps with no pathname. Two caveats before you rely on that: glibc's threshold starts at 128 KiB but is raised dynamically, up to 32 MiB on 64-bit, once a large block has been freed, and a musl or uClibc-ng rootfs behaves differently again. Check it on your own board rather than assuming.&lt;/p&gt;

&lt;p&gt;That difference is the whole reason a kernel engineer should care about this. Under memory pressure the kernel can reclaim clean file-backed pages for free. It cannot do that with the arena's anonymous pages: once written, they have nowhere to go but swap. Most embedded products ship without swap at all. Where zram is configured there is a swap device, so the page frame really is freed and a compressed copy is kept in the zram pool — but activation tensors are dense numeric data and compress far worse than ordinary heap, and every refault costs decompression on the inference path. So of the two halves of your model's memory, the weights are the half the kernel can drop for free, and the tensor arena is the half that stays where it is until the process exits. When you size DRAM, the arena is the number with no give in it.&lt;/p&gt;

&lt;p&gt;The weights half is its own problem, governed by the numeric format you ship and by how the memory system moves those bytes, covered separately in &lt;a href="https://www.techveda.live/2026/07/15/4-bit-weight-quantization-edge/" rel="noopener noreferrer"&gt;4-Bit Weight Quantization: Why the Memory System Decides&lt;/a&gt;. The rest of this article is about the half that cannot be reclaimed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number the model team gives you is wrong
&lt;/h2&gt;

&lt;p&gt;Ask for the memory requirement and you will usually get the sum of the tensor sizes, because that is easy to compute. For MobileNet v2 at 32-bit float the intermediate portion of that sum is about 26 MB, roughly twice the size of the model file. Measure the process on the board and you will see nothing like 26 MB of anonymous memory. You will see something near 6 MB. Here is the calculation you were handed, in full, which runs on the host rather than the board because it needs the full framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;

&lt;span class="n"&gt;interp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Interpreter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model.tflite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allocate_tensors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tensor_details&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&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 class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dtype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;itemsize&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sum of all tensor bytes:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what that loop actually sums: every tensor the interpreter reports, weights and constants included, for the primary subgraph only. It is the shape of the estimate you will be handed, not a measurement of the arena. The intermediate share of it is what the naive column of the table further down measures.&lt;/p&gt;

&lt;p&gt;The gap is the allocation plan, and because it depends on the shape of the graph rather than on the weight count, it produces a specific failure. A team sizes DRAM from one model they measured, then ships a second model with similar accuracy and a similar file size, and the second one needs considerably more anonymous memory because its graph has a different shape. Nothing in the model file warns you, and the arena grows quietly across a project: every skip connection added to improve a metric raises the peak by an amount nobody computed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a tensor arena actually is
&lt;/h2&gt;

&lt;p&gt;A network is a directed acyclic graph: nodes are operators, edges are tensors holding intermediate results. The runtime allocates memory for those edges before inference rather than during it, for reasons a kernel engineer will recognise immediately. Calling the allocator inside the inference loop adds time that is neither small nor predictable, and an allocation that can happen mid-inference can also fail mid-inference. So the runtime asks a different question up front: not how much memory all these tensors need, but what is the smallest single block in which all of them can live, given that they do not all need to be alive at once. That block is the tensor arena, and computing it is a scheduling problem rather than an allocation problem.&lt;/p&gt;

&lt;p&gt;The answer can be far smaller than the sum because execution is sequential. At any moment one operator is running and only its inputs and outputs must be valid; everything produced earlier and already consumed is dead memory that can be handed to a later tensor. If the graph were a plain chain, two buffers sized for the widest tensor would do. Real graphs are not chains, and residual connections that keep a tensor alive across many operators are what make the problem hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The record that makes reuse possible
&lt;/h2&gt;

&lt;p&gt;Every runtime that does this well builds the same small record per tensor: its size, the index of the first operator that touches it and the index of the last, where the indices come from the topological sort that is also the execution order. In LiteRT you can read it directly in &lt;code&gt;tensorflow/lite/simple_memory_arena.h&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ArenaAllocWithUsageInterval&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;first_node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;last_node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the idea in five fields. &lt;code&gt;first_node&lt;/code&gt; and &lt;code&gt;last_node&lt;/code&gt; define a usage interval, and two tensors whose intervals overlap can never share bytes. The planner assigns every tensor an &lt;code&gt;offset&lt;/code&gt; into one buffer such that no two overlapping tensors overlap in memory, and makes the buffer as small as it can. The same class tracks a &lt;code&gt;high_water_mark_&lt;/code&gt; and ships a debug dump behind a weak symbol, so a build that links &lt;code&gt;lite:simple_memory_arena_debug_dump&lt;/code&gt; can print arena utilisation and live tensors per operator.&lt;/p&gt;

&lt;h2&gt;
  
  
  One layer decides your minimum arena size
&lt;/h2&gt;

&lt;p&gt;Three derived quantities explain everything downstream. The &lt;em&gt;operator profile&lt;/em&gt; of an operator is the set of tensors alive while it runs. The &lt;em&gt;operator breadth&lt;/em&gt; is the sum of their sizes. And for a contiguous tensor arena, the theoretical minimum arena size is the maximum operator breadth across the whole graph. It is a lower bound, not a promise: the published measurements reach it on most networks and miss it on some.&lt;/p&gt;

&lt;p&gt;If the arena does not fit your budget, one specific layer is responsible. Not the model in general, not the weight count. Every tensor in an operator's profile must be resident while that operator runs, so no allocation strategy can produce an arena smaller than the widest operator. That changes what you say in the review meeting: you can name the layer and hand it back, and the request stops being "please make the model smaller" and becomes "this layer requires N megabytes minimum, can it be restructured".&lt;/p&gt;

&lt;p&gt;The planning itself is cheap. These are approximation algorithms for an NP-complete problem, they run in milliseconds, and a runtime can afford to compute several plans at startup and keep the smallest. Be careful where you go looking for them, though. The &lt;code&gt;MemoryStrategy&lt;/code&gt; enum — NAIVE, EQUALITY, GREEDY_IN_ORDER, GREEDY_BY_BREADTH, GREEDY_BY_SIZE, GREEDY_BEST and MINCOSTFLOW — belongs to the GPU delegate's planner. The CPU arena you are measuring does not consult it. &lt;code&gt;SimpleMemoryArena::Allocate&lt;/code&gt; implements its own search for the smallest gap between already-placed allocations, ordered by &lt;code&gt;ArenaPlanner&lt;/code&gt;, so that pair of files is what to read if you want to know what produced your arena.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the naive number ranks two models backwards
&lt;/h2&gt;

&lt;p&gt;MobileNet v2 and DeepLab v3 differ by a factor of 1.85 in naive intermediate memory. After packing they need almost the same arena, 5.742 MB against 4.653 MB. Size a board from the naive numbers and you would provision DeepLab for more than ten times the memory it needs, and you would have the relative cost of the two models backwards. The full set is below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;th&gt;Naive (MB)&lt;/th&gt;
&lt;th&gt;Packed arena (MB)&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepLab v3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;48.642&lt;/td&gt;
&lt;td&gt;4.653&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.5x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inception v3&lt;/td&gt;
&lt;td&gt;54.010&lt;/td&gt;
&lt;td&gt;7.914&lt;/td&gt;
&lt;td&gt;6.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BlazeFace&lt;/td&gt;
&lt;td&gt;2.698&lt;/td&gt;
&lt;td&gt;0.492&lt;/td&gt;
&lt;td&gt;5.5x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MobileNet v2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;26.313&lt;/td&gt;
&lt;td&gt;5.742&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.6x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PoseNet&lt;/td&gt;
&lt;td&gt;28.556&lt;/td&gt;
&lt;td&gt;6.271&lt;/td&gt;
&lt;td&gt;4.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MobileNet v1&lt;/td&gt;
&lt;td&gt;19.248&lt;/td&gt;
&lt;td&gt;4.594&lt;/td&gt;
&lt;td&gt;4.2x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Intermediate tensor memory only, 32-bit float, packed greedy by size. Published measurements; see Further reading. At int8 the absolute figures fall by roughly four times, but the ratios are what the sizing argument rests on and those are a property of the graph.&lt;/p&gt;

&lt;p&gt;The ratio column is why a naive estimate is not a conservative estimate. It is wrong by a different amount for every graph, so it does not preserve the ordering between models, and it cannot be scaled into a tensor arena figure by applying a fixed factor.&lt;/p&gt;

&lt;p&gt;Greedy by size reaches the theoretical minimum on five of these six networks; on DeepLab v3 a strip-packing method beats it by 7.2 per cent, which is the argument for computing two plans and keeping the smaller. Tighter packing should also mean a smaller working set and better cache behaviour. The source claims up to 10 per cent better inference speed from that effect but publishes no measurement for it, so treat it as something to test on your board rather than a number to budget against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the process keeps growing after it starts
&lt;/h2&gt;

&lt;p&gt;Here is where a straightforward measurement misleads you, and the cause is demand paging. The tensor arena is an anonymous mapping, so its pages are not resident until they are written. The runtime reserves the address range up front, but the kernel allocates physical pages only on the first write, one minor fault at a time. A read of untouched anonymous memory maps the shared zero page and costs nothing.&lt;/p&gt;

&lt;p&gt;The consequence is that the process footprint climbs across the first several inferences and settles only once every offset in the tensor arena has been written at least once. A model whose plan places a large tensor in a branch that only executes for certain inputs may not touch those pages for a long time. So a single-shot measurement understates the requirement, sometimes badly, and the number you budget against must be the high-water mark rather than the current value.&lt;/p&gt;

&lt;p&gt;Two files give you that. &lt;code&gt;VmHWM&lt;/code&gt; in the process status file under &lt;code&gt;/proc&lt;/code&gt; is the peak resident set size the process has ever reached, and writing 5 to &lt;code&gt;clear_refs&lt;/code&gt; in the same directory resets it, which is how you scope a peak to one run. The &lt;code&gt;smaps_rollup&lt;/code&gt; file gives a single summed entry across all mappings, including &lt;code&gt;Rss&lt;/code&gt;, &lt;code&gt;Anonymous&lt;/code&gt;, &lt;code&gt;AnonHugePages&lt;/code&gt; and the rollup-only fields &lt;code&gt;Pss_Anon&lt;/code&gt;, &lt;code&gt;Pss_File&lt;/code&gt; and &lt;code&gt;Pss_Shmem&lt;/code&gt;. That anonymous-versus-file split is exactly the reclaimable-versus-not split from earlier, measured rather than assumed, and it is the single most informative thing you can read about an inference process. &lt;code&gt;Pss_Shmem&lt;/code&gt; is worth watching too: dma-buf and most accelerator allocations land there rather than in &lt;code&gt;Pss_Anon&lt;/code&gt;. Reading the file walks every page table in the process, so it perturbs what it measures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sizing the cgroup instead of guessing
&lt;/h2&gt;

&lt;p&gt;Once you know what the tensor arena costs in practice, put a boundary around it rather than hoping. In cgroup v2, &lt;code&gt;memory.max&lt;/code&gt; is the hard limit: reach it and fail to reclaim, and the OOM killer is invoked inside the cgroup. &lt;code&gt;memory.high&lt;/code&gt; is the throttle: exceed it and the processes are throttled and put under heavy reclaim pressure, and the documentation is explicit that going over it never invokes the OOM killer.&lt;/p&gt;

&lt;p&gt;For an inference workload on an embedded board that distinction needs care rather than a default, and the reason is specific. On a swapless board the kernel does not scan the anonymous lists at all: reclaim is steered onto the file lists instead. So exceeding &lt;code&gt;memory.high&lt;/code&gt; does not shrink the arena by a single byte. What it does is evict the other half of your model — the mapped weights — and force them back in from storage, while the task is separately throttled by a penalty sleep. You pay in refault latency and scheduling delay and get nothing back.&lt;/p&gt;

&lt;p&gt;The practical arrangement follows from that. Use &lt;code&gt;memory.max&lt;/code&gt; as the real boundary. Put &lt;code&gt;memory.high&lt;/code&gt; a little below it as a monitoring tripwire rather than as a control, so the counter moves before anything is killed. And set &lt;code&gt;memory.min&lt;/code&gt; at roughly the mapped weight size, so the reclaim that does happen cannot evict the model out from under the process.&lt;/p&gt;

&lt;p&gt;For measurement, &lt;code&gt;memory.peak&lt;/code&gt; records the maximum usage for the cgroup and its descendants since the cgroup was created or since the most recent reset. The reset semantics have a trap in them: writing any non-empty string resets the value only for subsequent reads &lt;em&gt;through that same file descriptor&lt;/em&gt;. A shell sequence that writes with &lt;code&gt;tee&lt;/code&gt; and then reads with &lt;code&gt;cat&lt;/code&gt; opens a second descriptor and quietly reports the never-reset watermark instead. Either hold one descriptor open across the run, or use a fresh cgroup per run and skip the reset. And &lt;code&gt;memory.events&lt;/code&gt; carries the counters that tell you afterwards what happened, including &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;oom&lt;/code&gt; and &lt;code&gt;oom_kill&lt;/code&gt;; &lt;code&gt;memory.events.local&lt;/code&gt; is the non-hierarchical version, which is the one you want for a single-cgroup harness. A rising &lt;code&gt;high&lt;/code&gt; count with no &lt;code&gt;oom_kill&lt;/code&gt; is a system quietly paying for reclaim, which is the state most teams never notice.&lt;/p&gt;

&lt;p&gt;That state has its own instrument, and it is the one to reach for. Pressure stall information reports how much work was lost to waiting on memory: system-wide in &lt;code&gt;/proc/pressure/memory&lt;/code&gt;, and per cgroup in &lt;code&gt;memory.pressure&lt;/code&gt;. Both carry two lines. The &lt;code&gt;some&lt;/code&gt; line is the share of time in which at least one task was stalled on memory. The &lt;code&gt;full&lt;/code&gt; line is the share in which every non-idle task was stalled at once, which the kernel documentation describes as thrashing, and which on an inference board means the CPU is not doing your work at all. Each line gives ten, sixty and three hundred second averages as percentages, plus a cumulative total in microseconds that catches short spikes the averages flatten out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/infer/memory.pressure
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/pressure/memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a latency-sensitive product the number to alarm on is &lt;code&gt;full&lt;/code&gt; avg10 for the inference cgroup. Anything sustained there means the board is spending real time refaulting rather than inferring, and given what the previous paragraphs established, what it is refaulting is your model weights rather than the tensor arena. The system-wide file also accepts a trigger, so a supervisor can wait on a threshold rather than sample: writing &lt;code&gt;some 150000 1000000&lt;/code&gt; to it asks to be woken when partial memory stall passes 150 ms within any one-second window, with the file descriptor polled for &lt;code&gt;POLLPRI&lt;/code&gt;. Accepted windows run from 500 ms to 10 s. Pressure stall information has to be built into the kernel, so confirm the files exist on your board before designing a supervisor around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the arena cannot be handed to an accelerator
&lt;/h2&gt;

&lt;p&gt;One more property matters if the model runs on anything other than the CPU. The tensor arena is contiguous in virtual address space, not in physical memory. Its pages are ordinary anonymous pages scattered across whatever the page allocator had, and nothing about a tensor arena guarantees the physical adjacency a device may need.&lt;/p&gt;

&lt;p&gt;That is fine for a CPU, and fine for a device behind an IOMMU or one that can scatter-gather from a descriptor list. It is not fine for a master that cannot do either, which needs physically contiguous memory from CMA or a DMA heap. Even an accelerator that could technically reach the arena usually wants its own buffers anyway, for cache-coherency and vendor layout reasons. This is why a hardware delegate allocates separately rather than being pointed at the arena, and why the arena figure and the process footprint diverge once you enable an NPU or GPU path. The arena tells you what the CPU-side plan costs. It does not tell you what the accelerator's buffers cost, and those come from a different allocator with tighter constraints. I have not verified how any specific vendor delegate accounts for its own memory, so measure that path separately rather than reasoning from the arena number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops working
&lt;/h2&gt;

&lt;p&gt;The tensor arena scheme rests on one assumption: that every intermediate tensor's size is known before the first inference. The clearest break is dynamic shapes. A graph where a tensor's extent depends on the input cannot be planned in one pass; the workaround is to plan repeatedly, allocating what is known, executing until the first dynamic size resolves and planning again. That works, but it puts allocation back inside the inference path along with the latency variance that pre-allocation existed to remove. If you are consulted on architecture for a latency-sensitive product, this is a real reason to argue for static shapes.&lt;/p&gt;

&lt;p&gt;Worse, the arena does not shrink again. &lt;code&gt;ResizableAlignedBuffer::Resize&lt;/code&gt; skips reallocation when the new size is smaller, so once a dynamic-shape run has grown the arena it stays grown for the life of the process. One unusually large input sets your footprint for the next three months of uptime.&lt;/p&gt;

&lt;p&gt;Two further limits. The execution order is taken as given, so the planner optimises for a fixed topological sort and does not reorder operators to reduce the peak — offline tools exist that do reorder, which is worth knowing before you accept the number as final. And none of this applies to training, where activations must stay alive for the backward pass, so any on-device fine-tuning you are asked to support has a memory profile unrelated to the inference numbers you measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring a tensor arena on your own board
&lt;/h2&gt;

&lt;p&gt;The board below runs a Yocto image built with &lt;code&gt;MACHINE = "rock-5b"&lt;/code&gt;. Put the benchmark tool in the image rather than copying a binary onto a running board, so that what you measure is the rootfs you ship. The community &lt;code&gt;meta-tensorflow-lite&lt;/code&gt; layer carries a recipe for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bitbake-layers add-layer ../meta-tensorflow-lite
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'IMAGE_INSTALL:append = " tensorflow-lite-benchmark"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; conf/local.conf
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bitbake core-image-base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything from here runs on the board. Start on the kernel side, because those numbers are the ones you will defend in a design review. The process must still be alive when you read &lt;code&gt;/proc&lt;/code&gt;, so run it long and in the background rather than sequentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /usr/share/tensorflow/lite/tools/benchmark
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--num_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &amp;amp; &lt;span class="nv"&gt;BM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;VmHWM /proc/&lt;span class="nv"&gt;$BM&lt;/span&gt;/status
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^(Rss|Pss_Anon|Pss_File|Pss_Shmem|Anonymous|AnonHugePages|Swap):"&lt;/span&gt; /proc/&lt;span class="nv"&gt;$BM&lt;/span&gt;/smaps_rollup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the arena really is one anonymous mapping by listing the mappings with no pathname, largest first. Selecting on field count rather than on the absence of a slash is what keeps the heap, the stack and the vDSO out of the list, since those carry a bracketed name in the sixth field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NF==5 {split($1,a,"-"); print ((strtonum("0x" a[2]) - strtonum("0x" a[1]))/1048576) " MB " $1}'&lt;/span&gt; /proc/&lt;span class="nv"&gt;$BM&lt;/span&gt;/maps | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the first-write behaviour directly. The minor fault count bounds the arena from above rather than isolating it, because it also counts the binary, the libraries and any delegate buffers. Disable the warm-up run so only one inference is measured, and diff a dry run against a real one if you want the arena's own share:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;perf &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; minor-faults,major-faults ./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--num_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--warmup_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;perf &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; minor-faults,major-faults ./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bound it with a cgroup. The memory controller has to be enabled in the parent before the interface files exist in the child, and the workload has to actually be moved into the cgroup, or you will read zeroes from a well-formed but empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; +memory &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/cgroup.subtree_control
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /sys/fs/cgroup/infer
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;64M &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/infer/memory.max
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo $$ &amp;gt; /sys/fs/cgroup/infer/cgroup.procs; exec ./benchmark_model --graph=model.tflite --num_runs=2000'&lt;/span&gt;
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/infer/memory.peak /sys/fs/cgroup/infer/memory.events.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh cgroup per run is what makes that final read trustworthy. Do not reach for the reset unless you are driving it from a program that can hold the descriptor open, for the reason given earlier.&lt;/p&gt;

&lt;p&gt;Then go to the runtime for the plan itself. This flag prints the interpreter's internal state before the first inference, including the allocated size of each tensor, which is the plan rather than a guess about it. The second samples the tool's own footprint at a fixed interval, 50 ms by default; the sampling perturbs latency, so do not read latency and peak memory from the same run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--print_preinvoke_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--num_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--report_peak_memory_footprint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--memory_footprint_check_interval_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model has dynamic-shaped tensors, use the post-invoke state rather than the pre-invoke state, because the sizes are not known until the graph has run. Two flags then change the trade-off, but they are not independent: enabling the second also enables the first, so measure baseline, then the first alone, then both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--print_postinvoke_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--release_dynamic_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="gp"&gt;root@rock-5b:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./benchmark_model &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model.tflite &lt;span class="nt"&gt;--optimize_memory_for_large_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, back on the host, read the planner your build actually uses rather than trusting an article about it. For a CPU inference process that means the arena and its planner, not the GPU delegate's strategy enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"smallest gap|best_offset"&lt;/span&gt; tensorflow/lite/simple_memory_arena.cc
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"MemoryStrategy"&lt;/span&gt; tensorflow/lite/simple_memory_arena.cc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command returns nothing, and that is the point. The CPU arena never consults &lt;code&gt;MemoryStrategy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Budget DRAM off the tensor arena figure, not the weights figure. The weights are the half the kernel can drop for free.&lt;/li&gt;
&lt;li&gt;A naive estimate is not a conservative one. It is wrong by a different factor for every graph, so it does not even preserve the ordering between two models.&lt;/li&gt;
&lt;li&gt;Name the layer instead of asking for a smaller model. The maximum operator breadth is the bound no allocation strategy can beat.&lt;/li&gt;
&lt;li&gt;Measure &lt;code&gt;VmHWM&lt;/code&gt; over a long run, never a single inference, and reset it with &lt;code&gt;clear_refs&lt;/code&gt; to scope a peak to one run.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;memory.max&lt;/code&gt; as the boundary, &lt;code&gt;memory.high&lt;/code&gt; below it only as a tripwire, and &lt;code&gt;memory.min&lt;/code&gt; at the weight size so reclaim cannot evict the model.&lt;/li&gt;
&lt;li&gt;Alarm on &lt;code&gt;full&lt;/code&gt; avg10 in the cgroup's &lt;code&gt;memory.pressure&lt;/code&gt;. It is the one number that tells you the board is refaulting rather than inferring.&lt;/li&gt;
&lt;li&gt;Measure the accelerator path separately. The arena is virtually contiguous only, and delegate buffers come from a different allocator.&lt;/li&gt;
&lt;li&gt;Dynamic shapes, training and delegate buffers all fall outside the plan, and an arena grown by one large input never shrinks back.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my inference process keep growing for the first few seconds?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Because the tensor arena is an anonymous mapping whose pages become resident only when first written. The address range is reserved up front, but physical pages arrive one minor fault at a time, so the resident set climbs until every offset in the arena has been touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which part of an edge model can the kernel reclaim under memory pressure?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Only the weights, and only when the runtime maps the model file rather than copying it. Those pages are clean and file-backed, so the kernel can drop them and read them back later. The arena is anonymous, so without a swap device it cannot be reclaimed at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which layer decides how big my tensor arena has to be?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The operator with the largest breadth, meaning the largest total size of all tensors that must be resident while it runs. That is a lower bound no allocation strategy can beat, though a given strategy may not reach it exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use memory.high or memory.max for an inference cgroup?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use &lt;code&gt;memory.max&lt;/code&gt; as the real boundary and &lt;code&gt;memory.high&lt;/code&gt; below it only as a tripwire. On a swapless board the kernel steers reclaim entirely onto the file lists, so exceeding &lt;code&gt;memory.high&lt;/code&gt; evicts the mapped weights and throttles the task without shrinking the arena at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I tell whether the board is losing time to memory pressure?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Read &lt;code&gt;memory.pressure&lt;/code&gt; in the inference cgroup, or &lt;code&gt;/proc/pressure/memory&lt;/code&gt; system-wide. The &lt;code&gt;full&lt;/code&gt; line is the share of time in which every non-idle task was stalled on memory, so a sustained &lt;code&gt;full&lt;/code&gt; avg10 means the board is refaulting rather than inferring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an NPU or GPU use the tensor arena directly?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Not in general. The arena is contiguous in virtual address space but its physical pages are scattered, so a DMA master without an IOMMU cannot use it. Delegates allocate their own buffers through a different path, which is why the arena figure and the process footprint diverge once acceleration is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Yury Pisarchyk and Juhyun Lee, "Efficient Memory Management for Deep Neural Net Inference", ReCoML Workshop of MLSys 2020 — &lt;a href="https://arxiv.org/abs/2001.03288" rel="noopener noreferrer"&gt;arXiv:2001.03288&lt;/a&gt;. Source of the strategy definitions, the minimum-size argument and the measured footprints quoted above.&lt;/li&gt;
&lt;li&gt;Juhyun Lee and Yury Pisarchyk, "Optimizing TensorFlow Lite Runtime Memory", TensorFlow engineering blog, 2 October 2020 — &lt;a href="https://blog.tensorflow.org/2020/10/optimizing-tensorflow-lite-runtime.html" rel="noopener noreferrer"&gt;blog.tensorflow.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tensorflow/lite/simple_memory_arena.h&lt;/code&gt; — &lt;a href="https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/simple_memory_arena.h" rel="noopener noreferrer"&gt;the CPU arena and the usage-interval record&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tensorflow/lite/delegates/gpu/common/memory_management.h&lt;/code&gt; — &lt;a href="https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/delegates/gpu/common/memory_management.h" rel="noopener noreferrer"&gt;the MemoryStrategy enum&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/tools/benchmark/README.md" rel="noopener noreferrer"&gt;Benchmark tool flags&lt;/a&gt;, including the memory reporting options used above.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html" rel="noopener noreferrer"&gt;Control Group v2&lt;/a&gt; — the definitions of memory.max, memory.high, memory.peak, memory.events and memory.pressure quoted here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/accounting/psi.html" rel="noopener noreferrer"&gt;PSI — Pressure Stall Information&lt;/a&gt; — the some/full definitions, the averaging windows and the trigger format.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kernel.org/doc/Documentation/ABI/testing/procfs-smaps_rollup" rel="noopener noreferrer"&gt;procfs smaps_rollup ABI&lt;/a&gt; and &lt;a href="https://man7.org/linux/man-pages/man5/proc_pid_smaps.5.html" rel="noopener noreferrer"&gt;proc_pid_smaps(5)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.man7.org/linux/man-pages/man3/mallopt.3.html" rel="noopener noreferrer"&gt;mallopt(3)&lt;/a&gt; for the mmap threshold that puts the arena in its own mapping.&lt;/li&gt;
&lt;li&gt;T. Sekiyama and others, "Profile-guided memory optimization for deep neural networks" — &lt;a href="https://arxiv.org/abs/1804.10001" rel="noopener noreferrer"&gt;arXiv:1804.10001&lt;/a&gt;, the strip-packing method referenced above.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I teach Linux kernel and embedded Linux engineering at &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;TECH VEDA&lt;/a&gt;. If your team is sizing memory for an edge AI product and wants to reason about it from the kernel side rather than the framework side, that is the kind of thing our &lt;a href="https://www.techveda.live/linux-systems-engineering/" rel="noopener noreferrer"&gt;Linux Systems Engineering&lt;/a&gt; track covers.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>embedded</category>
      <category>machinelearning</category>
      <category>performance</category>
    </item>
    <item>
      <title>systemd vs BusyBox init: Which Init System Fits Your Device?</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:13:43 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/systemd-vs-busybox-init-which-init-system-fits-your-device-17ie</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/systemd-vs-busybox-init-which-init-system-fits-your-device-17ie</guid>
      <description>&lt;p&gt;For most embedded products with a fixed set of services and no user-installable software, BusyBox init is the right default: small, no dependency chain, and its behaviour fits in one readable file. Choose systemd when the device genuinely needs supervised, interdependent services — readiness ordering, watchdog-backed liveness detection, resource limits — and accept in return a much larger image, a kernel floor of 5.10, a dependency set you maintain for the life of the product, and (if you build with Buildroot) a glibc toolchain for the whole system.&lt;/p&gt;

&lt;p&gt;The choice of &lt;strong&gt;systemd vs BusyBox init&lt;/strong&gt; is one of the first structural decisions in an embedded Linux product, and one of the hardest to reverse. PID 1 defines how services declare dependencies, how failures are detected, and which libraries stay in the image forever. Many teams inherit the choice from the vendor BSP without recording why, then work around it for two years. The problem is the unrecorded decision, not the inherited default.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this decision arises
&lt;/h2&gt;

&lt;p&gt;The decision arises once you move past a single application binary. A device that boots, runs one process and reboots on failure needs nothing beyond a supervising parent. The forces appear when the product gains a second and third long-running service with an ordering relationship, and when support asks why a unit stopped responding without rebooting. Four forces decide it: the &lt;strong&gt;image budget&lt;/strong&gt; from the bill of materials, doubled in an A/B layout; the &lt;strong&gt;service topology&lt;/strong&gt;, meaning whether any service can fail without exiting; the &lt;strong&gt;platform floor&lt;/strong&gt;, meaning the C library and kernel version you are committed to; and the &lt;strong&gt;maintenance horizon&lt;/strong&gt;, meaning how long you must ship security updates.&lt;/p&gt;

&lt;p&gt;One clarification first, because it causes real design errors. Init and device management are two decisions, not one. Yocto's &lt;code&gt;INIT_MANAGER&lt;/code&gt; values bundle them, but the device manager has its own variable, &lt;code&gt;VIRTUAL-RUNTIME_dev_manager&lt;/code&gt;, with documented values &lt;code&gt;udev&lt;/code&gt;, &lt;code&gt;busybox-mdev&lt;/code&gt; and &lt;code&gt;systemd&lt;/code&gt;. Buildroot keeps them in separate menus, and its &lt;em&gt;/dev management&lt;/em&gt; choice is hidden only when &lt;code&gt;BR2_INIT_SYSTEMD&lt;/code&gt; is selected. Either build system will run eudev under BusyBox init, so a USB modem or a camera that needs real udev rules does not oblige you to take systemd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Yocto: distro config or local.conf
&lt;/span&gt;&lt;span class="n"&gt;INIT_MANAGER&lt;/span&gt; = &lt;span class="s2"&gt;"sysvinit"&lt;/span&gt;       &lt;span class="c"&gt;# Poky default, SysVinit + udev
&lt;/span&gt;&lt;span class="n"&gt;INIT_MANAGER&lt;/span&gt; = &lt;span class="s2"&gt;"mdev-busybox"&lt;/span&gt;   &lt;span class="c"&gt;# BusyBox init + BusyBox mdev
&lt;/span&gt;&lt;span class="n"&gt;INIT_MANAGER&lt;/span&gt; = &lt;span class="s2"&gt;"systemd"&lt;/span&gt;        &lt;span class="c"&gt;# systemd + udev
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buildroot's &lt;em&gt;System configuration → Init system&lt;/em&gt; menu is longer than most write-ups suggest: four general-purpose options — &lt;code&gt;BR2_INIT_BUSYBOX&lt;/code&gt; (the default), &lt;code&gt;BR2_INIT_SYSV&lt;/code&gt;, &lt;code&gt;BR2_INIT_OPENRC&lt;/code&gt; and &lt;code&gt;BR2_INIT_SYSTEMD&lt;/code&gt; — plus four special-purpose ones that are mostly container reapers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options: systemd vs BusyBox init
&lt;/h2&gt;

&lt;h3&gt;
  
  
  BusyBox init
&lt;/h3&gt;

&lt;p&gt;BusyBox init lives inside the BusyBox multi-call binary you almost certainly already ship. It reads &lt;code&gt;/etc/inittab&lt;/code&gt; at startup; entries take the form &lt;code&gt;&amp;lt;id&amp;gt;:&amp;lt;runlevels&amp;gt;:&amp;lt;action&amp;gt;:&amp;lt;process&amp;gt;&lt;/code&gt;, with the action one of &lt;code&gt;sysinit&lt;/code&gt;, &lt;code&gt;wait&lt;/code&gt;, &lt;code&gt;once&lt;/code&gt;, &lt;code&gt;respawn&lt;/code&gt;, &lt;code&gt;askfirst&lt;/code&gt;, &lt;code&gt;restart&lt;/code&gt;, &lt;code&gt;ctrlaltdel&lt;/code&gt; or &lt;code&gt;shutdown&lt;/code&gt;. The runlevel field is unused. Buildroot's default &lt;code&gt;inittab&lt;/code&gt; mounts a few filesystems, runs &lt;code&gt;/etc/init.d/rcS&lt;/code&gt; and starts a getty.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Myth:&lt;/strong&gt; "&lt;code&gt;respawn&lt;/code&gt; gives me service supervision."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An entry marked &lt;code&gt;respawn&lt;/code&gt; is restarted when it exits, and that is the whole of the guarantee. There is no back-off and no start limit, so a service that crashes at startup restarts about once a second, paced only by the one-second sleep in init's main loop, until someone power-cycles the board. BusyBox's own documentation in &lt;code&gt;init/init.c&lt;/code&gt; states: "Unlike sysvinit, BusyBox init does not stop processes from respawning out of control." A process that is alive but no longer working never exits, so it is never restarted at all.&lt;/p&gt;

&lt;p&gt;BusyBox also ships pieces people assume it lacks. The &lt;code&gt;watchdog&lt;/code&gt; applet takes &lt;code&gt;-T N&lt;/code&gt; (reboot after N seconds if not reset, default 60), &lt;code&gt;-t N&lt;/code&gt; (reset every N seconds) and &lt;code&gt;-F&lt;/code&gt; (foreground), so you can run it from &lt;code&gt;inittab&lt;/code&gt; as a &lt;code&gt;respawn&lt;/code&gt; entry. The &lt;code&gt;syslogd&lt;/code&gt; applet accepts &lt;code&gt;-C[size_kb]&lt;/code&gt; to log to a shared-memory circular buffer that &lt;code&gt;logread&lt;/code&gt; reads — on a flash-based device, often exactly the logging policy you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In its favour:&lt;/strong&gt; negligible size on top of BusyBox; no new libraries; a boot sequence one engineer can read in full before changing it; no C library or kernel constraint beyond BusyBox's own; and failure behaviour that is exactly what you wrote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Against it:&lt;/strong&gt; ordering is whatever order you wrote the script in, so a race between two services can remain undetected until a unit fails at a customer site; a hung service stays hung; there is no restart back-off; and supervision beyond &lt;code&gt;respawn&lt;/code&gt; is yours to write, test and document.&lt;/p&gt;

&lt;h3&gt;
  
  
  systemd
&lt;/h3&gt;

&lt;p&gt;systemd replaces init with a service manager. Services are declarative unit files rather than shell scripts, they start in parallel, and the manager tracks them using control groups. Three mechanisms carry most of the value on a device, and each of the three is commonly misread.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Myth:&lt;/strong&gt; "systemd makes service ordering reliable."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Ordering is not readiness.&lt;/strong&gt; &lt;code&gt;After=&lt;/code&gt; orders unit start-up; it does not wait for the dependency to be usable. &lt;code&gt;Type=notify&lt;/code&gt; does: the service sends &lt;code&gt;READY=1&lt;/code&gt; through &lt;code&gt;sd_notify&lt;/code&gt;, and systemd "will proceed with starting follow-up units after this notification message has been sent." That is what removes sleep loops from your boot path, and it is also the real migration cost, because it means changing the application. Teams that convert &lt;code&gt;rcS&lt;/code&gt; into units without adopting &lt;code&gt;Type=notify&lt;/code&gt; keep their race and conclude systemd did not help.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Myth:&lt;/strong&gt; "systemd automatically detects hung services."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Liveness detection is two independent settings, both off by default.&lt;/strong&gt; A unit's &lt;code&gt;WatchdogSec=&lt;/code&gt; makes the service ping PID 1: it calls &lt;code&gt;sd_notify&lt;/code&gt; with &lt;code&gt;WATCHDOG=1&lt;/code&gt;, and if the gap between two calls exceeds the configured time, systemd marks the service failed and terminates it with &lt;code&gt;SIGABRT&lt;/code&gt; (or &lt;code&gt;WatchdogSignal=&lt;/code&gt;), restarting it under a suitable &lt;code&gt;Restart=&lt;/code&gt; value. It "defaults to 0, which disables this feature." Separately, &lt;code&gt;RuntimeWatchdogSec=&lt;/code&gt; in &lt;code&gt;systemd-system.conf&lt;/code&gt; makes PID 1 ping the hardware watchdog — &lt;code&gt;WatchdogDevice=&lt;/code&gt;, default &lt;code&gt;/dev/watchdog0&lt;/code&gt; — so the board resets if PID 1 itself stops. It also defaults to 0. Escalating a hung service into a board reset therefore needs three things: the unit setting, the manager setting, and an application that actually calls &lt;code&gt;sd_notify&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The start limit will stop restarting your service.&lt;/strong&gt; &lt;code&gt;DefaultStartLimitIntervalSec=&lt;/code&gt; is 10 seconds and &lt;code&gt;DefaultStartLimitBurst=&lt;/code&gt; is 5, so the sixth start inside that window is refused. Units "which are configured for &lt;code&gt;Restart=&lt;/code&gt;, and which reach the start limit are not attempted to be restarted anymore."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Myth:&lt;/strong&gt; "&lt;code&gt;Restart=always&lt;/code&gt; means the service always comes back."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The consequence is invisible on a desktop, where an administrator sees the failed state and runs &lt;code&gt;systemctl reset-failed&lt;/code&gt;. On a headless device in the field nobody sees it, and the product stops working while PID 1 continues to report a normal system state. &lt;code&gt;StartLimitAction=&lt;/code&gt; exists for this and defaults to &lt;code&gt;none&lt;/code&gt;; on a product, setting it to &lt;code&gt;reboot&lt;/code&gt; and raising &lt;code&gt;RestartSec=&lt;/code&gt; above its 100 ms default is usually correct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Sensor gateway&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sensor-bus.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;notify              # follow-up units wait for READY=1&lt;/span&gt;
&lt;span class="py"&gt;NotifyAccess&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;
&lt;span class="py"&gt;WatchdogSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s          # also needs RuntimeWatchdogSec= in system.conf&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="py"&gt;StartLimitBurst&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;StartLimitAction&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;reboot  # do not give up silently on a headless device&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In its favour:&lt;/strong&gt; readiness ordering rather than implied ordering; detection of a service that has stopped responding, in addition to one that has exited; per-service resource limits and sandboxing, auditable with &lt;code&gt;systemd-analyze security&lt;/code&gt; (it reports an exposure level from 0.0 to 10.0, where a high value means little sandboxing); integrated logging; boot analysis through &lt;code&gt;systemd-analyze critical-chain&lt;/code&gt;; and one operational interface new engineers already know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Against it:&lt;/strong&gt; size and dependencies. The Yocto documentation states that using systemd increases the &lt;code&gt;core-image-minimal&lt;/code&gt; image size by 160% for &lt;code&gt;qemux86-64&lt;/code&gt; on Mickledore (4.2), compared to SysVinit. Note what that compares: systemd against SysVinit, not against BusyBox init. A BusyBox-init image is smaller than a SysVinit one, so treat 160% as a lower bound on the gap this decision creates.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Myth:&lt;/strong&gt; "systemd is too big for embedded."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That lower bound is a percentage of a reference image with almost nothing in it, which is why it travels badly. On a product carrying a graphics stack or a Python runtime, the same absolute delta is a small fraction of the total. The size objection is decisive on a 64 MB flash part and close to irrelevant on a 4 GB one, which is why the number has to come from your own image rather than from a published comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency set is the more durable cost.&lt;/strong&gt; Upstream requires glibc 2.34 or newer (musl 1.2.6 only when built with &lt;code&gt;-Dlibc=musl&lt;/code&gt;), sets a minimum kernel baseline of 5.10 and recommends 5.14, below which it sets an &lt;code&gt;old-kernel&lt;/code&gt; taint flag. Buildroot's &lt;code&gt;package/systemd/Config.in&lt;/code&gt; is stricter: among other gates it depends on &lt;code&gt;BR2_TOOLCHAIN_USES_GLIBC&lt;/code&gt;, an MMU, kernel headers of at least 5.4 and GCC 8, and selects merged &lt;code&gt;/usr&lt;/code&gt;, D-Bus (or dbus-broker), libcap, util-linux, kmod and timezone data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The options this decision does not take
&lt;/h3&gt;

&lt;p&gt;A decision record is judged partly on the alternatives it rejects. &lt;strong&gt;SysVinit&lt;/strong&gt; sits between the two: shell-script services, runlevels, and udev rather than mdev. It suits an inherited body of &lt;code&gt;/etc/init.d/&lt;/code&gt; scripts and is a poor choice starting fresh, costing more than BusyBox init without the supervision that justifies systemd. &lt;strong&gt;OpenRC&lt;/strong&gt; is the genuine middle option; Buildroot ships it as &lt;code&gt;BR2_INIT_OPENRC&lt;/code&gt;, depending only on an MMU, dynamic libraries and a non-uClibc toolchain, so it is available on musl where systemd is not. &lt;strong&gt;s6, runit and finit&lt;/strong&gt; offer stronger supervision than &lt;code&gt;respawn&lt;/code&gt; on similar terms. All are rejected here for one reason: they add a framework your team must learn and integrate, without the ecosystem, vendor BSP alignment or hiring pool that make systemd worth its cost. If your team already runs one, that reasoning does not apply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A trimmed systemd&lt;/strong&gt; deserves naming too, because a reviewer will ask what a minimal build costs. Read the option list for your own release rather than a figure from a blog post: in Yocto, &lt;code&gt;bitbake -e systemd&lt;/code&gt; and look at &lt;code&gt;PACKAGECONFIG&lt;/code&gt;; in Buildroot, the &lt;code&gt;BR2_PACKAGE_SYSTEMD_*&lt;/code&gt; sub-options. Then apply the sentence that matters — trimming moves the size number, not the kernel floor, the C library constraint, or the count of upstream projects you track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure four things before you decide
&lt;/h2&gt;

&lt;p&gt;An argument about &lt;strong&gt;systemd vs BusyBox init&lt;/strong&gt; that contains no numbers from your own product will not survive a design review. Four measurements take an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size delta on your image, not a reference image.&lt;/strong&gt; In Buildroot, run &lt;code&gt;make clean all&lt;/code&gt; then &lt;code&gt;make graph-size&lt;/code&gt; with each init selected; it writes &lt;code&gt;output/graphs/graph-size.pdf&lt;/code&gt;, &lt;code&gt;package-size-stats.csv&lt;/code&gt; and &lt;code&gt;file-size-stats.csv&lt;/code&gt;, and &lt;code&gt;size-stats-compare&lt;/code&gt; diffs two of those CSVs. In Yocto, set &lt;code&gt;INHERIT += "buildhistory"&lt;/code&gt; and &lt;code&gt;BUILDHISTORY_COMMIT = "1"&lt;/code&gt;, build with each &lt;code&gt;INIT_MANAGER&lt;/code&gt; value, then read &lt;code&gt;installed-package-sizes.txt&lt;/code&gt; or run &lt;code&gt;buildhistory-diff&lt;/code&gt;. Report it twice: absolute bytes on the image you ship, and a percentage of one A/B slot. On a 400 MB image with a graphics stack, a few megabytes is not an argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resident memory, usually the binding constraint below 256 MB.&lt;/strong&gt; Flash is what teams budget; RAM is what runs out. Under systemd, PID 1, the journal daemon, the D-Bus daemon and udevd are four resident processes BusyBox init does not have. Sum &lt;code&gt;Pss&lt;/code&gt; from &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps_rollup&lt;/code&gt;, or use &lt;code&gt;systemd-cgtop -m&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your real service topology.&lt;/strong&gt; Count long-running processes that must survive a crash; under five, BusyBox init is almost certainly right. For each, write down what must be ready before it starts and count the pairs. Count how many can hang without exiting — anything with a blocking I/O loop or a third-party SDK. Count how many you did not write. Then count the &lt;code&gt;sleep&lt;/code&gt; calls and lock files in your current &lt;code&gt;rcS&lt;/code&gt;: three or more means you have already built a service manager, badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boot time, before assuming either option helps.&lt;/strong&gt; Under systemd, &lt;code&gt;systemd-analyze time&lt;/code&gt; splits firmware, loader, kernel and userspace, and &lt;code&gt;critical-chain&lt;/code&gt; shows the path that gates boot. Under either init, &lt;code&gt;initcall_debug printk.time=1&lt;/code&gt; covers the kernel side, and instrumenting &lt;code&gt;rcS&lt;/code&gt; covers user space.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemd-analyze critical-chain
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; up _ &amp;lt; /proc/uptime&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rcS-done &lt;/span&gt;&lt;span class="nv"&gt;$up&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/kmsg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only number a product review should accept is a GPIO your application toggles, measured from power-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;systemd vs BusyBox init&lt;/strong&gt;, the default for a fixed-function device — a sensor gateway, a controller, a camera node, a kiosk — with a known set of services is BusyBox init. It matches the product's complexity and leaves the C library and kernel version open. This is also the Buildroot developers' recommendation: BusyBox init is sufficient for most embedded systems.&lt;/p&gt;

&lt;p&gt;Choose systemd when at least one of these is true, and be honest about whether it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform is already systemd-based and has sufficient flash. Diverging from the vendor BSP is not a one-time task; it re-opens at every BSP bump, and it should be budgeted as recurring engineering. Most silicon-vendor reference distributions ship systemd, so verify what yours does before assuming a free choice.&lt;/li&gt;
&lt;li&gt;Services depend on each other in ways a linear script cannot express, or you already order them with sleep loops and lock files.&lt;/li&gt;
&lt;li&gt;A service can hang without exiting and the product must notice. &lt;code&gt;WatchdogSec=&lt;/code&gt; plus &lt;code&gt;RuntimeWatchdogSec=&lt;/code&gt; answers this; &lt;code&gt;respawn&lt;/code&gt; does not.&lt;/li&gt;
&lt;li&gt;You need per-service resource limits or sandboxing, so one service cannot starve another. On a device hosting a customer application this is the strongest argument in systemd's favour.&lt;/li&gt;
&lt;li&gt;The device runs software your team does not control: customer applications, containers, third-party agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two conditions rule out systemd regardless of everything above. If your toolchain is musl or uClibc-ng and you build with Buildroot, systemd is unavailable without changing the C library for the whole product, and OpenRC is the middle option there. That is Buildroot's packaging constraint rather than an upstream one, since upstream supports musl when systemd is built with &lt;code&gt;-Dlibc=musl&lt;/code&gt;. If your kernel is older than 5.10, upstream systemd does not support it at all, which is a reason to update the kernel before adopting systemd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consequences
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Boot time does not improve automatically.&lt;/strong&gt; Parallel startup helps a given service reach ready state sooner, but the Yocto documentation is direct about the whole-system effect: systemd also provides more services by default, therefore increasing the total system boot time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Myth:&lt;/strong&gt; "systemd makes the device boot faster."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The honest argument for systemd on boot time is that it provides the measurement tools, not that the device boots sooner. Total boot time is set by what you start and in what order, which is a property of your image rather than of PID 1. Our series on &lt;a href="https://www.techveda.live/2026/08/07/embedded-linux-boot-time-phases/" rel="noopener noreferrer"&gt;boot time phases&lt;/a&gt; and &lt;a href="https://www.techveda.live/2026/08/13/embedded-linux-boot-time-kernel-userspace/" rel="noopener noreferrer"&gt;measuring the kernel and user space&lt;/a&gt; covers the method under either init.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the logs go is decided once, for the life of the product.&lt;/strong&gt; systemd's integrated logging is a real benefit, and on raw NAND or eMMC a persistent journal is also a wear mechanism. Set &lt;code&gt;Storage=volatile&lt;/code&gt; and the journal lives in &lt;code&gt;/run/log/journal&lt;/code&gt;, touching no flash. The documented default has changed across releases — older systemd documents &lt;code&gt;auto&lt;/code&gt;, current systemd documents &lt;code&gt;persistent&lt;/code&gt; — so run &lt;code&gt;systemd-analyze cat-config systemd/journald.conf&lt;/code&gt; on your target rather than trusting one figure. If the journal stays in RAM, bound it: &lt;code&gt;RuntimeMaxUse=&lt;/code&gt; defaults to 10% of the filesystem it sits on, capped at 4G, which on a tmpfs means RAM. Under BusyBox init you reach the same place by default, with &lt;code&gt;syslogd -C&lt;/code&gt; and &lt;code&gt;logread&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency set becomes your security surface.&lt;/strong&gt; D-Bus, udev, util-linux, kmod and libcap are five more upstream projects to track for the supported life of the device. Produce the number rather than asserting it: build with each init selected and diff the manifests. &lt;code&gt;make legal-info&lt;/code&gt; in Buildroot writes &lt;code&gt;output/legal-info/manifest.csv&lt;/code&gt;; a Yocto build with CVE checking and an SBOM class gives the equivalent. That difference is the maintenance cost, as a component count you can put in the record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layout constraints propagate, including to a read-only root.&lt;/strong&gt; systemd requires a build prefix of &lt;code&gt;/usr&lt;/code&gt; and does not support split-usr systems; a separate &lt;code&gt;/usr&lt;/code&gt; must be mounted from the initrd. On a read-only root — standard on A/B devices, and enabled in Yocto with &lt;code&gt;IMAGE_FEATURES += "read-only-rootfs"&lt;/code&gt; — the handling of &lt;code&gt;/var&lt;/code&gt; becomes an explicit choice, which Buildroot exposes as &lt;code&gt;BR2_INIT_SYSTEMD_VAR_FACTORY&lt;/code&gt; (default), &lt;code&gt;_OVERLAYFS&lt;/code&gt; and &lt;code&gt;_NONE&lt;/code&gt;. This qualifies one argument for BusyBox init: on a read-only root you cannot edit &lt;code&gt;rcS&lt;/code&gt; on the target, so the benefit is a boot sequence short enough to reason about, not one editable in the field. On a PREEMPT_RT product, upstream additionally recommends &lt;code&gt;CONFIG_RT_GROUP_SCHED=n&lt;/code&gt; under systemd, because real-time group scheduling requires an explicit budget per unit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing BusyBox init means owning the supervision logic.&lt;/strong&gt; The unthrottled crash loop described earlier is yours to contain. Set &lt;code&gt;panic=&lt;/code&gt; on the kernel command line so a dead PID 1 still resets the board, feed the hardware watchdog from a small dedicated process rather than the application, and write the supervision design down as a document rather than leaving it in &lt;code&gt;rcS&lt;/code&gt;. The staffing risk is not that BusyBox init is hard to learn; it is that only its author knows what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this recommendation is wrong
&lt;/h2&gt;

&lt;p&gt;The recommendation fails when "fixed-function" is doing more work than it can bear. An instrument cluster or a medical monitor is fixed-function in the product sense and still runs a dozen interdependent services with real failure modes. The useful test is not what the product does, it is the topology count from the measurement section. It also fails for a team whose background is server or desktop Linux: the argument that BusyBox init is simpler assumes an engineer who reads shell more fluently than unit files, and that assumption is worth checking rather than presuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In systemd vs BusyBox init, default to BusyBox init for fixed-function devices, as Buildroot's developers recommend — and record why, so the next team does not repeat the argument.&lt;/li&gt;
&lt;li&gt;Deviate when the platform is already systemd-based, when a service can hang without exiting, or when the device runs software you do not control.&lt;/li&gt;
&lt;li&gt;Neither option supervises by default. BusyBox init does not throttle respawns; systemd's &lt;code&gt;WatchdogSec=&lt;/code&gt;, &lt;code&gt;RuntimeWatchdogSec=&lt;/code&gt; and &lt;code&gt;StartLimitAction=&lt;/code&gt; are all off or inert until you set them.&lt;/li&gt;
&lt;li&gt;systemd sets a platform floor: kernel 5.10 minimum, 5.14 recommended, merged &lt;code&gt;/usr&lt;/code&gt;, glibc 2.34 — and in Buildroot, a glibc toolchain for the whole product.&lt;/li&gt;
&lt;li&gt;Bring four numbers to the review: size delta on your own image, resident memory, service topology count, and boot time measured to application readiness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use systemd with a musl toolchain?&lt;/strong&gt;&lt;br&gt;
Upstream systemd lists musl 1.2.6 or newer as a requirement when built with &lt;code&gt;-Dlibc=musl&lt;/code&gt;, so upstream support exists. Buildroot's systemd package still declares &lt;code&gt;depends on BR2_TOOLCHAIN_USES_GLIBC&lt;/code&gt;, so choosing systemd in Buildroot means choosing a glibc toolchain for the whole product. On musl in Buildroot, OpenRC is the dependency-ordered alternative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the minimum kernel version for systemd?&lt;/strong&gt;&lt;br&gt;
The upstream systemd README sets 5.10 as the minimum baseline and states that kernels below it are not supported at all. Version 5.14 is the recommended baseline; below it, systemd sets an &lt;code&gt;old-kernel&lt;/code&gt; taint flag and upstream support is limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does BusyBox init detect a service that has stopped responding?&lt;/strong&gt;&lt;br&gt;
No. An &lt;code&gt;inittab&lt;/code&gt; entry with the &lt;code&gt;respawn&lt;/code&gt; action restarts a process when it exits, but a process still running and no longer doing useful work will not be restarted, and BusyBox's own documentation states that it does not stop processes from respawning out of control. Detecting a hung service needs a watchdog protocol you implement yourself, or systemd's &lt;code&gt;WatchdogSec=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the licence difference affect the decision?&lt;/strong&gt;&lt;br&gt;
Not materially. systemd is LGPL-2.1-or-later with exceptions noted in its tree, and BusyBox is distributed under version 2 of the GPL only. Both run as separate processes from your application, so the compliance obligation is broadly the same. The one case worth noting is linking &lt;code&gt;libsystemd&lt;/code&gt; into a proprietary daemon to obtain &lt;code&gt;sd_notify&lt;/code&gt;; the readiness protocol is a documented datagram to &lt;code&gt;$NOTIFY_SOCKET&lt;/code&gt; with reference implementations published upstream, so you can implement it without linking anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.yoctoproject.org/dev-manual/init-manager.html" rel="noopener noreferrer"&gt;Yocto Project — Selecting an Initialization Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.yoctoproject.org/dev-manual/build-quality.html" rel="noopener noreferrer"&gt;Yocto Project — Maintaining Build Output Quality&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buildroot.org/downloads/manual/manual.html" rel="noopener noreferrer"&gt;The Buildroot user manual — init system, graph-size and legal-info&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/buildroot/buildroot/blob/master/system/Config.in" rel="noopener noreferrer"&gt;Buildroot — &lt;code&gt;system/Config.in&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/systemd/systemd/blob/main/README" rel="noopener noreferrer"&gt;systemd README — requirements and baselines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html" rel="noopener noreferrer"&gt;systemd.service(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-system.conf.html" rel="noopener noreferrer"&gt;systemd-system.conf(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/journald.conf.html" rel="noopener noreferrer"&gt;journald.conf(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://busybox.net/downloads/BusyBox.html" rel="noopener noreferrer"&gt;BusyBox documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I teach Linux kernel, device drivers and embedded Linux at &lt;a href="https://www.techveda.live" rel="noopener noreferrer"&gt;TECH VEDA&lt;/a&gt;. If your team is working through init selection, build-system configuration or root filesystem layout on real hardware, our &lt;a href="https://www.techveda.live/embedded-linux-yocto/" rel="noopener noreferrer"&gt;Embedded Linux and Yocto training&lt;/a&gt; covers exactly this ground in hands-on labs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>embedded</category>
      <category>yocto</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Linux 7.3: AF_ALG Restricted by Default, YOLOX on Etnaviv</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Tue, 25 Aug 2026 05:01:05 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/linux-73-afalg-restricted-by-default-yolox-on-etnaviv-2eib</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/linux-73-afalg-restricted-by-default-yolox-on-etnaviv-2eib</guid>
      <description>&lt;p&gt;Linux 7.3's merge window has produced the clearest security change of the cycle: the AF_ALG user-space crypto interface is now restricted by default, controlled by an allowlist, while new AES library APIs mark out the replacement path inside the kernel. Intel has proposed DRM Fabric, a vendor-neutral topology layer for accelerator interconnects, days after AMD posted its UALink enablement series. The open-source Etnaviv stack now runs YOLOX object detection on NXP i.MX 8M Plus class NPUs, RISC-V gains initial support for eleven ISA extensions in mainline, and a seven-branch stable/LTS release wave plus Buildroot updates make this a heavy patch-intake week.&lt;/p&gt;

&lt;p&gt;The theme this week is boundaries moving. The kernel is narrowing what user space may touch (AF_ALG), widening what mainline abstracts (accelerator fabrics, NPU inference), and firming up what platforms must guarantee (RISC-V profile extensions). Each of those shifts lands on embedded teams as either a migration task or a planning signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this edition
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AF_ALG is restricted by default in Linux 7.3.&lt;/strong&gt; A new sysctl limits the user-space crypto interface to an allowlist, and new AES library APIs define the in-kernel replacement path. &lt;em&gt;— audit before you rebase&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intel proposes DRM Fabric.&lt;/strong&gt; A vendor-neutral topology layer for scale-up accelerator interconnects, posted days after AMD's UALink enablement series. &lt;em&gt;— long-term watch&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YOLOX runs on the open Etnaviv NPU stack.&lt;/strong&gt; New operations bring a stronger object-detection model to Vivante NPUs in the i.MX 8M Plus and Amlogic A311D. &lt;em&gt;— evaluate now&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RISC-V lands eleven ISA extensions in 7.3.&lt;/strong&gt; Indirect CSR access, counter delegation, and QoS tagging move the platform toward server-class expectations. &lt;em&gt;— planning signal&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven stable/LTS kernels and a Buildroot release wave shipped in one day.&lt;/strong&gt; 7.1.10 down to 5.10.266, plus Buildroot security point releases. &lt;em&gt;— patch now&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Linux 7.3 puts AF_ALG behind an allowlist
&lt;/h2&gt;

&lt;p&gt;The crypto pull for Linux 7.3, sent by maintainer Herbert Xu and now merged, contains the change this audience should read first: a new &lt;code&gt;af_alg_restrict&lt;/code&gt; sysctl that defaults to 1. With the default in force, AF_ALG — the socket interface that lets user-space programs drive the kernel's internal crypto algorithms — only accepts the algorithms on a curated allowlist. Eric Biggers authored the series, which also admits the additional ciphers cryptsetup needs and makes &lt;code&gt;cbc(paes)&lt;/code&gt; available to privileged callers only.&lt;/p&gt;

&lt;p&gt;This is the follow-through on a deprecation that has been building for several cycles. Linux 7.2 formally deprecated AF_ALG after zero-copy support and hardware offload had already been dropped; the maintainers have described the interface as a large attack surface with little remaining justification. 7.3 converts that position from an advisory into a default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your product uses AF_ALG, a kernel rebase to 7.3 can break it silently: the interface now defaults to allowlist-only operation.&lt;/strong&gt; The failure mode is not a build error but a runtime one — socket or algorithm setup fails on a device that worked on 7.2.&lt;/p&gt;

&lt;p&gt;The same pull also prunes embedded crypto drivers. The &lt;code&gt;crypto_rng&lt;/code&gt; interfaces of the Allwinner sun8i-ce/sun8i-ss and NXP CAAM PRNGs were removed, the Qualcomm RNG moved fully under &lt;code&gt;drivers/char/hw_random&lt;/code&gt;, and the Qualcomm qce crypto engine was marked BROKEN even as it gained runtime PM and interconnect-scaling work. Products on i.MX, Allwinner, or Qualcomm silicon that read random numbers or run crypto through these paths should re-check their configs on 7.3.&lt;/p&gt;

&lt;p&gt;The constructive half of the story landed in a separate pull from Eric Biggers: library APIs for the AES modes the kernel actually uses — ECB, CBC, CBC-CTS, CTR, XCTR, XTS, GCM and CCM — with full documentation, wired into the traditional crypto API so the self-tests cover them. The old AES-GCM library is gone. Proof-of-concept conversions across kernel subsystems removed roughly 1,900 lines, and the stated plan is to migrate architecture-optimized AES code into the library in later cycles. In-kernel users get a simpler, faster path; user-space users are being pointed back at user-space crypto libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to check your device
&lt;/h3&gt;

&lt;p&gt;Search your user space for AF_ALG use before you plan a 7.3 rebase, and inspect the new sysctl once you are on a 7.3 kernel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"AF_ALG"&lt;/span&gt; rootfs/usr/ 2&amp;gt;/dev/null
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sysctl &lt;span class="nt"&gt;-a&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep &lt;/span&gt;af_alg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common indirect users include libkcapi and OpenSSL engines configured for kernel crypto. If you find a dependency, migrate it to a user-space implementation, or plan to manage the allowlist deliberately and treat that as product configuration, not a default you inherit. Kernel-module authors using skcipher or AEAD for AES should track the new library APIs — that is where the maintained, optimized path is heading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intel proposes DRM Fabric for accelerator interconnects
&lt;/h2&gt;

&lt;p&gt;Intel engineer Konstantin Sinyuk posted a twelve-patch RFC introducing "drm/fabric": vendor-neutral, protocol-agnostic topology infrastructure for scale-up accelerator interconnects. The model is a strict hierarchy — a fabric groups the endpoints of one interconnect instance, an endpoint is one accelerator attachment holding physical ports, a port reports lane capability and operational state, and a peer names the directly adjacent accelerator or switch port. The core records direct adjacency only; route computation, switch forwarding, and the data path stay with vendor drivers and fabric controllers. The uAPI is Generic Netlink rather than sysfs, with query and provisioning operations, a synthetic provider for testing, and KUnit plus netlink selftests in the series.&lt;/p&gt;

&lt;p&gt;The timing matters. Days earlier, AMD's Alex Deucher posted a 95-patch series of about 7,000 lines enabling UALink — the open standard for linking up to 1,024 accelerators in one domain — in the AMDGPU driver, including remote memory access without copies and a dedicated address space with remote TLB shootdowns. Two vendors converging on mainline infrastructure for accelerator fabrics in the same fortnight means the interconnect layer is becoming ordinary kernel plumbing rather than a per-vendor SDK concern.&lt;/p&gt;

&lt;p&gt;For embedded engineers this is a direction signal rather than a task. The pattern — netlink-based topology objects with vendor providers underneath — is the same shape as devlink in networking, and it is the shape multi-accelerator edge boxes will inherit when today's datacenter interconnects scale down.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it means for platform teams
&lt;/h3&gt;

&lt;p&gt;Nothing here is merged; an RFC can change shape completely. Track the discussion if you build systems with more than one accelerator, because the uAPI that emerges will be the stable interface your orchestration and monitoring tools consume. Teams evaluating UALink-class hardware should note that topology discovery, error surfacing, and provisioning are being standardized now — the review threads are where objections still count.&lt;/p&gt;

&lt;h2&gt;
  
  
  YOLOX object detection on the open Etnaviv NPU stack
&lt;/h2&gt;

&lt;p&gt;Tomeu Vizoso, working in partnership with Ideas On Board, has brought YOLOX object detection up on the Etnaviv driver stack for Vivante VIP NPUs — the neural accelerators in the NXP i.MX 8M Plus and the Amlogic A311D. YOLOX, an Apache-2.0 model from Megvii, is a meaningful step up in capability from SSDLite MobileDet, the model the stack supported until now.&lt;/p&gt;

&lt;p&gt;The interesting part is what had to be built. FullyConnected now runs directly on the NN convolution cores. Reshape, Split, and Concatenate are handled purely as metadata changes, costing no hardware cycles. ReLU is fused into the output stage, Absolute and Logistic run as lookup-table operations on the tensor-processing cores, Subtract is lowered to a convolution, and Transpose is either fused or executed as a TP operation. Feature maps in signed 8-bit integers were added as well, which for some models increases accuracy at identical computational cost.&lt;/p&gt;

&lt;p&gt;Each of those is a general capability, not a YOLOX special case — the operation coverage of the open stack just widened for every model that needs those primitives. Open NPU stacks are moving from proof-of-concept classification demos toward the detection workloads products actually ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to evaluate it
&lt;/h3&gt;

&lt;p&gt;If you ship i.MX 8M Plus or A311D hardware, put the open stack on your benchmark board alongside the vendor SDK and measure your own model, not the demo. Check three things: whether your model's operations are now covered, what accuracy you get from signed INT8 feature maps, and what the end-to-end latency looks like against the blob-based path. A fully mainline inference stack removes an entire vendor-SDK dependency from your BSP lifecycle — that is worth a day of benchmarking to validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  RISC-V in 7.3: eleven extensions and server-class plumbing
&lt;/h2&gt;

&lt;p&gt;The RISC-V pull for 7.3, sent by maintainer Paul Walmsley and merged, adds initial definitions and discovery for eleven ISA extensions: Smcsrind and Sscsrind (indirect CSR access), Smcntrpmf (privilege-mode filtering for the cycle and instret counters), Ssccfg and Smcdeleg (counter delegation to supervisor mode), Zicclsm, Ziccamoa, Ziccif, Ziccrse and Za64rs (cache, atomicity, and reservation-set guarantees from the profile specifications), and Ssqosid — quality-of-service resource tagging, which also gains CPU context-switch support via the new &lt;code&gt;srmcfg&lt;/code&gt; CSR handling.&lt;/p&gt;

&lt;p&gt;Around the extensions, the pull carries platform work with a clear direction: restart and power-off through UEFI runtime services when available, ACPI table upgrade support for debugging (matching ARM64 and x86), a smaller CFI shadow-stack allocation (2GB down to 512MB of address space), TLB-flush improvements using ASIDs and the Svinval extension, and build-time extraction of vDSO offsets to cut boot overhead. XIP kernel leftovers were removed.&lt;/p&gt;

&lt;p&gt;Read together, this is the plumbing a platform needs before serious server and industrial deployment: delegated performance counters, QoS partitioning, firmware-managed power control, and profile-level guarantees that software can rely on across vendors. The extension list looks obscure item by item; collectively it is RISC-V being fitted for the same expectations Arm servers met a decade ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it means for SoC selection
&lt;/h3&gt;

&lt;p&gt;When evaluating RISC-V silicon for products with multi-year lifetimes, ask vendors which of these extensions their cores implement — counter delegation and Ssqosid in particular separate application-class designs from microcontroller-class ones. For BSP teams, the profile guarantees (Zicclsm, Ziccamoa, Ziccif, Ziccrse, Za64rs) are exposed through the kernel's hwprobe interface, so user space can detect them without vendor-specific code. Devices shipping mixed workloads should watch Ssqosid: cache and bandwidth partitioning is how you keep an inference thread from starving your control loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  A seven-branch stable wave and a Buildroot release day
&lt;/h2&gt;

&lt;p&gt;On 23 August the stable series updated across every maintained branch in one wave: 7.1.10 on stable, and 6.18.46, 6.12.105, 6.6.153, 6.1.184, 5.15.217 and 5.10.266 on longterm. The same day, Buildroot shipped its 2026.08-rc2 release candidate together with two security bugfix releases, 2026.05.2 and 2025.02.17, covering both maintained stable branches; 2026.08-rc1 had arrived on 18 August. On the Yocto side, the 6.0.1 (Wrynose) and 5.0.17 (Scarthgap) point releases carry matching userspace fixes, including BusyBox CVE patches.&lt;/p&gt;

&lt;p&gt;None of these releases is significant on its own, which is exactly why the wave gets missed. A coordinated multi-branch wave usually means fixes the stable maintainers wanted everywhere at once, and a build-system security release the same day means the userspace side of your image has pending patches too. The intake work is the story.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it means for BSP teams
&lt;/h3&gt;

&lt;p&gt;Diff the changelog of the branch you ship — networking, filesystem, and driver fixes in these waves regularly resolve field issues that were never reported upstream by name. Teams on 5.10 and 5.15 should confirm the projected end-of-life dates for those branches on kernel.org and have a migration plan that does not assume indefinite updates. Buildroot users on 2026.05 or 2025.02 should take the security point releases now, and start validating 2026.08-rc2 in CI so the stable release requires no extra work when it arrives. Treat the wave as one intake task: pin the new versions, rebuild, run your regression suite once.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/Linux-7.3-AF-ALG-Restrictions" rel="noopener noreferrer"&gt;New AF_ALG restrictions with Linux 7.3 (Phoronix)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/DRM-Fabric-Proposal" rel="noopener noreferrer"&gt;Intel engineers propose DRM Fabric for Linux (Phoronix)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.phoronix.com/news/UALink-Series-2-Linux" rel="noopener noreferrer"&gt;AMD posts UALink enablement series for the Linux kernel (Phoronix)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tomeuvizoso.net/2026/08/etnaviv-npu-update-22-yolox-support.html" rel="noopener noreferrer"&gt;Etnaviv NPU update 22: YOLOX support (Tomeu Vizoso)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/" rel="noopener noreferrer"&gt;kernel.org release listing (stable/longterm versions)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buildroot.org/news.html" rel="noopener noreferrer"&gt;Buildroot news: 2026.08-rc2, 2026.05.2, 2025.02.17&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.yoctoproject.org/migration-guides/release-notes-6.0.1.html" rel="noopener noreferrer"&gt;Yocto Project 6.0.1 (Wrynose) release notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— Raghu Bharadwaj&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want to go deeper than the news? TECH VEDA runs hands-on Linux kernel and embedded Linux training — see &lt;a href="https://www.techveda.live/" rel="noopener noreferrer"&gt;techveda.live&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>embedded</category>
      <category>riscv</category>
    </item>
    <item>
      <title>Linux genpd Power Domains: Why a Rail Won’t Power Off?</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Tue, 25 Aug 2026 04:59:00 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/linux-genpd-power-domains-why-a-rail-wont-power-off-2b6e</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/linux-genpd-power-domains-why-a-rail-wont-power-off-2b6e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A genpd power domain removes power only when every device attached to it is runtime-suspended, every subdomain is already off, and the governor agrees. When a rail stays on, the cause is one of the conditions &lt;code&gt;genpd_power_off()&lt;/code&gt; checks before it calls the provider's &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt; callback. Those conditions are a device that never runtime-suspended, a subdomain still counted as on, an always-on flag, a system suspend in progress, a resume-latency constraint the governor will not accept, or the boot-time &lt;code&gt;stay_on&lt;/code&gt; hold introduced in Linux 6.17. Start at &lt;code&gt;/sys/kernel/debug/pm_genpd/pm_genpd_summary&lt;/code&gt;, which shows three of those conditions directly and narrows down the rest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On most embedded SoCs, power is not gated per device. It is gated per block: one switch feeds the GPU and its MMU, another feeds the display controller together with the DSI host and the PHY. The Linux kernel models these blocks as &lt;strong&gt;genpd power domains&lt;/strong&gt;. This article explains, from the code, how genpd power domains decide that a block may lose power, and how to find out which condition is holding your rail on.&lt;/p&gt;

&lt;p&gt;It sits one level below an earlier post on the per-device layer, &lt;a href="https://www.techveda.live/2026/07/18/runtime-pm-device-never-suspends/" rel="noopener noreferrer"&gt;Runtime PM: Why Your Device Never Suspends&lt;/a&gt;. Runtime PM tracks whether one device is idle; genpd decides whether the shared rail feeding several devices may be switched off. They fail for different reasons.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it decides&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Device driver&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Calls &lt;code&gt;pm_runtime_get()&lt;/code&gt; and &lt;code&gt;pm_runtime_put()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Runtime PM&lt;/strong&gt; (one device)&lt;/td&gt;
&lt;td&gt;Is this one device idle? The answer appears as the &lt;em&gt;runtime status&lt;/em&gt; column in &lt;code&gt;pm_genpd_summary&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;genpd&lt;/strong&gt; (one domain, several devices)&lt;/td&gt;
&lt;td&gt;May the shared rail be switched off? This is &lt;code&gt;genpd_power_off()&lt;/code&gt;, and it is what the rest of this article is about.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provider driver&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt; callback drives the hardware sequence and waits for the acknowledgement.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SoC and PMIC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The rail actually drops. Nothing above this row can confirm that it did.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where the genpd code lives
&lt;/h2&gt;

&lt;p&gt;The data structures are in &lt;code&gt;include/linux/pm_domain.h&lt;/code&gt;. The core logic, the governors and the debugfs interface are in &lt;code&gt;drivers/pmdomain/&lt;/code&gt; — the core in &lt;code&gt;drivers/pmdomain/core.c&lt;/code&gt;, the in-tree governors in &lt;code&gt;drivers/pmdomain/governor.c&lt;/code&gt;. The core moved there in Linux 6.8; on 6.7 and earlier it is at &lt;code&gt;drivers/base/power/domain.c&lt;/code&gt;. Everything quoted below is from Linux 6.17, and one mechanism described later is new in that release. The framework needs &lt;code&gt;CONFIG_PM_GENERIC_DOMAINS&lt;/code&gt;, which platform code selects rather than you enabling it in &lt;code&gt;menuconfig&lt;/code&gt;; &lt;code&gt;CONFIG_PM_GENERIC_DOMAINS_OF&lt;/code&gt; and &lt;code&gt;CONFIG_PM_GENERIC_DOMAINS_SLEEP&lt;/code&gt; then default to &lt;code&gt;y&lt;/code&gt; when &lt;code&gt;OF&lt;/code&gt; and &lt;code&gt;PM_SLEEP&lt;/code&gt; are set. The debugfs interface is guarded by &lt;code&gt;CONFIG_DEBUG_FS&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;zcat /proc/config.gz | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'PM_GENERIC_DOMAINS|CONFIG_DEBUG_FS='&lt;/span&gt;
&lt;span class="go"&gt;CONFIG_PM_GENERIC_DOMAINS=y
CONFIG_PM_GENERIC_DOMAINS_SLEEP=y
CONFIG_PM_GENERIC_DOMAINS_OF=y
CONFIG_DEBUG_FS=y
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Inside struct generic_pm_domain
&lt;/h2&gt;

&lt;p&gt;A provider driver allocates a &lt;code&gt;struct generic_pm_domain&lt;/code&gt;, fills in the two hardware callbacks, and registers it. These are the fields that decide power-off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* include/linux/pm_domain.h - fields discussed here, comments condensed */&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;generic_pm_domain&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt;        &lt;span class="n"&gt;parent_links&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="cm"&gt;/* links where this domain is the parent */&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt;        &lt;span class="n"&gt;child_links&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="cm"&gt;/* links where this domain is the child */&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt;        &lt;span class="n"&gt;dev_list&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="cm"&gt;/* devices attached to this domain */&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;dev_power_governor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gov&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;atomic_t&lt;/span&gt;                &lt;span class="n"&gt;sd_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="cm"&gt;/* subdomains currently powered on */&lt;/span&gt;
        &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;gpd_status&lt;/span&gt;         &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="cm"&gt;/* GENPD_STATE_ON or GENPD_STATE_OFF */&lt;/span&gt;
        &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;            &lt;span class="n"&gt;device_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;            &lt;span class="n"&gt;suspended_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* system suspend device counter */&lt;/span&gt;
        &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;            &lt;span class="n"&gt;prepared_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* suspend counter of prepared devices */&lt;/span&gt;
        &lt;span class="n"&gt;bool&lt;/span&gt;                    &lt;span class="n"&gt;stay_on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="cm"&gt;/* stay powered-on during boot */&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;power_off&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;generic_pm_domain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;power_on&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;generic_pm_domain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;genpd_power_state&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;            &lt;span class="n"&gt;state_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;            &lt;span class="n"&gt;state_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="cm"&gt;/* state the domain enters when off */&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration is &lt;code&gt;pm_genpd_init(genpd, gov, is_off)&lt;/code&gt;, where the third argument states whether the hardware is already off at registration. Hierarchy is declared with &lt;code&gt;pm_genpd_add_subdomain(parent, child)&lt;/code&gt;. For Device Tree, the provider then calls &lt;code&gt;of_genpd_add_provider_simple()&lt;/code&gt; for a single domain or &lt;code&gt;of_genpd_add_provider_onecell()&lt;/code&gt; for several, and consumer nodes reference it with &lt;code&gt;power-domains = &amp;lt;&amp;amp;pd 0&amp;gt;&lt;/code&gt; against the provider's &lt;code&gt;#power-domain-cells&lt;/code&gt;. Attachment happens automatically during probe, but only for a device with exactly one &lt;code&gt;power-domains&lt;/code&gt; entry: &lt;code&gt;genpd_dev_pm_attach()&lt;/code&gt; counts the phandles and returns without attaching when the count is not one. A device listing two domains is silently left unmanaged unless its driver calls &lt;code&gt;dev_pm_domain_attach_by_name()&lt;/code&gt;, which keys on &lt;code&gt;power-domain-names&lt;/code&gt;, or &lt;code&gt;devm_pm_domain_attach_list()&lt;/code&gt;. Nothing reports this, so it is worth checking early on a new board. Idle states come from a &lt;code&gt;domain-idle-states&lt;/code&gt; property in the provider node; without it the domain gets one default state and can only ever report &lt;code&gt;off-0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading pm_genpd_summary
&lt;/h2&gt;

&lt;p&gt;The genpd core creates a debugfs directory named &lt;code&gt;pm_genpd&lt;/code&gt; holding a global file called &lt;code&gt;pm_genpd_summary&lt;/code&gt;. Read it first on any board where power is higher than expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; debugfs none /sys/kernel/debug
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /sys/kernel/debug/pm_genpd/pm_genpd_summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two header lines and the separator are printed verbatim by &lt;code&gt;summary_show()&lt;/code&gt;. An illustrative result on a board with a display domain and a GPU domain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;domain                          status          children        performance
    /device                         runtime status                  managed by
------------------------------------------------------------------------------
pd_display                      on                              0
                                                pd_dsi
    fd4a0000.display                active                      0           SW
pd_dsi                          on                              0
    fd4b0000.dsi                    active                      0           SW
pd_gpu                          off-0                           0
    fde60000.gpu                    suspended                   0           SW
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout is not obvious on a first reading, because subdomains appear on their own indented line rather than beside the domain that owns them. The same output as a tree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Effect on power-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pd_display&lt;/code&gt; (domain)&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;Held on by both rows below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;code&gt;fd4a0000.display&lt;/code&gt; (device)&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;td&gt;Not runtime-suspended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;code&gt;pd_dsi&lt;/code&gt; (subdomain)&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;Counted in &lt;code&gt;sd_count&lt;/code&gt;, checked first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;code&gt;fd4b0000.dsi&lt;/code&gt; (device)&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;td&gt;Holds &lt;code&gt;pd_dsi&lt;/code&gt; on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pd_gpu&lt;/code&gt; (domain)&lt;/td&gt;
&lt;td&gt;off-0&lt;/td&gt;
&lt;td&gt;Powered off, in idle state 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;code&gt;fde60000.gpu&lt;/code&gt; (device)&lt;/td&gt;
&lt;td&gt;suspended&lt;/td&gt;
&lt;td&gt;Nothing blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; column shows &lt;code&gt;on&lt;/code&gt;, or &lt;code&gt;off-N&lt;/code&gt; where N is &lt;code&gt;state_idx&lt;/code&gt;, the index of the idle state the domain entered — a domain with several idle states will show &lt;code&gt;off-0&lt;/code&gt;, &lt;code&gt;off-1&lt;/code&gt; and so on. Child domain names print on a continuation line indented by 48 characters, so they land under the &lt;code&gt;children&lt;/code&gt; heading rather than on the domain's own row. Device rows are indented four spaces and carry the device's Runtime PM status, then its performance state, then &lt;code&gt;HW&lt;/code&gt; or &lt;code&gt;SW&lt;/code&gt; for hardware- or software-controlled mode.&lt;/p&gt;

&lt;p&gt;Read the Runtime PM status closely. &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;suspended&lt;/code&gt;, &lt;code&gt;suspending&lt;/code&gt; and &lt;code&gt;resuming&lt;/code&gt; are the normal values, but &lt;code&gt;unsupported&lt;/code&gt; means &lt;code&gt;disable_depth&lt;/code&gt; is non-zero: the driver never called &lt;code&gt;pm_runtime_enable()&lt;/code&gt;, or disabled it again. Such a device can never satisfy &lt;code&gt;pm_runtime_suspended()&lt;/code&gt;, so it blocks its domain permanently. Rule that out first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why genpd power domains refuse to power off
&lt;/h2&gt;

&lt;p&gt;The decision for all genpd power domains is made in one function, &lt;code&gt;genpd_power_off()&lt;/code&gt;. It returns early, without touching the hardware, under any of the following conditions. The checks fall into four stages, and the domain stays on the moment any one of them fails:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What is tested&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;1. Domain flags&lt;/strong&gt; (no lists walked yet)&lt;/td&gt;
&lt;td&gt;Already off · &lt;code&gt;prepared_count &amp;gt; 0&lt;/code&gt; · &lt;code&gt;GENPD_FLAG_ALWAYS_ON&lt;/code&gt; · &lt;code&gt;GENPD_FLAG_RPM_ALWAYS_ON&lt;/code&gt; · &lt;code&gt;stay_on&lt;/code&gt; · &lt;code&gt;sd_count &amp;gt; 0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;2. Subdomains&lt;/strong&gt; (walks &lt;code&gt;parent_links&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Every child must already be in its deepest state, that is &lt;code&gt;state_idx == state_count - 1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;3. Devices&lt;/strong&gt; (walks &lt;code&gt;dev_list&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Any device with &lt;code&gt;rpm_always_on&lt;/code&gt; stops it outright; otherwise counts devices that are not runtime-suspended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Governor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;power_down_ok&lt;/code&gt; must agree, then &lt;code&gt;sd_count&lt;/code&gt; is tested once more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GENPD_NOTIFY_PRE_OFF&lt;/code&gt; notifiers, then the provider's &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;On success&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sets &lt;code&gt;GENPD_STATE_OFF&lt;/code&gt;, increments &lt;code&gt;usage&lt;/code&gt;, then tries each parent domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;On failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Increments &lt;code&gt;rejected&lt;/code&gt; for that state and leaves the domain on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The individual conditions, in the order the code tests them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The domain is already off (&lt;code&gt;status&lt;/code&gt; is not on).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prepared_count &amp;gt; 0&lt;/code&gt;, meaning a system suspend is in progress for devices in this domain.&lt;/li&gt;
&lt;li&gt;The domain has &lt;code&gt;GENPD_FLAG_ALWAYS_ON&lt;/code&gt;, or has &lt;code&gt;GENPD_FLAG_RPM_ALWAYS_ON&lt;/code&gt; (on except during system suspend).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stay_on&lt;/code&gt; is still set — the boot-time hold described in the next section.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sd_count &amp;gt; 0&lt;/code&gt;: at least one subdomain is powered on. This is tested again after the governor call, in case a subdomain began powering on in the meantime.&lt;/li&gt;
&lt;li&gt;Any child domain is not in its deepest state, that is &lt;code&gt;state_idx&lt;/code&gt; is below &lt;code&gt;state_count - 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Any attached device has &lt;code&gt;rpm_always_on&lt;/code&gt; set, which a driver requests with &lt;code&gt;dev_pm_genpd_rpm_always_on()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;More than one attached device is not runtime-suspended. The core counts devices where &lt;code&gt;pm_runtime_suspended()&lt;/code&gt; is false, and also counts an IRQ-safe device attached to a domain that is not IRQ-safe, that is, one without &lt;code&gt;GENPD_FLAG_IRQ_SAFE&lt;/code&gt;. Exactly one such device is tolerated only when the call came from the domain's own runtime-suspend path, because that device's status has not been updated yet.&lt;/li&gt;
&lt;li&gt;The governor refuses. The common choice is &lt;code&gt;simple_qos_governor&lt;/code&gt;, and the QoS in the name is the point: its &lt;code&gt;default_power_down_ok&lt;/code&gt; does not predict how long the domain will be idle. It adds the state's &lt;code&gt;power_off_latency_ns&lt;/code&gt; and &lt;code&gt;power_on_latency_ns&lt;/code&gt;, then refuses if any attached device's effective PM QoS resume-latency constraint, or any subdomain's &lt;code&gt;max_off_time_ns&lt;/code&gt;, is less than or equal to that total. It refuses because a consumer could not tolerate the wake-up delay, not because the idle window looked short. The governor that does predict idle duration is &lt;code&gt;pm_domain_cpu_gov&lt;/code&gt;, for CPU domains under &lt;code&gt;CONFIG_CPU_IDLE&lt;/code&gt;. A provider passing &lt;code&gt;NULL&lt;/code&gt; to &lt;code&gt;pm_genpd_init()&lt;/code&gt; has no governor, skips this check, and always uses state index 0.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all of that passes, the core runs the &lt;code&gt;GENPD_NOTIFY_PRE_OFF&lt;/code&gt; notifier chain and then calls the provider's &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt;. If either step fails, genpd increments &lt;code&gt;states[state_idx].rejected&lt;/code&gt; and leaves the domain on. Note also that a provider with no &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt; callback at all is treated as a success: the domain is marked off in software with no hardware action. On success the core sets &lt;code&gt;GENPD_STATE_OFF&lt;/code&gt;, increments the state's &lt;code&gt;usage&lt;/code&gt; counter, then walks up through &lt;code&gt;child_links&lt;/code&gt; and attempts to power off each parent domain in turn.&lt;/p&gt;

&lt;p&gt;One point is frequently misunderstood: the comparison of &lt;code&gt;suspended_count&lt;/code&gt; against &lt;code&gt;device_count&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; part of this path. That comparison lives in &lt;code&gt;genpd_sync_power_off()&lt;/code&gt;, which runs only in the noirq and syscore stages of system suspend. Runtime power-off counts devices by walking &lt;code&gt;dev_list&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boot-time hold: stay_on and sync_state
&lt;/h2&gt;

&lt;p&gt;Of all the conditions that keep genpd power domains powered, this is the one most likely to be unfamiliar — and the one to check your kernel version against first. The &lt;code&gt;stay_on&lt;/code&gt; field, &lt;code&gt;GENPD_FLAG_NO_STAY_ON&lt;/code&gt; and &lt;code&gt;of_genpd_sync_state()&lt;/code&gt; are new in Linux 6.17. On 6.16 and earlier this mechanism does not exist and grepping for it will find nothing. Among the current long-term branches that means only 6.18.y carries it; 6.12.y, 6.6.y, 6.1.y, 5.15.y and 5.10.y do not, so most products shipping on a long-term kernel today will not see this behaviour at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pm_genpd_init()&lt;/code&gt; always calls &lt;code&gt;genpd_set_stay_on()&lt;/code&gt;. Built with &lt;code&gt;CONFIG_PM_GENERIC_DOMAINS_OF&lt;/code&gt;, it sets &lt;code&gt;stay_on = true&lt;/code&gt; for any domain already powered on, unless the provider passed &lt;code&gt;GENPD_FLAG_NO_STAY_ON&lt;/code&gt;; built without it, a second definition hard-codes the field to false:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#ifdef CONFIG_PM_GENERIC_DOMAINS_OF
&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;genpd_set_stay_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;generic_pm_domain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;is_off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stay_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;genpd_is_no_stay_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;is_off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While &lt;code&gt;stay_on&lt;/code&gt; is true, &lt;code&gt;genpd_power_off()&lt;/code&gt; returns immediately. The purpose is to protect devices that firmware left running — a display that is already showing a splash screen, for example — from losing power before their driver has probed. The hold is released by the driver-model &lt;code&gt;sync_state()&lt;/code&gt; callback, once every consumer of the provider has probed. The core clears the flag and immediately retries the power-off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;of_genpd_sync_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;generic_pm_domain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;mutex_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gpd_list_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;list_for_each_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gpd_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpd_list_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;of_fwnode_handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;genpd_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                        &lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stay_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                        &lt;span class="n"&gt;genpd_power_off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                        &lt;span class="n"&gt;genpd_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genpd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;mutex_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gpd_list_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lifetime of the hold, on Linux 6.17 and newer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point in boot&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firmware left the domain powered. The provider registers it, &lt;code&gt;pm_genpd_init()&lt;/code&gt; calls &lt;code&gt;genpd_set_stay_on()&lt;/code&gt;, and &lt;code&gt;stay_on&lt;/code&gt; becomes true.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drivers probing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;genpd_power_off()&lt;/code&gt; returns immediately every time. The rail is protected while consumers are still coming up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Every consumer probes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The driver model calls &lt;code&gt;sync_state()&lt;/code&gt;. &lt;code&gt;of_genpd_sync_state()&lt;/code&gt; clears &lt;code&gt;stay_on&lt;/code&gt; and retries the power-off straight away.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One never probes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sync_state()&lt;/code&gt; never runs, &lt;code&gt;stay_on&lt;/code&gt; stays true, and the domain remains powered for the whole boot. A missing driver or a permanently deferred probe is enough.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical consequence: if one consumer of a genpd provider never probes — a missing driver, a failed regulator, a permanently deferred probe — &lt;code&gt;sync_state()&lt;/code&gt; never fires, &lt;code&gt;stay_on&lt;/code&gt; is never cleared, and the domain stays powered for the life of the boot. The default policy is the strict one — &lt;code&gt;fw_devlink.sync_state=strict&lt;/code&gt; waits indefinitely for consumers to probe successfully — so &lt;code&gt;fw_devlink.sync_state=timeout&lt;/code&gt; on the kernel command line is a quick way to test whether this is what you are looking at.&lt;/p&gt;

&lt;p&gt;The symptom is a domain that reads &lt;code&gt;on&lt;/code&gt; in &lt;code&gt;pm_genpd_summary&lt;/code&gt; with every listed device showing &lt;code&gt;suspended&lt;/code&gt;. Do not stop there: an always-on flag, a per-device &lt;code&gt;rpm_always_on&lt;/code&gt;, a subdomain not in its deepest state, and a governor refusal all produce exactly the same output. The summary cannot separate them, which is why the next section and the checklist above matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The per-domain debugfs files
&lt;/h2&gt;

&lt;p&gt;Alongside the global summary, genpd creates one directory per domain containing &lt;code&gt;current_state&lt;/code&gt;, &lt;code&gt;sub_domains&lt;/code&gt;, &lt;code&gt;idle_states&lt;/code&gt;, &lt;code&gt;active_time&lt;/code&gt;, &lt;code&gt;total_idle_time&lt;/code&gt; and &lt;code&gt;devices&lt;/code&gt;. A &lt;code&gt;perf_state&lt;/code&gt; file appears only for providers that implement &lt;code&gt;set_performance_state&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /sys/kernel/debug/pm_genpd/pd_gpu/
&lt;span class="go"&gt;active_time  current_state  devices  idle_states  sub_domains  total_idle_time
&lt;/span&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /sys/kernel/debug/pm_genpd/pd_gpu/idle_states
&lt;span class="go"&gt;State          Time Spent(ms) Usage      Rejected   Above      Below
S0             412            37         2          0          0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Usage&lt;/code&gt; and &lt;code&gt;Rejected&lt;/code&gt; are the useful pair. A rising &lt;code&gt;Rejected&lt;/code&gt; count means the core decided to power off but the transition failed — usually the provider's &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt; callback returning an error, though a &lt;code&gt;GENPD_NOTIFY_PRE_OFF&lt;/code&gt; notifier veto increments the same counter. Either way the next place to look is that callback. A &lt;code&gt;Usage&lt;/code&gt; count of zero with &lt;code&gt;Rejected&lt;/code&gt; also at zero means the domain was never asked to power off at all, which points back to the list above.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Above&lt;/code&gt; and &lt;code&gt;Below&lt;/code&gt; record how often the chosen idle state was too deep or too shallow for the residency actually achieved, but they are only maintained by &lt;code&gt;pm_domain_cpu_gov&lt;/code&gt;, which is the sole caller that sets &lt;code&gt;reflect_residency&lt;/code&gt;. On a domain running &lt;code&gt;simple_qos_governor&lt;/code&gt; or no governor they stay at zero, as above, so do not read anything into that. Unlike the summary, &lt;code&gt;devices&lt;/code&gt; and &lt;code&gt;sub_domains&lt;/code&gt; print one bare name per line with no header.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical order of investigation
&lt;/h2&gt;

&lt;p&gt;Use this order when genpd power domains stay on and the cause is not yet known.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read &lt;code&gt;pm_genpd_summary&lt;/code&gt; and find the domain that is &lt;code&gt;on&lt;/code&gt; when you expect it off.&lt;/li&gt;
&lt;li&gt;Any device row that is not &lt;code&gt;suspended&lt;/code&gt; is a Runtime PM problem in that driver, not a genpd problem. A row reading &lt;code&gt;unsupported&lt;/code&gt; means runtime PM was never enabled for that device.&lt;/li&gt;
&lt;li&gt;If every device is &lt;code&gt;suspended&lt;/code&gt;, check the continuation line and confirm each subdomain is also off.&lt;/li&gt;
&lt;li&gt;If devices and subdomains are all idle, the cause is one of the conditions the summary cannot show. Check them directly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/kernel/debug/devices_deferred
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'GENPD_FLAG_ALWAYS_ON\|GENPD_FLAG_RPM_ALWAYS_ON'&lt;/span&gt; drivers/pmdomain/
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/devices/platform/fde60000.gpu/power/pm_qos_resume_latency_us
&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/kernel/debug/regulator/regulator_summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first shows consumers still waiting to probe, which on Linux 6.17 is what keeps &lt;code&gt;stay_on&lt;/code&gt; set. The second finds an always-on flag in the provider driver. The third shows a resume-latency constraint tight enough to make &lt;code&gt;default_power_down_ok&lt;/code&gt; refuse; this attribute exists only when the driver called &lt;code&gt;dev_pm_qos_expose_latency_limit()&lt;/code&gt;, so its absence does not rule the cause out. The fourth is the check most often skipped: &lt;code&gt;off-0&lt;/code&gt; means only that the core believes the domain is off. Confirm the rail actually dropped, through the regulator summary or a meter on the sense point, before concluding the software is correct.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the domain does power off but not as often as expected, read &lt;code&gt;idle_states&lt;/code&gt;. A rising &lt;code&gt;Rejected&lt;/code&gt; count means the core did decide to power off and the transition failed, so the problem is in the provider's callback rather than in the checklist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Working through power domains on real silicon is a large part of driver work, and it is covered in our &lt;a href="https://www.techveda.live/linux-device-drivers/" rel="noopener noreferrer"&gt;Linux device drivers training&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Runtime PM decides whether a device is idle; genpd power domains decide whether a shared rail may be switched off. Both must agree.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;genpd_power_off()&lt;/code&gt; tests a fixed set of conditions in a fixed order. Match the symptom to one of them instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;off-N&lt;/code&gt; in &lt;code&gt;pm_genpd_summary&lt;/code&gt; is not an error — N is the index of the idle state the domain entered.&lt;/li&gt;
&lt;li&gt;A domain that is &lt;code&gt;on&lt;/code&gt; while all its devices are &lt;code&gt;suspended&lt;/code&gt; has several possible causes that the summary cannot tell apart: an always-on flag, a per-device &lt;code&gt;rpm_always_on&lt;/code&gt;, a governor refusal, or, on Linux 6.17 and newer, the &lt;code&gt;stay_on&lt;/code&gt; hold.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simple_qos_governor&lt;/code&gt; refuses on PM QoS resume-latency constraints, not on a predicted idle window. That changes what you go looking for.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Rejected&lt;/code&gt; counter in &lt;code&gt;idle_states&lt;/code&gt; separates a core-level refusal from a transition that was attempted and failed.&lt;/li&gt;
&lt;li&gt;genpd reporting &lt;code&gt;off-0&lt;/code&gt; means the core believes the domain is off. Confirm the rail separately.&lt;/li&gt;
&lt;li&gt;The core moved to &lt;code&gt;drivers/pmdomain/core.c&lt;/code&gt; in Linux 6.8; before that it is &lt;code&gt;drivers/base/power/domain.c&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does "off-0" mean in pm_genpd_summary?&lt;/strong&gt;&lt;br&gt;
It means the domain is powered off and entered idle state index 0. The number after the hyphen is &lt;code&gt;state_idx&lt;/code&gt;, so a domain with several idle states can show &lt;code&gt;off-0&lt;/code&gt;, &lt;code&gt;off-1&lt;/code&gt; and so on. Only a powered-on domain prints a plain &lt;code&gt;on&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every device in my domain is suspended but the domain still reads "on". Why?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;pm_genpd_summary&lt;/code&gt; cannot distinguish the remaining causes, so check them in turn: &lt;code&gt;GENPD_FLAG_ALWAYS_ON&lt;/code&gt; or &lt;code&gt;GENPD_FLAG_RPM_ALWAYS_ON&lt;/code&gt; in the provider driver, a device that called &lt;code&gt;dev_pm_genpd_rpm_always_on()&lt;/code&gt;, a subdomain not in its deepest state, and a governor refusing on a resume-latency constraint. On Linux 6.17 and newer there is one more: the boot-time &lt;code&gt;stay_on&lt;/code&gt; hold, which is released only when the provider's &lt;code&gt;sync_state()&lt;/code&gt; callback runs after every consumer has probed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between GENPD_FLAG_ALWAYS_ON and GENPD_FLAG_RPM_ALWAYS_ON?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;GENPD_FLAG_ALWAYS_ON&lt;/code&gt; keeps the domain powered at all times. &lt;code&gt;GENPD_FLAG_RPM_ALWAYS_ON&lt;/code&gt; keeps it powered during normal operation but allows a power-off during system suspend, which runs through &lt;code&gt;genpd_sync_power_off()&lt;/code&gt; instead. Both make &lt;code&gt;genpd_power_off()&lt;/code&gt; return before it reaches the device and subdomain checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between the Rejected and Usage counters in idle_states?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;Usage&lt;/code&gt; counts the times the domain successfully entered that idle state. &lt;code&gt;Rejected&lt;/code&gt; counts the times the core decided to power off but the transition failed, usually because the provider's &lt;code&gt;-&amp;gt;power_off()&lt;/code&gt; callback returned an error, though a &lt;code&gt;GENPD_NOTIFY_PRE_OFF&lt;/code&gt; notifier veto increments the same counter. A rising &lt;code&gt;Rejected&lt;/code&gt; count points at the provider driver and its hardware sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://elixir.bootlin.com/linux/v6.17/source/include/linux/pm_domain.h" rel="noopener noreferrer"&gt;include/linux/pm_domain.h (Linux 6.17)&lt;/a&gt; — &lt;code&gt;struct generic_pm_domain&lt;/code&gt;, the &lt;code&gt;GENPD_FLAG_*&lt;/code&gt; definitions and the full genpd API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/pmdomain/core.c" rel="noopener noreferrer"&gt;drivers/pmdomain/core.c&lt;/a&gt; — &lt;code&gt;genpd_power_off()&lt;/code&gt;, &lt;code&gt;genpd_set_stay_on()&lt;/code&gt;, &lt;code&gt;of_genpd_sync_state()&lt;/code&gt; and the debugfs implementation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kernel.org/doc/Documentation/devicetree/bindings/power/power-domain.yaml" rel="noopener noreferrer"&gt;Documentation/devicetree/bindings/power/power-domain.yaml&lt;/a&gt; — the &lt;code&gt;power-domains&lt;/code&gt; and &lt;code&gt;#power-domain-cells&lt;/code&gt; bindings.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kernel.org/doc/html/latest/power/runtime_pm.html" rel="noopener noreferrer"&gt;Runtime Power Management Framework for I/O Devices&lt;/a&gt; — the per-device layer that genpd builds on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lwn.net/Articles/675674/" rel="noopener noreferrer"&gt;PM: SoC idle support using PM domains&lt;/a&gt; (LWN) — background on idle states and the genpd governors.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>embedded</category>
      <category>drivers</category>
    </item>
    <item>
      <title>Kernel Cleanup Helpers: What Your LTS Actually Has</title>
      <dc:creator>Raghu Bharadwaj</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:38:01 +0000</pubDate>
      <link>https://dev.to/raghu_bharadwaj_404e60eb0c/kernel-cleanup-helpers-what-your-lts-actually-has-3h6i</link>
      <guid>https://dev.to/raghu_bharadwaj_404e60eb0c/kernel-cleanup-helpers-what-your-lts-actually-has-3h6i</guid>
      <description>&lt;p&gt;The kernel cleanup helpers — &lt;code&gt;__free()&lt;/code&gt;, &lt;code&gt;guard()&lt;/code&gt; and &lt;code&gt;scoped_guard()&lt;/code&gt; — let the compiler release a resource when a variable leaves scope, replacing the goto ladder in a driver's error path. The core arrived in mainline 6.5 and is now in every supported longterm series, down to 5.10. Everything built on top of it arrived later and reached the stable series at point releases rather than at &lt;code&gt;.0&lt;/code&gt;, so your major version number does not tell you what you have. On an old vendor tree the practical answer is &lt;code&gt;guard()&lt;/code&gt; plus a locally-defined free helper.&lt;/p&gt;

&lt;p&gt;Most embedded teams ship a longterm kernel and assume anything added to mainline in the last three years is out of reach until the next BSP jump. For the kernel cleanup helpers that assumption is wrong, but the correction is not simply "you can use them". The core macros are everywhere. The pieces you actually reach for — a free helper for the specific thing you allocated, a guard for a lock that can fail — landed at scattered point releases across the stable series, and a couple of the oldest trees still use a compiler standard that fights the coding pattern these helpers require. This is what that looks like on real trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem kernel cleanup helpers remove
&lt;/h2&gt;

&lt;p&gt;The kernel documentation calls the "goto error" pattern notorious for introducing subtle resource leaks, and tedious and error prone to extend once a function already carries several unwind conditions. Concretely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;mydev_count_channels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;mydev&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;np&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;ret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;np&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;of_get_child_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;of_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ENODEV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;mutex_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mydev_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;out_unlock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;of_get_child_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;EINVAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;out_unlock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;out_unlock:&lt;/span&gt;
        &lt;span class="n"&gt;mutex_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;of_node_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release sequence sits at the bottom, separated from the acquisitions at the top. Adding a third resource means adding a label, placing a release call correctly within the existing sequence, and re-checking every existing &lt;code&gt;goto&lt;/code&gt;. The usual way it breaks is smaller than that: someone adds an early-return check near the top, does not notice a resource is already held above it, and returns directly. The compiler accepts it and the refcount leaks quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the compiler does instead
&lt;/h2&gt;

&lt;p&gt;The kernel cleanup helpers move that bookkeeping to the compiler, using the &lt;code&gt;cleanup&lt;/code&gt; variable attribute that GCC and Clang both support. You attach a function to a variable; the compiler calls it when the variable leaves scope, on every exit path, including paths added later by someone who has not read the rest of the function.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DEFINE_FREE()&lt;/code&gt; creates the wrapper — a name, a type, and an expression using &lt;code&gt;_T&lt;/code&gt; for the variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFINE_FREE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kfree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;kfree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_T&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test looks redundant, since &lt;code&gt;kfree(NULL)&lt;/code&gt; is safe. It is there for the code generator. On an ownership-transfer path the pointer is handed to the caller and set to NULL first, so the compiler sees this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;kfree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Value propagation and dead-code elimination reduce that to &lt;code&gt;return p;&lt;/code&gt; — the cleanup call disappears. Without the test the compiler cannot make the deduction and the call survives. That elimination applies to the success path specifically; on a real error path the release happens, as it must.&lt;/p&gt;

&lt;p&gt;Three helpers exist for that transfer, because ownership is the one thing scope-based cleanup cannot infer: &lt;code&gt;no_free_ptr(p)&lt;/code&gt; nulls the variable and returns the value (with must-check semantics); &lt;code&gt;return_ptr(p)&lt;/code&gt; is the same as a return statement; and &lt;code&gt;retain_and_null_ptr(p)&lt;/code&gt; covers the case where a function you called has already taken ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  A conversion that builds on old trees
&lt;/h2&gt;

&lt;p&gt;Here is the same function converted. Note the helper definition — this is the part that matters for portability, and the reason is in the next section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Define once, under a name no kernel header uses. */&lt;/span&gt;
&lt;span class="n"&gt;DEFINE_FREE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mydev_of_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;of_node_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_T&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;mydev_count_channels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;mydev&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt; &lt;span class="n"&gt;__free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mydev_of_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="n"&gt;of_get_child_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;of_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ENODEV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&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;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mydev_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ret&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;of_get_child_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;EINVAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The labels are gone, every error path is a plain &lt;code&gt;return&lt;/code&gt;, and the release is stated where the resource is acquired. The early-return mistake stops being a mistake: a return added by someone unfamiliar with the function still drops the node reference and releases the lock.&lt;/p&gt;

&lt;p&gt;The cleanup order is also correct. &lt;code&gt;np&lt;/code&gt; is declared before the guard, so the compiler releases in reverse — mutex first, then the node reference — matching the original's tail exactly. That is safe here because dropping a node reference does not need the lock. When it does, the ordering becomes a trap, covered at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  guard() and scoped_guard() are not the same thing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;guard(name)(args)&lt;/code&gt; holds the lock to the end of the &lt;em&gt;enclosing&lt;/em&gt; scope.&lt;/strong&gt; Written inside an &lt;code&gt;if&lt;/code&gt; block, it releases at the end of that block, not at the end of the function — what you want if you were thinking about the block, a surprise if you were thinking about the function. The kernel documentation spells this out with an example and the explicit note that the lock is held for the remainder of the &lt;code&gt;if&lt;/code&gt; block. &lt;code&gt;scoped_guard(name, args) { … }&lt;/code&gt; binds the lock to the braces that follow it instead. Use &lt;code&gt;guard()&lt;/code&gt; when the lock covers the rest of the function, &lt;code&gt;scoped_guard()&lt;/code&gt; when the critical section is narrower.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which kernels have which core helpers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Mainline&lt;/th&gt;
&lt;th&gt;In the longterm series&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;__free&lt;/code&gt;, &lt;code&gt;guard&lt;/code&gt;, &lt;code&gt;scoped_guard&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;All — 5.10.y, 5.15.y, 6.1.y, 6.6.y, 6.12.y, 6.18.y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conditional guards&lt;/td&gt;
&lt;td&gt;6.8&lt;/td&gt;
&lt;td&gt;6.6.y from &lt;strong&gt;6.6.70&lt;/strong&gt;; 6.12.y; 6.18.y. Absent from 5.10.y, 5.15.y, 6.1.y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retain_and_null_ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6.16&lt;/td&gt;
&lt;td&gt;6.12.y in a late point release; 6.18.y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ACQUIRE&lt;/code&gt;, &lt;code&gt;ACQUIRE_ERR&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;6.17&lt;/td&gt;
&lt;td&gt;6.18.y only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mainline column is from the release tags. The longterm column is a snapshot taken while writing, and stable branches keep moving — which is exactly why the conditional-guard row has to name a point release rather than a series. The 5.10.y, 5.15.y and 6.1.y headers are byte-identical to each other and to the one that shipped in 6.5: those three got the core and nothing since.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subsystem helpers move separately
&lt;/h2&gt;

&lt;p&gt;The table above covers &lt;code&gt;include/linux/cleanup.h&lt;/code&gt;. It says nothing about whether the helper you actually need exists, and those live in the subsystem headers, on their own schedules.&lt;/p&gt;

&lt;p&gt;The node-reference helper is the case in point, because it is the one an of_node conversion reaches for. &lt;code&gt;DEFINE_FREE(device_node, …)&lt;/code&gt; arrived in mainline 6.9 — four releases after the core. It reached 6.6.y at 6.6.49 and 5.15.y only in the 5.15.21x range. It is not in 5.10.y at all. A vendor tree pinned anywhere below those points has the core macros and no node helper, so &lt;code&gt;__free(device_node)&lt;/code&gt; does not compile even though &lt;code&gt;guard()&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;The same unevenness shows up elsewhere. On 6.6.70 the mutex header offers conditional variants for trylock and interruptible acquires, but not the killable one; a file using &lt;code&gt;scoped_guard(mutex_kill, …)&lt;/code&gt; fails there despite the conditional-guard check passing. On a 6.6.30 tree the slab header defines exactly one free helper, &lt;code&gt;DEFINE_FREE(kfree, void *, if (_T) kfree(_T))&lt;/code&gt; — no &lt;code&gt;kvfree&lt;/code&gt;, no &lt;code&gt;kfree_sensitive&lt;/code&gt;, and written in the plain-NULL form rather than the error-pointer-tolerant one used in current mainline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the version number is not the answer
&lt;/h2&gt;

&lt;p&gt;Conditional guards are in 6.6.y — from 6.6.70, not 6.6.0. A BSP pinned at 6.6.30 does not have them, and does not have the node helper either. Vendors pin at whatever point release they forked from, stable series pick features up continuously, and the gap between those two facts is where the time goes. There is no version arithmetic that resolves it. You have to look at the headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'define (DEFINE_GUARD_COND|scoped_cond_guard|ACQUIRE_ERR)'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KDIR&lt;/span&gt;&lt;span class="s2"&gt;/include/linux/cleanup.h"&lt;/span&gt;
&lt;span class="go"&gt;define DEFINE_GUARD_COND
define scoped_cond_guard
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines means conditional guards but no &lt;code&gt;ACQUIRE&lt;/code&gt; family — a 6.12.y, or a 6.6.y at 6.6.70 or later. No output, with the header present, means the core only: 6.1.y or older, or a 6.6.x below 6.6.70. Current mainline prints five lines, because &lt;code&gt;DEFINE_GUARD_COND&lt;/code&gt; is defined three times there under an argument-count dispatch.&lt;/p&gt;

&lt;p&gt;For the subsystem helper, grep the header you actually need — and match both spellings, because lock guards are frequently declared through the &lt;code&gt;DEFINE_LOCK_GUARD&lt;/code&gt; generators rather than &lt;code&gt;DEFINE_GUARD&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;raghu@techveda.org:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'DEFINE_(FREE|GUARD|LOCK_GUARD_[01])'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KDIR&lt;/span&gt;&lt;span class="s2"&gt;/include/linux/of.h"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KDIR&lt;/span&gt;&lt;span class="s2"&gt;/include/linux/mutex.h"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searching only for &lt;code&gt;DEFINE_GUARD&lt;/code&gt; returns nothing for mutex.h on current mainline, which would wrongly suggest &lt;code&gt;guard(mutex)&lt;/code&gt; is unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing one file that builds on three kernels
&lt;/h2&gt;

&lt;p&gt;This is the case the helpers make awkward, and it splits in two.&lt;/p&gt;

&lt;p&gt;Macro features can be tested directly, because that is what they are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#ifdef scoped_cond_guard
&lt;/span&gt;        &lt;span class="n"&gt;scoped_cond_guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutex_try&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;EBUSY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#else
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mutex_trylock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;EBUSY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="cm"&gt;/* ... hand-written unwind ... */&lt;/span&gt;
        &lt;span class="n"&gt;mutex_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free helpers cannot. &lt;code&gt;__free(device_node)&lt;/code&gt; expands to &lt;code&gt;__cleanup(__free_device_node)&lt;/code&gt;, and &lt;code&gt;__free_device_node&lt;/code&gt; is a static function generated by the macro, not a macro itself — so there is nothing for &lt;code&gt;#ifdef&lt;/code&gt; to test. Defining your own &lt;code&gt;DEFINE_FREE(device_node, …)&lt;/code&gt; to fill the gap collides on trees that already have it.&lt;/p&gt;

&lt;p&gt;The way out is the one used in the conversion above: define your own helper under a name no kernel header will ever use, and use it unconditionally on every tree. It costs one line and removes the version question entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFINE_FREE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mydev_of_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;of_node_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_T&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The old-toolchain problem on 5.10 and 5.15
&lt;/h2&gt;

&lt;p&gt;One more obstacle applies to the two oldest longterm series, and it is the first thing that will stop a conversion there. Both 5.10.y and 5.15.y still build with &lt;code&gt;-std=gnu89&lt;/code&gt; and &lt;code&gt;-Wdeclaration-after-statement&lt;/code&gt;. Scope-based cleanup requires declaring a variable at the point of acquisition, part-way through a function — which is exactly the pattern that warning exists to reject. Every converted function produces warnings on those trees, and a BSP built with &lt;code&gt;-Werror&lt;/code&gt; will fail outright.&lt;/p&gt;

&lt;p&gt;From 6.1.y onward the kernel builds &lt;code&gt;-std=gnu11&lt;/code&gt; and the warning is gone, so the problem is confined to the two oldest series. If those are in your matrix, the honest position is that the helpers are available but the coding style they require is not, unless you can change the build flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which branch to actually convert
&lt;/h2&gt;

&lt;p&gt;Put those constraints together and the advice narrows usefully.&lt;/p&gt;

&lt;p&gt;Convert the branch you intend to upstream. Mainline driver code uses these helpers as a matter of course now, and a submission that hand-rolls unwind paths where a guard would do will attract review comments. That branch has every helper, the newest toolchain, and a reviewer population that expects the style.&lt;/p&gt;

&lt;p&gt;Leave the product branches alone unless you have a specific reason. Rewriting every error path in a driver that takes vendor patches on one branch and upstream fixes on another turns clean cherry-picks into three-way merges for the next two years, and on 5.15 you are fighting the compiler standard as well. The leak risk being avoided is real but small; the merge cost is certain.&lt;/p&gt;

&lt;p&gt;Where you do convert on an older tree, &lt;code&gt;guard()&lt;/code&gt; is the safe half. It is present everywhere, needs no subsystem support, and replaces the most error-prone part of the unwind — the lock. Pair it with a locally-defined free helper and the file stays portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordering trap
&lt;/h2&gt;

&lt;p&gt;Cleanup functions run in reverse order of definition — last defined, first released. That is usually right, since it mirrors acquisition order. It goes wrong when a function holds a lock and also owns an allocation whose release needs that lock held: if the allocation is declared before the lock is taken, it is released &lt;em&gt;after&lt;/em&gt; the lock drops, outside the protected region. The kernel documentation walks through exactly this bug and annotates the offending call as happening with no lock held.&lt;/p&gt;

&lt;p&gt;The fix is a habit. Define and initialise each resource in one statement, in the order the resources are acquired, rather than grouping declarations at the top of the function. The &lt;code&gt;__free(...) = NULL&lt;/code&gt;-at-the-top pattern is what creates the interdependency, and the documentation recommends against it for that reason. The related rule is not to mix &lt;code&gt;goto&lt;/code&gt; and cleanup helpers in one function — a goto can jump between scopes in ways the cleanup attribute does not account for, so convert every resource in a routine or leave the routine alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The core kernel cleanup helpers are in every supported longterm series, down to 5.10. &lt;code&gt;guard()&lt;/code&gt; in particular is safe to adopt anywhere.&lt;/li&gt;
&lt;li&gt;Everything else — conditional guards, and the per-subsystem free helpers — arrived at scattered point releases. A major version number does not tell you what you have.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__free(device_node)&lt;/code&gt; is unavailable on most pinned vendor trees. Define your own helper under a private name and the problem disappears.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#ifdef&lt;/code&gt; works for macro features such as &lt;code&gt;scoped_cond_guard&lt;/code&gt;, but not for &lt;code&gt;__free&lt;/code&gt; helpers, which expand to a generated static function.&lt;/li&gt;
&lt;li&gt;5.10.y and 5.15.y build &lt;code&gt;-std=gnu89&lt;/code&gt; with &lt;code&gt;-Wdeclaration-after-statement&lt;/code&gt;, which fights the declare-at-acquisition pattern the helpers require.&lt;/li&gt;
&lt;li&gt;Convert the branch you intend to upstream; leave product branches alone unless there is a reason beyond tidiness.&lt;/li&gt;
&lt;li&gt;Cleanup runs in reverse order of definition, so declare each resource in acquisition order, and never mix goto with cleanup helpers in one function.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/core-api/cleanup.html" rel="noopener noreferrer"&gt;Scope-based Cleanup Helpers — The Linux Kernel documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/master/include/linux/cleanup.h" rel="noopener noreferrer"&gt;include/linux/cleanup.h — mainline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/04f2933d375e" rel="noopener noreferrer"&gt;Merge tag core_guards_for_6.5_rc1 — the merge that introduced the infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/master/include/linux/of.h" rel="noopener noreferrer"&gt;include/linux/of.h — the device-tree node free helper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/master/include/linux/mutex.h" rel="noopener noreferrer"&gt;include/linux/mutex.h — the mutex guard and its conditional variants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gregkh/linux/blob/v6.6.69/include/linux/cleanup.h" rel="noopener noreferrer"&gt;include/linux/cleanup.h at v6.6.69 — before the conditional guards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gregkh/linux/blob/v6.6.70/include/linux/cleanup.h" rel="noopener noreferrer"&gt;include/linux/cleanup.h at v6.6.70 — after&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/" rel="noopener noreferrer"&gt;The Linux Kernel Archives — current stable and longterm series&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I teach Linux kernel, device driver and embedded Linux engineering at &lt;a href="https://www.techveda.live/linux-device-drivers/" rel="noopener noreferrer"&gt;TECH VEDA&lt;/a&gt;. If you work across several vendor kernels and want to go deeper on driver internals, that is what the Linux Device Drivers programme covers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>embedded</category>
      <category>c</category>
    </item>
  </channel>
</rss>
