<?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>embedded on SecMate Blog</title><link>https://blog.secmate.dev/tags/embedded/</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 06:06:13 +0000</lastBuildDate><atom:link href="https://blog.secmate.dev/tags/embedded/index.xml" rel="self" type="application/rss+xml"/><item><title>From USB to ESP: CPG-Guided Local Analysis</title><link>https://blog.secmate.dev/posts/espressif-usb-cpg-findings/</link><pubDate>Wed, 19 Aug 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>Embedded</category><guid>https://blog.secmate.dev/posts/espressif-usb-cpg-findings/</guid><description>How SecMate uses source-backed CPG slices and local models to analyze three disclosed vulnerabilities in Espressif USB host drivers.</description><content:encoded><![CDATA[<p>Frontier systems such as Mythos can inspect a repository <a href="#ref19">[19]</a>. That is not the hard part. Security work still needs a record of the code that was analyzed, the paths considered, the assumptions left open, and the source behind a finding.</p>
<p>SecMate was built for cases where source code cannot leave the operator&rsquo;s environment and where repeatedly handing a model a large repository is too expensive. Our current workflow runs <code>gpt-oss</code>. It does not give the model a repository and hope for the best. It first recovers the program relations that matter, then gives the model a small question with the relevant source attached.</p>
<p>We do not call this model agnostic. The prompts, tools, context, and validation process are tuned for the models we use today. Switching to another model family, such as DeepSeek or Qwen, would require integration and evaluation work. Adopting a new version of a model already in use would also require evaluation and, if its interface or behavior changed, integration work. For a fixed source revision, build configuration, semantic models, candidate query, and evidence package, the graph extraction is repeatable. The model’s conclusion remains a judgment.</p>
<p>We presented this work at LeHack 2026 in <a href="https://lehack.org/2026/tracks/conferences/#from-usb-to-esp-security-vulnerabilities-in-espressif-firmware" rel="noopener noreferrer" target="_blank"><em>From USB to ESP: Security Vulnerabilities in Espressif Firmware</em></a>. This post is the analysis part: three vulnerabilities in Espressif USB host drivers that SecMate found automatically with this workflow. Researchers validated them, completed responsible disclosure, and received CVE assignments. The cases span bounds, lifetime, and concurrency.</p>
<h2 id="the-engineering-problem">The Engineering Problem</h2>
<p>These constraints are common in product security. The source may contain customer intellectual property, fall under an NDA, or live in an environment with data residency requirements. Sending it to an external model service may be prohibited. Some teams also want control over what leaves their environment. That can include whether a provider attaches provenance metadata to generated files. Anthropic&rsquo;s marking of generated images is one current example <a href="#ref20">[20]</a>. Cost matters too. A security analysis reads the same repository repeatedly as it follows paths, revisits callers and callees, and gathers support for each result.</p>
<p>SecMate was built for those constraints. Its analysis and inference run inside infrastructure controlled by the operator. The workflow uses the open-weight <code>gpt-oss-20b</code> and <code>gpt-oss-120b</code> models. OpenAI states that the smaller model can run with 16 GB of memory and that the larger model fits on a single 80 GB GPU <a href="#ref1">[1]</a>.</p>
<p>Keeping inference local solves where it runs. It does not decide what code the model should see. A repository that is too large or noisy for a hosted model is still too large or noisy on premises.</p>
<h2 id="why-reduce-before-reasoning">Why Reduce Before Reasoning</h2>
<p>Security bugs are rarely contained in one function. A length can enter through a parser, cross several fields, influence an allocation, and later reach a copy. A pointer can be stored in a structure, freed during teardown, and used again through a local alias.</p>
<p>Giving a model every file leaves it with two jobs: discover which code matters, then decide whether that code is vulnerable. The first job consumes most of the context and must be repeated for every candidate. It also makes the result difficult to audit because the model&rsquo;s conclusion is no longer attached to a small, inspectable path.</p>
<p>SecMate treats the first job as program analysis. It indexes an exact source revision with its build context and semantic models. For each candidate, it extracts the relevant calls, control conditions, values, aliases, and source locations into a small package for review.</p>
<p>A frontier model can search with <code>rg</code>, open files, and follow a hypothesis. That is useful. Without an equivalent persistent analysis layer, it must find and retain those relationships while it investigates. SecMate keeps them outside the model.</p>
<p>For a fixed analysis configuration, that extraction follows the same graph relations every time. A reviewer can inspect the source path without rerunning the model.</p>
<h2 id="method-from-repository-to-evidence-slice">Method: From Repository to Evidence Slice</h2>
<p>Here is the path from a USB descriptor to a reportable finding.</p>
<style>
  :root{--ink-red:#a4123a;--ink-purple:#6d28d9;--ink-amber:#8a5200}
  html.dark{--ink-red:#ff8fa8;--ink-purple:#c4b5fd;--ink-amber:#fbbf24}
  .figure-scroll{width:100%;overflow-x:auto;margin:1.75rem 0}
  .sc-bg{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.4}
  .sc-panel{fill:none;stroke:var(--color-text-secondary);stroke-width:1.2;stroke-dasharray:5 6;opacity:.38}
  .sc-file{fill:var(--color-text-secondary);font-family:var(--font-mono,ui-monospace,Menlo,monospace);opacity:.85}
  .sc-node{fill:var(--color-muted-bg);stroke:color-mix(in srgb,var(--color-text-secondary) 42%,transparent);stroke-width:1.4}
  .sc-entry{fill:var(--color-muted-bg);stroke:var(--color-accent);stroke-width:2.6}
  .sc-sink{fill:var(--color-accent);stroke:var(--color-accent);stroke-width:2}
  .sc-free{fill:var(--color-muted-bg);stroke:var(--color-secondary);stroke-width:2.6}
  .sc-hit{fill:color-mix(in srgb,#e11d48 26%,var(--color-muted-bg));stroke:#e11d48;stroke-width:2.6}
  .sc-dim{opacity:.28}
  .sc-num{fill:var(--color-text);font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-weight:700}
  .sc-num-inv{fill:#0b1220;font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-weight:700}
  .sc-title{fill:var(--color-text);font-family:var(--font-sans,system-ui,sans-serif)}
  .sc-muted{fill:var(--color-text-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
  .sc-tag{font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-weight:700}
  .sc-ast{stroke:var(--color-text-secondary);stroke-width:1.3;fill:none;stroke-dasharray:2 4;opacity:.6}
  .sc-faint{opacity:.2}
  .figure-scroll svg{display:block;width:100%;min-width:720px;height:auto}
  .finding-summary{display:grid;gap:.8rem;margin:2rem 0}
  .finding-summary__item{display:grid;grid-template-columns:1.15fr 1fr 2.1fr 1.1fr 1fr;gap:1rem;margin:0;padding:1rem;border:1px solid var(--color-border);border-radius:8px;background:var(--color-bg-card)}
  .finding-summary__field{min-width:0}
  .finding-summary dt{margin:0 0 .35rem;color:var(--color-text-secondary);font-size:.68rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase}
  .finding-summary dd{margin:0;color:var(--color-text);font-size:.88rem;line-height:1.45;overflow-wrap:anywhere}
  .evidence-package{margin:1.75rem 0;border:1px solid var(--color-border);border-radius:8px;background:var(--color-muted-bg);overflow:hidden}
  .evidence-package__head{padding:.75rem 1.1rem;border-bottom:1px solid var(--color-border);color:var(--color-text);font-weight:700;font-size:.92rem}
  .evidence-package__grid{display:grid;grid-template-columns:auto minmax(0,1fr);gap:.7rem 1.2rem;margin:0;padding:1.1rem}
  .evidence-package__grid dt{color:var(--color-text-secondary);font-size:.68rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding-top:.2rem}
  .evidence-package__grid dd{margin:0;color:var(--color-text);font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-size:.8rem;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere}
  .ep-bad{color:var(--ink-red);font-weight:600}
  .ep-alias{color:var(--ink-purple)}
  .ep-data{color:var(--color-secondary)}
  @media (max-width:800px){.finding-summary__item{grid-template-columns:1fr 1fr}.finding-summary__field--wide{grid-column:1/-1}}
  @media (max-width:600px){.evidence-package__grid{grid-template-columns:1fr;gap:.25rem}.evidence-package__grid dd{margin-bottom:.6rem}}
  @media (max-width:480px){.finding-summary__item{grid-template-columns:1fr}.finding-summary__field--wide{grid-column:auto}}
  @media print{
    .figure-scroll{overflow:visible!important;break-inside:avoid}
    .figure-scroll svg{width:auto!important;max-width:100%!important;min-width:0!important;height:auto!important;max-height:82vh!important;margin:0 auto!important}
    .evidence-package,.finding-summary__item{break-inside:avoid}
  }
</style>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 660" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ar-title ar-desc">
  <title id="ar-title">The SecMate pipeline from device input to disclosed finding</title>
  <desc id="ar-desc">Attacker-controlled USB input enters a build-aware program index, which is enriched into a CPG with analysis overlays. API and semantic models classify operations on the graph. Class-specific traversals produce a bounded evidence slice, a local model tests the hypothesis, and a researcher validates before disclosure. The three Espressif CVEs are the outcome.</desc>
  <defs>
    <marker id="ar-gray" 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>
      .ar-bg{fill:var(--color-muted-bg);stroke:var(--color-border);stroke-width:1.4}
      .ar-node{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.5}
      .ar-input{fill:color-mix(in srgb,var(--color-secondary) 13%,var(--color-bg-card));stroke:var(--color-secondary);stroke-width:1.6}
      .ar-model{fill:var(--color-bg-card);stroke:var(--color-text-secondary);stroke-width:1.5;stroke-dasharray:6 4}
      .ar-cve{fill:color-mix(in srgb,#dc2626 8%,var(--color-bg-card));stroke:#dc2626;stroke-width:1.4}
      .ar-strong{fill:var(--color-text);font-family:var(--font-sans,system-ui,sans-serif);font-weight:700}
      .ar-code{fill:var(--color-text);font-family:var(--font-mono,ui-monospace,Menlo,monospace)}
      .ar-muted{fill:var(--color-text-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
      .ar-flow{stroke:var(--color-text-secondary);stroke-width:2;fill:none;marker-end:url(#ar-gray)}
    </style>
  </defs>
  <rect class="ar-bg" x="1" y="1" width="898" height="658" rx="12"/>
  <text class="ar-strong" x="36" y="42" font-size="20">From USB device to disclosed finding</text>
  <text class="ar-muted" x="36" y="66" font-size="13">The pipeline, the models that feed it, and the split between model review and human validation.</text>
  <rect class="ar-input" x="60" y="96" width="300" height="58" rx="7"/>
  <text class="ar-strong" x="76" y="118" font-size="13">USB device</text>
  <text class="ar-muted" x="76" y="138" font-size="11">attacker-controlled descriptors, lengths, timing</text>
  <path class="ar-flow" d="M210 154 V180"/>
  <rect class="ar-node" x="60" y="184" width="300" height="74" rx="7"/>
  <text class="ar-strong" x="76" y="206" font-size="13">Selected build targets</text>
  <text class="ar-muted" x="76" y="226" font-size="11">exact commit · compilation commands</text>
  <text class="ar-muted" x="76" y="244" font-size="11">macros, configuration, generated sources</text>
  <path class="ar-flow" d="M210 258 V280"/>
  <rect class="ar-node" x="60" y="284" width="300" height="74" rx="7"/>
  <text class="ar-strong" x="76" y="306" font-size="13">Enriched CPG and analysis overlays</text>
  <text class="ar-muted" x="76" y="326" font-size="11">source span and provenance on every node</text>
  <text class="ar-muted" x="76" y="344" font-size="11">control flow, dependences, call/return, aliases</text>
  <path class="ar-flow" d="M210 358 V380"/>
  <text class="ar-muted" x="222" y="374" font-size="11">class-specific traversal</text>
  <rect class="ar-node" x="60" y="384" width="300" height="58" rx="7"/>
  <text class="ar-strong" x="76" y="406" font-size="13">Evidence slice</text>
  <text class="ar-muted" x="76" y="426" font-size="11">bounded, source-backed package per candidate</text>
  <path class="ar-flow" d="M210 442 V464"/>
  <rect class="ar-node" x="60" y="468" width="300" height="58" rx="7"/>
  <text class="ar-strong" x="76" y="490" font-size="13">Local model review</text>
  <text class="ar-muted" x="76" y="510" font-size="11">tests the bounded hypothesis</text>
  <path class="ar-flow" d="M210 526 V548"/>
  <rect class="ar-node" x="60" y="552" width="300" height="58" rx="7"/>
  <text class="ar-strong" x="76" y="574" font-size="13">Researcher validation</text>
  <text class="ar-muted" x="76" y="594" font-size="11">accepts, rejects, or requests more evidence</text>
  <rect class="ar-model" x="480" y="284" width="340" height="106" rx="7"/>
  <text class="ar-strong" x="496" y="308" font-size="13">API and semantic models</text>
  <text class="ar-muted" x="496" y="328" font-size="11">allocator and release summaries</text>
  <text class="ar-muted" x="496" y="346" font-size="11">copy and write semantics, locking primitives</text>
  <text class="ar-muted" x="496" y="364" font-size="11">project-specific function summaries</text>
  <text class="ar-muted" x="496" y="382" font-size="11">callback and task registration</text>
  <path class="ar-flow" d="M480 337 C400 337 300 356 218 368"/>
  <text class="ar-muted" x="352" y="330" font-size="11" text-anchor="middle">classifies operations</text>
  <rect class="ar-cve" x="480" y="470" width="340" height="44" rx="7"/>
  <text class="ar-code" x="496" y="488" font-size="12">CVE-2025-68656</text>
  <text class="ar-muted" x="496" y="506" font-size="11">HID descriptor use-after-free</text>
  <rect class="ar-cve" x="480" y="528" width="340" height="44" rx="7"/>
  <text class="ar-code" x="496" y="546" font-size="12">CVE-2025-68657</text>
  <text class="ar-muted" x="496" y="564" font-size="11">HID close-path double-free race</text>
  <rect class="ar-cve" x="480" y="586" width="340" height="44" rx="7"/>
  <text class="ar-code" x="496" y="604" font-size="12">CVE-2025-68622</text>
  <text class="ar-muted" x="496" y="622" font-size="11">UVC descriptor stack overflow</text>
  <path class="ar-flow" d="M360 581 C420 581 428 550 474 550"/>
  <text class="ar-muted" x="408" y="566" font-size="11">disclosed</text>
</svg>
</div>
<p><em>Figure 1. The pipeline used in this article. The model reviews a focused hypothesis. A researcher decides whether it supports disclosure.</em></p>
<h3 id="1-build-a-program-index">1. Build a program index</h3>
<p>Full-codebase analysis is not a glob over sources and headers. For this C/C++ work, the unit is a translation unit plus the compilation command used for it. Headers are interpreted under specific macro definitions, include paths, and build options. The same <code>.c</code> file may appear more than once under different configurations. Clang&rsquo;s JSON compilation database records those per-translation-unit commands <a href="#ref18">[18]</a>. Other languages need an equivalent front end for their compilation or runtime model. SecMate starts from an exact commit, the compilation commands, the macros and configuration in force, and any generated sources. The frontend preprocesses and parses the translation units, records source spans, resolves declarations and types, and links calls across files where the available information permits it.</p>
<p>Every node retains its file, line, and column. In this C/C++ analysis, it also retains the relevant build variant, translation unit, and macro expansion context. Within a run, a reviewer can open the exact line and build variant behind a claim.</p>
<p>That scope is part of the result. Conditional compilation, generated sources, missing build metadata, function pointers, and unresolved external code can all limit coverage. They must remain visible as unresolved facts instead of being silently treated as complete.</p>
<h3 id="2-build-the-ast-then-identify-what-it-is-missing">2. Build the AST, then identify what it is missing</h3>
<p>The Abstract Syntax Tree records declarations, expressions, calls, assignments, and scopes. A call node shows that call syntax exists. It does not necessarily identify every function that can run. An <code>if</code> node records a branch. It does not establish which later statements the condition controls.</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 470" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ast-t ast-d">
<title id="ast-t">The AST gives local structure only</title>
<desc id="ast-d">The syntax tree records local structure in usb_host_hid.c and the two functions in hid_host.c. It does not by itself resolve the control flow, data flow, or calls that connect them.</desc>
<defs>
<marker id="ast-cf" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#d97706"/></marker>
<marker id="ast-dd" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-accent)"/></marker>
<marker id="ast-cl" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-secondary)"/></marker>
<marker id="ast-vu" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#e11d48"/></marker>
<style>
.ast-cf{stroke:#d97706;stroke-width:2.2;fill:none;marker-end:url(#ast-cf)}
.ast-dd{stroke:var(--color-accent);stroke-width:2.2;fill:none;marker-end:url(#ast-dd)}
.ast-cl{stroke:var(--color-secondary);stroke-width:2.4;fill:none;marker-end:url(#ast-cl)}
.ast-vu{stroke:#e11d48;stroke-width:2.6;fill:none;marker-end:url(#ast-vu)}
</style>
</defs>
<rect class="sc-bg" x="1" y="1" width="898" height="468" rx="12"/>
<text class="sc-title" x="32" y="40" font-size="18" font-weight="700">The AST is local structure, not behavior</text>
<text class="sc-muted" x="32" y="62" font-size="12.5">Syntax is local; resolved cross-file behavior is not yet represented.</text>
<rect class="sc-panel" x="28" y="96" width="248" height="316" rx="9"/>
<text class="sc-file" x="44" y="122" font-size="12">usb_host_hid.c</text>
<rect class="sc-panel" x="300" y="72" width="300" height="340" rx="9"/>
<text class="sc-file" x="316" y="98" font-size="12">hid_host.c · get_descriptor()</text>
<rect class="sc-panel" x="624" y="112" width="248" height="300" rx="9"/>
<text class="sc-file" x="640" y="138" font-size="12">hid_host.c · transfer()</text>
<path class="sc-ast" d="M85 179 L97 197"/>
<path class="sc-ast" d="M121 221 L179 253"/>
<path class="sc-ast" d="M142 137 L94 160"/>
<path class="sc-ast" d="M75 306 L118 335"/>
<path class="sc-ast" d="M144 342 L198 333"/>
<path class="sc-ast" d="M385 161 L360 215"/>
<path class="sc-ast" d="M406 153 L499 200"/>
<path class="sc-ast" d="M363 240 L430 288"/>
<path class="sc-ast" d="M506 217 L486 235"/>
<path class="sc-ast" d="M523 220 L538 248"/>
<path class="sc-ast" d="M375 337 L415 368"/>
<path class="sc-ast" d="M439 377 L492 373"/>
<path class="sc-ast" d="M696 179 L712 248"/>
<path class="sc-ast" d="M772 211 L824 239"/>
<path class="sc-ast" d="M674 249 L698 260"/>
<path class="sc-ast" d="M730 273 L782 293"/>
<path class="sc-ast" d="M811 287 L829 259"/>
<path class="sc-ast" d="M685 319 L668 259"/>
<circle class="sc-node" cx="78" cy="168" r="11"/>
<circle class="sc-node" cx="152" cy="132" r="9"/>
<circle class="sc-node" cx="108" cy="214" r="13"/>
<circle class="sc-node" cx="196" cy="262" r="12"/>
<circle class="sc-node" cx="66" cy="300" r="9"/>
<circle class="sc-node" cx="132" cy="344" r="10"/>
<circle class="sc-node" cx="214" cy="330" r="9"/>
<circle class="sc-node" cx="392" cy="146" r="14"/>
<circle class="sc-node" cx="352" cy="232" r="12"/>
<circle class="sc-node" cx="446" cy="300" r="13"/>
<circle class="sc-node" cx="516" cy="208" r="12"/>
<circle class="sc-node" cx="474" cy="246" r="9"/>
<circle class="sc-node" cx="546" cy="262" r="9"/>
<circle class="sc-node" cx="366" cy="330" r="9"/>
<circle class="sc-node" cx="428" cy="378" r="9"/>
<circle class="sc-node" cx="508" cy="372" r="9"/>
<circle class="sc-node" cx="694" cy="168" r="9"/>
<circle class="sc-node" cx="762" cy="206" r="9"/>
<circle class="sc-node" cx="664" cy="244" r="9"/>
<circle class="sc-node" cx="716" cy="268" r="13"/>
<circle class="sc-node" cx="802" cy="300" r="14"/>
<circle class="sc-node" cx="838" cy="246" r="9"/>
<circle class="sc-node" cx="688" cy="330" r="9"/>
<path class="sc-ast" d="M32 440 H58"/>
<text class="sc-muted" x="66" y="444" font-size="11.5">syntax (within a file)</text>
</svg>
</div>
<p><em>Figure 2. The syntax tree of the three files involved in the HID finding. It records local syntax, but not resolved cross-file control flow, data flow, or call/return behavior. The descriptor length read in one function and the release and later use in another are not related by those semantic edges here.</em></p>
<p>Security analysis therefore needs relations that are not present in the AST:</p>
<table>
<thead>
<tr>
<th>Security question</th>
<th>Required relation or analysis</th>
</tr>
</thead>
<tbody>
<tr>
<td>What can execute next?</td>
<td>Control Flow Graph (CFG)</td>
</tr>
<tr>
<td>Which condition governs this operation?</td>
<td>dominance, post-dominance, and control dependence</td>
</tr>
<tr>
<td>Where did this value come from?</td>
<td>definitions, uses, and data dependence</td>
</tr>
<tr>
<td>Does the path cross a function boundary correctly?</td>
<td>call graph and call/return context</td>
</tr>
<tr>
<td>Do two expressions refer to the same object?</td>
<td>points-to and alias analysis</td>
</tr>
<tr>
<td>Can two paths execute concurrently on the same object?</td>
<td>task or callback concurrency, shared-state, and synchronization analysis</td>
</tr>
</tbody>
</table>
<p>A CFG over-approximates possible execution order. It does not prove that every graph path is feasible. Alias and indirect call resolution are approximations too. Those limits matter when a candidate is validated.</p>
<h3 id="3-enrich-the-graph">3. Enrich the graph</h3>
<p>The Code Property Graph described by Yamaguchi and colleagues brings syntax, control flow, and program dependence into one queryable representation <a href="#ref5">[5]</a>. At repository scale, the graph also needs resolved symbols and types, call and return relations, and alias information. Control and data dependence support slicing, which retains the statements relevant to a chosen operation and value <a href="#ref2">[2]</a> <a href="#ref3">[3]</a>. Interprocedural traversal must preserve call and return context instead of accepting arbitrary paths through the call graph <a href="#ref4">[4]</a>.</p>
<p>Not every relation has the same status. Syntax and source spans come from the frontend. Control flow, dependence, call targets, and aliases are computed. Context-sensitive call and return relations, alias results, and classifications such as allocations, releases, copies, and locks sit on top of the base graph. Modern CPG specifications describe those calculated layers as overlays <a href="#ref17">[17]</a>. A parser error and an imprecise alias result create different kinds of uncertainty.</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 470" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="cpg-t cpg-d">
<title id="cpg-t">The Code Property Graph links the same nodes across files</title>
<desc id="cpg-d">Control flow, data dependence and call edges connect the same nodes across the three files, so an attacker-controlled source can be followed to a sink.</desc>
<defs>
<marker id="cpg-cf" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#d97706"/></marker>
<marker id="cpg-dd" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-accent)"/></marker>
<marker id="cpg-cl" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-secondary)"/></marker>
<marker id="cpg-vu" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#e11d48"/></marker>
<style>
.cpg-cf{stroke:#d97706;stroke-width:2.2;fill:none;marker-end:url(#cpg-cf)}
.cpg-dd{stroke:var(--color-accent);stroke-width:2.2;fill:none;marker-end:url(#cpg-dd)}
.cpg-cl{stroke:var(--color-secondary);stroke-width:2.4;fill:none;marker-end:url(#cpg-cl)}
.cpg-vu{stroke:#e11d48;stroke-width:2.6;fill:none;marker-end:url(#cpg-vu)}
</style>
</defs>
<rect class="sc-bg" x="1" y="1" width="898" height="468" rx="12"/>
<text class="sc-title" x="32" y="40" font-size="18" font-weight="700">Code Property Graph</text>
<text class="sc-muted" x="32" y="62" font-size="12.5">The same nodes, now carrying execution order, value flow, and calls across files.</text>
<rect class="sc-panel" x="28" y="96" width="248" height="316" rx="9"/>
<text class="sc-file" x="44" y="122" font-size="12">usb_host_hid.c</text>
<rect class="sc-panel" x="300" y="72" width="300" height="340" rx="9"/>
<text class="sc-file" x="316" y="98" font-size="12">hid_host.c · get_descriptor()</text>
<rect class="sc-panel" x="624" y="112" width="248" height="300" rx="9"/>
<text class="sc-file" x="640" y="138" font-size="12">hid_host.c · transfer()</text>
<path class="sc-ast" d="M85 179 L97 197"/>
<path class="sc-ast" d="M121 221 L179 253"/>
<path class="sc-ast" d="M142 137 L94 160"/>
<path class="sc-ast" d="M75 306 L118 335"/>
<path class="sc-ast" d="M144 342 L198 333"/>
<path class="sc-ast" d="M385 161 L360 215"/>
<path class="sc-ast" d="M406 153 L499 200"/>
<path class="sc-ast" d="M363 240 L430 288"/>
<path class="sc-ast" d="M506 217 L486 235"/>
<path class="sc-ast" d="M523 220 L538 248"/>
<path class="sc-ast" d="M375 337 L415 368"/>
<path class="sc-ast" d="M439 377 L492 373"/>
<path class="sc-ast" d="M696 179 L712 248"/>
<path class="sc-ast" d="M772 211 L824 239"/>
<path class="sc-ast" d="M674 249 L698 260"/>
<path class="sc-ast" d="M730 273 L782 293"/>
<path class="sc-ast" d="M811 287 L829 259"/>
<path class="sc-ast" d="M685 319 L668 259"/>
<path class="cpg-cf" d="M121 221 L179 253"/>
<path class="cpg-cf" d="M385 161 L360 215"/>
<path class="cpg-cf" d="M363 240 L430 288"/>
<path class="cpg-cf" d="M482 239 L502 221"/>
<path class="cpg-dd" d="M397 161 L439 281"/>
<path class="cpg-dd" d="M508 219 L458 284"/>
<path class="cpg-cl" d="M208 255 Q314 245 374 157"/>
<path class="cpg-cl" d="M407 152 Q520 291 697 261"/>
<path class="cpg-cl" d="M407 152 Q587 243 782 293"/>
<circle class="sc-node" cx="78" cy="168" r="11"/>
<circle class="sc-node" cx="152" cy="132" r="9"/>
<circle class="sc-entry" cx="108" cy="214" r="13"/>
<circle class="sc-node" cx="196" cy="262" r="12"/>
<circle class="sc-node" cx="66" cy="300" r="9"/>
<circle class="sc-node" cx="132" cy="344" r="10"/>
<circle class="sc-node" cx="214" cy="330" r="9"/>
<circle class="sc-node" cx="392" cy="146" r="14"/>
<circle class="sc-node" cx="352" cy="232" r="12"/>
<circle class="sc-node" cx="446" cy="300" r="13"/>
<circle class="sc-node" cx="516" cy="208" r="12"/>
<circle class="sc-node" cx="474" cy="246" r="9"/>
<circle class="sc-node" cx="546" cy="262" r="9"/>
<circle class="sc-node" cx="366" cy="330" r="9"/>
<circle class="sc-node" cx="428" cy="378" r="9"/>
<circle class="sc-node" cx="508" cy="372" r="9"/>
<circle class="sc-node" cx="694" cy="168" r="9"/>
<circle class="sc-node" cx="762" cy="206" r="9"/>
<circle class="sc-node" cx="664" cy="244" r="9"/>
<circle class="sc-node" cx="716" cy="268" r="13"/>
<circle class="sc-sink" cx="802" cy="300" r="14"/>
<circle class="sc-node" cx="838" cy="246" r="9"/>
<circle class="sc-node" cx="688" cy="330" r="9"/>
<text class="sc-tag" x="108" y="184" font-size="11" text-anchor="middle" fill="var(--color-accent)">source</text>
<text class="sc-tag" x="802" y="336" font-size="11" text-anchor="middle" fill="var(--color-accent)">sink</text>
<path class="cpg-cf" d="M32 440 H58"/>
<text class="sc-muted" x="66" y="444" font-size="11.5">control flow</text>
<path class="cpg-dd" d="M147 440 H173"/>
<text class="sc-muted" x="181" y="444" font-size="11.5">data dependence</text>
<path class="cpg-cl" d="M281 440 H307"/>
<text class="sc-muted" x="315" y="444" font-size="11.5">call · across files</text>
</svg>
</div>
<p><em>Figure 3. The same nodes, now carrying execution order, value flow, and resolved calls. The graph can now be traversed from an attacker-controlled source to a sink across file and function boundaries.</em></p>
<h3 id="4-run-security-analyses-on-the-cpg">4. Run security analyses on the CPG</h3>
<p>Lifetime is not an AST edge. To find a use-after-free, the analysis has to connect object identity, aliases, execution order, a release, and a later use of the same object. The same applies to the other bug classes:</p>
<ul>
<li>a bounds analysis starts at a copy or write and recovers the input length, destination extent, governing checks, and interprocedural path;</li>
<li>a lifetime analysis connects allocation, aliases, release, replacement, and later use of the same abstract object;</li>
<li>a race or double-free analysis identifies a candidate overlap between task or callback paths, then compares their access to shared state and the synchronization that should order them.</li>
</ul>
<p>The graph does not know on its own that <code>usb_host_transfer_free()</code> ends an object&rsquo;s lifetime, that <code>usb_host_transfer_alloc()</code> creates or replaces one, or that a project function provides synchronization. SecMate gets that knowledge from API and semantic models. They cover allocators, releases, copies, writes, locking primitives, and project functions. Those models are explicit inputs to the analysis.</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 470" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ann-t ann-d">
<title id="ann-t">Semantic models annotate the graph</title>
<desc id="ann-d">Entry points, security-relevant sinks and lifetime operations such as free are classified on the graph by API and semantic models before any traversal runs.</desc>
<defs>
<marker id="ann-cf" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#d97706"/></marker>
<marker id="ann-dd" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-accent)"/></marker>
<marker id="ann-cl" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-secondary)"/></marker>
<marker id="ann-vu" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#e11d48"/></marker>
<style>
.ann-cf{stroke:#d97706;stroke-width:2.2;fill:none;marker-end:url(#ann-cf)}
.ann-dd{stroke:var(--color-accent);stroke-width:2.2;fill:none;marker-end:url(#ann-dd)}
.ann-cl{stroke:var(--color-secondary);stroke-width:2.4;fill:none;marker-end:url(#ann-cl)}
.ann-vu{stroke:#e11d48;stroke-width:2.6;fill:none;marker-end:url(#ann-vu)}
</style>
</defs>
<rect class="sc-bg" x="1" y="1" width="898" height="468" rx="12"/>
<text class="sc-title" x="32" y="40" font-size="18" font-weight="700">Discovery and annotation</text>
<text class="sc-muted" x="32" y="62" font-size="12.5">API and semantic models mark entry points, sinks, and lifetime operations.</text>
<rect class="sc-panel" x="28" y="96" width="248" height="316" rx="9"/>
<text class="sc-file" x="44" y="122" font-size="12">usb_host_hid.c</text>
<rect class="sc-panel" x="300" y="72" width="300" height="340" rx="9"/>
<text class="sc-file" x="316" y="98" font-size="12">hid_host.c · get_descriptor()</text>
<rect class="sc-panel" x="624" y="112" width="248" height="300" rx="9"/>
<text class="sc-file" x="640" y="138" font-size="12">hid_host.c · transfer()</text>
<path class="sc-ast" d="M85 179 L97 197"/>
<path class="sc-ast" d="M121 221 L179 253"/>
<path class="sc-ast" d="M142 137 L94 160"/>
<path class="sc-ast" d="M75 306 L118 335"/>
<path class="sc-ast" d="M144 342 L198 333"/>
<path class="sc-ast" d="M385 161 L360 215"/>
<path class="sc-ast" d="M406 153 L499 200"/>
<path class="sc-ast" d="M363 240 L430 288"/>
<path class="sc-ast" d="M506 217 L486 235"/>
<path class="sc-ast" d="M523 220 L538 248"/>
<path class="sc-ast" d="M375 337 L415 368"/>
<path class="sc-ast" d="M439 377 L492 373"/>
<path class="sc-ast" d="M696 179 L712 248"/>
<path class="sc-ast" d="M772 211 L824 239"/>
<path class="sc-ast" d="M674 249 L698 260"/>
<path class="sc-ast" d="M730 273 L782 293"/>
<path class="sc-ast" d="M811 287 L829 259"/>
<path class="sc-ast" d="M685 319 L668 259"/>
<circle class="sc-node" cx="78" cy="168" r="11"/>
<circle class="sc-node" cx="152" cy="132" r="9"/>
<circle class="sc-entry" cx="108" cy="214" r="13"/>
<text class="sc-num" x="108" y="218" font-size="11" text-anchor="middle">in</text>
<circle class="sc-node" cx="196" cy="262" r="12"/>
<circle class="sc-node" cx="66" cy="300" r="9"/>
<circle class="sc-node" cx="132" cy="344" r="10"/>
<circle class="sc-node" cx="214" cy="330" r="9"/>
<circle class="sc-node" cx="392" cy="146" r="14"/>
<text class="sc-num" x="392" y="150" font-size="11" text-anchor="middle">1</text>
<circle class="sc-node" cx="352" cy="232" r="12"/>
<text class="sc-num" x="352" y="236" font-size="11" text-anchor="middle">2</text>
<circle class="sc-free" cx="446" cy="300" r="13"/>
<text class="sc-num" x="446" y="304" font-size="11" text-anchor="middle">3</text>
<circle class="sc-node" cx="516" cy="208" r="12"/>
<text class="sc-num" x="516" y="212" font-size="11" text-anchor="middle">4</text>
<circle class="sc-node" cx="474" cy="246" r="9"/>
<circle class="sc-node" cx="546" cy="262" r="9"/>
<circle class="sc-node" cx="366" cy="330" r="9"/>
<circle class="sc-node" cx="428" cy="378" r="9"/>
<circle class="sc-node" cx="508" cy="372" r="9"/>
<circle class="sc-node" cx="694" cy="168" r="9"/>
<circle class="sc-node" cx="762" cy="206" r="9"/>
<circle class="sc-node" cx="664" cy="244" r="9"/>
<circle class="sc-node" cx="716" cy="268" r="13"/>
<text class="sc-num" x="716" y="272" font-size="11" text-anchor="middle">5</text>
<circle class="sc-sink" cx="802" cy="300" r="14"/>
<text class="sc-num-inv" x="802" y="304" font-size="11" text-anchor="middle">6</text>
<circle class="sc-node" cx="838" cy="246" r="9"/>
<circle class="sc-node" cx="688" cy="330" r="9"/>
<text class="sc-tag" x="108" y="184" font-size="11" text-anchor="middle" fill="var(--color-accent)">entry point</text>
<text class="sc-tag" x="446" y="334" font-size="11" text-anchor="middle" fill="var(--color-secondary)">free()</text>
<text class="sc-tag" x="802" y="336" font-size="11" text-anchor="middle" fill="var(--color-accent)">sink</text>
<circle class="sc-entry" cx="38" cy="440" r="7"/>
<text class="sc-muted" x="52" y="444" font-size="11.5">entry points</text>
<circle class="sc-free" cx="168" cy="440" r="7"/>
<text class="sc-muted" x="182" y="444" font-size="11.5">release and lifetime operations</text>
<circle class="sc-sink" cx="416" cy="440" r="7"/>
<text class="sc-muted" x="430" y="444" font-size="11.5">sinks</text>
</svg>
</div>
<p><em>Figure 4. Entry points, sinks, and lifetime operations such as the release are not syntactic facts. API and semantic models classify them on the graph before any vulnerability traversal runs.</em></p>
<p>Each analysis follows only the relations relevant to its question. Resolved call and return context stays with the path. So do the branch conditions. Before disclosure, the researcher checks concurrency eligibility, path feasibility, alias precision, and build coverage.</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 470" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="slice-t slice-d">
<title id="slice-t">The traversal keeps only the path that carries the evidence</title>
<desc id="slice-d">A lifetime traversal from the free operation retains the entry point, the governing branch, the release and the later use; the rest of the graph is dropped from the evidence slice.</desc>
<defs>
<marker id="slice-cf" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#d97706"/></marker>
<marker id="slice-dd" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-accent)"/></marker>
<marker id="slice-cl" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-secondary)"/></marker>
<marker id="slice-vu" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#e11d48"/></marker>
<style>
.slice-cf{stroke:#d97706;stroke-width:2.2;fill:none;marker-end:url(#slice-cf)}
.slice-dd{stroke:var(--color-accent);stroke-width:2.2;fill:none;marker-end:url(#slice-dd)}
.slice-cl{stroke:var(--color-secondary);stroke-width:2.4;fill:none;marker-end:url(#slice-cl)}
.slice-vu{stroke:#e11d48;stroke-width:2.6;fill:none;marker-end:url(#slice-vu)}
</style>
</defs>
<rect class="sc-bg" x="1" y="1" width="898" height="468" rx="12"/>
<text class="sc-title" x="32" y="40" font-size="18" font-weight="700">The right analysis, then the slice</text>
<text class="sc-muted" x="32" y="62" font-size="12.5">A lifetime traversal keeps six nodes; everything else leaves the evidence package.</text>
<rect class="sc-panel" x="28" y="96" width="248" height="316" rx="9"/>
<text class="sc-file" x="44" y="122" font-size="12">usb_host_hid.c</text>
<rect class="sc-panel" x="300" y="72" width="300" height="340" rx="9"/>
<text class="sc-file" x="316" y="98" font-size="12">hid_host.c · get_descriptor()</text>
<rect class="sc-panel" x="624" y="112" width="248" height="300" rx="9"/>
<text class="sc-file" x="640" y="138" font-size="12">hid_host.c · transfer()</text>
<path class="sc-ast" d="M121 221 L179 253"/>
<path class="sc-ast" d="M385 161 L360 215"/>
<path class="sc-ast" d="M406 153 L499 200"/>
<path class="sc-ast" d="M363 240 L430 288"/>
<path class="sc-ast" d="M730 273 L782 293"/>
<g class="sc-faint"><path class="sc-ast" d="M85 179 L97 197"/></g>
<g class="sc-faint"><path class="sc-ast" d="M142 137 L94 160"/></g>
<g class="sc-faint"><path class="sc-ast" d="M75 306 L118 335"/></g>
<g class="sc-faint"><path class="sc-ast" d="M144 342 L198 333"/></g>
<g class="sc-faint"><path class="sc-ast" d="M506 217 L486 235"/></g>
<g class="sc-faint"><path class="sc-ast" d="M523 220 L538 248"/></g>
<g class="sc-faint"><path class="sc-ast" d="M375 337 L415 368"/></g>
<g class="sc-faint"><path class="sc-ast" d="M439 377 L492 373"/></g>
<g class="sc-faint"><path class="sc-ast" d="M696 179 L712 248"/></g>
<g class="sc-faint"><path class="sc-ast" d="M772 211 L824 239"/></g>
<g class="sc-faint"><path class="sc-ast" d="M674 249 L698 260"/></g>
<g class="sc-faint"><path class="sc-ast" d="M811 287 L829 259"/></g>
<g class="sc-faint"><path class="sc-ast" d="M685 319 L668 259"/></g>
<path class="slice-cf" d="M121 221 L179 253"/>
<path class="slice-cf" d="M385 161 L360 215"/>
<path class="slice-cf" d="M363 240 L430 288"/>
<path class="slice-cf" d="M455 288 L504 223"/>
<path class="slice-dd" d="M397 161 L439 281"/>
<path class="slice-cl" d="M208 255 Q305 229 374 157"/>
<path class="slice-vu" d="M407 152 Q523 284 697 261"/>
<path class="slice-vu" d="M407 152 Q598 214 782 293"/>
<circle class="sc-node sc-dim" cx="78" cy="168" r="11"/>
<circle class="sc-node sc-dim" cx="152" cy="132" r="9"/>
<circle class="sc-entry" cx="108" cy="214" r="13"/>
<text class="sc-num" x="108" y="218" font-size="11" text-anchor="middle">in</text>
<circle class="sc-node" cx="196" cy="262" r="12"/>
<circle class="sc-node sc-dim" cx="66" cy="300" r="9"/>
<circle class="sc-node sc-dim" cx="132" cy="344" r="10"/>
<circle class="sc-node sc-dim" cx="214" cy="330" r="9"/>
<circle class="sc-node" cx="392" cy="146" r="14"/>
<text class="sc-num" x="392" y="150" font-size="11" text-anchor="middle">1</text>
<circle class="sc-node" cx="352" cy="232" r="12"/>
<text class="sc-num" x="352" y="236" font-size="11" text-anchor="middle">2</text>
<circle class="sc-free" cx="446" cy="300" r="13"/>
<text class="sc-num" x="446" y="304" font-size="11" text-anchor="middle">3</text>
<circle class="sc-node" cx="516" cy="208" r="12"/>
<text class="sc-num" x="516" y="212" font-size="11" text-anchor="middle">4</text>
<circle class="sc-node sc-dim" cx="474" cy="246" r="9"/>
<circle class="sc-node sc-dim" cx="546" cy="262" r="9"/>
<circle class="sc-node sc-dim" cx="366" cy="330" r="9"/>
<circle class="sc-node sc-dim" cx="428" cy="378" r="9"/>
<circle class="sc-node sc-dim" cx="508" cy="372" r="9"/>
<circle class="sc-node sc-dim" cx="694" cy="168" r="9"/>
<circle class="sc-node sc-dim" cx="762" cy="206" r="9"/>
<circle class="sc-node sc-dim" cx="664" cy="244" r="9"/>
<circle class="sc-hit" cx="716" cy="268" r="13"/>
<text class="sc-num" x="716" y="272" font-size="11" text-anchor="middle">5</text>
<circle class="sc-hit" cx="802" cy="300" r="14"/>
<text class="sc-num" x="802" y="304" font-size="11" text-anchor="middle">6</text>
<circle class="sc-node sc-dim" cx="838" cy="246" r="9"/>
<circle class="sc-node sc-dim" cx="688" cy="330" r="9"/>
<text class="sc-tag" x="108" y="184" font-size="11" text-anchor="middle" fill="var(--color-accent)">source</text>
<text class="sc-tag" x="446" y="334" font-size="11" text-anchor="middle" fill="var(--color-secondary)">free</text>
<text class="sc-tag" x="742" y="242" font-size="11" fill="#e11d48">use-after-free</text>
<path class="slice-cf" d="M32 440 H58"/>
<text class="sc-muted" x="66" y="444" font-size="11.5">control flow</text>
<path class="slice-dd" d="M147 440 H173"/>
<text class="sc-muted" x="181" y="444" font-size="11.5">data dependence</text>
<path class="slice-cl" d="M281 440 H307"/>
<text class="sc-muted" x="315" y="444" font-size="11.5">call</text>
<path class="slice-vu" d="M346 440 H372"/>
<text class="sc-muted" x="380" y="444" font-size="11.5">lifetime violation</text>
</svg>
</div>
<p><em>Figure 5. The lifetime traversal starts at the release and keeps the entry point, the governing branch, the reallocation, and the later uses. Those nodes and their edges become the evidence slice; the rest of the graph never reaches the model.</em></p>
<h3 id="5-materialize-the-evidence-slice">5. Materialize the evidence slice</h3>
<p>For each candidate, SecMate collects the source nodes, call chain, control predicates, data or alias edges, and unresolved assumptions needed for review. The local model sees that package, not the repository. It tests a specific vulnerability hypothesis against the extracted path. A researcher then checks the source path, attack preconditions, and exploitability claim before disclosure.</p>
<p>The graph recovers program relations. The analysis selects the relevant path. The model reviews that path. Candidate discovery and path extraction happen before model review. A SecMate researcher validates every claim before it is reported. The model&rsquo;s output is one input to that review.</p>
<p>This is what one such package looks like for the use-after-free presented later in this article:</p>
<div class="evidence-package">
  <div class="evidence-package__head">Evidence package — HID descriptor use-after-free (sanitized)</div>
  <dl class="evidence-package__grid">
    <dt>Hypothesis</dt>
    <dd>use after free of usb_transfer_t through a stale local alias</dd>
    <dt>Source</dt>
    <dd>usb_host_hid / hid_host.c · usb_class_request_get_descriptor()</dd>
    <dt>Call path</dt>
    <dd>enumerate_device()
  → get_report_descriptor()
  → usb_class_request_get_descriptor()</dd>
    <dt>Object</dt>
    <dd>urb_t reached through hid_device->ctrl_xfer, owning its data_buffer</dd>
    <dt>Lifetime</dt>
    <dd><span class="ep-alias">ctrl_xfer (local) aliases the urb_t</span> — captured before the branch
usb_host_transfer_free() — <span class="ep-bad">releases the urb_t and its data_buffer</span>
usb_host_transfer_alloc(&amp;hid_device->ctrl_xfer) — on success the field refers to a new urb_t
<span class="ep-bad">ctrl_xfer->data_buffer — dereference after that lifetime ended</span></dd>
    <dt>Path condition</dt>
    <dd><span class="ep-data">ctrl_size &lt; USB_SETUP_PACKET_SIZE + req->wLength</span> — right side device-controlled</dd>
    <dt>Unresolved</dt>
    <dd>indirect callback target — resolved with medium confidence</dd>
  </dl>
</div>
<p><em>Figure 6. A sanitized evidence package for the HID use-after-free: hypothesis, bounded path, object lifetime, governing condition, and the facts the analysis could not resolve. This package is what the local model reviews, and what the researcher audits afterward.</em></p>
<p>LLMxCPG and <code>codebadger</code> also use CPGs to guide repository investigation <a href="#ref6">[6]</a> <a href="#ref7">[7]</a>.</p>
<h2 id="applying-the-method-to-espressif-usb-host">Applying the Method to Espressif USB Host</h2>
<p>ESP-IDF is Espressif&rsquo;s development framework for its systems-on-chip <a href="#ref10">[10]</a>. The <code>esp-usb</code> repository contains USB host and device class drivers distributed through the ESP Component Registry <a href="#ref11">[11]</a>.</p>
<p>We focused on USB host mode in the HID and UVC class drivers. Espressif documents the USB Host Library as the lowest public-facing API layer of the host stack. Class drivers sit above it and handle class-specific descriptors, transfers, callbacks, and device lifecycle events <a href="#ref12">[12]</a>.</p>
<p>A connected USB device controls the descriptors and lengths consumed during enumeration. It also controls when it disconnects and how it behaves during transfers. That makes the class-driver boundary attacker-controlled even though the attack requires physical access. The findings affect host-side drivers, not Espressif&rsquo;s USB device mode.</p>
<p>The following sections cite the public advisories and fixing commits for the affected versions, vulnerability details, and patches.</p>
<h2 id="the-espressif-findings">The Espressif Findings</h2>
<div class="finding-summary" aria-label="Espressif USB vulnerability summary">
  <dl class="finding-summary__item">
    <div class="finding-summary__field"><dt>CVE</dt><dd><a href="https://github.com/espressif/esp-usb/security/advisories/GHSA-2pm2-62mr-c9x7">CVE-2025-68656</a></dd></div>
    <div class="finding-summary__field"><dt>Component</dt><dd><code>usb_host_hid</code></dd></div>
    <div class="finding-summary__field finding-summary__field--wide"><dt>Analysis problem</dt><dd>Stale alias after transfer reallocation</dd></div>
    <div class="finding-summary__field"><dt>Affected</dt><dd><code>1.0.4</code> and earlier</dd></div>
    <div class="finding-summary__field"><dt>Fixed</dt><dd><code>1.1.0</code></dd></div>
  </dl>
  <dl class="finding-summary__item">
    <div class="finding-summary__field"><dt>CVE</dt><dd><a href="https://github.com/espressif/esp-usb/security/advisories/GHSA-gp8r-qjfr-gqfv">CVE-2025-68657</a></dd></div>
    <div class="finding-summary__field"><dt>Component</dt><dd><code>usb_host_hid</code></dd></div>
    <div class="finding-summary__field finding-summary__field--wide"><dt>Analysis problem</dt><dd>Concurrent teardown of the same transfer</dd></div>
    <div class="finding-summary__field"><dt>Affected</dt><dd><code>1.0.4</code> and earlier</dd></div>
    <div class="finding-summary__field"><dt>Fixed</dt><dd><code>1.1.0</code></dd></div>
  </dl>
  <dl class="finding-summary__item">
    <div class="finding-summary__field"><dt>CVE</dt><dd><a href="https://github.com/espressif/esp-usb/security/advisories/GHSA-g65h-9ggq-9827">CVE-2025-68622</a></dd></div>
    <div class="finding-summary__field"><dt>Component</dt><dd><code>usb_host_uvc</code></dd></div>
    <div class="finding-summary__field finding-summary__field--wide"><dt>Analysis problem</dt><dd>Descriptor length copied into a fixed stack buffer</dd></div>
    <div class="finding-summary__field"><dt>Affected</dt><dd><code>2.3.1</code> and earlier</dd></div>
    <div class="finding-summary__field"><dt>Fixed</dt><dd><code>2.4.0</code></dd></div>
  </dl>
</div>
<p>GitHub classifies all three advisories as Moderate. Their CVSS 3.1 base scores are <code>6.8</code>, <code>6.4</code>, and <code>6.8</code>. The affected and fixed versions above come from the published advisories <a href="#ref8">[8]</a> <a href="#ref13">[13]</a> <a href="#ref15">[15]</a>.</p>
<h3 id="cve-2025-68656-hid-descriptor-use-after-free">CVE-2025-68656: HID Descriptor Use-After-Free</h3>
<p>The first finding shows the method on a concrete lifetime path. During enumeration, a HID device advertises <code>wReport<wbr>Descriptor<wbr>Length</code>. The host uses that value for <code>req-&gt;wLength</code>. If the current control transfer is too small, <code>usb_class_request_<wbr>get_descriptor()</code> frees <code>hid_device-&gt;<wbr>ctrl_xfer</code> and allocates a larger transfer. The local <code>ctrl_xfer</code> pointer was captured before the free and remains unchanged:</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="kt">usb_transfer_t</span> <span class="o">*</span><span class="n">ctrl_xfer</span> <span class="o">=</span> <span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">const</span> <span class="kt">size_t</span> <span class="n">ctrl_size</span> <span class="o">=</span> <span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="o">-&gt;</span><span class="n">data_buffer_size</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">ctrl_size</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">USB_SETUP_PACKET_SIZE</span> <span class="o">+</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">wLength</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nf">usb_host_transfer_free</span><span class="p">(</span><span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nf">HID_RETURN_ON_ERROR</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nf">usb_host_transfer_alloc</span><span class="p">(</span><span class="n">USB_SETUP_PACKET_SIZE</span> <span class="o">+</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">wLength</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                                <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s">&#34;Unable to allocate transfer buffer for EP0&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kt">usb_setup_packet_t</span> <span class="o">*</span><span class="n">setup</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">(</span><span class="kt">usb_setup_packet_t</span> <span class="o">*</span><span class="p">)</span><span class="n">ctrl_xfer</span><span class="o">-&gt;</span><span class="n">data_buffer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">setup</span><span class="o">-&gt;</span><span class="n">bmRequestType</span> <span class="o">=</span> <span class="n">USB_BM_REQUEST_TYPE_DIR_IN</span> <span class="o">|</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">                       <span class="n">USB_BM_REQUEST_TYPE_TYPE_STANDARD</span> <span class="o">|</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">                       <span class="n">USB_BM_REQUEST_TYPE_RECIP_INTERFACE</span><span class="p">;</span>
</span></span></code></pre></div><p>The AST contains every statement above. The security evidence is in the relationships between them:</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 470" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="uaf-t uaf-d">
<title id="uaf-t">What the release actually frees, and what the local pointer still refers to</title>
<desc id="uaf-d">The hid_device_t field ctrl_xfer refers to an urb_t, whose transfer.data_buffer refers to a separate heap buffer. usb_host_transfer_free releases both objects. The local ctrl_xfer pointer was captured before the branch and still refers to the released urb_t when the setup packet is written through it.</desc>
<defs>
<marker id="uaf-own" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#8b5cf6"/></marker>
<marker id="uaf-buf" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="var(--color-secondary)"/></marker>
<marker id="uaf-bad" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#e11d48"/></marker>
<style>
.uf-bg{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.4}
.uf-box{fill:var(--color-muted-bg);stroke:color-mix(in srgb,var(--color-text-secondary) 45%,transparent);stroke-width:1.5}
.uf-dead{fill:color-mix(in srgb,#e11d48 6%,var(--color-muted-bg));stroke:#e11d48;stroke-width:1.8;stroke-dasharray:6 4}
.uf-inner{fill:none;stroke:color-mix(in srgb,var(--color-text-secondary) 40%,transparent);stroke-width:1.2}
.uf-hl-own{fill:color-mix(in srgb,#8b5cf6 18%,transparent);stroke:#8b5cf6;stroke-width:1.4;stroke-dasharray:4 3}
.uf-hl-buf{fill:color-mix(in srgb,var(--color-secondary) 16%,transparent);stroke:var(--color-secondary);stroke-width:1.4;stroke-dasharray:4 3}
.uf-stale{fill:color-mix(in srgb,#e11d48 8%,var(--color-muted-bg));stroke:#e11d48;stroke-width:1.8}
.uf-name{fill:var(--color-text);font-family:var(--font-sans,system-ui,sans-serif);font-weight:700}
.uf-f{fill:var(--color-text);font-family:var(--font-mono,ui-monospace,Menlo,monospace);opacity:.92}
.uf-title{fill:var(--color-text);font-family:var(--font-sans,system-ui,sans-serif)}
.uf-muted{fill:var(--color-text-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
.uf-red{fill:var(--ink-red);font-family:var(--font-sans,system-ui,sans-serif)}
.uf-purple{fill:var(--ink-purple);font-family:var(--font-sans,system-ui,sans-serif)}
.uf-cy{fill:var(--color-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
.uf-own{stroke:#8b5cf6;stroke-width:2.2;fill:none;marker-end:url(#uaf-own)}
.uf-buf{stroke:var(--color-secondary);stroke-width:2.2;fill:none;marker-end:url(#uaf-buf)}
.uf-bad{stroke:#e11d48;stroke-width:2.4;fill:none;stroke-dasharray:6 4;marker-end:url(#uaf-bad)}
</style>
</defs>
<rect class="uf-bg" x="1" y="1" width="898" height="468" rx="12"/>
<text class="uf-title" x="32" y="38" font-size="18" font-weight="700">CVE-2025-68656: the release frees two objects, the local pointer refers to one of them</text>
<text class="uf-muted" x="32" y="60" font-size="12.5">Ownership runs left to right. Everything inside the dashed red outline is released by a single call.</text>
<rect class="uf-box" x="32" y="88" width="246" height="182" rx="8"/>
<text class="uf-name" x="155" y="114" font-size="13" text-anchor="middle">hid_device_t</text>
<text class="uf-f" x="44" y="142" font-size="10">SemaphoreHandle_t device_busy</text>
<text class="uf-f" x="44" y="164" font-size="10">SemaphoreHandle_t ctrl_xfer_done</text>
<rect class="uf-hl-own" x="40" y="172" width="230" height="22" rx="4"/>
<text class="uf-f" x="44" y="187" font-size="10">usb_transfer_t *ctrl_xfer</text>
<text class="uf-f" x="44" y="216" font-size="10">usb_device_handle_t dev_hdl</text>
<text class="uf-f" x="44" y="238" font-size="10">uint8_t dev_addr</text>
<rect class="uf-dead" x="326" y="76" width="266" height="248" rx="8"/>
<text class="uf-name" x="459" y="102" font-size="13" text-anchor="middle">urb_t</text>
<text class="uf-f" x="338" y="128" font-size="10">TAILQ_ENTRY tailq_entry</text>
<text class="uf-f" x="338" y="148" font-size="10">void *hcd_ptr</text>
<text class="uf-f" x="338" y="168" font-size="10">uint32_t hcd_var</text>
<text class="uf-f" x="338" y="188" font-size="10">void *usb_host_client</text>
<rect class="uf-inner" x="334" y="200" width="250" height="112" rx="6"/>
<text class="uf-muted" x="344" y="216" font-size="9.5">usb_transfer_t transfer</text>
<rect class="uf-hl-buf" x="340" y="224" width="238" height="22" rx="4"/>
<text class="uf-f" x="344" y="239" font-size="10">uint8_t *data_buffer</text>
<text class="uf-f" x="344" y="262" font-size="10">size_t data_buffer_size</text>
<text class="uf-f" x="344" y="282" font-size="10">int num_bytes</text>
<text class="uf-f" x="344" y="302" font-size="10">usb_transfer_cb_t callback</text>
<rect class="uf-dead" x="646" y="140" width="218" height="120" rx="8"/>
<text class="uf-name" x="755" y="168" font-size="13" text-anchor="middle">data_buffer</text>
<text class="uf-f" x="660" y="196" font-size="10">uint8_t[]</text>
<text class="uf-f" x="660" y="220" font-size="10">initial: 512 B</text>
<text class="uf-f" x="660" y="242" font-size="10">realloc: align(8 + N)</text>
<path class="uf-own" d="M270 183 C296 183 300 172 320 168"/>
<text class="uf-purple" x="292" y="215" font-size="10.5" text-anchor="middle">field</text>
<path class="uf-buf" d="M578 235 C608 235 614 214 640 206"/>
<text class="uf-cy" x="606" y="256" font-size="10.5" text-anchor="middle">buffer</text>
<rect class="uf-stale" x="32" y="330" width="246" height="66" rx="8"/>
<text class="uf-f" x="46" y="356" font-size="11">ctrl_xfer (local)</text>
<text class="uf-red" x="46" y="378" font-size="10.5">captured before the branch</text>
<path class="uf-bad" d="M278 356 C330 356 356 348 388 330"/>
<text class="uf-red" x="300" y="410" font-size="10.5">still refers to the released urb_t</text>
<text class="uf-red" x="868" y="300" font-size="11.5" font-weight="700" text-anchor="end">usb_host_transfer_free() releases both</text>
<text class="uf-f" x="868" y="322" font-size="9.5" text-anchor="end">heap_caps_free(urb-&gt;transfer.data_buffer);</text>
<text class="uf-f" x="868" y="340" font-size="9.5" text-anchor="end">heap_caps_free(urb);</text>
<text class="uf-muted" x="32" y="432" font-size="11">On success, reallocation replaces <tspan class="uf-f" font-size="10.5">hid_device-&gt;ctrl_xfer</tspan> but not the local copy. On error, the function returns before</text>
<text class="uf-muted" x="32" y="450" font-size="11">this dereference; on success, <tspan class="uf-f" font-size="10.5">ctrl_xfer-&gt;data_buffer</tspan> reaches memory whose lifetime has ended.</text>
</svg>
</div>
<p><em>Figure 7. A single <code>usb_host_transfer_free()</code> releases both the <code>urb_t</code> and the <code>data_buffer</code> it owns. The local <code>ctrl_xfer</code> pointer was read from the field before the branch. If reallocation succeeds, the field receives a new transfer but the local pointer still refers to the released <code>urb_t</code> when the setup packet is written through it; if allocation fails, the error path returns before that write.</em></p>
<p>The function then writes the USB setup packet through <code>ctrl_xfer-&gt;data_buffer</code>. Espressif&rsquo;s advisory describes attacker-controlled writes to freed heap memory, arbitrary memory corruption, and potential code execution <a href="#ref8">[8]</a>.</p>
<p>Espressif&rsquo;s fix validates the requested length, performs any required reallocation, and only then reads the field into the local pointer <a href="#ref9">[9]</a>:</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">req</span><span class="o">-&gt;</span><span class="n">wLength</span> <span class="o">&gt;</span> <span class="n">HID_MAX_REPORT_DESC_LEN</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">return</span> <span class="n">ESP_ERR_INVALID_SIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// Argument checks and lock acquisition omitted.
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="n">ctrl_size</span> <span class="o">&lt;</span> <span class="n">required_size</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nf">usb_host_transfer_free</span><span class="p">(</span><span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">usb_host_transfer_alloc</span><span class="p">(</span><span class="n">required_size</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                                <span class="o">&amp;</span><span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="p">)</span> <span class="o">!=</span> <span class="n">ESP_OK</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nf">hid_device_unlock</span><span class="p">(</span><span class="n">hid_device</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">return</span> <span class="n">ESP_ERR_NO_MEM</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="kt">usb_transfer_t</span> <span class="o">*</span><span class="n">ctrl_xfer</span> <span class="o">=</span> <span class="n">hid_device</span><span class="o">-&gt;</span><span class="n">ctrl_xfer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kt">usb_setup_packet_t</span> <span class="o">*</span><span class="n">setup</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">(</span><span class="kt">usb_setup_packet_t</span> <span class="o">*</span><span class="p">)</span><span class="n">ctrl_xfer</span><span class="o">-&gt;</span><span class="n">data_buffer</span><span class="p">;</span>
</span></span></code></pre></div><p>The reported stale alias disappears when the alias is created after the field replacement. The same patch caps report descriptors at 2048 bytes, handles allocation failure, and enables tests for large report descriptors. This is the finding we took into the exploitation work.</p>
<h3 id="cve-2025-68657-hid-close-path-race">CVE-2025-68657: HID Close-Path Race</h3>
<p>The second HID issue is in teardown rather than descriptor parsing. USB event handling and application code can both reach <code>hid_host_device_close()</code> while sharing the same <code>hid_iface_t</code> state. Existing FreeRTOS critical sections and <code>device_busy</code> serialize parts of this path, but they do not make the READY-state check and transfer release one atomic operation across both callers. Two callers can therefore proceed to free the same <code>usb_transfer_t</code> <a href="#ref13">[13]</a>.</p>
<p>This requires evidence from two execution paths. A single-function pattern can find the release operation, but it cannot establish competing ownership or determine whether the existing synchronization covers the state-check-to-release transition.</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 620" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="race-title race-desc">
  <title id="race-title">Two potentially concurrent teardown paths over the same transfer</title>
  <desc id="race-desc">The USB event task and the application task can both enter the close path, both observe the READY state, and both release the same transfer object. Existing synchronization does not make the region between the check and the release atomic across both callers in the vulnerable version. The fix holds an open_close_mutex across the check, interface removal, and release.</desc>
  <defs>
    <marker id="rc-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 Z" fill="#d97706"/></marker>
    <marker id="rc-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 Z" fill="#e11d48"/></marker>
    <style>
      .rc-bg{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.4}
      .rc-lane{fill:none;stroke:var(--color-border);stroke-width:1.4;stroke-dasharray:4 4}
      .rc-node{fill:var(--color-muted-bg);stroke:color-mix(in srgb,var(--color-text-secondary) 45%,transparent);stroke-width:1.5}
      .rc-conc{fill:var(--color-muted-bg);stroke:color-mix(in srgb,var(--color-text-secondary) 55%,transparent);stroke-width:1.6}
      .rc-bad{fill:color-mix(in srgb,#e11d48 12%,var(--color-muted-bg));stroke:#e11d48;stroke-width:1.8}
      .rc-fix{fill:color-mix(in srgb,var(--color-accent) 12%,var(--color-muted-bg));stroke:var(--color-accent);stroke-width:1.6}
      .rc-shared{fill:color-mix(in srgb,#8b5cf6 12%,var(--color-muted-bg));stroke:#8b5cf6;stroke-width:1.6}
      .rc-text{fill:var(--color-text);font-family:var(--font-mono,ui-monospace,Menlo,monospace)}
      .rc-title{fill:var(--color-text);font-family:var(--font-sans,system-ui,sans-serif)}
      .rc-muted{fill:var(--color-text-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
      .rc-red{fill:var(--ink-red);font-family:var(--font-sans,system-ui,sans-serif)}
            .rc-purple{fill:var(--ink-purple);font-family:var(--font-sans,system-ui,sans-serif)}
      .rc-cf{stroke:#d97706;stroke-width:2;fill:none;marker-end:url(#rc-amber)}
      .rc-danger{stroke:#e11d48;stroke-width:2.5;fill:none;marker-end:url(#rc-red)}
      .rc-nohb{stroke:#e11d48;stroke-width:2;stroke-dasharray:8 5;fill:none}
    </style>
  </defs>
  <rect class="rc-bg" x="1" y="1" width="898" height="618" rx="12"/>
  <text class="rc-title" x="36" y="42" font-size="20" font-weight="700">CVE-2025-68657: two teardown paths, one transfer</text>
  <text class="rc-muted" x="36" y="66" font-size="13">Both lanes may execute in parallel; existing synchronization does not cover the whole transition.</text>
  <rect class="rc-lane" x="52" y="90" width="330" height="300" rx="8"/>
  <rect class="rc-lane" x="518" y="90" width="330" height="300" rx="8"/>
  <text class="rc-title" x="217" y="116" font-size="13" font-weight="700" text-anchor="middle">USB event task</text>
  <text class="rc-title" x="683" y="116" font-size="13" font-weight="700" text-anchor="middle">Application task</text>
  <rect class="rc-conc" x="82" y="134" width="270" height="46" rx="7"/>
  <text class="rc-text" x="217" y="162" font-size="12" text-anchor="middle">hid_host_device_close()</text>
  <rect class="rc-conc" x="548" y="134" width="270" height="46" rx="7"/>
  <text class="rc-text" x="683" y="162" font-size="12" text-anchor="middle">hid_host_device_close()</text>
  <path class="rc-cf" d="M217 180 V212"/>
  <path class="rc-cf" d="M683 180 V212"/>
  <rect class="rc-node" x="82" y="216" width="270" height="46" rx="7"/>
  <text class="rc-text" x="217" y="244" font-size="12" text-anchor="middle">state == READY</text>
  <rect class="rc-node" x="548" y="216" width="270" height="46" rx="7"/>
  <text class="rc-text" x="683" y="244" font-size="12" text-anchor="middle">state == READY</text>
  <path class="rc-cf" d="M217 262 V300"/>
  <path class="rc-cf" d="M683 262 V300"/>
  <rect class="rc-bad" x="82" y="304" width="270" height="52" rx="7"/>
  <text class="rc-text" x="217" y="326" font-size="12" text-anchor="middle">free(transfer T)</text>
  <text class="rc-red" x="217" y="346" font-size="11" text-anchor="middle">releases the shared object</text>
  <rect class="rc-bad" x="548" y="304" width="270" height="52" rx="7"/>
  <text class="rc-text" x="683" y="326" font-size="12" text-anchor="middle">free(transfer T)</text>
  <text class="rc-red" x="683" y="346" font-size="11" text-anchor="middle">releases the shared object</text>
  <path class="rc-nohb" d="M382 240 H518"/>
  <text class="rc-red" x="450" y="232" font-size="11" text-anchor="middle">existing synchronization</text>
  <text class="rc-red" x="450" y="262" font-size="11" text-anchor="middle">does not cover the full transition</text>
  <rect class="rc-shared" x="330" y="404" width="240" height="52" rx="7"/>
  <text class="rc-text" x="450" y="426" font-size="12" text-anchor="middle">shared usb_transfer_t T</text>
  <text class="rc-purple" x="450" y="446" font-size="11" text-anchor="middle">same abstract object in both lanes</text>
  <path class="rc-danger" d="M217 356 C217 392 300 412 324 424"/>
  <path class="rc-danger" d="M683 356 C683 392 600 412 576 424"/>
  <text class="rc-red" x="450" y="482" font-size="12" text-anchor="middle" font-weight="700">second release is a double free</text>
  <rect class="rc-fix" x="82" y="508" width="736" height="86" rx="7"/>
  <text class="rc-title" x="102" y="532" font-size="12" font-weight="700">Fix: one lock covers check, removal, and release</text>
  <text class="rc-text" x="102" y="554" font-size="12">lock(open_close_mutex)  →  check READY  →  remove interface  →  release transfer  →  unlock</text>
  <text class="rc-muted" x="102" y="576" font-size="11">The mutex establishes the happens-before relation the vulnerable version was missing.</text>
</svg>
</div>
<p><em>Figure 8. The analysis needs evidence of three things: two paths that may execute in parallel, the same abstract transfer object reached from both, and a gap in the synchronization of the state-check-to-release transition. The fix adds a mutex that covers that transition.</em></p>
<p>Espressif&rsquo;s fix adds an <code>open_close_mutex</code> and holds it across the open and close paths, including the state check and interface removal <a href="#ref14">[14]</a>. The advisory also records an important practical limit: the READY-to-free window is estimated at 0.1 to 1 microsecond, while detach handling and application reaction occur on a millisecond scale. The proof of concept widened that window to demonstrate the issue deterministically <a href="#ref13">[13]</a>.</p>
<h3 id="cve-2025-68622-uvc-descriptor-stack-overflow">CVE-2025-68622: UVC Descriptor Stack Overflow</h3>
<p>The UVC finding is a direct bounds path in <code>print_vs_frame_mjpeg_desc()</code>. When configuration-descriptor printing is enabled, the function reads the device-controlled <code>bLength</code> and copies that many bytes into <code>raw_desc[25]</code>, a 100-byte stack buffer:</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="kt">uint32_t</span> <span class="n">raw_desc</span><span class="p">[</span><span class="mi">25</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kt">uint32_t</span> <span class="n">desc_size</span> <span class="o">=</span> <span class="p">((</span><span class="k">const</span> <span class="kt">uvc_frame_desc_t</span> <span class="o">*</span><span class="p">)</span><span class="n">_desc</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">bLength</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nf">memcpy</span><span class="p">(</span><span class="n">raw_desc</span><span class="p">,</span> <span class="n">_desc</span><span class="p">,</span> <span class="n">desc_size</span><span class="p">);</span>
</span></span></code></pre></div><p>A UVC frame descriptor can contain a variable-length frame-interval array. The vulnerable code did not compare <code>desc_size</code> with <code>sizeof(raw_desc)</code> before the copy. A malicious camera could therefore corrupt the stack during enumeration when <code>CONFIG_UVC_<wbr>PRINTF_<wbr>CONFIGURATION_<wbr>DESCRIPTOR</code> was enabled <a href="#ref15">[15]</a>.</p>
<div class="figure-scroll" role="region" tabindex="0" aria-label="Figure, scrollable">
<svg viewBox="0 0 900 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ovf-title ovf-desc">
  <title id="ovf-title">The bounds constraint for the UVC descriptor stack overflow</title>
  <desc id="ovf-desc">A device-controlled bLength becomes desc_size and reaches memcpy as the copy length. The destination raw_desc is a fixed 25-element array of 32-bit words, 100 bytes. The required guard comparing desc_size with the destination size is absent, and the whole path exists only when descriptor printing is enabled in the build configuration.</desc>
  <defs>
    <marker id="ov-gray" 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="ov-lime" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 Z" fill="var(--color-accent)"/></marker>
    <style>
      .ov-bg{fill:var(--color-bg-card);stroke:var(--color-border);stroke-width:1.4}
      .ov-node{fill:var(--color-muted-bg);stroke:color-mix(in srgb,var(--color-text-secondary) 45%,transparent);stroke-width:1.5}
      .ov-input{fill:var(--color-muted-bg);stroke:var(--color-accent);stroke-width:2.2}
      .ov-bad{fill:color-mix(in srgb,#e11d48 12%,var(--color-muted-bg));stroke:#e11d48;stroke-width:1.8}
      .ov-guard{fill:var(--color-muted-bg);stroke:#e11d48;stroke-width:1.6;stroke-dasharray:6 4}
      .ov-build{fill:color-mix(in srgb,#d97706 10%,var(--color-muted-bg));stroke:#d97706;stroke-width:1.5}
      .ov-text{fill:var(--color-text);font-family:var(--font-mono,ui-monospace,Menlo,monospace)}
      .ov-title{fill:var(--color-text);font-family:var(--font-sans,system-ui,sans-serif)}
      .ov-muted{fill:var(--color-text-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
      .ov-red{fill:var(--ink-red);font-family:var(--font-sans,system-ui,sans-serif)}
      .ov-amber{fill:var(--ink-amber);font-family:var(--font-sans,system-ui,sans-serif)}
      .ov-cyantxt{fill:var(--color-secondary);font-family:var(--font-sans,system-ui,sans-serif)}
      .ov-cf{stroke:var(--color-text-secondary);stroke-width:2;fill:none;marker-end:url(#ov-gray)}
      .ov-data{stroke:var(--color-accent);stroke-width:2;stroke-dasharray:5 4;fill:none;marker-end:url(#ov-lime)}
    </style>
  </defs>
  <rect class="ov-bg" x="1" y="1" width="898" height="558" rx="12"/>
  <text class="ov-title" x="36" y="42" font-size="20" font-weight="700">CVE-2025-68622: copy length against destination extent</text>
  <text class="ov-muted" x="36" y="66" font-size="13">The bug is the missing relation between the two sides of the copy.</text>
  <rect class="ov-build" x="36" y="92" width="828" height="42" rx="7"/>
  <text class="ov-text" x="56" y="119" font-size="12">CONFIG_UVC_PRINTF_CONFIGURATION_DESCRIPTOR = y</text>
  <text class="ov-amber" x="600" y="119" font-size="11">build condition — path exists only here</text>
  <text class="ov-muted" x="36" y="176" font-size="11" font-weight="700">SOURCE → SINK</text>
  <rect class="ov-input" x="36" y="190" width="230" height="56" rx="7"/>
  <text class="ov-text" x="56" y="214" font-size="12">descriptor.bLength</text>
  <text class="ov-muted" x="56" y="234" font-size="11">device-controlled</text>
  <path class="ov-data" d="M266 218 H316"/>
  <rect class="ov-node" x="320" y="190" width="180" height="56" rx="7"/>
  <text class="ov-text" x="410" y="214" font-size="12" text-anchor="middle">desc_size</text>
  <text class="ov-muted" x="410" y="234" font-size="11" text-anchor="middle">copy length</text>
  <path class="ov-data" d="M500 218 H550"/>
  <rect class="ov-bad" x="554" y="184" width="310" height="68" rx="7"/>
  <text class="ov-text" x="574" y="208" font-size="12">memcpy(raw_desc, _desc, desc_size)</text>
  <text class="ov-red" x="574" y="230" font-size="11">copy length is not bounded by the stack buffer</text>
  <text class="ov-muted" x="36" y="300" font-size="11" font-weight="700">DESTINATION EXTENT</text>
  <rect class="ov-node" x="36" y="314" width="230" height="76" rx="7"/>
  <text class="ov-text" x="56" y="338" font-size="12">uint32_t raw_desc[25]</text>
  <text class="ov-muted" x="56" y="358" font-size="11">25 × sizeof(uint32_t)</text>
  <text class="ov-muted" x="56" y="376" font-size="11">= 100 bytes of stack storage</text>
  <path class="ov-cf" d="M266 352 C400 352 480 300 606 258"/>
  <text class="ov-muted" x="410" y="330" font-size="11">bounds the destination</text>
  <rect class="ov-guard" x="330" y="410" width="534" height="100" rx="7"/>
  <text class="ov-title" x="350" y="436" font-size="12" font-weight="700">The predicate that decides the outcome</text>
  <text class="ov-text" x="350" y="462" font-size="12">required:</text><text class="ov-text" x="470" y="462" font-size="12">desc_size &lt;= sizeof(raw_desc)</text>
  <text class="ov-text" x="350" y="488" font-size="12">observed on this path:</text><text class="ov-red" x="520" y="488" font-size="12" font-weight="700">no applicable bound</text>
  <rect class="ov-node" x="36" y="410" width="270" height="100" rx="7"/>
  <text class="ov-title" x="56" y="436" font-size="12" font-weight="700">What the analysis must recover</text>
  <text class="ov-muted" x="56" y="458" font-size="11">the untrusted value and its flow</text>
  <text class="ov-muted" x="56" y="476" font-size="11">the destination extent</text>
  <text class="ov-muted" x="56" y="494" font-size="11">every guard on the path between them</text>
</svg>
</div>
<p><em>Figure 9. A bounds finding appears when a device-controlled copy length is not checked against the destination extent on the analyzed path. Here the destination holds 100 bytes and no predicate on that path relates the two. The build condition is part of the evidence because the vulnerable path is compiled only when descriptor printing is enabled.</em></p>
<p>Espressif fixed the issue in commit <code>77a38b1</code> by removing the temporary stack copy and reading the descriptor directly. The commit applies the same change to both MJPEG and frame-based descriptor printers <a href="#ref16">[16]</a>.</p>
<h2 id="fixes">Fixes</h2>
<p>Users of the affected components should update <code>usb_host_hid</code> to <code>1.1.0</code> or later and <code>usb_host_uvc</code> to <code>2.4.0</code> or later. Products that vendor or fork these components should verify that the three fixing commits are present rather than relying only on package metadata.</p>
<p>The three findings also show why one generic code excerpt is not enough for automated vulnerability research. The UVC overflow needs a source, destination extent, and missing bound. The HID use-after-free needs object identity across an alias, free, field replacement, and later use. The close-path race needs two potentially concurrent teardown paths over the same shared object and the synchronization governing their state-check-to-release transition.</p>
<p>Static analysis identifies the stale write in <code>CVE-2025-68656</code> and reconstructs the device-controlled path to it. Turning that into a working exploit asked a different set of questions: allocator behavior, heap reuse, and how much a malicious USB device actually controls on the ESP32 target. That is where the analysis stops and the target-specific work begins.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] OpenAI. &ldquo;Introducing gpt-oss.&rdquo; August 5, 2025. <a href="https://openai.com/index/introducing-gpt-oss/" rel="noopener noreferrer" target="_blank">Product and model specifications</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] Mark Weiser. &ldquo;Program Slicing.&rdquo; <em>IEEE Transactions on Software Engineering</em>, 1984. <a href="https://doi.org/10.1109/TSE.1984.5010248" rel="noopener noreferrer" target="_blank">DOI</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] Jeanne Ferrante, Karl J. Ottenstein, and Joe D. Warren. &ldquo;The Program Dependence Graph and Its Use in Optimization.&rdquo; <em>ACM Transactions on Programming Languages and Systems</em>, 1987. <a href="https://doi.org/10.1145/24039.24041" rel="noopener noreferrer" target="_blank">DOI</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] Thomas Reps, Susan Horwitz, and Mooly Sagiv. &ldquo;Precise Interprocedural Dataflow Analysis via Graph Reachability.&rdquo; <em>POPL</em>, 1995. <a href="https://doi.org/10.1145/199448.199462" rel="noopener noreferrer" target="_blank">DOI</a></p>
</li>
<li>
<p><a id="ref5"></a>[5] Fabian Yamaguchi, Nico Golde, Daniel Arp, and Konrad Rieck. &ldquo;Modeling and Discovering Vulnerabilities with Code Property Graphs.&rdquo; <em>IEEE Symposium on Security and Privacy</em>, 2014. <a href="https://www.ieee-security.org/TC/SP2014/papers/ModelingandDiscoveringVulnerabilitieswithCodePropertyGraphs.pdf" rel="noopener noreferrer" target="_blank">Paper</a></p>
</li>
<li>
<p><a id="ref6"></a>[6] Ahmed Lekssays, Hamza Mouhcine, Khang Tran, Ting Yu, and Issa Khalil. &ldquo;LLMxCPG: Context-Aware Vulnerability Detection Through Code Property Graph-Guided Large Language Models.&rdquo; <em>34th USENIX Security Symposium</em>, 2025. <a href="https://www.usenix.org/conference/usenixsecurity25/presentation/lekssays" rel="noopener noreferrer" target="_blank">Paper and presentation</a></p>
</li>
<li>
<p><a id="ref7"></a>[7] Ahmed Lekssays. &ldquo;Bridging Code Property Graphs and Language Models for Program Analysis.&rdquo; <em>Software Vulnerability Management Workshop @ ICSE 2026</em>. <a href="https://arxiv.org/abs/2603.24837" rel="noopener noreferrer" target="_blank">Paper</a></p>
</li>
<li>
<p><a id="ref8"></a>[8] Espressif. &ldquo;USB Host HID Descriptor Use-After-Free Vulnerability.&rdquo; <a href="https://github.com/espressif/esp-usb/security/advisories/GHSA-2pm2-62mr-c9x7" rel="noopener noreferrer" target="_blank">GHSA-2pm2-62mr-c9x7</a></p>
</li>
<li>
<p><a id="ref9"></a>[9] Espressif. &ldquo;Changed the handling of reallocating ctrl_xfer buffer on large report descriptors&rdquo; (<code>81b37c9</code>, November 25, 2025). <a href="https://github.com/espressif/esp-usb/commit/81b37c96593c0bec92ef14c6ee6bf8cab8d8f660" rel="noopener noreferrer" target="_blank">Fix commit</a></p>
</li>
<li>
<p><a id="ref10"></a>[10] Espressif. &ldquo;Espressif IoT Development Framework.&rdquo; <a href="https://github.com/espressif/esp-idf" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref11"></a>[11] Espressif. &ldquo;Espressif ESP-USB.&rdquo; <a href="https://github.com/espressif/esp-usb" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref12"></a>[12] Espressif. &ldquo;USB Host.&rdquo; <em>ESP-IDF Programming Guide</em>. <a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/usb_host.html" rel="noopener noreferrer" target="_blank">Documentation</a></p>
</li>
<li>
<p><a id="ref13"></a>[13] Espressif. &ldquo;Double-Free Race Condition in USB Host HID Device Close Path.&rdquo; <a href="https://github.com/espressif/esp-usb/security/advisories/GHSA-gp8r-qjfr-gqfv" rel="noopener noreferrer" target="_blank">GHSA-gp8r-qjfr-gqfv</a></p>
</li>
<li>
<p><a id="ref14"></a>[14] Espressif. &ldquo;Fixed race condition in hid_host_device_close()&rdquo; (<code>cd28106</code>, January 9, 2026). <a href="https://github.com/espressif/esp-usb/commit/cd28106e9f72ac2719682c06f94601f9f034390b" rel="noopener noreferrer" target="_blank">Fix commit</a></p>
</li>
<li>
<p><a id="ref15"></a>[15] Espressif. &ldquo;Stack buffer overflow in UVC descriptor printing.&rdquo; <a href="https://github.com/espressif/esp-usb/security/advisories/GHSA-g65h-9ggq-9827" rel="noopener noreferrer" target="_blank">GHSA-g65h-9ggq-9827</a></p>
</li>
<li>
<p><a id="ref16"></a>[16] Espressif. &ldquo;Fixed potential buffer overflow in descriptor printing&rdquo; (<code>77a38b1</code>, November 21, 2025). <a href="https://github.com/espressif/esp-usb/commit/77a38b15a17f6e3c7aeb620eb4aeaf61d5194cc0" rel="noopener noreferrer" target="_blank">Fix commit</a></p>
</li>
<li>
<p><a id="ref17"></a>[17] The Code Property Graph specification. &ldquo;Overlays.&rdquo; <a href="https://cpg.joern.io/" rel="noopener noreferrer" target="_blank">Specification</a></p>
</li>
<li>
<p><a id="ref18"></a>[18] Clang. &ldquo;JSON Compilation Database Format Specification.&rdquo; <a href="https://clang.llvm.org/docs/JSONCompilationDatabase.html" rel="noopener noreferrer" target="_blank">Documentation</a></p>
</li>
<li>
<p><a id="ref19"></a>[19] Anthropic. &ldquo;Assessing Claude Mythos Preview’s cybersecurity capabilities.&rdquo; April 2026. <a href="https://www.anthropic.com/research/mythos-preview" rel="noopener noreferrer" target="_blank">Research</a></p>
</li>
<li>
<p><a id="ref20"></a>[20] Anthropic. &ldquo;How Claude marks AI-generated content.&rdquo; <a href="https://support.claude.com/en/articles/16266773-how-claude-marks-ai-generated-content" rel="noopener noreferrer" target="_blank">Support documentation</a></p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item><item><title>Four Vulnerabilities in Tuya's Arduino TuyaOpen Framework</title><link>https://blog.secmate.dev/posts/tuya-arduino-tuyaopen-vulnerabilities-disclosure/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><atom:updated>2026-05-21T00:00:00+00:00</atom:updated><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>IoT</category><guid>https://blog.secmate.dev/posts/tuya-arduino-tuyaopen-vulnerabilities-disclosure/</guid><description>SecMate found four vulnerabilities across Tuya's Arduino framework libraries: DNSServer, TuyaIoT, WiFiMulti, and WiFiUDP. The attacker paths include same-LAN DNS and UDP traffic, malicious AP connections, and malicious DP events delivered through the Tuya cloud path.</description><content:encoded><![CDATA[<h1 id="four-vulnerabilities-in-tuyas-arduino-tuyaopen-framework">Four Vulnerabilities in Tuya&rsquo;s Arduino TuyaOpen Framework</h1>
<p>Tuya is an IoT platform vendor. Its developer documentation describes a public-cloud platform for smart-device development, covering hardware development tools, app tools, cloud services, and smart-industry development <a href="#ref1">[1]</a>.</p>
<p>SecMate found four vulnerabilities in <a href="https://github.com/tuya/arduino-TuyaOpen" rel="noopener noreferrer" target="_blank">arduino-tuyaopen</a> <a href="#ref2">[2]</a>, Tuya&rsquo;s Arduino framework for connected devices. The issues affect DNS parsing, Tuya Data Point handling, Wi-Fi credential storage, and UDP packet reception: <code>CVE-2026-28519</code>, <code>CVE-2026-28521</code>, <code>CVE-2026-28520</code>, and <code>CVE-2026-28522</code>.</p>
<p>The bugs are in reusable framework libraries. They do not form one exploit chain, and the evidence does not support a claim that every TuyaOpen device is affected.</p>
<p>All four issues were fixed in commit <code>54ac201</code> on February 25, 2026 <a href="#ref6">[6]</a>. The fixes later shipped in the <code>1.2.1</code> release line <a href="#ref8">[8]</a>.</p>
<h2 id="vulnerability-map">Vulnerability Map</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Vulnerability</th>
<th>CVE</th>
<th>Affected code</th>
<th>Reachability</th>
</tr>
</thead>
<tbody>
<tr>
<td>DNSServer</td>
<td>DNS QNAME heap overflow</td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2026-0001?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=tuya-arduino-tuyaopen-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="tuya-arduino-tuyaopen-vulnerabilities-disclosure" data-post-category="security"><code>CVE-2026-28519</code></a></td>
<td><code>libraries/DNSServer/src/DNSServer.cpp</code></td>
<td>Same-network DNS input when firmware exposes <code>DNSServer</code></td>
</tr>
<tr>
<td>TuyaIoT</td>
<td>Out-of-bounds read in DP event helper</td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2026-0002?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=tuya-arduino-tuyaopen-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="tuya-arduino-tuyaopen-vulnerabilities-disclosure" data-post-category="security"><code>CVE-2026-28521</code></a></td>
<td><code>libraries/TuyaIoT/src/TuyaIoT.cpp</code></td>
<td>API boundary; caller passes unchecked <code>index</code></td>
</tr>
<tr>
<td>WiFiMulti</td>
<td>Heap off-by-one in Wi-Fi credential storage</td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2026-0004?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=tuya-arduino-tuyaopen-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="tuya-arduino-tuyaopen-vulnerabilities-disclosure" data-post-category="security"><code>CVE-2026-28520</code></a></td>
<td><code>libraries/WiFi/src/WiFiMulti.cpp</code></td>
<td>API boundary; application passes SSID or passphrase to <code>addAP()</code></td>
</tr>
<tr>
<td>WiFiUDP</td>
<td>Null-pointer dereference in UDP receive path</td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2026-0003?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=tuya-arduino-tuyaopen-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="tuya-arduino-tuyaopen-vulnerabilities-disclosure" data-post-category="security"><code>CVE-2026-28522</code></a></td>
<td><code>libraries/WiFi/src/WiFiUdp.cpp</code></td>
<td>Incoming UDP traffic under low-memory conditions</td>
</tr>
</tbody>
</table>
<h2 id="affected-code-and-use-cases">Affected Code and Use Cases</h2>
<p>The vulnerable implementations referenced in this post are from public repository code around commit <code>eaec2c903e2e629f9c53da789c39e12ea226d0b2</code> <a href="#ref2">[2]</a>. Public history places the affected paths between August and November 2024, with fixes landing on February 25, 2026 <a href="#ref3">[3]</a> <a href="#ref4">[4]</a> <a href="#ref5">[5]</a> <a href="#ref6">[6]</a>.</p>
<p><code>arduino-tuyaopen</code> ships as an Arduino board package for targets including <code>t2</code>, <code>t3</code>, <code>t5</code>, <code>ln882h</code>, and <code>esp32</code> <a href="#ref2">[2]</a>. The relevant libraries are used for captive-portal DNS handling, UDP packet reception, Wi-Fi connection fallback, and Tuya cloud DP processing.</p>
<p>At a reachability level, the DNS and UDP issues are adjacent-network issues: the attacker must be on the same LAN or AP-side network path as the device. The WiFiMulti issue is a local Wi-Fi/provisioning issue: the device must consume attacker-controlled AP or Wi-Fi credential data. The TuyaIoT DP issue is a device-control-path issue: attacker-controlled DP event data must reach the device through the Tuya cloud path.</p>
<p>A product does not inherit all four attack paths by default. Exposure depends on whether the affected library is present in the firmware and connected to the corresponding attacker-controlled input.</p>
<h2 id="recommendations">Recommendations</h2>
<ul>
<li>Update <code>arduino-tuyaopen</code> to the <code>1.2.1</code> release line or later, or to a commit containing <code>54ac201</code>.</li>
<li>If TuyaOpen libraries were vendored into firmware, check whether the February 25, 2026 fixes are present.</li>
<li>If firmware exposes captive-portal DNS handling, reject malformed or oversized QNAME inputs before copying into fixed buffers.</li>
<li>If firmware uses <code>WiFiUDP</code>, test receive paths under allocation failure and low-memory conditions.</li>
<li>Treat Wi-Fi provisioning data and Tuya DP indexes as untrusted unless the application enforces bounds before calling framework helpers.</li>
</ul>
<h2 id="timeline">Timeline</h2>
<div style="position: relative; padding-left: 1.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: 0.35rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-border);"></div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">February 22, 2026</div>
    <div style="color: var(--color-text);">SecMate reports four issues in <code>arduino-tuyaopen</code> to Tuya Security.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">February 25, 2026</div>
    <div style="color: var(--color-text);">The public fix commit lands in <code>arduino-tuyaopen</code> <a href="#ref6">[6]</a>.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">March 11, 2026</div>
    <div style="color: var(--color-text);">Tuya SRC announcement <a href="https://src.tuya.com/announcement/32">32</a> is available.</div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">March 12, 2026</div>
    <div style="color: var(--color-text);">SecMate publishes the four public disclosure pages linked in the vulnerability map.</div>
  </div>
</div>
<h2 id="takeaway">Takeaway</h2>
<p>The common issue is not Tuya cloud. It is memory safety in reusable firmware framework code. DNS helpers, UDP receive paths, Wi-Fi provisioning helpers, and cloud-event accessors often become part of product firmware.</p>
<p>The highest-confidence impacts are same-network memory corruption where the DNS helper is exposed, denial of service in UDP receive paths under low-memory conditions, and memory-safety failures in helper APIs that downstream firmware may treat as safe building blocks.</p>
<p>For the full list of our disclosures, see our disclosure page at <a href="https://secmate.dev/disclosures?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=tuya-arduino-tuyaopen-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="tuya-arduino-tuyaopen-vulnerabilities-disclosure" data-post-category="security">secmate.dev/disclosures</a>.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] Tuya. &ldquo;Tuya Developer Platform.&rdquo; <a href="https://developer.tuya.com/en/docs/iot/introduction-of-tuya?id=K914joffendwh" rel="noopener noreferrer" target="_blank">Documentation</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] Tuya. &ldquo;<code>arduino-tuyaopen</code> repository.&rdquo; <a href="https://github.com/tuya/arduino-TuyaOpen" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] Tuya. &ldquo;<code>add ble&amp;wifi libraries</code> (<code>b6d68ec</code>, August 6, 2024).&rdquo; <a href="https://github.com/tuya/arduino-TuyaOpen/commit/b6d68ec1be2b67fd286a1d9f76d7d2a2a6041748" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] Tuya. &ldquo;<code>add http mqtt</code> (<code>89be820</code>, November 21, 2024).&rdquo; <a href="https://github.com/tuya/arduino-TuyaOpen/commit/89be82061b5892546ffb61dec303f5c3985bdee8" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref5"></a>[5] Tuya. &ldquo;<code>Update TuyaIoT libraries</code> (<code>53bf5bc</code>, August 27, 2024).&rdquo; <a href="https://github.com/tuya/arduino-TuyaOpen/commit/53bf5bcade07718a70737f55ece0481739f89e85" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref6"></a>[6] Tuya. &ldquo;<code>Fix security issues</code> (<code>54ac201</code>, February 25, 2026).&rdquo; <a href="https://github.com/tuya/arduino-TuyaOpen/commit/54ac201c458d040af49d375514f291360acc9ff9" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref7"></a>[7] Tuya SRC. &ldquo;Announcement 32.&rdquo; <a href="https://src.tuya.com/announcement/32" rel="noopener noreferrer" target="_blank">Announcement page</a></p>
</li>
<li>
<p><a id="ref8"></a>[8] Tuya. &ldquo;<code>1.2.0</code> to <code>1.2.1</code> comparison.&rdquo; <a href="https://github.com/tuya/arduino-TuyaOpen/compare/1.2.0...1.2.1" rel="noopener noreferrer" target="_blank">Repository diff</a></p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.png" medium="image"/></item><item><title>Memory Safety Vulnerabilities in Renesas FSP Cryptographic and Networking Code</title><link>https://blog.secmate.dev/posts/renesas-fsp-vulnerabilities/</link><pubDate>Thu, 23 Apr 2026 04:00:00 +0000</pubDate><atom:updated>2026-04-23T04:00:00+00:00</atom:updated><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>Embedded</category><guid>https://blog.secmate.dev/posts/renesas-fsp-vulnerabilities/</guid><description>SecMate found seven vulnerabilities in Renesas FSP, including flaws in RSA verification, AES-XTS finalization, GCM decrypt handling, and the RMAC receive path.</description><content:encoded><![CDATA[<h1 id="memory-safety-vulnerabilities-in-renesas-fsp-cryptographic-and-networking-code">Memory Safety Vulnerabilities in Renesas FSP Cryptographic and Networking Code</h1>
<p>SecMate found seven vulnerabilities in Renesas FSP <a href="#ref1">[1]</a>, the Flexible Software Package for Renesas RA microcontrollers <a href="#ref2">[2]</a>. FSP is the vendor software stack for RA devices and bundles BSP code, HAL drivers, middleware, and security integrations, including hardware-backed cryptography and Mbed TLS support.</p>
<img src="/images/renesas-architecture.svg" alt="FSP layered architecture: BSP, HAL drivers, middleware, security modules, and RTOS support" style="width: 100%; max-width: 800px; height: auto; margin: 1.5rem auto; display: block;">
<p>In the <code>v6.1.0</code> FSP codebase we analyzed at commit <code>ed247f90fb9bc390ebd36f1a0b7847b0ff44f322</code> <a href="#ref3">[3]</a>, we found issues in RSA verification, the hardware-accelerated AES-XTS path, GCM decrypt handling, and the RMAC receive path. These are implementation flaws in FSP integration code around hardware-backed cryptography and networking. They are not flaws in AES, GCM, CCM, XTS, or RSA as algorithms.</p>
<h2 id="advisory-map">Advisory Map</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Vulnerability</th>
<th>SecMate Advisory</th>
<th>Renesas Advisory</th>
</tr>
</thead>
<tbody>
<tr>
<td>RMAC</td>
<td>Read buffer overflow in <code>r_rmac.c</code></td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2025-0021?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">SECMATE-2025-0021</a></td>
<td><a href="https://github.com/renesas/fsp/issues/431" rel="noopener noreferrer" target="_blank">renesas/fsp#431</a></td>
</tr>
<tr>
<td>RSA</td>
<td>Signature verification pointer underflow in <code>r_sce_rsa.c</code></td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2025-0019?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">SECMATE-2025-0019</a></td>
<td><a href="https://github.com/renesas/fsp/issues/423" rel="noopener noreferrer" target="_blank">renesas/fsp#423</a></td>
</tr>
<tr>
<td>AES-XTS</td>
<td>Encrypt and decrypt finalization overflows in <code>aes_alt_process.c</code></td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2025-0020?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">SECMATE-2025-0020</a></td>
<td><a href="https://github.com/renesas/fsp/issues/427" rel="noopener noreferrer" target="_blank">renesas/fsp#427</a></td>
</tr>
<tr>
<td>GCM</td>
<td>Tag length buffer overflow in <code>gcm_alt_process.c</code></td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2025-0022?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">SECMATE-2025-0022</a></td>
<td><a href="https://github.com/renesas/fsp/issues/428" rel="noopener noreferrer" target="_blank">renesas/fsp#428</a></td>
</tr>
<tr>
<td>CCM</td>
<td>Tag length buffer overflow in <code>ccm_alt_process.c</code></td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2025-0023?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">SECMATE-2025-0023</a></td>
<td><a href="https://github.com/renesas/fsp/issues/430" rel="noopener noreferrer" target="_blank">renesas/fsp#430</a></td>
</tr>
<tr>
<td>CCM adaptor</td>
<td>MAC length buffer overflow in <code>r_sce_AES_adapt.c</code></td>
<td><a href="https://secmate.dev/disclosures/SECMATE-2025-0024?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">SECMATE-2025-0024</a></td>
<td><a href="https://github.com/renesas/fsp/issues/429" rel="noopener noreferrer" target="_blank">renesas/fsp#429</a></td>
</tr>
</tbody>
</table>
<p>The AES-XTS advisory covers two findings, one in the encrypt finalization path and one in the decrypt finalization path.</p>
<h2 id="affected-builds-and-use-cases">Affected Builds and Use Cases</h2>
<p>SecMate&rsquo;s published disclosure advisories are available on the SecMate disclosures page <a href="#ref4">[4]</a>. The table above links both the SecMate advisory and the corresponding Renesas public advisory for each vulnerability group.</p>
<p>These cryptography-related findings do not apply to every RA build. In FSP, the accelerated AES and AEAD paths are compiled for devices with <code>SCE5</code>, <code>SCE5B</code>, <code>SCE7</code>, <code>SCE9</code>, or <code>RSIP</code> support, and the XTS path is excluded on <code>RA2</code> parts.</p>
<p>In downstream products, these APIs may be used for signature verification, authenticated decryption, encrypted storage, firmware update, or other security-sensitive processing. Exposure therefore depends on whether untrusted inputs can reach those wrappers.</p>
<p>The RMAC finding is separate from the cryptographic code paths. It affects products using the Ethernet receive API if application code provides a buffer smaller than the received frame.</p>
<p>Renesas PSIRT acknowledged the reports and published public advisories on GitHub.</p>
<h2 id="recommendations">Recommendations</h2>
<ul>
<li>If you use affected RA builds, review the Renesas advisories and update to <code>FSP v6.3.0</code> or later.</li>
<li>In downstream products, check whether untrusted inputs can reach FSP wrappers used for signature verification, authenticated decryption, encrypted storage, or firmware update handling.</li>
<li>Add negative tests for malformed signatures, AEAD metadata, XTS lengths, and oversized Ethernet frames.</li>
<li>If you use <code>r_rmac</code>, verify that receive buffers are sized for the largest frame your product accepts.</li>
</ul>
<h2 id="timeline">Timeline</h2>
<div style="position: relative; padding-left: 1.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: 0.35rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-border);"></div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">October 20, 2025</div>
    <div style="color: var(--color-text);">SecMate reports the seven FSP vulnerabilities against commit <code>ed247f90fb9bc390ebd36f1a0b7847b0ff44f322</code> (<code>v6.1.0</code>).</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">December 18, 2025</div>
    <div style="color: var(--color-text);">Renesas releases <code>FSP v6.3.0</code>, which contains the public fixes <a href="#ref5">[5]</a>.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">February 4, 2026</div>
    <div style="color: var(--color-text);">Renesas confirms that the FSP disclosure track covered all seven reported FSP vulnerabilities and that each would receive its own advisory.</div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">February 10, 2026</div>
    <div style="color: var(--color-text);">Renesas tells SecMate the public FSP advisories were published on GitHub.</div>
  </div>
</div>
<h2 id="takeaway">Takeaway</h2>
<p>The common thread is not broken cryptography. It is memory safety in the FSP code that exposes cryptographic and Ethernet functionality to RA applications. Hardware-backed cryptography still depends on the surrounding integration code.</p>
<p>For the full list of our disclosures, see our disclosure page <a href="#ref4">[4]</a>. If you are building embedded systems and want to find vulnerabilities before attackers do, <a href="https://secmate.dev/register?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_register" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">reach out</a>.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] Renesas. &ldquo;Renesas Flexible Software Package.&rdquo; <em>GitHub</em>. <a href="https://github.com/renesas/fsp" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] Renesas. &ldquo;Renesas Flexible Software Package Documentation.&rdquo; <a href="https://renesas.github.io/fsp/" rel="noopener noreferrer" target="_blank">Documentation</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] Renesas. &ldquo;FSP v6.1.0 analyzed commit&rdquo; (<code>ed247f90fb9bc390ebd36f1a0b7847b0ff44f322</code>). <a href="https://github.com/renesas/fsp/commit/ed247f90fb9bc390ebd36f1a0b7847b0ff44f322" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] SecMate. &ldquo;Disclosures.&rdquo; <a href="https://secmate.dev/disclosures?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=renesas-fsp-vulnerabilities&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="renesas-fsp-vulnerabilities" data-post-category="security">Disclosure page</a></p>
</li>
<li>
<p><a id="ref5"></a>[5] Renesas. &ldquo;FSP v6.3.0.&rdquo; <em>GitHub Releases</em>. <a href="https://github.com/renesas/fsp/releases/tag/v6.3.0" rel="noopener noreferrer" target="_blank">Release</a></p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item><item><title>Six Vulnerabilities in Siemens SICAM SIAPP SDK</title><link>https://blog.secmate.dev/posts/siemens-siapp-sdk-vulnerabilities-disclosure/</link><pubDate>Wed, 15 Apr 2026 00:00:00 +0000</pubDate><atom:updated>2026-04-15T00:00:00+00:00</atom:updated><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>IoT</category><guid>https://blog.secmate.dev/posts/siemens-siapp-sdk-vulnerabilities-disclosure/</guid><description>SecMate's automated analysis found six vulnerabilities in Siemens SICAM SIAPP SDK before 2.1.7, spanning UNIX socket helper flaws and sample application bugs used to build custom SIAPP applications. Five of the affected code paths were present in the public repository from June 2020; Siemens fixed all six in version 2.1.7 and published advisory SSA-903736 on March 10, 2026.</description><content:encoded><![CDATA[<h1 id="six-vulnerabilities-in-siemens-sicam-siapp-sdk">Six Vulnerabilities in Siemens SICAM SIAPP SDK</h1>
<p>SecMate&rsquo;s automated analysis found six vulnerabilities in Siemens SICAM SIAPP SDK before version <code>2.1.7</code>. The set includes one out-of-bounds write in shipped sample code, one stack buffer overflow in the shipped <code>DemoProject</code>, two stack overflows in UNIX socket path handling, one command injection in UNIX socket permission handling, and one uncontrolled file or socket deletion issue caused by unchecked <code>unlink()</code>.</p>
<p>SIAPP SDK is Siemens&rsquo; development kit for building custom containerized applications that run on SICAM A8000 equipment used in substation automation, distribution grids, and other power-automation environments. These findings concern the SIAPP SDK path for custom applications; they do not, by themselves, imply that the core SICAM A8000 firmware or Siemens-provided standard functionality is affected by default.</p>
<p>Taken together, the bugs fall into two buckets: four issues in the SDK&rsquo;s UNIX socket helper code and two in sample applications Siemens ships as part of the developer workflow.</p>
<p>Exploitability depends on how downstream applications use the SDK, whether untrusted input reaches the affected paths, and whether hardening measures are applied.</p>
<p>In the public repository history, <strong>five of the six affected code paths trace back to the initial public commit on June 10, 2020</strong> and remained there until Siemens fixed them in <code>SIAPP SDK 2.1.7</code> on <strong>January 16, 2026</strong>. That is <strong>2,046 days</strong> in the public repository.</p>
<p>The sixth affected code path appears later, on <strong>March 19, 2021</strong>, and remained until the same <code>2.1.7</code> fix, for <strong>1,764 days</strong> in the public repository.</p>
<p>Siemens published these issues in advisory <code>SSA-903736</code> on <strong>March 10, 2026</strong> <a href="#ref1">[1]</a>.</p>
<h2 id="what-is-sicam-siapp-sdk">What Is SICAM SIAPP SDK?</h2>
<p>SIAPP SDK is Siemens&rsquo; development kit for building and emulating <strong>SIAPPs</strong>, containerized applications intended to run on the <a href="https://www.siemens.com/global/en/products/energy/energy-automation-and-smart-grid/substation-automation/automation-and-remote-terminal-units-sicam-a8000-series.html?_originalHost=marketplace.siemens.com&amp;language=en_US" rel="noopener noreferrer" target="_blank">SICAM A8000</a> family <a href="#ref2">[2]</a> <a href="#ref3">[3]</a> <a href="#ref4">[4]</a> <a href="#ref5">[5]</a>. Siemens&rsquo; documentation describes SIAPP as an application model for <strong>CP-8031</strong> and <strong>CP-8050</strong> processor modules; developers build an app as a Docker image, emulate it locally, and then load it onto the device <a href="#ref3">[3]</a> <a href="#ref4">[4]</a>.</p>
<p>The SDK includes:</p>
<ul>
<li>build and emulation tooling</li>
<li>an <strong>EdgeData API</strong> for exchanging signals with the platform <a href="#ref6">[6]</a></li>
<li>example applications such as <code>DemoProject</code>, <code>DemoSshd</code>, and <code>CodeSnippets</code></li>
<li>a local simulation environment for testing SIAPP behavior off-device</li>
</ul>
<p>Siemens markets <a href="https://www.siemens.com/global/en/products/energy/energy-automation-and-smart-grid/substation-automation/automation-and-remote-terminal-units-sicam-a8000-series.html?_originalHost=marketplace.siemens.com&amp;language=en_US" rel="noopener noreferrer" target="_blank">SICAM A8000</a> for <strong>substation automation</strong>, <strong>distribution grids</strong>, <strong>renewable integration</strong>, and <strong>industrial power</strong> <a href="#ref5">[5]</a>. This equipment is used to monitor and control parts of the electrical grid and adjacent industrial power systems. Its SIAPP material describes use cases such as transformer controllers, energy-storage optimization, weather-driven renewable control, and energy-trading optimization <a href="#ref4">[4]</a>, and the launch material frames the platform for customers such as <strong>major energy suppliers, municipal utilities, and industrial companies</strong> <a href="#ref7">[7]</a>. The SIAPP SDK is the official development kit for building custom applications in that environment.</p>
<p>The developer workflow in the repository is straightforward <a href="#ref3">[3]</a>:</p>
<ol>
<li>Write a SIAPP project with a <code>Dockerfile</code></li>
<li>Build it with <code>build.py</code></li>
<li>Test it with <code>run.py</code> and the local simulation flow</li>
<li>Deploy it to a SICAM A8000 target</li>
</ol>
<p>The SDK ships starter applications to accelerate that process. <code>DemoProject</code> is a small web application exposing configured signals. <code>DemoSshd</code> is a test container with SSH access. <code>CodeSnippets</code> contains small example programs showing how to use the EdgeData API. That matters because this is the code developers are most likely to copy into real projects.</p>
<p>Siemens&rsquo; advisory is careful on scope: these are not generic internet-wide bugs in every installation. As Siemens states, they are exploitable <strong>if the API is used improperly or hardening measures are not applied</strong> <a href="#ref1">[1]</a>. In practice, that puts weight on the SDK itself, because developers learn the safe and unsafe paths from the code Siemens ships.</p>
<h2 id="public-repository-timeline">Public Repository Timeline</h2>
<p>Five of the six vulnerabilities trace back to the initial public commit on <strong>June 10, 2020</strong> <a href="#ref8">[8]</a> <a href="#ref10">[10]</a>:</p>
<ul>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25569" rel="noopener noreferrer" target="_blank">CVE-2026-25569</a> in <code>CodeSnippets/src/helper.c</code></li>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25571" rel="noopener noreferrer" target="_blank">CVE-2026-25571</a> in <code>edgedataapi/src/edgedata.cpp</code></li>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25572" rel="noopener noreferrer" target="_blank">CVE-2026-25572</a> in <code>edgedataapi/src/edgedata.cpp</code></li>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25573" rel="noopener noreferrer" target="_blank">CVE-2026-25573</a> in <code>edgedataapi/src/edgedata.cpp</code></li>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25605" rel="noopener noreferrer" target="_blank">CVE-2026-25605</a> in <code>edgedataapi/src/edgedata.cpp</code></li>
</ul>
<p>The one later issue was introduced on <strong>March 19, 2021</strong> <a href="#ref9">[9]</a>:</p>
<ul>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25570" rel="noopener noreferrer" target="_blank">CVE-2026-25570</a> in <code>DemoProject/src/demo.cpp</code></li>
</ul>
<p>Four of the six vulnerabilities live in the UNIX socket IPC helper code used by the SDK. The other two live in sample applications Siemens ships as part of the developer workflow.</p>
<h2 id="vulnerability-1-codesnippets-helper-out-of-bounds-write-cve-2026-25569-11ref11">Vulnerability #1: CodeSnippets Helper Out-of-Bounds Write (CVE-2026-25569 <a href="#ref11">[11]</a>)</h2>
<p><strong>Location</strong>: <code>helper_get_edgedata_text()</code> in <code>CodeSnippets/src/helper.c</code><br>
<strong>Introduced</strong>: <code>ff331c2</code> on June 10, 2020<br>
<strong>Fixed</strong>: <code>e4edda8</code> on January 16, 2026<br>
<strong>Siemens advisory</strong>: CVSS v3.1 7.4 <a href="#ref1">[1]</a></p>
<p>This issue is in the <code>CodeSnippets</code> sample application, not in the shared IPC helper. The helper <code>helper_get_edgedata_text()</code> formats multiple fields into a caller-provided buffer and advances <code>pos</code> using each <code>snprintf()</code> return value without validating truncation:</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">pos</span> <span class="o">+=</span> <span class="p">(</span><span class="kt">uint32_t</span><span class="p">)</span><span class="nf">snprintf</span><span class="p">(</span><span class="o">&amp;</span><span class="n">edgedata_to_text</span><span class="p">[</span><span class="n">pos</span><span class="p">],</span> <span class="p">(</span><span class="n">max_len_edgedata_to_text</span><span class="o">-</span><span class="n">pos</span><span class="p">),</span> <span class="s">&#34;Topic: %s&#34;</span><span class="p">,</span> <span class="n">edge_data</span><span class="o">-&gt;</span><span class="n">topic</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">pos</span> <span class="o">+=</span> <span class="p">(</span><span class="kt">uint32_t</span><span class="p">)</span><span class="nf">snprintf</span><span class="p">(</span><span class="o">&amp;</span><span class="n">edgedata_to_text</span><span class="p">[</span><span class="n">pos</span><span class="p">],</span> <span class="p">(</span><span class="n">max_len_edgedata_to_text</span><span class="o">-</span><span class="n">pos</span><span class="p">),</span> <span class="s">&#34;, Type:&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">pos</span> <span class="o">+=</span> <span class="nf">helper_get_data_type_text</span><span class="p">(</span><span class="n">edge_data</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">edgedata_to_text</span><span class="p">[</span><span class="n">pos</span><span class="p">],</span> <span class="p">(</span><span class="n">max_len_edgedata_to_text</span><span class="o">-</span><span class="n">pos</span><span class="p">));</span>
</span></span></code></pre></div><p>If a topic exceeds remaining space, <code>pos</code> can move beyond the real buffer boundary, and subsequent writes can go out of bounds. In the shipped examples, <code>discover.c</code>, <code>subscribe.c</code>, and <code>simple_dido.c</code> call this helper with fixed 300-byte stack buffers, so oversized topic strings can trigger stack corruption.</p>
<h2 id="vulnerability-2-demoproject-floating-point-formatting-stack-overflow-cve-2026-25570-12ref12">Vulnerability #2: DemoProject Floating-Point Formatting Stack Overflow (CVE-2026-25570 <a href="#ref12">[12]</a>)</h2>
<p><strong>Location</strong>: <code>convert_value_to_str()</code> in <code>DemoProject/src/demo.cpp</code><br>
<strong>Introduced</strong>: <code>4139bfb</code> on March 19, 2021<br>
<strong>Fixed</strong>: <code>e4edda8</code> on January 16, 2026<br>
<strong>Siemens advisory</strong>: CVSS v3.1 7.4 <a href="#ref1">[1]</a></p>
<p>This issue is in the <code>DemoProject</code> sample application. <code>convert_value_to_str()</code> writes to fixed-size buffers (including <code>char value_str[50]</code>) and used unbounded <code>sprintf()</code> for floating-point values:</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">case</span> <span class="nl">E_EDGE_DATA_TYPE_FLOAT32</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">   <span class="nf">sprintf</span><span class="p">(</span><span class="n">out_value</span><span class="p">,</span> <span class="s">&#34;%f&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">-&gt;</span><span class="n">float32</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">   <span class="nf">sprintf</span><span class="p">(</span><span class="n">out_type</span><span class="p">,</span> <span class="s">&#34;FLOAT32&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">   <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">case</span> <span class="nl">E_EDGE_DATA_TYPE_DOUBLE64</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">   <span class="nf">sprintf</span><span class="p">(</span><span class="n">out_value</span><span class="p">,</span> <span class="s">&#34;%lf&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">-&gt;</span><span class="n">double64</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">   <span class="nf">sprintf</span><span class="p">(</span><span class="n">out_type</span><span class="p">,</span> <span class="s">&#34;DOUBLE64&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">   <span class="k">break</span><span class="p">;</span>
</span></span></code></pre></div><p>Large floating-point values can expand to strings longer than 50 bytes, causing stack overflow in <code>value_str</code>. The formatter is used in <code>/edgedata/get</code> and <code>/edgedata/get_events</code> response paths, so oversized <code>DOUBLE64</code> values can trigger memory corruption during XML serialization.</p>
<h2 id="vulnerability-3-client-unix-socket-path-overflow-cve-2026-25571-13ref13">Vulnerability #3: Client UNIX Socket Path Overflow (CVE-2026-25571 <a href="#ref13">[13]</a>)</h2>
<p><strong>Location</strong>: <code>edgedata_ipc_unix_client_connect()</code> in <code>edgedataapi/src/edgedata.cpp</code><br>
<strong>Introduced</strong>: <code>ff331c2</code> on June 10, 2020<br>
<strong>Fixed</strong>: <code>e4edda8</code> on January 16, 2026<br>
<strong>Siemens advisory</strong>: CVSS v3.1 5.1 <a href="#ref1">[1]</a></p>
<p>This is a client-side length validation issue in the shared UNIX socket helper. <code>edgedata_ipc_unix_client_connect()</code> copies a caller-provided socket path into <code>sockaddr_un.sun_path</code> using <code>strcpy()</code> without bounds checking:</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="nf">strcpy</span><span class="p">(</span><span class="n">addr</span><span class="p">.</span><span class="n">sun_path</span><span class="p">,</span> <span class="n">fd</span><span class="o">-&gt;</span><span class="n">read_channel_name</span><span class="p">.</span><span class="nf">c_str</span><span class="p">());</span>
</span></span></code></pre></div><p>On Linux, <code>sun_path</code> is fixed-size (commonly 108 bytes). Longer paths can overflow the stack buffer before <code>connect()</code>. Exploitation depends on downstream integration: if untrusted or oversized input is passed into this API, the overflow is reachable.</p>
<h2 id="vulnerability-4-server-unix-socket-path-overflow-cve-2026-25572-14ref14">Vulnerability #4: Server UNIX Socket Path Overflow (CVE-2026-25572 <a href="#ref14">[14]</a>)</h2>
<p><strong>Location</strong>: <code>edgedata_ipc_unix_server_listen()</code> in <code>edgedataapi/src/edgedata.cpp</code><br>
<strong>Introduced</strong>: <code>ff331c2</code> on June 10, 2020<br>
<strong>Fixed</strong>: <code>e4edda8</code> on January 16, 2026<br>
<strong>Siemens advisory</strong>: CVSS v3.1 5.1 <a href="#ref1">[1]</a></p>
<p>This is the server-side twin of the previous bug. The same unbounded copy is used when preparing the server socket address:</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="nf">strcpy</span><span class="p">(</span><span class="n">addr</span><span class="p">.</span><span class="n">sun_path</span><span class="p">,</span> <span class="n">fd</span><span class="o">-&gt;</span><span class="n">read_channel_name</span><span class="p">.</span><span class="nf">c_str</span><span class="p">());</span>
</span></span></code></pre></div><p>If a downstream application accepts an untrusted socket path and forwards it into the SDK, the server helper can overflow the stack before <code>bind()</code> runs. As with the client-side variant, the default examples do not expose this path directly to attackers; the precondition is still misuse at the API boundary.</p>
<h2 id="vulnerability-5-unix-socket-helper-command-injection-cve-2026-25573-15ref15">Vulnerability #5: UNIX Socket Helper Command Injection (CVE-2026-25573 <a href="#ref15">[15]</a>)</h2>
<p><strong>Location</strong>: <code>edgedata_ipc_unix_server_listen()</code> in <code>edgedataapi/src/edgedata.cpp</code><br>
<strong>Introduced</strong>: <code>ff331c2</code> on June 10, 2020<br>
<strong>Fixed</strong>: <code>e4edda8</code> on January 16, 2026<br>
<strong>Siemens advisory</strong>: CVSS v3.1 7.4 <a href="#ref1">[1]</a></p>
<p>This issue is more serious because the vulnerable code does not just mishandle memory. It builds shell commands from caller-controlled strings and executes them:</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="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">snprintf</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">cmd</span><span class="p">),</span> <span class="s">&#34;chown %s %s&#34;</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">addr</span><span class="p">.</span><span class="n">sun_path</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">system</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">snprintf</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">cmd</span><span class="p">),</span> <span class="s">&#34;chmod 777 %s&#34;</span><span class="p">,</span> <span class="n">addr</span><span class="p">.</span><span class="n">sun_path</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">system</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
</span></span></code></pre></div><p>If an application exposes <code>user</code> or <code>channel_name</code> to untrusted input, shell metacharacters become part of the command line interpreted by <code>/bin/sh</code>. This is not an abstract quoting issue. The helper literally concatenates caller-controlled data into a shell command and runs it.</p>
<p>As with the path overflows, exploitability depends on integration. The repository examples do not themselves expose arbitrary remote input for these parameters. But once a downstream application lets an untrusted user influence socket ownership or socket path values, the bug becomes command injection.</p>
<h2 id="vulnerability-6-unchecked-unlink-in-unix-socket-setup-cve-2026-25605-16ref16">Vulnerability #6: Unchecked <code>unlink()</code> in UNIX Socket Setup (CVE-2026-25605 <a href="#ref16">[16]</a>)</h2>
<p><strong>Location</strong>: <code>edgedata_ipc_unix_server_listen()</code> in <code>edgedataapi/src/edgedata.cpp</code><br>
<strong>Introduced</strong>: <code>ff331c2</code> on June 10, 2020<br>
<strong>Fixed</strong>: <code>e4edda8</code> on January 16, 2026<br>
<strong>Siemens advisory</strong>: CVSS v3.1 6.7 <a href="#ref1">[1]</a></p>
<p>Before binding the server socket, the helper called:</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="nf">unlink</span><span class="p">(</span><span class="n">fd</span><span class="o">-&gt;</span><span class="n">read_channel_name</span><span class="p">.</span><span class="nf">c_str</span><span class="p">());</span>
</span></span></code></pre></div><p>There was no validation that the path pointed to an existing socket, no restriction to a dedicated socket directory, and no <code>lstat()</code> / <code>S_ISSOCK</code> guard. If a downstream application lets an untrusted party influence <code>channel_name</code>, the process can be induced to delete any file or socket it has permission to remove.</p>
<p>This does not automatically mean arbitrary file deletion from the network. The vulnerable operation is local to the API boundary, so the precondition is still that a caller passes untrusted data into the helper. But once that happens, the delete happens before the bind, with no type check at all.</p>
<h2 id="what-siemens-fixed-in-217">What Siemens Fixed in <code>2.1.7</code></h2>
<p>The <code>SIAPP SDK 2.1.7</code> commit includes direct fixes in the affected code paths:</p>
<ul>
<li><code>strcpy()</code> on <code>sun_path</code> was replaced with explicit length checks and bounded copies</li>
<li>unconditional <code>unlink()</code> was replaced with <code>lstat()</code> and socket-type validation</li>
<li>shell-based <code>chown</code> and <code>chmod</code> calls were replaced with <code>getpwnam()</code>, <code>chown()</code>, and <code>chmod()</code></li>
<li><code>sprintf()</code> was replaced with bounded <code>snprintf()</code>, and the float formatting was changed from <code>%f</code> / <code>%lf</code> to <code>%g</code></li>
<li>the <code>CodeSnippets</code> helper was rewritten to check truncation after each append and to cap copied topic length</li>
</ul>
<h2 id="disclosure-timeline">Disclosure Timeline</h2>
<div style="position: relative; padding-left: 1.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: 0.35rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-border);"></div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 3, 2026</div>
    <div style="color: var(--color-text);">SecMate reports six vulnerabilities to Siemens ProductCERT, including two affecting example code only <a href="#ref17">[17]</a></div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 5, 2026</div>
    <div style="color: var(--color-text);">Siemens ProductCERT confirms the case was opened as <code>#33016</code> on the first business day after the January 3 report <a href="#ref17">[17]</a></div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 16, 2026</div>
    <div style="color: var(--color-text);">Siemens fixes all six issues in <a href="https://github.com/siemens/siapp-sdk/commit/e4edda8">SIAPP SDK 2.1.7</a> <a href="#ref10">[10]</a></div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Mar 10, 2026</div>
    <div style="color: var(--color-text);">Siemens publishes advisory <a href="https://cert-portal.siemens.com/productcert/html/ssa-903736.html">SSA-903736</a> covering <code>CVE-2026-25569</code>, <code>CVE-2026-25570</code>, <code>CVE-2026-25571</code>, <code>CVE-2026-25572</code>, <code>CVE-2026-25573</code>, and <code>CVE-2026-25605</code> <a href="#ref1">[1]</a></div>
  </div>
</div>
<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>Several of these patterns sat for years in a vendor SDK used to build software for power-automation environments.</p>
<p>Four of these vulnerabilities were in reusable UNIX socket SDK code, and two were in sample applications shipped to show developers how to build on the platform.</p>
<p>For SDK maintainers, especially in embedded and industrial systems, the lesson is simple: helper APIs and sample projects are part of the security boundary.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] Siemens ProductCERT. &ldquo;SSA-903736: Multiple vulnerabilities in SICAM SIAPP SDK before V2.1.7.&rdquo; Published March 10, 2026. <a href="https://cert-portal.siemens.com/productcert/html/ssa-903736.html" rel="noopener noreferrer" target="_blank">Advisory</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] Siemens. &ldquo;siapp-sdk.&rdquo; <a href="https://github.com/siemens/siapp-sdk" rel="noopener noreferrer" target="_blank">GitHub repository</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] Siemens. &ldquo;SIAPP SDK README.&rdquo; <a href="https://github.com/siemens/siapp-sdk/blob/master/README.md" rel="noopener noreferrer" target="_blank">Repository documentation</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] Siemens. &ldquo;SICAM application - SIAPP.&rdquo; <a href="https://www.siemens.com/global/en/products/energy/energy-automation-and-smart-grid/substation-automation/sicam-application-siapp.html" rel="noopener noreferrer" target="_blank">Product page</a></p>
</li>
<li>
<p><a id="ref5"></a>[5] Siemens. &ldquo;Automation and remote terminal units - SICAM A8000 Series.&rdquo; <a href="https://www.siemens.com/global/en/products/energy/energy-automation-and-smart-grid/substation-automation/automation-and-remote-terminal-units-sicam-a8000-series.html?_originalHost=marketplace.siemens.com&amp;language=en_US" rel="noopener noreferrer" target="_blank">Product page</a></p>
</li>
<li>
<p><a id="ref6"></a>[6] Siemens. &ldquo;EDGEDATAAPI.md.&rdquo; <a href="https://github.com/siemens/siapp-sdk/blob/master/EDGEDATAAPI.md" rel="noopener noreferrer" target="_blank">Repository documentation</a></p>
</li>
<li>
<p><a id="ref7"></a>[7] Siemens Press. &ldquo;Siemens launches new platform to create customer-specific apps for distribution grids.&rdquo; Published July 7, 2020. <a href="https://press.siemens.com/global/en/pressrelease/siemens-launches-new-platform-create-customer-specific-apps-distribution-grids" rel="noopener noreferrer" target="_blank">Press release</a></p>
</li>
<li>
<p><a id="ref8"></a>[8] Siemens. &ldquo;initial commit&rdquo; (<code>ff331c2</code>, June 10, 2020). <a href="https://github.com/siemens/siapp-sdk/commit/ff331c2" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref9"></a>[9] Siemens. &ldquo;SIAPP SDK 1.1.1&rdquo; (<code>4139bfb</code>, March 19, 2021). <a href="https://github.com/siemens/siapp-sdk/commit/4139bfb" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref10"></a>[10] Siemens. &ldquo;SIAPP SDK 2.1.7&rdquo; (<code>e4edda8</code>, January 16, 2026). <a href="https://github.com/siemens/siapp-sdk/commit/e4edda8" rel="noopener noreferrer" target="_blank">Commit</a></p>
</li>
<li>
<p><a id="ref11"></a>[11] NVD. &ldquo;CVE-2026-25569.&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25569" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref12"></a>[12] NVD. &ldquo;CVE-2026-25570.&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25570" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref13"></a>[13] NVD. &ldquo;CVE-2026-25571.&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25571" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref14"></a>[14] NVD. &ldquo;CVE-2026-25572.&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25572" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref15"></a>[15] NVD. &ldquo;CVE-2026-25573.&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25573" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref16"></a>[16] NVD. &ldquo;CVE-2026-25605.&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25605" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref17"></a>[17] Email correspondence between SecMate and Siemens ProductCERT regarding <code>siapp-sdk</code>, January 3, 2026 to March 25, 2026. On file.</p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item><item><title>Four Memory Safety Vulnerabilities in Golioth's IoT Firmware</title><link>https://blog.secmate.dev/posts/golioth-vulnerabilities-disclosure/</link><pubDate>Wed, 25 Feb 2026 00:00:00 +0000</pubDate><atom:updated>2026-02-25T00:00:00+00:00</atom:updated><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>IoT</category><guid>https://blog.secmate.dev/posts/golioth-vulnerabilities-disclosure/</guid><description>SecMate discovered four memory safety vulnerabilities across Golioth's Firmware SDK and Pouch BLE protocol: an unauthenticated BLE heap overflow (CVE-2026-23750), a LightDB State integer underflow (CVE-2026-23748), an assert guarded stack overflow (CVE-2026-23747), and a CoAP blockwise path OOB read (CVE-2026-23749). With server certificate validation disabled, three form a theoretical chain from BLE proximity to RCE.</description><content:encoded><![CDATA[<h1 id="four-memory-safety-vulnerabilities-in-golioths-iot-firmware">Four Memory Safety Vulnerabilities in Golioth&rsquo;s IoT Firmware</h1>
<p>We discovered four memory safety vulnerabilities in Golioth&rsquo;s Firmware SDK and Pouch BLE protocol: an unauthenticated BLE heap overflow, an integer underflow causing out-of-bounds reads, a stack buffer overflow guarded only by <code>assert()</code>, and an off-by-one <code>strncpy</code> bug. Each is independently triggerable under its own preconditions.</p>
<p>Three of the four chain together into a path from unauthenticated BLE proximity to remote code execution. Golioth&rsquo;s defense-in-depth blocks this chain: server certificate validation (<code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code>) is enabled by default and rejects injected certificates at step two.</p>
<h2 id="what-is-golioth">What is Golioth?</h2>
<p><a href="https://golioth.io/" rel="noopener noreferrer" target="_blank">Golioth</a> <a href="#ref1">[1]</a> is an IoT device infrastructure platform for fleet management, OTA updates, data routing, and device monitoring. It ships two open-source products relevant to this research:</p>
<p>The <strong>Golioth Firmware SDK</strong> <a href="#ref1">[1]</a> is a C library for device-to-cloud communication over CoAP/DTLS <a href="#ref3">[3]</a>. It supports LightDB State (device-shadow key/value store), OTA firmware updates, logging, and settings. The SDK targets Zephyr RTOS, nRF Connect SDK, ESP-IDF, and Infineon ModusToolbox, running on hardware including the nRF9160, nRF52840, and ESP32.</p>
<p><strong>Golioth Pouch</strong> <a href="#ref2">[2]</a> is a non-IP protocol for device-to-cloud communication through BLE gateways. Pouch uses BLE GATT for the device-to-gateway leg, with end-to-end encryption to the Golioth cloud. It is designed around an <strong>untrusted gateway model</strong>: gateways relay data but cannot inspect it. A BLE GATT server on the device exchanges certificates with a gateway, which then brokers the encrypted session.</p>
<p>SecMate&rsquo;s automated analysis identified <strong>four memory safety vulnerabilities</strong> spanning both products.</p>
<p>One finding is particularly worth highlighting: a stack buffer overflow whose only bounds check is <code>assert()</code>, which the C standard defines as a debugging aid and which is compiled out of every release build. This pattern, using <code>assert()</code> as a security boundary, is common across embedded codebases and is almost always a vulnerability in production.</p>
<h2 id="architecture-overview">Architecture Overview</h2>
<h3 id="firmware-sdk-device-to-cloud">Firmware SDK: Device to Cloud</h3>
<p>The Firmware SDK handles all cloud communication over CoAP/DTLS. The device authenticates to Golioth using a pre-provisioned certificate or PSK, then exchanges CoAP messages for state synchronization (LightDB State), firmware updates, and telemetry. Payloads arrive as raw byte buffers with an associated <code>payload_size</code>, and the parsing code trusts that relationship implicitly. The vulnerable code paths are in <code>lightdb_state.c</code>, <code>payload_utils.c</code>, <code>coap_blockwise.c</code>, and <code>coap_client.c</code>.</p>
<h3 id="pouch-non-ip-gateway-protocol">Pouch: Non-IP Gateway Protocol</h3>
<p>Pouch is a non-IP protocol designed to route device traffic through BLE gateways to the Golioth cloud. Its security model is built around an <strong>untrusted gateway assumption</strong>: gateways are treated as opaque relays that forward data but cannot inspect or modify it, thanks to end-to-end encryption between the device and cloud. This is a deliberate design choice: the protocol explicitly considers the scenario where an intermediary gateway is compromised.</p>
<p>What makes the first vulnerability particularly notable is that Pouch&rsquo;s threat model accounts for gateway compromise but <em>not</em> for unauthenticated BLE-level write access. The server certificate characteristic is registered with <code>BT_GATT_PERM_WRITE</code>, meaning <strong>any BLE client in range can write to it without pairing, bonding, or encryption</strong>. The untrusted gateway model protects data in transit, but the certificate exchange itself has no access control. This is the entry point for the attack chain.</p>
<h2 id="vulnerability-1-ble-gatt-server-certificate-heap-overflow-cve-2026-23750-4ref4">Vulnerability #1: BLE GATT Server Certificate Heap Overflow (CVE-2026-23750 <a href="#ref4">[4]</a>)</h2>
<p><strong>Location</strong>: <code>server_cert_write()</code> in <code>server_cert_characteristic.c</code> (Golioth Pouch)</p>
<p>This is the lead vulnerability and the entry point for the full chain. Any BLE client within radio range can trigger it: no pairing, no bonding, no authentication of any kind.</p>
<h3 id="the-bug">The Bug</h3>
<p>The <code>server_cert_write</code> handler processes incoming BLE GATT writes to the server certificate characteristic. When the first fragment arrives, it allocates a fixed-size heap buffer. For every fragment, it blindly appends the payload:</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="c1">// server_cert_characteristic.c:105-153
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">server_cert_write</span><span class="p">(</span><span class="k">struct</span> <span class="n">bt_conn</span> <span class="o">*</span><span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                                 <span class="k">const</span> <span class="k">struct</span> <span class="n">bt_gatt_attr</span> <span class="o">*</span><span class="n">attr</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                                 <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                                 <span class="kt">uint16_t</span> <span class="n">len</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                                 <span class="kt">uint16_t</span> <span class="n">offset</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                                 <span class="kt">uint8_t</span> <span class="n">flags</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">struct</span> <span class="n">golioth_ble_gatt_server_cert_ctx</span> <span class="o">*</span><span class="n">ctx</span> <span class="o">=</span> <span class="n">attr</span><span class="o">-&gt;</span><span class="n">user_data</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kt">bool</span> <span class="n">is_first</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kt">bool</span> <span class="n">is_last</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">payload</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="kt">ssize_t</span> <span class="n">payload_len</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nf">golioth_ble_gatt_packetizer_decode</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">payload</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">is_first</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">is_last</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&gt;=</span> <span class="n">payload_len</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">return</span> <span class="nf">BT_GATT_ERR</span><span class="p">(</span><span class="n">BT_ATT_ERR_UNLIKELY</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">is_first</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="nf">free</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span> <span class="o">=</span> <span class="nf">malloc</span><span class="p">(</span><span class="n">CONFIG_POUCH_SERVER_CERT_MAX_LEN</span><span class="p">);</span>  <span class="c1">// Fixed-size allocation
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"></span>        <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">size</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="k">return</span> <span class="nf">BT_GATT_ERR</span><span class="p">(</span><span class="n">BT_ATT_ERR_INSUFFICIENT_RESOURCES</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="c1">// ** VULNERABILITY: No check that ctx-&gt;cert.size + payload_len
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span>    <span class="c1">//    &lt;= CONFIG_POUCH_SERVER_CERT_MAX_LEN **
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">buffer</span><span class="p">[</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">size</span><span class="p">],</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_len</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">cert</span><span class="p">.</span><span class="n">size</span> <span class="o">+=</span> <span class="n">payload_len</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><p>The critical issue is the <code>memcpy</code> call: it copies <code>payload_len</code> bytes into <code>ctx-&gt;cert.buffer</code> at offset <code>ctx-&gt;cert.size</code> without ever checking whether the cumulative size exceeds <code>CONFIG_POUCH_SERVER_CERT_MAX_LEN</code>. The packetizer decode function returns a length derived directly from the GATT write size. No upper bound is enforced.</p>
<h3 id="the-permission-model">The Permission Model</h3>
<p>The characteristic is registered with:</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="c1">// server_cert_characteristic.c:155-161
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nf">GOLIOTH_BLE_GATT_CHARACTERISTIC</span><span class="p">(</span><span class="n">server_cert</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                <span class="p">(</span><span class="k">const</span> <span class="k">struct</span> <span class="n">bt_uuid</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">golioth_ble_gatt_server_cert_chrc_uuid</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">                                <span class="n">BT_GATT_CHRC_READ</span> <span class="o">|</span> <span class="n">BT_GATT_CHRC_WRITE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">                                <span class="n">BT_GATT_PERM_READ</span> <span class="o">|</span> <span class="n">BT_GATT_PERM_WRITE</span><span class="p">,</span>  <span class="c1">// No auth/encryption
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>                                <span class="n">server_cert_serial_read</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">                                <span class="n">server_cert_write</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">                                <span class="o">&amp;</span><span class="n">server_cert_chrc_ctx</span><span class="p">);</span>
</span></span></code></pre></div><p><code>BT_GATT_PERM_WRITE</code> grants write access to any connected BLE client. Compare this with <code>BT_GATT_PERM_WRITE_ENCRYPT</code> or <code>BT_GATT_PERM_WRITE_AUTHEN</code>, which require an encrypted link or authenticated pairing respectively. The weakest permission level was chosen.</p>
<h3 id="impact">Impact</h3>
<p>An attacker can overflow the heap buffer in two ways:</p>
<ol>
<li><strong>Single large write</strong>: Send a GATT write whose decoded payload exceeds <code>CONFIG_POUCH_SERVER_CERT_MAX_LEN</code> in one shot.</li>
<li><strong>Fragmented overflow</strong>: Send multiple fragments whose cumulative size exceeds the allocation.</li>
</ol>
<p>On MCUs with deterministic heap layouts (no ASLR), the immediate impact is a crash (denial of service). Depending on the allocator implementation and heap layout, heap metadata corruption may also enable controlled overwrites, but exploitation reliability varies by target and configuration.</p>
<p>Beyond heap overflow, there is a subtler but equally dangerous impact: <strong>certificate injection</strong>. If the attacker writes a valid but attacker-controlled certificate <em>within</em> the buffer bounds (no overflow needed, just a normal-sized write), the device will accept it via <code>pouch_server_certificate_set()</code> and use it for subsequent encrypted sessions. This establishes a man-in-the-middle position without triggering any crash. Certificate injection opens the door to everything that follows.</p>
<p>Note: Pouch provides a server certificate validation option (<code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code>, enabled by default) that checks injected certificates against an embedded CA. When this validation is active, the certificate injection is rejected. The attack chain requires this validation to be disabled, a configuration the build system explicitly warns against for production use.</p>
<h2 id="vulnerability-2-lightdb-state-string-underflow-out-of-bounds-read-cve-2026-23748-5ref5">Vulnerability #2: LightDB State String Underflow Out-of-Bounds Read (CVE-2026-23748 <a href="#ref5">[5]</a>)</h2>
<p><strong>Location</strong>: <code>lightdb_state.c</code> (Golioth Firmware SDK)</p>
<h3 id="chain-context">Chain Context</h3>
<p>With a rogue certificate injected via Vulnerability #1 and server certificate validation disabled, the attacker can sit in the middle of the device encrypted session to the Golioth cloud. From this position, they can craft arbitrary CoAP responses to any LightDB State query the device makes.</p>
<h3 id="the-bug-1">The Bug</h3>
<p>When the SDK receives a LightDB State response for a string value, it strips the surrounding JSON quotes by copying from <code>payload + 1</code> for <code>payload_size - 2</code> bytes:</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="c1">// lightdb_state.c:408-414
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">case</span> <span class="nl">LIGHTDB_GET_TYPE_STRING</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// Remove the leading and trailing quote to get the raw string value
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>    <span class="kt">size_t</span> <span class="n">nbytes</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">buf_size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">payload_size</span> <span class="o">-</span> <span class="mi">2</span><span class="p">);</span>  <span class="c1">// &lt;-- UNDERFLOW
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">(</span><span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">,</span> <span class="n">payload</span> <span class="o">+</span> <span class="mi">1</span> <span class="cm">/* skip quote */</span><span class="p">,</span> <span class="n">nbytes</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">[</span><span class="n">nbytes</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="k">break</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="the-math">The Math</h3>
<p><code>payload_size</code> is a <code>size_t</code>, an unsigned type. When the server responds with a 1-byte payload:</p>
<pre tabindex="0"><code>payload_size = 1

payload_size - 2 = 1 - 2
                 = (size_t)(-1)
                 = 0xFFFFFFFF  (on 32-bit MCU)
                 = 4,294,967,295
</code></pre><p>The <code>min()</code> macro then selects <code>ldb_response-&gt;buf_size - 1</code> (the smaller value, e.g., 63 or 127 depending on the caller&rsquo;s buffer). The <code>memcpy</code> then reads that many bytes starting from <code>payload + 1</code>, which is already one byte past the end of the actual 1-byte payload buffer.</p>
<h3 id="why-this-passes-the-null-check">Why This Passes the Null Check</h3>
<p>The code does check for null payloads before reaching this path:</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="c1">// lightdb_state.c:387-391
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="nf">golioth_payload_is_null</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">is_null</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>But <code>golioth_payload_is_null()</code> only returns true for <code>NULL</code> pointers, zero-length payloads, or payloads starting with the string <code>&quot;null&quot;</code>. A 1-byte payload like <code>'&quot;'</code> passes through.</p>
<h3 id="impact-1">Impact</h3>
<p>The <code>memcpy</code> reads <code>buf_size - 1</code> bytes from memory adjacent to the payload buffer. On embedded devices with flat memory models, this can leak:</p>
<ul>
<li>DTLS session keys stored nearby in heap memory</li>
<li>WiFi/BLE credentials</li>
<li>Other application secrets</li>
</ul>
<p>Even without useful data in adjacent memory, the out-of-bounds read will likely fault on unmapped regions, causing a device crash (DoS).</p>
<h2 id="vulnerability-3-assert-guarded-memcpy-stack-buffer-overflow-cve-2026-23747-6ref6">Vulnerability #3: Assert-Guarded Memcpy Stack Buffer Overflow (CVE-2026-23747 <a href="#ref6">[6]</a>)</h2>
<p><strong>Location</strong>: <code>golioth_payload_as_int()</code> / <code>golioth_payload_as_float()</code> in <code>payload_utils.c</code> (Golioth Firmware SDK)</p>
<h3 id="chain-context-1">Chain Context</h3>
<p>Still operating as MITM via the injected certificate, the attacker crafts an oversized integer or float payload in response to a LightDB State query.</p>
<h3 id="the-bug-2">The Bug</h3>
<p>The payload parsing helpers use <code>assert()</code> as their sole bounds check:</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="c1">// payload_utils.c:15-23
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kt">int32_t</span> <span class="nf">golioth_payload_as_int</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">payload</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">payload_size</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="c1">// Copy payload to a NULL-terminated string
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nf">assert</span><span class="p">(</span><span class="n">payload_size</span> <span class="o">&lt;=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">value</span><span class="p">));</span>   <span class="c1">// &lt;-- Only defense
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>    <span class="c1">// &lt;-- Unbounded in release
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nf">strtol</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// payload_utils.c:25-33
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="kt">float</span> <span class="nf">golioth_payload_as_float</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">payload</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">payload_size</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1">// Copy payload to a NULL-terminated string
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>    <span class="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">32</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nf">assert</span><span class="p">(</span><span class="n">payload_size</span> <span class="o">&lt;=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">value</span><span class="p">));</span>   <span class="c1">// &lt;-- Only defense
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>    <span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>    <span class="c1">// &lt;-- Unbounded in release
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="nf">strtof</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>These functions are called directly from the network-facing <code>on_payload()</code> callback:</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="c1">// lightdb_state.c:395-396
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">case</span> <span class="nl">LIGHTDB_GET_TYPE_INT</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="o">*</span><span class="n">ldb_response</span><span class="o">-&gt;</span><span class="n">i</span> <span class="o">=</span> <span class="nf">golioth_payload_as_int</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">break</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="the-anti-pattern-assert-as-a-security-check">The Anti-Pattern: <code>assert()</code> as a Security Check</h3>
<p>The C standard defines <code>assert()</code> as a <em>debugging aid</em>. When the macro <code>NDEBUG</code> is defined, which is <strong>standard practice in release/production builds</strong>, <code>assert()</code> expands to nothing:</p>
<pre tabindex="0"><code>// &lt;assert.h&gt; with NDEBUG defined:
#define assert(expression) ((void)0)
</code></pre><p>Here is what the compiler sees in each build configuration:</p>
<p><strong>Debug build</strong> (assert active):</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="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// assert fires, aborts if payload_size &gt; 12
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="p">((</span><span class="n">payload_size</span> <span class="o">&lt;=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="o">?</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="mi">0</span> <span class="o">:</span> <span class="nf">__assert_fail</span><span class="p">(...));</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>Release build</strong> (NDEBUG defined):</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="kt">char</span> <span class="n">value</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// assert compiled away --- nothing here
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="p">((</span><span class="kt">void</span><span class="p">)</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nf">memcpy</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">payload_size</span><span class="p">);</span>  <span class="c1">// copies whatever the network sent
</span></span></span></code></pre></div><p>In the release build, <code>memcpy</code> copies <code>payload_size</code> bytes, directly controlled by the network, into a 12-byte (or 32-byte) stack buffer with no bounds check whatsoever.</p>
<h3 id="impact-2">Impact</h3>
<p>On MCUs where stack canaries are not enabled (common in Zephyr and ESP-IDF default configurations for size and performance reasons), this is a textbook stack buffer overflow:</p>
<ul>
<li>The attacker sends a LightDB State integer response with 100+ bytes of payload.</li>
<li><code>memcpy</code> writes past <code>value[12]</code>, overwriting the saved frame pointer, return address, and any other locals on the stack.</li>
<li>On Cortex-M (ARM Thumb), the saved return address on the stack is loaded into the PC. The attacker controls the PC.</li>
<li>With a deterministic memory layout and a known firmware image, this enables control flow hijack and potential RCE on embedded targets without common mitigations.</li>
</ul>
<p>The float variant (<code>value[32]</code>) requires a slightly larger payload but is otherwise identical.</p>
<h2 id="vulnerability-4-coap-blockwise-unterminated-path-out-of-bounds-read-cve-2026-23749-7ref7">Vulnerability #4: CoAP Blockwise Unterminated Path Out-of-Bounds Read (CVE-2026-23749 <a href="#ref7">[7]</a>)</h2>
<p><strong>Location</strong>: <code>blockwise_transfer_init()</code> in <code>coap_blockwise.c</code> (Golioth Firmware SDK)</p>
<p>This vulnerability is locally triggered (by the application&rsquo;s own path strings) rather than network-exploitable. We include it to complete the pattern of C memory safety hazards found across the SDK.</p>
<h3 id="the-bug-3">The Bug</h3>
<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="c1">// coap_blockwise.c:76-94
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="kt">int</span> <span class="nf">blockwise_transfer_init</span><span class="p">(</span><span class="k">struct</span> <span class="n">blockwise_transfer</span> <span class="o">*</span><span class="n">ctx</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                                   <span class="k">struct</span> <span class="n">golioth_client</span> <span class="o">*</span><span class="n">client</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                                   <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">path_prefix</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                                   <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">path</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                                   <span class="k">enum</span> <span class="n">golioth_content_type</span> <span class="n">content_type</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">strlen</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</span><span class="p">)</span>  <span class="c1">// allows ==
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>    <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nf">strncpy</span><span class="p">(</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">path</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</span><span class="p">);</span>  <span class="c1">// no NUL if len == max
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><p>The buffer is declared as <code>char path[CONFIG_GOLIOTH_COAP_MAX_PATH_LEN + 1]</code>, providing space for a NUL terminator. However, when <code>strlen(path)</code> is <em>exactly</em> <code>CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</code>:</p>
<ol>
<li>The <code>&gt;</code> check passes (it should be <code>&gt;=</code>).</li>
<li><code>strncpy</code> copies exactly <code>CONFIG_GOLIOTH_COAP_MAX_PATH_LEN</code> bytes and does <strong>not</strong> append a NUL terminator (this is <code>strncpy</code>&rsquo;s documented behavior when the source length equals or exceeds the count).</li>
<li>Later, <code>strlen(ctx-&gt;path)</code> in <code>coap_client.c</code> reads past the buffer searching for a NUL byte.</li>
</ol>
<p>This is a classic <code>strncpy</code> misunderstanding. Many C developers assume <code>strncpy</code> always NUL-terminates. It does not when the source string fills the destination.</p>
<h3 id="impact-3">Impact</h3>
<p>The out-of-bounds <code>strlen()</code> read can cause:</p>
<ul>
<li>A crash if it walks into unmapped memory</li>
<li>Incorrect path construction if it finds a stale NUL byte further in memory</li>
<li>On safety-critical IoT devices (medical, industrial), even a locally-triggered crash can have real-world consequences</li>
</ul>
<h2 id="theoretical-chain-ble-proximity-to-device-compromise">Theoretical Chain: BLE Proximity to Device Compromise</h2>
<p>The following attack chain is <strong>theoretical</strong>. It has not been demonstrated end to end. It requires <code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code> to be disabled, which is not default and the build system explicitly warns against for production use. With certificate validation enabled, the default, the chain breaks at step 2 and injected certificates are rejected.</p>
<p>That said, the chain illustrates how individual memory safety bugs compound when defense-in-depth is weakened. If certificate validation were disabled, three of the four vulnerabilities could connect into a path from unauthenticated BLE proximity to arbitrary code execution:</p>
<div style="position: relative; padding-left: 2.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: calc(0.25rem + 10px); top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-accent);"></div>
  <div style="position: relative; padding-bottom: 1.5rem;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-text-secondary); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">1</div>
    <div style="font-weight: 600; color: var(--color-text);">BLE Discovery (unauthenticated)</div>
    <div style="font-size: 0.875rem; color: var(--color-text-secondary);">Attacker scans for BLE GATT services, discovers server cert characteristic. No pairing or bonding required.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.5rem;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">2</div>
    <div style="font-weight: 600; color: var(--color-text);">Certificate Injection <span style="font-size: 0.8rem; font-weight: 400; color: var(--color-accent);">(Vuln 1)</span></div>
    <div style="font-size: 0.875rem; color: var(--color-text-secondary);">Write attacker-controlled cert via GATT (<code>BT_GATT_PERM_WRITE</code>, no auth). Device stores it via <code>pouch_server_certificate_set()</code>.</div>
  </div>
  <div style="position: relative; padding-bottom: 1.5rem;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">3</div>
    <div style="font-weight: 600; color: var(--color-text);">MITM Established</div>
    <div style="font-size: 0.875rem; color: var(--color-text-secondary);">Device authenticates server using injected cert. Attacker holds the private key, intercepts and proxies the connection.</div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -2.25rem; top: 0.15rem; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--color-bg); line-height: 22px; text-align: center;">4</div>
    <div style="font-weight: 600; color: var(--color-text);">Exploitation <span style="font-size: 0.8rem; font-weight: 400; color: var(--color-accent);">(Vuln 2 or Vuln 3)</span></div>
  </div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin: 1rem 0 1.5rem;">
<div style="background: var(--color-bg-card); border: 1px solid #f59e0b; border-radius: 8px; padding: 1rem 1.25rem;">
<div style="font-weight: 600; color: var(--color-text); margin-bottom: 0.25rem;">Memory Disclosure</div>
<div style="font-size: 0.8rem; font-weight: 600; color: #f59e0b; margin-bottom: 0.5rem;">Vuln 2</div>
<div style="font-size: 0.85rem; color: var(--color-text-secondary); line-height: 1.5;">Send 1-byte string response → <code>size_t</code> underflow → OOB read → leak adjacent heap memory (keys, creds)</div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid #dc2626; border-radius: 8px; padding: 1rem 1.25rem;">
<div style="font-weight: 600; color: var(--color-text); margin-bottom: 0.25rem;">Remote Code Execution</div>
<div style="font-size: 0.8rem; font-weight: 600; color: #dc2626; margin-bottom: 0.5rem;">Vuln 3</div>
<div style="font-size: 0.85rem; color: var(--color-text-secondary); line-height: 1.5;">Send oversized integer response → <code>assert()</code> absent in release → <code>memcpy</code> overflows 12-byte stack buffer → overwrite return address → RCE</div>
</div>
</div>
<h3 id="step-by-step-walkthrough">Step-by-Step Walkthrough</h3>
<ol>
<li>
<p><strong>BLE Discovery</strong> (~10-100m range): The attacker uses a BLE scanner (e.g., <code>nRF Connect</code>, a custom <code>bleak</code> script) to discover the Golioth GATT service and the server certificate characteristic UUID.</p>
</li>
<li>
<p><strong>Certificate Injection</strong>: The attacker writes a valid, attacker-controlled X.509 certificate to the characteristic. Because <code>BT_GATT_PERM_WRITE</code> requires no authentication, this succeeds on first connection. The device calls <code>pouch_server_certificate_set()</code> and persists the certificate. No overflow is needed for this step. The certificate just needs to be within <code>CONFIG_POUCH_SERVER_CERT_MAX_LEN</code>.</p>
</li>
<li>
<p><strong>MITM Established</strong>: When the device next initiates a session to the Golioth cloud, it uses the injected certificate to authenticate the server. The attacker, holding the corresponding private key, terminates the connection from the device and opens a separate connection to the real Golioth cloud, proxying traffic between the two. This step requires <code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code> to be disabled; with validation enabled (the default), the injected certificate is rejected against the embedded CA and the chain breaks here.</p>
</li>
<li>
<p><strong>Exploitation</strong>: From the MITM position, the attacker waits for, or triggers, a LightDB State query from the device, then:</p>
<ul>
<li><strong>For memory disclosure (Vuln 2)</strong>: Responds with a 1-byte payload to a string GET, causing the underflow and OOB read. Leaked memory may contain DTLS keys or other secrets.</li>
<li><strong>For code execution (Vuln 3)</strong>: Responds with an oversized payload to an integer GET. In release builds, the assert is gone, and <code>memcpy</code> overwrites the stack. On Cortex-M with no ASLR or stack canaries, the return address is overwritten with an attacker-controlled value.</li>
</ul>
</li>
</ol>
<h3 id="feasibility-and-limitations">Feasibility and Limitations</h3>
<ul>
<li><strong>BLE range constraint</strong>: The initial step requires physical proximity. In practice, directional antennas can extend BLE range significantly, and many IoT deployments are in physically accessible locations.</li>
<li><strong>Certificate persistence</strong>: Whether the injected certificate persists across reboots depends on the Pouch configuration and storage backend. If persistent, the MITM survives power cycles.</li>
<li><strong>Release build requirement</strong>: Vulnerability 3 requires that <code>NDEBUG</code> is defined. This is the default for production firmware on all supported platforms.</li>
<li><strong>Certificate validation</strong>: The MITM step requires <code>CONFIG_POUCH_VALIDATE_SERVER_CERT</code> to be disabled. This option is enabled by default and validates server certificates against an embedded CA. When enabled, certificate injection is blocked.</li>
<li><strong>Vulnerability 4 is independent</strong>: The CoAP blockwise path issue is locally triggered and is not part of this chain.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Four memory safety vulnerabilities across two Golioth IoT products. Each is independently triggerable under its own preconditions. The BLE heap overflow, Vuln 1, is the most immediately dangerous: any device running Pouch with BLE enabled can be crashed or have its certificate store corrupted by an unauthenticated attacker in radio range.</p>
<p>The theoretical chain from BLE to RCE requires certificate validation to be disabled, which is not the default. But the individual bugs do not require special configuration. A 1-byte CoAP response triggers the integer underflow. An oversized integer payload overwrites the stack in any release build. These are real risks independent of any chain.</p>
<p>The bugs themselves are not exotic. They are <code>memcpy</code> without bounds checks, <code>assert</code> instead of <code>if</code>, <code>strncpy</code> without NUL termination, unsigned subtraction without underflow guards. These are patterns that every C programmer learns to avoid, and that every C codebase eventually contains unless the review process catches them.</p>
<p>Embedded firmware needs the same level of scrutiny as kernel code. It runs with equivalent privilege (there is no lower ring on a Cortex-M), handles untrusted input from multiple interfaces (BLE, WiFi, CoAP, MQTT), and deploys to devices that may be physically accessible to attackers and difficult to update after deployment.</p>
<h2 id="disclosure-and-fix">Disclosure and Fix</h2>
<p>We reported all four vulnerabilities to Golioth through coordinated disclosure. The team responded promptly and confirmed all fixes before the disclosure deadline. SecMate would like to thank the Golioth team and maintainers for their responsiveness and security awareness. Fixes are available in Firmware SDK v0.22.0 and Pouch commit <a href="https://github.com/golioth/pouch/commit/1b2219a1" rel="noopener noreferrer" target="_blank">1b2219a1</a>.</p>
<h3 id="disclosure-timeline">Disclosure Timeline</h3>
<div style="position: relative; padding-left: 1.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: 0.35rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-border);"></div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Oct 31, 2025</div>
    <div style="color: var(--color-text);">SecMate sent initial report to Golioth (<code>security@golioth.io</code>) with findings and proof-of-concept code</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Nov 5, 2025</div>
    <div style="color: var(--color-text);">Golioth confirmed receipt and began internal review</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 7, 2026</div>
    <div style="color: var(--color-text);">SecMate followed up; noted 90-day disclosure window ending ~Jan 29, 2026</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 9, 2026</div>
    <div style="color: var(--color-text);">Golioth confirmed all reported vulnerabilities have been addressed</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 15, 2026</div>
    <div style="color: var(--color-text);">Golioth confirmed no CVEs assigned; agreed to end-of-January disclosure; SecMate offered to handle CVE assignment</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #f59e0b; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 16, 2026</div>
    <div style="color: var(--color-text);">CVE IDs assigned by <a href="https://vulncheck.com/">VulnCheck</a>: CVE-2026-23747, CVE-2026-23748, CVE-2026-23749, CVE-2026-23750</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 22, 2026</div>
    <div style="color: var(--color-text);">Version ranges provided to VulnCheck; CVE records finalized for Firmware SDK vulnerabilities</div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Jan 30, 2026</div>
    <div style="color: var(--color-text);">Public disclosure</div>
  </div>
</div>
<h3 id="cve-summary">CVE Summary</h3>
<div style="display: grid; gap: 0.75rem; margin: 1.5rem 0;">
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: color-mix(in srgb, #dc2626 12%, transparent); color: #dc2626; border: 1px solid color-mix(in srgb, #dc2626 25%, transparent);">CVE-2026-23750</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">BLE GATT Server Cert Heap Overflow</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Pouch &middot; Introduced v0.1.0 &middot; Fixed in <a href="https://github.com/golioth/pouch/commit/1b2219a1" style="color: var(--color-accent);">1b2219a1</a></div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: #f59e0b; border: 1px solid color-mix(in srgb, #f59e0b 25%, transparent);">CVE-2026-23748</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">LightDB State String Underflow OOB Read</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Firmware SDK &middot; Introduced v0.10.0 &middot; Fixed in <a href="https://github.com/golioth/golioth-firmware-sdk/commit/d7f55b38" style="color: var(--color-accent);">d7f55b38</a> (v0.22.0)</div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: #f59e0b; border: 1px solid color-mix(in srgb, #f59e0b 25%, transparent);">CVE-2026-23747</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">Assert-Guarded Memcpy Stack Overflow</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Firmware SDK &middot; Introduced v0.10.0 &middot; Fixed in <a href="https://github.com/golioth/golioth-firmware-sdk/commit/57eac06" style="color: var(--color-accent);">57eac06</a> (v0.22.0)</div>
</div>
<div style="background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; padding: 0.875rem 1.25rem;">
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.375rem;"><span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.8rem; font-weight: 600; font-family: var(--font-mono); border-radius: 4px; background: var(--color-muted-bg); color: var(--color-text-secondary); border: 1px solid var(--color-border);">CVE-2026-23749</span><span style="font-weight: 600; color: var(--color-text); font-size: 0.9rem;">CoAP Blockwise Unterminated Path OOB Read</span></div>
<div style="font-size: 0.825rem; color: var(--color-text-secondary);">Firmware SDK &middot; Introduced v0.19.1 &middot; Fixed in <a href="https://github.com/golioth/golioth-firmware-sdk/commit/0e788217" style="color: var(--color-accent);">0e788217</a> (v0.22.0)</div>
</div>
</div>
<h2 id="whats-next">What&rsquo;s Next</h2>
<p>This is our second public disclosure, after the <a href="/posts/libcoap-vulnerabilities-disclosure/">libcoap vulnerabilities</a> we published earlier. These four CVEs bring our public total to six, with more findings across embedded and systems projects still in coordinated disclosure.</p>
<p>For the full list, see our <a href="https://secmate.dev/disclosures?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=golioth-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="golioth-vulnerabilities-disclosure" data-post-category="security">disclosure page</a>. If you&rsquo;re building on embedded systems and want to find vulnerabilities before attackers do, <a href="https://secmate.dev/register?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=golioth-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_register" data-post-slug="golioth-vulnerabilities-disclosure" data-post-category="security">reach out</a>.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] Golioth. &ldquo;Golioth Firmware SDK&rdquo; <em>GitHub</em>. <a href="https://github.com/golioth/golioth-firmware-sdk" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] Golioth. &ldquo;Golioth Pouch&rdquo; <em>GitHub</em>. <a href="https://github.com/golioth/pouch" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] IETF. &ldquo;RFC 7252 - The Constrained Application Protocol (CoAP)&rdquo; <em>IETF Datatracker</em>, June 2014. <a href="https://datatracker.ietf.org/doc/html/rfc7252" rel="noopener noreferrer" target="_blank">RFC</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] NVD. &ldquo;CVE-2026-23750 - BLE GATT Server Certificate Heap Overflow in Golioth Pouch&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23750" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref5"></a>[5] NVD. &ldquo;CVE-2026-23748 - LightDB State String Underflow OOB Read in Golioth Firmware SDK&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23748" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref6"></a>[6] NVD. &ldquo;CVE-2026-23747 - Assert-Guarded Memcpy Stack Overflow in Golioth Firmware SDK&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23747" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref7"></a>[7] NVD. &ldquo;CVE-2026-23749 - CoAP Blockwise Unterminated Path OOB Read in Golioth Firmware SDK&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-23749" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item><item><title>How SecMate Discovered two Vulnerabilities in libcoap</title><link>https://blog.secmate.dev/posts/libcoap-vulnerabilities-disclosure/</link><pubDate>Wed, 14 Jan 2026 00:00:00 +0000</pubDate><atom:updated>2026-01-14T00:00:00+00:00</atom:updated><dc:creator>Maxime Rossi Bellom</dc:creator><dc:creator>Ramtine Tofighi Shirazi</dc:creator><category>Security</category><category>Vulnerability Research</category><category>IoT</category><guid>https://blog.secmate.dev/posts/libcoap-vulnerabilities-disclosure/</guid><description>SecMate's AI-powered static analysis tool discovered two vulnerabilities in libcoap: an out-of-bounds write in address resolution (CVE-2025-34468) and an out-of-bounds read in OSCORE parsing (CVE-2025-59391). Here's the technical breakdown.</description><content:encoded><![CDATA[<h1 id="how-secmate-discovered-vulnerabilities-in-libcoap">How SecMate Discovered Vulnerabilities in libcoap</h1>
<p>We discovered two memory-safety issues in libcoap, a widely deployed IoT library. The first is an out-of-bounds write in a fixed buffer reachable via proxy requests that can crash servers with a single UDP packet. The second is an out-of-bounds read in OSCORE configuration parsing that can crash the parser on malformed input. Both vulnerabilities have amplified impact on the constrained embedded devices the library was built for.</p>
<h2 id="about-secmate">About SecMate</h2>
<p><a href="https://secmate.dev?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=libcoap-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_secmate" data-post-slug="libcoap-vulnerabilities-disclosure" data-post-category="security">SecMate</a> combines static analysis with AI to find security vulnerabilities in source code, specifically in embedded devices and low-level code.</p>
<p>So far, we have reported <strong>more than 60 vulnerabilities</strong> across embedded and systems projects, most still under <a href="https://secmate.dev/disclosures?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=libcoap-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="libcoap-vulnerabilities-disclosure" data-post-category="security">coordinated disclosure</a>.</p>
<p>Today, we are sharing two of our findings in libcoap (the first two CVEs uncovered with SecMate), a widely-used CoAP implementation for IoT devices.</p>
<h2 id="what-is-libcoap">What is libcoap?</h2>
<p>libcoap <a href="#ref1">[1]</a> is a C implementation of the Constrained Application Protocol (CoAP), standardized as RFC 7252 <a href="#ref2">[2]</a>. Think of CoAP as &ldquo;HTTP for IoT&rdquo;: a lightweight protocol for devices that can&rsquo;t afford the overhead of full HTTP/TCP stacks.</p>
<p>The library is mature and widely deployed:</p>
<ul>
<li>Supports multiple TLS backends (OpenSSL, GnuTLS, Mbed TLS, wolfSSL)</li>
<li>Runs on everything from Linux servers to ESP32 microcontrollers</li>
<li>Used in smart home devices, industrial sensors, and critical infrastructure</li>
</ul>
<p>Here is the catch: the devices running libcoap are often <em>constrained</em>. Limited memory, no MMU, no ASLR, no stack canaries. When you find a memory-safety issue in this context, the impact is amplified.</p>
<h2 id="vulnerability-1-static-buffer-overflow-in-address-resolution-cve-2025-34468-3ref3">Vulnerability #1: Static Buffer Overflow in Address Resolution (CVE-2025-34468 <a href="#ref3">[3]</a>)</h2>
<p><strong>Location</strong>: <code>coap_resolve_address_info()</code> in <code>src/coap_address.c</code></p>
<h3 id="the-bug">The Bug</h3>
<p>This vulnerability affects libcoap servers running in <strong>proxy mode</strong>. The vulnerable function resolves hostnames for CoAP proxy requests, copying the hostname into a fixed 256-byte static buffer without checking the length:</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">static</span> <span class="kt">char</span> <span class="n">addrstr</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span>  <span class="c1">// Fixed-size static buffer
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nf">memset</span><span class="p">(</span><span class="n">addrstr</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">addrstr</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">address</span> <span class="o">&amp;&amp;</span> <span class="n">address</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">addrstr</span><span class="p">,</span> <span class="n">address</span><span class="o">-&gt;</span><span class="n">s</span><span class="p">,</span> <span class="n">address</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">);</span>  <span class="c1">// No bounds check!
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">else</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">addrstr</span><span class="p">,</span> <span class="s">&#34;localhost&#34;</span><span class="p">,</span> <span class="mi">9</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nf">getaddrinfo</span><span class="p">(</span><span class="n">addrstr</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">hints</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">res</span><span class="p">);</span>
</span></span></code></pre></div><p>The <code>address-&gt;length</code> field comes directly from parsing CoAP options. An attacker controls this value.</p>
<h3 id="the-attack">The Attack</h3>
<p>When a CoAP server runs in proxy mode, it accepts <code>Proxy-Uri</code> options that specify where to forward requests. The hostname from this URI flows directly into the vulnerable <code>memcpy()</code>:</p>
<ol>
<li>A CoAP request arrives with a <code>Proxy-Uri</code> option containing an oversized hostname (&gt;256 bytes)</li>
<li>The proxy extracts the hostname and passes it to <code>coap_resolve_address_info()</code></li>
<li>The function calls <code>memcpy(addrstr, address-&gt;s, address-&gt;length)</code> without bounds checking</li>
<li>The fixed-size buffer overflows, corrupting adjacent memory</li>
<li>The server crashes or misbehaves; the exact impact depends on layout and mitigations</li>
</ol>
<h3 id="impact">Impact</h3>
<p>On a standard Linux system, this bug is likely to crash the server (Denial of Service). On embedded devices without memory protection, memory corruption can have broader consequences, but practical exploitation depends on layout and mitigations. One UDP packet, no authentication required when proxy mode is enabled.</p>
<h2 id="vulnerability-2-out-of-bounds-read-in-oscore-parsing-cve-2025-59391-4ref4">Vulnerability #2: Out-of-Bounds Read in OSCORE Parsing (CVE-2025-59391 <a href="#ref4">[4]</a>)</h2>
<p><strong>Location</strong>: <code>get_split_entry()</code> in <code>src/coap_oscore.c</code></p>
<h3 id="the-bug-1">The Bug</h3>
<p>OSCORE (Object Security for Constrained RESTful Environments) is a security layer for CoAP. libcoap parses OSCORE configuration files that include boolean fields. The parsing code uses <code>memcmp()</code> with a user-controlled length:</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">case</span> <span class="nl">COAP_ENC_BOOL</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">memcmp</span><span class="p">(</span><span class="s">&#34;true&#34;</span><span class="p">,</span> <span class="n">begin</span><span class="p">,</span> <span class="n">end</span> <span class="o">-</span> <span class="n">begin</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">value</span><span class="o">-&gt;</span><span class="n">u</span><span class="p">.</span><span class="n">value_int</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="nf">memcmp</span><span class="p">(</span><span class="s">&#34;false&#34;</span><span class="p">,</span> <span class="n">begin</span><span class="p">,</span> <span class="n">end</span> <span class="o">-</span> <span class="n">begin</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">value</span><span class="o">-&gt;</span><span class="n">u</span><span class="p">.</span><span class="n">value_int</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span></code></pre></div><p>The problem: <code>end - begin</code> comes from the configuration file. If an attacker provides a value like <code>trueAAAAAAAA...</code> (thousands of characters), the <code>memcmp()</code> reads past the static string <code>&quot;true&quot;</code> into adjacent memory.</p>
<h3 id="impact-1">Impact</h3>
<p>This issue requires the ability to provide malicious OSCORE configuration input, which is typically a local or trusted provisioning path rather than a network-facing vector. The realistic outcome is a crash or parse failure on malformed configs.</p>
<h3 id="theoretical-oracle-attack">Theoretical Oracle Attack</h3>
<p>In theory, if a local attacker could repeatedly provide different config files and observe success/failure signals from the parser, they could use this as an oracle to infer adjacent <code>.rodata</code> bytes one at a time. The diagram below illustrates what such an attack would look like:</p>
<svg viewBox="0 0 440 100" style="width: 100%; max-width: 440px; height: auto; margin: 1.5rem auto; display: block; font-family: monospace;">
  <style>
    .svg-label { fill: var(--color-text-secondary); }
    .svg-bg { fill: var(--color-muted-bg); stroke: var(--color-border); }
    .svg-known { fill: color-mix(in srgb, var(--color-accent) 30%, var(--color-bg-card)); stroke: var(--color-accent); }
    .svg-unknown { fill: var(--color-bg-card); stroke: var(--color-border); }
    .svg-text { fill: var(--color-text); }
    .svg-text-muted { fill: var(--color-text-secondary); }
  </style>
  <text x="220" y="14" text-anchor="middle" class="svg-label" font-size="11">memory layout</text>
  <rect x="10" y="22" width="420" height="44" rx="4" class="svg-bg" stroke-width="1"/>
  <rect x="15" y="26" width="36" height="36" rx="3" class="svg-known" stroke-width="2"/><text x="33" y="50" text-anchor="middle" class="svg-text" font-size="15">t</text>
  <rect x="56" y="26" width="36" height="36" rx="3" class="svg-known" stroke-width="2"/><text x="74" y="50" text-anchor="middle" class="svg-text" font-size="15">r</text>
  <rect x="97" y="26" width="36" height="36" rx="3" class="svg-known" stroke-width="2"/><text x="115" y="50" text-anchor="middle" class="svg-text" font-size="15">u</text>
  <rect x="138" y="26" width="36" height="36" rx="3" class="svg-known" stroke-width="2"/><text x="156" y="50" text-anchor="middle" class="svg-text" font-size="15">e</text>
  <rect x="179" y="26" width="36" height="36" rx="3" class="svg-known" stroke-width="2"/><text x="197" y="50" text-anchor="middle" class="svg-text" font-size="12">\0</text>
  <rect x="220" y="26" width="36" height="36" rx="3" class="svg-unknown" stroke-width="1"><animate attributeName="fill" values="var(--color-bg-card);#7f1d1d;#166534;#166534;#166534;#166534" dur="6s" repeatCount="indefinite"/><animate attributeName="stroke" values="var(--color-border);#dc2626;#22c55e;#22c55e;#22c55e;#22c55e" dur="6s" repeatCount="indefinite"/></rect>
  <text x="238" y="50" text-anchor="middle" class="svg-text-muted" font-size="15"><animate attributeName="opacity" values="1;1;0;0;0;0" dur="6s" repeatCount="indefinite"/>?</text>
  <text x="238" y="50" text-anchor="middle" class="svg-text" font-size="15"><animate attributeName="opacity" values="0;0;1;1;1;1" dur="6s" repeatCount="indefinite"/>f</text>
  <rect x="261" y="26" width="36" height="36" rx="3" class="svg-unknown" stroke-width="1"><animate attributeName="fill" values="var(--color-bg-card);var(--color-bg-card);var(--color-bg-card);#7f1d1d;#166534;#166534" dur="6s" repeatCount="indefinite"/><animate attributeName="stroke" values="var(--color-border);var(--color-border);var(--color-border);#dc2626;#22c55e;#22c55e" dur="6s" repeatCount="indefinite"/></rect>
  <text x="279" y="50" text-anchor="middle" class="svg-text-muted" font-size="15"><animate attributeName="opacity" values="1;1;1;1;0;0" dur="6s" repeatCount="indefinite"/>?</text>
  <text x="279" y="50" text-anchor="middle" class="svg-text" font-size="15"><animate attributeName="opacity" values="0;0;0;0;1;1" dur="6s" repeatCount="indefinite"/>a</text>
  <rect x="302" y="26" width="36" height="36" rx="3" class="svg-unknown" stroke-width="1"/><text x="320" y="50" text-anchor="middle" class="svg-text-muted" font-size="15">?</text>
  <rect x="343" y="26" width="36" height="36" rx="3" class="svg-unknown" stroke-width="1"/><text x="361" y="50" text-anchor="middle" class="svg-text-muted" font-size="15">?</text>
  <rect x="384" y="26" width="36" height="36" rx="3" class="svg-unknown" stroke-width="1"/><text x="402" y="50" text-anchor="middle" class="svg-text-muted" font-size="15">?</text>
  <text x="135" y="88" text-anchor="start" font-size="15" font-weight="bold" class="svg-label">true\0</text>
  <text x="200" y="88" text-anchor="start" font-size="15" font-weight="bold" class="svg-label"><animate attributeName="opacity" values="1;0;0;0;0;0" dur="6s" calcMode="discrete" repeatCount="indefinite"/>...</text>
  <text x="200" y="88" text-anchor="start" font-size="15" font-weight="bold" fill="#dc2626"><animate attributeName="opacity" values="0;1;0;0;0;0" dur="6s" calcMode="discrete" repeatCount="indefinite"/>Y ✗</text>
  <text x="200" y="88" text-anchor="start" font-size="15" font-weight="bold" fill="#22c55e"><animate attributeName="opacity" values="0;0;1;0;0;0" dur="6s" calcMode="discrete" repeatCount="indefinite"/>f ✓</text>
  <text x="200" y="88" text-anchor="start" font-size="15" font-weight="bold" fill="#dc2626"><animate attributeName="opacity" values="0;0;0;1;0;0" dur="6s" calcMode="discrete" repeatCount="indefinite"/>fK ✗</text>
  <text x="200" y="88" text-anchor="start" font-size="15" font-weight="bold" fill="#22c55e"><animate attributeName="opacity" values="0;0;0;0;1;0" dur="6s" calcMode="discrete" repeatCount="indefinite"/>fa ✓</text>
  <text x="200" y="88" text-anchor="start" font-size="15" font-weight="bold" fill="#22c55e"><animate attributeName="opacity" values="0;0;0;0;0;1" dur="6s" calcMode="discrete" repeatCount="indefinite"/>fa</text>
</svg>
<p>However, this attack is impractical: OSCORE configuration is loaded from local files, so an attacker would need repeated local access to provide different config files and observe results. At that point, they likely have access that makes this leak scenario moot.</p>
<blockquote class="admonition admonition-insight">
<strong>Why This Bug is Easy to Miss</strong>
<p>This vulnerability is subtle: the out-of-bounds read in <code>memcmp()</code> may not crash or produce obvious symptoms during normal operation. Spotting it requires reasoning about how data flows through multiple operations, something traditional static analysis tools typically miss.</p>
</blockquote>
<h2 id="the-embedded-security-challenge">The Embedded Security Challenge</h2>
<p>Both vulnerabilities share a common theme: <strong>they are worse on the devices libcoap is designed for</strong>.</p>
<p>Desktop and server systems have decades of hardening:</p>
<ul>
<li>ASLR randomizes memory layout</li>
<li>Stack canaries detect buffer overflows</li>
<li>DEP/NX prevents code execution from data segments</li>
<li>Sandboxing limits blast radius</li>
</ul>
<p>Constrained embedded devices often have none of these. A microcontroller running FreeRTOS or bare metal has:</p>
<ul>
<li>Flat, predictable memory layout</li>
<li>No memory protection unit (MPU) or it is disabled for performance</li>
<li>No operating system to enforce isolation</li>
<li>Direct hardware access for any code that runs</li>
</ul>
<p>This isn&rsquo;t a criticism of libcoap. It is the reality of their target environment. But it means bugs like these deserve extra scrutiny.</p>
<blockquote class="admonition admonition-insight">
<strong>Key Insight</strong>
<p>When auditing code for constrained devices, assume the worst-case exploitation scenario. Mitigations you take for granted on desktop systems simply don't exist.</p>
</blockquote>
<h2 id="disclosure-and-fix">Disclosure and Fix</h2>
<p>We reported both vulnerabilities to the libcoap maintainers through coordinated disclosure. The team responded quickly and professionally, and fixes are now available in version 4.3.5a.</p>
<h3 id="disclosure-timeline">Disclosure Timeline</h3>
<div style="position: relative; padding-left: 1.5rem; margin: 1.5rem 0;">
  <div style="position: absolute; left: 0.35rem; top: 0.5rem; bottom: 0.5rem; width: 2px; background: var(--color-border);"></div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 3, 2025</div>
    <div style="color: var(--color-text);">SecMate reported OSCORE out-of-bounds read to libcoap security team</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 3, 2025</div>
    <div style="color: var(--color-text);">Maintainer acknowledged and confirmed the issue is reproducible</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 10, 2025</div>
    <div style="color: var(--color-text);">SecMate reported static buffer overflow to libcoap security team</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 10, 2025</div>
    <div style="color: var(--color-text);">Maintainer acknowledged the second vulnerability</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 15, 2025</div>
    <div style="color: var(--color-text);">Fix for OSCORE issue merged (<a href="https://github.com/obgm/libcoap/pull/1730">PR #1730</a>)</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 15, 2025</div>
    <div style="color: var(--color-text);">Fix for static buffer overflow merged (<a href="https://github.com/obgm/libcoap/pull/1737">PR #1737</a>)</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--color-text-secondary); border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Sep 15, 2025</div>
    <div style="color: var(--color-text);">CVE requests submitted to MITRE for both issues</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Nov 27, 2025</div>
    <div style="color: var(--color-text);">libcoap v4.3.5a released with patches for both vulnerabilities</div>
  </div>
  <div style="position: relative; padding-bottom: 1.25rem;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #f59e0b; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Dec 8, 2025</div>
    <div style="color: var(--color-text);">CVE-2025-59391 assigned by MITRE <span style="color: var(--color-text-secondary);">(85 days after request)</span></div>
  </div>
  <div style="position: relative;">
    <div style="position: absolute; left: -1.15rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: #f59e0b; border: 2px solid var(--color-bg);"></div>
    <div style="font-size: 0.75rem; color: var(--color-text-secondary); font-weight: 600;">Dec 31, 2025</div>
    <div style="color: var(--color-text);">CVE-2025-34468 assigned by <a href="https://vulncheck.com/">VulnCheck</a> <span style="color: var(--color-text-secondary);">(same day, MITRE never responded)</span></div>
  </div>
</div>
<p>We want to thank the libcoap team, particularly Jon Shallow, for their responsiveness in addressing these issues. The maintainer responded on the same day for both reports. Maintaining open-source security infrastructure is often thankless work, and their quick turnaround made the ecosystem safer.</p>
<h2 id="whats-next">What&rsquo;s Next</h2>
<p>These two CVEs are just a sample of what SecMate has found. We&rsquo;re continuing to analyze embedded codebases and report vulnerabilities through coordinated disclosure.</p>
<p>If you&rsquo;re curious about our other findings, check out our <a href="https://secmate.dev/disclosures?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=libcoap-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_disclosures" data-post-slug="libcoap-vulnerabilities-disclosure" data-post-category="security">disclosure page</a>. And if you&rsquo;re building embedded systems and want to find vulnerabilities before attackers do, <a href="https://secmate.dev/register?utm_source=blog&amp;utm_medium=body&amp;utm_campaign=libcoap-vulnerabilities-disclosure&amp;utm_content=security" rel="noopener noreferrer" target="_blank" data-cta-type="body_register" data-post-slug="libcoap-vulnerabilities-disclosure" data-post-category="security">reach out</a>.</p>
<h2 id="references">References</h2>
<ul>
<li>
<p><a id="ref1"></a>[1] libcoap. &ldquo;libcoap - C-Implementation of CoAP&rdquo; <em>GitHub</em>. <a href="https://github.com/obgm/libcoap" rel="noopener noreferrer" target="_blank">Repository</a></p>
</li>
<li>
<p><a id="ref2"></a>[2] IETF. &ldquo;RFC 7252 - The Constrained Application Protocol (CoAP)&rdquo; <em>IETF Datatracker</em>, June 2014. <a href="https://datatracker.ietf.org/doc/html/rfc7252" rel="noopener noreferrer" target="_blank">RFC</a></p>
</li>
<li>
<p><a id="ref3"></a>[3] NVD. &ldquo;CVE-2025-34468 - Static Buffer Overflow in libcoap&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-34468" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
<li>
<p><a id="ref4"></a>[4] NVD. &ldquo;CVE-2025-59391 - Out-of-Bounds Read in libcoap&rdquo; <em>National Vulnerability Database</em>. <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-59391" rel="noopener noreferrer" target="_blank">CVE</a></p>
</li>
</ul>
<hr>
<p><em>The SecMate Team</em></p>
]]></content:encoded><media:content url="https://blog.secmate.dev/images/og_image.jpg" medium="image"/></item></channel></rss>