<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>bootloader on SecMate Blog</title><link>https://blog.secmate.dev/tags/bootloader/</link><description>What we find. What we share.</description><generator>Hugo</generator><language>en-us</language><managingEditor>noreply@blog.secmate.dev//blog.secmate.dev/ (SecMate Team)</managingEditor><lastBuildDate>Tue, 25 Aug 2026 10:39:55 +0000</lastBuildDate><atom:link href="https://blog.secmate.dev/tags/bootloader/index.xml" rel="self" type="application/rss+xml"/><item><title>CVE-2026-20753: Integer Overflow in Slim Bootloader's ext2 Reader</title><link>https://blog.secmate.dev/posts/slim-bootloader-ext2-group-descriptor-overflow/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>Firmware</category><guid>https://blog.secmate.dev/posts/slim-bootloader-ext2-group-descriptor-overflow/</guid><description>Technical analysis of CVE-2026-20753 in Intel Slim Bootloader: an ext2 allocation overflow with pre-OS impact under specific exploit conditions.</description><content:encoded><![CDATA[<p>SecMate&rsquo;s automated analysis identified this issue using the <code>gpt-oss-120b</code> model, after which SecMate researchers validated the finding and reported it to Intel PSIRT. The vulnerability was published as <code>CVE-2026-20753</code> / <code>INTEL-SA-01425</code> on May 12, 2026. Intel rates the issue High with CVSS 4.0 score <code>8.7</code>, describing it as an integer overflow in Slim Bootloader UEFI firmware that can allow escalation of privilege and local code execution when attack requirements are present <a href="#ref1">[1]</a>.</p>
<p>Slim Bootloader is Intel&rsquo;s open-source boot firmware project for systems that want a small, configurable boot path instead of a full general-purpose firmware stack <a href="#ref2">[2]</a>. It initializes platform hardware, uses Intel FSP for silicon initialization, and then hands control to a payload or operating system loader. In practice, that means it may parse filesystems and load boot artifacts before the operating system, kernel hardening, endpoint security, or disk policy is alive.</p>
<p>That pre-OS placement is what makes filesystem parser bugs more interesting than their code footprint suggests.</p>
<p>At a code level, the bug is a classic allocation-size overflow. At a platform level, it matters because the attacker-controlled filesystem image is parsed before the operating system, before most telemetry, and potentially before later boot artifacts become immutable.</p>
<h2 id="from-filesystem-geometry-to-heap-geometry">From Filesystem Geometry to Heap Geometry</h2>
<p>The affected code lives in <a href="https://github.com/slimbootloader/slimbootloader/blob/master/BootloaderCommonPkg/Library/Ext23Lib/Ext2Fs.c" rel="noopener noreferrer" target="_blank"><code>BootloaderCommonPkg/Library/Ext23Lib/Ext2Fs.c</code></a>, Slim Bootloader&rsquo;s ext2/3/4 reader. The interesting part is that ext2&rsquo;s filesystem geometry becomes allocator geometry inside the bootloader.</p>
<p>In the vulnerable path, <code>ReadSBlock()</code> reads the on-disk superblock and computes the number of block groups:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">FileSystem</span><span class="o">-&gt;</span><span class="n">Ext2FsNumCylinder</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nf">HOWMANY</span> <span class="p">(</span><span class="n">Ext2FsBlockCount</span> <span class="o">-</span> <span class="n">Ext2FsFirstDataBlock</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">           <span class="n">Ext2FsBlocksPerGroup</span><span class="p">);</span>
</span></span></code></pre></div><p>Those values come from the filesystem image. A crafted image can report a very large block count and a very small <code>BlocksPerGroup</code>, producing a large <code>Ext2FsNumCylinder</code>.</p>
<p><code>Ext2fsOpen()</code> then used that count directly to size the group descriptor table:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">FileSystem</span><span class="o">-&gt;</span><span class="n">Ext2FsGrpDes</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nf">AllocatePool</span> <span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">EXT2GD</span><span class="p">)</span> <span class="o">*</span> <span class="n">FileSystem</span><span class="o">-&gt;</span><span class="n">Ext2FsNumCylinder</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">Status</span> <span class="o">=</span> <span class="nf">ReadGDBlock</span> <span class="p">(</span><span class="n">File</span><span class="p">,</span> <span class="n">FileSystem</span><span class="p">);</span>
</span></span></code></pre></div><p>On 32-bit builds, the multiplication can wrap. The allocation then succeeds with a buffer smaller than the number of descriptors the parser believes it has.</p>
<p>The follow-on write is controlled by the same filesystem image. <code>ReadGDBlock()</code> walks the descriptor blocks and copies group descriptors into <code>FileSystem-&gt;Ext2FsGrpDes</code> using the original block-group count. In other words, the attacker first shapes the allocation through the superblock, then shapes the overflow stream through the group descriptor table.</p>
<p>This is the core of the bug: a disk-controlled filesystem geometry value crosses from &ldquo;metadata&rdquo; into &ldquo;heap layout&rdquo; before the OS exists.</p>
<div id="sb1-fig" style="width:100%;max-width:760px;margin:1.75rem auto;">
<svg viewBox="0 0 920 500" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="sb1-title sb1-desc" style="width:100%;height:auto;">
  <title id="sb1-title">ext2 metadata consumed by Slim Bootloader functions</title>
  <desc id="sb1-desc">An ext2 block-group layout showing the crafted superblock values and group descriptor table bytes consumed by ReadSBlock, Ext2fsOpen, and ReadGDBlock. The superblock drives a huge group count, Ext2fsOpen under-allocates because the 32-bit size multiplication wraps, and ReadGDBlock copies the descriptor table into that undersized buffer.</desc>
  <defs>
    <marker id="sb1-arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 Z" fill="var(--color-text-secondary)"/></marker>
    <style>
      .sb1 text{font-family:var(--font-mono,ui-monospace,Menlo,monospace);}
      .sb1 .ttl{font-family:var(--font-sans,system-ui,sans-serif);}
      .sb1-bg{fill:var(--color-muted-bg);stroke:var(--color-border);stroke-width:1.4;}
      .sb1-band{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.4;}
      .sb1-group{fill:color-mix(in srgb,var(--color-text-secondary) 9%,var(--color-bg-card));stroke:var(--color-text-secondary);stroke-width:1.5;}
      .sb1-plain{fill:color-mix(in srgb,var(--color-text-secondary) 5%,var(--color-bg-card));stroke:var(--color-border);stroke-width:1.4;}
      .sb1-meta{fill:color-mix(in srgb,#dc2626 13%,var(--color-bg-card));stroke:#dc2626;stroke-width:1.7;}
      .sb1-normal{fill:color-mix(in srgb,var(--color-secondary) 13%,var(--color-bg-card));stroke:var(--color-secondary);stroke-width:1.4;}
      .sb1-func{fill:color-mix(in srgb,var(--color-accent) 14%,var(--color-bg-card));stroke:var(--color-accent);stroke-width:1.7;}
      .sb1-warn{fill:color-mix(in srgb,#f59e0b 16%,var(--color-bg-card));stroke:#f59e0b;stroke-width:1.7;}
      .sb1-txt{fill:var(--color-text);}
      .sb1-muted{fill:var(--color-text-secondary);}
      .sb1-red{fill:#dc2626;}
      .sb1-amber{fill:#b45309;}
      .sb1-line{stroke:var(--color-text-secondary);stroke-width:2.2;fill:none;marker-end:url(#sb1-arrow);}
      .sb1-step{transition:opacity .55s ease;}
    </style>
  </defs>
  <g class="sb1">
  <rect class="sb1-bg" width="920" height="500" rx="16"/>
  <text class="ttl sb1-txt" x="44" y="46" font-size="20">ext2 metadata and the Slim Bootloader reader path</text>
  <g class="sb1-step" data-step="0">
    <rect class="sb1-band" x="44" y="100" width="832" height="190" rx="10"/>
    <rect class="sb1-plain" x="60" y="138" width="76" height="118" rx="7"/>
    <text class="sb1-txt" x="76" y="188" font-size="13">Boot</text>
    <text class="sb1-muted" x="76" y="208" font-size="12">block</text>
    <rect class="sb1-group" x="150" y="138" width="610" height="118" rx="8"/>
    <text class="sb1-txt" x="170" y="160" font-size="14">Block Group 0</text>
    <rect class="sb1-meta" x="170" y="176" width="188" height="62" rx="6"/>
    <text class="sb1-txt" x="186" y="196" font-size="12">Super Block</text>
    <text class="sb1-red" x="186" y="214" font-size="10">blocks_count = huge</text>
    <text class="sb1-red" x="186" y="230" font-size="10">blocks/group = tiny</text>
    <rect class="sb1-meta" x="368" y="176" width="132" height="62" rx="6"/>
    <text class="sb1-txt" x="384" y="196" font-size="12">Group Desc</text>
    <text class="sb1-txt" x="384" y="212" font-size="12">Table</text>
    <text class="sb1-red" x="384" y="230" font-size="10">GD0 ... GDn</text>
    <rect class="sb1-normal" x="510" y="176" width="54" height="62" rx="6"/>
    <text class="sb1-txt" x="522" y="199" font-size="10">Block</text>
    <text class="sb1-txt" x="522" y="215" font-size="10">bitmap</text>
    <rect class="sb1-normal" x="574" y="176" width="54" height="62" rx="6"/>
    <text class="sb1-txt" x="586" y="199" font-size="10">Inode</text>
    <text class="sb1-txt" x="586" y="215" font-size="10">bitmap</text>
    <rect class="sb1-normal" x="638" y="176" width="62" height="62" rx="6"/>
    <text class="sb1-txt" x="652" y="199" font-size="10">Inode</text>
    <text class="sb1-txt" x="652" y="215" font-size="10">table</text>
    <rect class="sb1-plain" x="708" y="176" width="38" height="62" rx="6"/>
    <text class="sb1-muted" x="727" y="212" font-size="13" text-anchor="middle">...</text>
    <rect class="sb1-group" x="774" y="138" width="36" height="118" rx="7"/>
    <text class="sb1-txt" x="792" y="202" font-size="11" text-anchor="middle">BG 1</text>
    <rect class="sb1-group" x="824" y="138" width="36" height="118" rx="7"/>
    <text class="sb1-txt" x="842" y="202" font-size="11" text-anchor="middle">BG n</text>
  </g>
  <g class="sb1-step" data-step="1">
    <path class="sb1-line" d="M264 240 C246 288, 208 318, 184 350"/>
    <rect class="sb1-func" x="80" y="356" width="210" height="64" rx="8"/>
    <text class="sb1-txt" x="185" y="382" font-size="14" text-anchor="middle">ReadSBlock()</text>
    <text class="sb1-muted" x="185" y="402" font-size="12" text-anchor="middle">computes group count</text>
  </g>
  <g class="sb1-step" data-step="2">
    <path class="sb1-line" d="M292 388 H346"/>
    <rect class="sb1-warn" x="356" y="356" width="210" height="64" rx="8"/>
    <text class="sb1-txt" x="461" y="382" font-size="14" text-anchor="middle">Ext2fsOpen()</text>
    <text class="sb1-amber" x="461" y="402" font-size="11" text-anchor="middle">allocates descriptor table</text>
    <text class="sb1-amber" x="461" y="446" font-size="11" text-anchor="middle">alloc size 32 &#215; N wraps UINTN on IA32 &#8594; undersized</text>
  </g>
  <g class="sb1-step" data-step="3">
    <path class="sb1-line" d="M568 388 H622"/>
    <path class="sb1-line" d="M434 240 C520 292, 660 318, 736 350"/>
    <rect class="sb1-func" x="632" y="356" width="210" height="64" rx="8"/>
    <text class="sb1-txt" x="737" y="382" font-size="14" text-anchor="middle">ReadGDBlock()</text>
    <text class="sb1-muted" x="737" y="402" font-size="12" text-anchor="middle">copies descriptor blocks</text>
  </g>
  </g>
</svg>
</div>
<script>
(function(){
  var f=document.getElementById('sb1-fig'); if(!f) return;
  var steps=f.querySelectorAll('.sb1-step');
  var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  steps.forEach(function(s){ if(s.getAttribute('data-step')!=='0') s.style.opacity=reduce?'1':'0'; });
  if(reduce) return;
  var done=false;
  var io=new IntersectionObserver(function(es){
    es.forEach(function(e){
      if(e.isIntersecting && !done){ done=true;
        steps.forEach(function(s){
          var n=parseInt(s.getAttribute('data-step'),10)||0;
          setTimeout(function(){ s.style.opacity='1'; }, n*700);
        });
      }
    });
  },{threshold:0.25});
  io.observe(f);
})();
</script>
<p><em>Figure 1. The ext2 image follows the usual block-group layout. The Super Block carries the crafted count fields, while the Group Descriptor Table provides the descriptor bytes consumed later by <code>ReadGDBlock()</code>.</em></p>
<h2 id="exploitability">Exploitability</h2>
<p>This is not a network bug. The attacker needs a path to make Slim Bootloader parse a crafted ext2/3 image during boot. Realistic paths include:</p>
<ul>
<li>a system configured to boot from removable media such as USB, SD, or eMMC-backed service media</li>
<li>a local privileged attacker who can modify the boot partition or replace a boot image</li>
<li>a supply-chain or provisioning path where boot media is assembled from untrusted input</li>
</ul>
<p>Intel&rsquo;s CVSS vector reflects that shape: local attack vector, low attack complexity, high privileges required, no user interaction, and attack requirements present <a href="#ref1">[1]</a>. That is a narrow entry point compared with remote exploitation, but the privilege gained is unusually valuable because the parser runs in firmware before the operating system can enforce its own trust boundaries.</p>
<p>Slim Bootloader also makes the 32-bit case a first-class concern. <code>BuildLoader.py</code> defaults to <code>ia32</code> when no architecture is specified <a href="#ref3">[3]</a>. Some platforms build X64 payloads, but the default build path and many CI/platform combinations are IA32, which is exactly where a <code>UINTN</code> allocation-size wrap is most direct.</p>
<p>The security boundary is not ordinary user-to-root escalation. The value is crossing from OS-level or provisioning-time control into pre-OS execution, where the attacker may affect boot artifacts before runtime protections, telemetry, or OS policy are active.</p>
<p>The practical exploit path is target-specific, but the high-level strategy is:</p>
<ul>
<li>craft ext2 metadata that forces a too-small descriptor allocation</li>
<li>place controlled group descriptor bytes so the copy corrupts adjacent bootloader heap state</li>
<li>seek control-flow corruption or alter later bootloader decisions before the OS payload is launched</li>
<li>use that pre-OS control to influence the kernel, initrd, command line, boot parameters, or a verified container after it has been accepted, depending on the target boot flow</li>
</ul>
<h2 id="heap-shape-and-mitigations">Heap Shape and Mitigations</h2>
<p>The OS-loader filesystem path binds <code>Ext23Lib</code> to <code>FullMemoryAllocationLib</code>, an EDK2-style pool allocator, not to the simpler bootloader-core bump allocator <a href="#ref4">[4]</a>. Small allocations are served from page-sized carved blocks, and leftover chunks are grouped into fixed size-class bins. Each bin has one doubly linked free list: <code>Pool.FreeList[bin]</code> is the list head, and the list may contain zero or many free chunks. Allocated chunks are not on a matching allocated list. They carry a <code>POOL_HEAD</code> and <code>POOL_TAIL</code>, and a chunk only becomes a <code>POOL_FREE</code> list node after it is freed <a href="#ref5">[5]</a>.</p>
<p>The important point is that the overflow does not need to hit another allocated object first. If the next physical chunk is free, the attacker can corrupt allocator metadata that later list operations trust.</p>
<div id="sb2-fig" style="width:100%;max-width:760px;margin:1.75rem auto;">
<svg viewBox="0 0 900 500" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="sb2-title sb2-desc" style="width:100%;height:auto;">
  <title id="sb2-title">FullMemoryAllocationLib carved block and free-list layout</title>
  <desc id="sb2-desc">A carved pool block contains an allocated Ext2FsGrpDes buffer followed by free chunks. The overflow corrupts the Link fields of an adjacent POOL_FREE chunk, which a later RemoveEntryList turns into an arbitrary write. A separate row shows the logical free list for one size bin; arrows are list links, not physical memory order.</desc>
  <defs>
    <marker id="sb2-arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 Z" fill="var(--color-text-secondary)"/></marker>
    <marker id="sb2-arrowr" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 Z" fill="#dc2626"/></marker>
    <style>
      .sb2 text{font-family:var(--font-mono,ui-monospace,Menlo,monospace);}
      .sb2 .ttl{font-family:var(--font-sans,system-ui,sans-serif);}
      .sb2-bg{fill:var(--color-muted-bg);stroke:var(--color-border);stroke-width:1.4;}
      .sb2-panel{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.5;}
      .sb2-mem{fill:color-mix(in srgb,var(--color-text-secondary) 7%,var(--color-bg-card));stroke:var(--color-border);stroke-width:1.4;}
      .sb2-head{fill:color-mix(in srgb,var(--color-text-secondary) 12%,var(--color-bg-card));stroke:var(--color-text-secondary);stroke-width:1.5;}
      .sb2-alloc{fill:color-mix(in srgb,var(--color-accent) 15%,var(--color-bg-card));stroke:var(--color-accent);stroke-width:1.6;}
      .sb2-allocmeta{fill:color-mix(in srgb,var(--color-accent) 8%,var(--color-bg-card));stroke:var(--color-accent);stroke-width:1.4;stroke-dasharray:4 3;}
      .sb2-free{fill:color-mix(in srgb,var(--color-secondary) 12%,var(--color-bg-card));stroke:var(--color-secondary);stroke-width:1.6;}
      .sb2-field{fill:color-mix(in srgb,var(--color-secondary) 20%,var(--color-bg-card));stroke:var(--color-secondary);stroke-width:1;}
      .sb2-fieldred{fill:#dc2626;fill-opacity:.2;stroke:#dc2626;stroke-width:1.6;}
      .sb2-ring{fill:none;stroke:#dc2626;stroke-width:2.4;}
      .sb2-txt{fill:var(--color-text);}
      .sb2-muted{fill:var(--color-text-secondary);}
      .sb2-acc{fill:var(--color-secondary);}
      .sb2-red{fill:#dc2626;}
      .sb2-link{stroke:var(--color-text-secondary);stroke-width:2.2;fill:none;marker-end:url(#sb2-arrow);}
      .sb2-backlink{stroke:var(--color-text-secondary);stroke-width:2;fill:none;marker-end:url(#sb2-arrow);opacity:.6;}
      .sb2-overflow{stroke:#dc2626;stroke-width:2.6;fill:none;marker-end:url(#sb2-arrowr);stroke-linecap:round;}
      .sb2-map{stroke:var(--color-text-secondary);stroke-width:1.6;stroke-dasharray:5 5;fill:none;opacity:.55;}
      .sb2-step{transition:opacity .55s ease;}
    </style>
  </defs>
  <g class="sb2">
  <rect class="sb2-bg" width="900" height="500" rx="16"/>
  <g class="sb2-step" data-step="0">
    <rect class="sb2-panel" x="44" y="36" width="812" height="206" rx="10"/>
    <text class="ttl sb2-txt" x="70" y="72" font-size="18">physical carved pool block / page</text>
    <text class="sb2-muted" x="70" y="97" font-size="12">contiguous memory returned by the page allocator</text>
    <rect class="sb2-mem" x="70" y="120" width="760" height="86" rx="8"/>
    <rect class="sb2-allocmeta" x="88" y="138" width="60" height="50"/>
    <text class="sb2-txt" x="118" y="168" font-size="11" text-anchor="middle">HEAD</text>
    <rect class="sb2-alloc" x="148" y="138" width="218" height="50"/>
    <text class="sb2-txt" x="257" y="158" font-size="14" text-anchor="middle">Ext2FsGrpDes</text>
    <text class="sb2-muted" x="257" y="178" font-size="10" text-anchor="middle">attacker-controlled copy</text>
    <rect class="sb2-allocmeta" x="366" y="138" width="56" height="50"/>
    <text class="sb2-txt" x="394" y="168" font-size="11" text-anchor="middle">TAIL</text>
    <rect class="sb2-free" x="422" y="138" width="176" height="50"/>
    <rect class="sb2-field" x="422" y="138" width="44" height="50"/>
    <text class="sb2-acc" x="444" y="158" font-size="8" text-anchor="middle">SIG</text>
    <text class="sb2-acc" x="444" y="174" font-size="8" text-anchor="middle">IDX</text>
    <rect class="sb2-field" x="466" y="138" width="66" height="50"/>
    <text class="sb2-acc" x="499" y="168" font-size="10" text-anchor="middle">Forward</text>
    <rect class="sb2-field" x="532" y="138" width="66" height="50"/>
    <text class="sb2-acc" x="565" y="168" font-size="10" text-anchor="middle">Back</text>
    <text class="sb2-txt" x="510" y="220" font-size="11" text-anchor="middle">POOL_FREE A</text>
    <rect class="sb2-free" x="598" y="138" width="176" height="50"/>
    <rect class="sb2-field" x="598" y="138" width="44" height="50"/>
    <text class="sb2-acc" x="620" y="158" font-size="8" text-anchor="middle">SIG</text>
    <text class="sb2-acc" x="620" y="174" font-size="8" text-anchor="middle">IDX</text>
    <rect class="sb2-field" x="642" y="138" width="66" height="50"/>
    <text class="sb2-acc" x="675" y="168" font-size="10" text-anchor="middle">Forward</text>
    <rect class="sb2-field" x="708" y="138" width="66" height="50"/>
    <text class="sb2-acc" x="741" y="168" font-size="10" text-anchor="middle">Back</text>
    <text class="sb2-txt" x="686" y="220" font-size="11" text-anchor="middle">POOL_FREE B</text>
    <text class="sb2-muted" x="802" y="168" font-size="16" text-anchor="middle">...</text>
  </g>
  <g class="sb2-step" data-step="1">
    <text class="sb2-red" x="556" y="98" font-size="12" text-anchor="middle">overflow corrupts Link</text>
    <path class="sb2-overflow" d="M360 136 Q 440 94 499 136"/>
  </g>
  <g class="sb2-step" data-step="2">
    <rect class="sb2-fieldred" x="466" y="138" width="66" height="50"/>
    <text class="sb2-acc" x="499" y="168" font-size="10" text-anchor="middle">Forward</text>
    <rect class="sb2-fieldred" x="532" y="138" width="66" height="50"/>
    <text class="sb2-acc" x="565" y="168" font-size="10" text-anchor="middle">Back</text>
  </g>
  <g class="sb2-step" data-step="3">
    <rect class="sb2-panel" x="44" y="278" width="812" height="178" rx="10"/>
    <text class="ttl sb2-txt" x="70" y="314" font-size="18">logical free list for one size bin</text>
    <text class="sb2-muted" x="70" y="338" font-size="12">list order is allocator metadata, not memory order</text>
    <rect class="sb2-head" x="88" y="374" width="150" height="48" rx="7"/>
    <text class="sb2-txt" x="163" y="403" font-size="13" text-anchor="middle">Pool.FreeList[k]</text>
    <rect class="sb2-free" x="330" y="370" width="126" height="56" rx="5"/>
    <text class="sb2-txt" x="393" y="394" font-size="13" text-anchor="middle">free B</text>
    <text class="sb2-acc" x="393" y="414" font-size="10" text-anchor="middle">Forward / Back</text>
    <rect class="sb2-free" x="548" y="370" width="126" height="56" rx="5"/>
    <text class="sb2-txt" x="611" y="394" font-size="13" text-anchor="middle">free A</text>
    <text class="sb2-acc" x="611" y="414" font-size="10" text-anchor="middle">Forward / Back</text>
    <text class="sb2-muted" x="746" y="403" font-size="16" text-anchor="middle">...</text>
  </g>
  <g class="sb2-step" data-step="4">
    <path class="sb2-link" d="M238 392 C270 376, 298 376, 330 392"/>
    <path class="sb2-link" d="M456 392 C490 376, 514 376, 548 392"/>
    <path class="sb2-link" d="M674 392 C704 382, 724 382, 742 392"/>
    <path class="sb2-backlink" d="M330 410 C298 426, 270 426, 238 410"/>
    <path class="sb2-backlink" d="M548 410 C514 426, 490 426, 456 410"/>
  </g>
  <g class="sb2-step" data-step="5">
    <path class="sb2-map" d="M686 226 C650 278, 520 323, 393 370"/>
    <path class="sb2-map" d="M510 226 C538 282, 586 320, 611 370"/>
  </g>
  <g class="sb2-step" data-step="6">
    <rect class="sb2-ring" x="544" y="366" width="134" height="64" rx="7"/>
  </g>
  </g>
</svg>
</div>
<script>
(function(){
  var f=document.getElementById('sb2-fig'); if(!f) return;
  var steps=f.querySelectorAll('.sb2-step');
  var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  steps.forEach(function(s){ if(s.getAttribute('data-step')!=='0') s.style.opacity=reduce?'1':'0'; });
  if(reduce) return;
  var done=false;
  var io=new IntersectionObserver(function(es){
    es.forEach(function(e){
      if(e.isIntersecting && !done){ done=true;
        steps.forEach(function(s){
          var n=parseInt(s.getAttribute('data-step'),10)||0;
          setTimeout(function(){ s.style.opacity='1'; }, n*480);
        });
      }
    });
  },{threshold:0.2});
  io.observe(f);
})();
</script>
<p><em>Figure 2. One carved pool block containing an allocated descriptor table followed by free chunks. The lower row shows the logical free list for one size bin; arrows show free-list links, not physical memory order.</em></p>
<p>The checks are late. When a chunk is freed, <code>CoreFreePoolI()</code> validates the <code>POOL_HEAD</code> signature, the <code>POOL_TAIL</code> signature, and that the size in the head matches the size in the tail. That can catch some corruption during cleanup, but it does not stop <code>ReadGDBlock()</code> from writing past <code>Ext2FsGrpDes</code> in the first place.</p>
<p>If the chunk sitting immediately after the allocated descriptor table is free, its memory currently contains a <code>POOL_FREE</code> structure whose <code>Link</code> field is a raw doubly linked <code>LIST_ENTRY</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">UINT32</span>          <span class="n">Signature</span><span class="p">;</span>   <span class="c1">// &#39;pfr0&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="n">UINT32</span>          <span class="n">Index</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">LIST_ENTRY</span>      <span class="n">Link</span><span class="p">;</span>        <span class="c1">// ForwardLink, BackLink
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="n">POOL_FREE</span><span class="p">;</span>
</span></span></code></pre></div><p>Because the overflow content is fully attacker-controlled ext2 group-descriptor data, the attacker can overwrite both <code>ForwardLink</code> and <code>BackLink</code> with arbitrary values. There is no safe-linking or runtime free-list integrity check on the allocate path: when the allocator later services another request from the corrupted chunk&rsquo;s bin, <code>CoreAllocatePoolI()</code> pops the first entry with <code>RemoveEntryList()</code>.</p>
<p><code>RemoveEntryList</code> performs the classic unsafe unlink:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">Entry</span><span class="o">-&gt;</span><span class="n">ForwardLink</span><span class="o">-&gt;</span><span class="n">BackLink</span> <span class="o">=</span> <span class="n">Entry</span><span class="o">-&gt;</span><span class="n">BackLink</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">Entry</span><span class="o">-&gt;</span><span class="n">BackLink</span><span class="o">-&gt;</span><span class="n">ForwardLink</span> <span class="o">=</span> <span class="n">Entry</span><span class="o">-&gt;</span><span class="n">ForwardLink</span><span class="p">;</span>
</span></span></code></pre></div><p>If the attacker corrupted <code>Entry-&gt;ForwardLink</code> to <code>TargetAddress</code> and <code>Entry-&gt;BackLink</code> to <code>Value</code>, the first assignment writes <code>Value</code> to <code>*(TargetAddress + sizeof(UINTN))</code>. The second performs the reciprocal list repair, writing <code>TargetAddress</code> through <code>Value</code>. On IA32 builds, all pointers are 32-bit, so the ext2 descriptor fields map cleanly onto the link pointers. This turns the overflow into a <strong>write-what-where-style allocator unlink primitive</strong> in a single allocation, with the usual linked-list side effect.</p>
<p>A similar unlink surface exists in the page-coalescing path. If a later <code>FreePool()</code> makes every chunk in the carved page free, <code>CoreFreePoolI()</code> walks the page and removes each free chunk from its bin before returning the page:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="n">Offset</span> <span class="o">&lt;</span> <span class="n">Granularity</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">Free</span> <span class="o">=</span> <span class="p">(</span><span class="n">POOL_FREE</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">NewPage</span><span class="p">[</span><span class="n">Offset</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nf">RemoveEntryList</span> <span class="p">(</span><span class="o">&amp;</span><span class="n">Free</span><span class="o">-&gt;</span><span class="n">Link</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">Offset</span> <span class="o">+=</span> <span class="nf">LIST_TO_SIZE</span> <span class="p">(</span><span class="n">Free</span><span class="o">-&gt;</span><span class="n">Index</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>If the overflow preserves the free chunk&rsquo;s <code>Signature</code> and <code>Index</code> while corrupting <code>Link</code>, that removal can reach the same unsafe unlink primitive.</p>
<p>In practice, an exploit would tune the wrapped allocation size so that <code>Ext2FsGrpDes</code> is followed by a free chunk from a predictable bin, then use the group-descriptor overflow to corrupt that chunk’s <code>Link</code> fields. A later allocation from that bin, or a free/coalescing path that removes the corrupted chunk from its free list, can trigger unsafe unlink and provide a strong target-specific exploitation building block. Depending on the platform memory layout and available control-flow or data targets, it may be usable to corrupt function pointers, saved return state, boot variables, or later bootloader decision data.</p>
<p>It is unclear from source alone whether heap pool memory is non-executable on every final platform build. What we did not find is evidence of NX/W^X enforcement in this path: <code>PagingLib</code> builds mappings with present and writable attributes, and the long-mode transition in <code>EnablePaging64.nasm</code> sets <code>EFER.LME</code> without setting <code>EFER.NXE</code> <a href="#ref6">[6]</a>. If pool memory is executable in the target build, turning the write primitive into code execution becomes much easier: attacker-controlled bytes could already live in a heap buffer, and the write primitive could redirect a later control-flow target to that buffer. If pool memory is not executable, the arbitrary write is still a strong primitive, but exploitation would likely need a target-specific control-flow or data-only path. We did not validate executable heap behavior on every affected production platform, so this should be read as a source-level hardening observation rather than a universal platform claim.</p>
<p>There are not many allocator-side mitigations to lean on. The local build configuration disables stack protectors in toolchain flags, uses explicit IA32 non-PIE flags, and shows no evidence of heap randomization, guard pages, or safe-linking-style free-list protection <a href="#ref7">[7]</a>.</p>
<p>The overflow payload is not a completely free-form byte stream. It is descriptor-shaped: block bitmap, inode bitmap, inode-table block addresses, counters, and reserved fields. That constraint matters, but these fields are still attacker-controlled enough to damage allocator metadata or redirect later filesystem reads if the heap layout lines up.</p>
<h2 id="pre-os-impact">Pre-OS Impact</h2>
<p>That is why this bug is in a different risk class from a user-space parser overflow. The immediate bug is a pre-OS heap overflow. The likely security consequence is target-specific allocator metadata corruption, arbitrary-write-style unlink behavior, or control-flow corruption. The higher-level platform consequence, when boot policy allows attacker-controlled media or writable boot partitions, is possible compromise of the boot chain before OS defenses are active.</p>
<p>A successful exploit can run before kernel protections, EDR, disk policy, and OS logging. It can also undermine the assurance normally associated with Secure Boot or verified boot for later OS artifacts: the firmware may correctly verify a signed component, but compromised bootloader control can tamper with memory after the trust decision or steer the boot flow around it. Whether this undermines a specific verified-boot chain depends on where verification occurs, what is measured, and whether later stages re-validate the loaded artifact.</p>
<p>This should not be confused with a generic Intel Boot Guard bypass. Boot Guard verifies earlier firmware stages. This issue is about gaining control inside an already-running Slim Bootloader flow by feeding it a crafted filesystem image.</p>
<p>Persistence is also plausible in the right deployment. If the attacker can keep the malicious boot partition, removable medium, recovery image, or service media in the boot path, the exploit is naturally re-triggered on every boot. If the compromised bootloader context exposes a firmware-update or storage-write path on the target, the same primitive could be used to install a more durable bootkit. Those details depend on platform policy and write protections, so persistence should be described as a realistic objective, not an automatic outcome.</p>
<h2 id="patch-review">Patch Review</h2>
<p>Intel recommends updating Slim Bootloader to commit <code>193129e6a1ea675527670785c8c75ed09b423211</code> from January 22, 2026 or later <a href="#ref1">[1]</a>. That commit is narrowly targeted and adds a pre-allocation overflow guard:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="p">((</span><span class="n">UINT32</span><span class="p">)(</span><span class="n">FileSystem</span><span class="o">-&gt;</span><span class="n">Ext2FsNumCylinder</span><span class="p">)</span> <span class="o">&gt;=</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="p">(</span><span class="n">MAX_UINTN</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">EXT2GD</span><span class="p">)))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">Status</span> <span class="o">=</span> <span class="n">EFI_DEVICE_ERROR</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">goto</span> <span class="n">out</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The intent is direct: reject an ext2 image whose computed descriptor count would make <code>sizeof(EXT2GD) * Ext2FsNumCylinder</code> overflow <code>UINTN</code> before calling <code>AllocatePool()</code> <a href="#ref8">[8]</a>.</p>
<p>The current <code>slimbootloader/</code> checkout contains additional hardening around the same area, notably in follow-up commits <code>3e853aa2</code> and <code>6e29e8b5</code>. The most relevant pieces are:</p>
<ul>
<li><code>Ext2SbValidate()</code> now rejects zero <code>BlocksPerGroup</code> and <code>INodesPerGroup</code>, oversized log block sizes, and invalid 64-bit group descriptor sizes.</li>
<li><code>ReadSBlock()</code> caps <code>Ext2FsNumCylinder</code> with <code>EXT2_MAX_BLOCK_GROUPS</code> (<code>0x100000</code>), limiting both overflow risk and excessive descriptor-table allocation.</li>
<li><code>Ext2fsOpen()</code> checks allocation failures for both the block buffer and descriptor table before use.</li>
<li><code>ReadGDBlock()</code> caps copies when the on-disk group descriptor size is larger than Slim Bootloader&rsquo;s in-memory <code>EXT2GD</code> structure.</li>
</ul>
<p>Commit <code>193129e6</code> is the minimum fix for the reported CVE. Deployments that can do so should prefer a later revision including <code>3e853aa2</code> and <code>6e29e8b5</code>, because those commits harden adjacent parser assumptions. The later hardening is the more complete parser posture: reject impossible filesystem geometry early, then keep allocation and copy sizes tied to bounded invariants.</p>
<h2 id="affected-systems-and-remediation">Affected Systems and Remediation</h2>
<p>Intel lists affected Slim Bootloader deployments across multiple Intel Core, Atom, Core Ultra, Xeon E, and Xeon D product families, depending on CPU ID and platform segment <a href="#ref1">[1]</a>. Operators should check the Intel advisory against their platform rather than infer exposure from processor branding alone.</p>
<p>Recommended actions:</p>
<ul>
<li>update Slim Bootloader to a fixed revision</li>
<li>treat boot media and service images as privileged inputs</li>
</ul>
<h2 id="the-lesson-for-boot-firmware">The Lesson for Boot Firmware</h2>
<p>This bug is a reminder that bootloaders do not just load files. They parse attacker-shaped data structures while sitting on the shortest path to platform control.</p>
<p>For firmware teams, the defensive rule is simple: any disk field that contributes to an allocation, loop bound, block address, or copy length needs a combined invariant check before use. In pre-OS code, &ldquo;valid enough to parse&rdquo; is not a security boundary.</p>
<p>SecMate also identified and shared ten additional security findings in the same broader research effort. Those issues are currently under coordinated disclosure.</p>
<h2 id="disclosure-timeline">Disclosure Timeline</h2>
<ul>
<li><strong>November 12, 2025</strong>: SecMate reported the ext2 group-descriptor allocation overflow to Intel PSIRT, including technical details and PoC material in an encrypted archive.</li>
<li><strong>November 13, 2025</strong>: Intel PSIRT acknowledged receipt and assigned tracking ID <code>3d1027ebdf49b610d7c5b294c23889a2</code>.</li>
<li><strong>November 20, 2025</strong>: Intel confirmed positive triage, stated that it intended to remediate the issue, assign a CVE, and coordinate disclosure.</li>
<li><strong>December 9, 2025</strong>: Intel shared the expected schedule: fix release to the repository in early February 2026, followed by an Intel Security Advisory with CVE in early May 2026.</li>
<li><strong>December 10, 2025</strong>: SecMate asked whether disclosure could happen once the fix was ready or should wait until May 2026.</li>
<li><strong>December 10, 2025</strong>: Intel requested delaying public disclosure until May 2026 so downstream consumers would have time to integrate the fixes.</li>
<li><strong>December 12, 2025</strong>: SecMate agreed to accommodate the May 2026 coordinated disclosure timeline.</li>
</ul>
<h2 id="references">References</h2>
<p><a id="ref1"></a>[1] <a href="https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01425.html" rel="noopener noreferrer" target="_blank">Intel Security Advisory INTEL-SA-01425</a></p>
<p><a id="ref2"></a>[2] <a href="https://github.com/slimbootloader/slimbootloader" rel="noopener noreferrer" target="_blank">Slim Bootloader project README</a></p>
<p><a id="ref3"></a>[3] <a href="https://github.com/slimbootloader/slimbootloader/blob/cdcfd3e3161e45fce6e7e6d088f068584de8219c/BuildLoader.py" rel="noopener noreferrer" target="_blank">Slim Bootloader <code>BuildLoader.py</code>, default IA32 build architecture</a></p>
<p><a id="ref4"></a>[4] <a href="https://github.com/slimbootloader/slimbootloader/blob/cdcfd3e3161e45fce6e7e6d088f068584de8219c/BootloaderCorePkg/BootloaderCorePkg.dsc" rel="noopener noreferrer" target="_blank">Slim Bootloader build configuration mapping <code>OsLoader.inf</code> to <code>FullMemoryAllocationLib</code></a></p>
<p><a id="ref5"></a>[5] <a href="https://github.com/slimbootloader/slimbootloader/blob/cdcfd3e3161e45fce6e7e6d088f068584de8219c/BootloaderCommonPkg/Library/FullMemoryAllocationLib/Pool.c" rel="noopener noreferrer" target="_blank">Slim Bootloader <code>FullMemoryAllocationLib</code> pool allocator</a></p>
<p><a id="ref6"></a>[6] <a href="https://github.com/slimbootloader/slimbootloader/blob/cdcfd3e3161e45fce6e7e6d088f068584de8219c/BootloaderCommonPkg/Library/PagingLib/PagingLib.c" rel="noopener noreferrer" target="_blank">Slim Bootloader <code>PagingLib</code> page-table setup without NX</a> and <a href="https://github.com/slimbootloader/slimbootloader/blob/cdcfd3e3161e45fce6e7e6d088f068584de8219c/MdePkg/Library/BaseLib/Ia32/EnablePaging64.nasm" rel="noopener noreferrer" target="_blank"><code>EnablePaging64.nasm</code> EFER.LME without NXE</a></p>
<p><a id="ref7"></a>[7] <a href="https://github.com/slimbootloader/slimbootloader/blob/cdcfd3e3161e45fce6e7e6d088f068584de8219c/BaseTools/Conf/tools_def.template" rel="noopener noreferrer" target="_blank">Slim Bootloader toolchain flags</a></p>
<p><a id="ref8"></a>[8] <a href="https://github.com/slimbootloader/slimbootloader/commit/193129e6a1ea675527670785c8c75ed09b423211" rel="noopener noreferrer" target="_blank">Slim Bootloader commit <code>193129e6</code>: Check to see if <code>Ext2FsGrpDes</code> size overflows <code>UINTN</code></a></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item></channel></rss>