<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[VyPr AI]]></title><description><![CDATA[Vulnerability discovery at the speed of light]]></description><link>https://blog.vyprsec.ai</link><image><url>https://substackcdn.com/image/fetch/$s_!0SbN!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4ca77ae-2697-4131-aaca-2468ab7ee09c_260x260.png</url><title>VyPr AI</title><link>https://blog.vyprsec.ai</link></image><generator>Substack</generator><lastBuildDate>Sun, 25 Oct 2026 06:10:51 GMT</lastBuildDate><atom:link href="https://blog.vyprsec.ai/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[VyPr AI AB]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[vyprsec@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[vyprsec@substack.com]]></itunes:email><itunes:name><![CDATA[Syafiq Al Atiiq]]></itunes:name></itunes:owner><itunes:author><![CDATA[Syafiq Al Atiiq]]></itunes:author><googleplay:owner><![CDATA[vyprsec@substack.com]]></googleplay:owner><googleplay:email><![CDATA[vyprsec@substack.com]]></googleplay:email><googleplay:author><![CDATA[Syafiq Al Atiiq]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[php2Shell: the RCE that put 70% of the web at risk]]></title><description><![CDATA[On October 20, 2026, word leaked that PHP, the language running roughly three quarters of the web&#8217;s known server-side code, had two unauthenticated remote-code-execution bugs in its core and that a working exploit tool already existed.]]></description><link>https://blog.vyprsec.ai/p/php2shell</link><guid isPermaLink="false">https://blog.vyprsec.ai/p/php2shell</guid><dc:creator><![CDATA[A. Rizqi Meydiarso]]></dc:creator><pubDate>Thu, 22 Oct 2026 09:05:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!0SbN!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4ca77ae-2697-4131-aaca-2468ab7ee09c_260x260.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On October 20, 2026, word leaked that PHP, the language running roughly three quarters of the web&#8217;s known server-side code, had two unauthenticated remote-code-execution bugs in its core and that a working exploit tool already existed. The details were supposed to stay under embargo until the official release, but they didn&#8217;t. Exploitation attempts showed up before the patch did, major Dutch sites pulled PHP offline as a precaution, and within a day PHP shipped fixed releases for every supported branch.</p><p>The two bugs are <strong><a href="https://nvd.nist.gov/vuln/detail/cve-2026-109618">CVE-2026-109618</a></strong> &#8220;RCE via OOM corruption&#8221; and <strong><a href="https://nvd.nist.gov/vuln/detail/cve-2026-109619">CVE-2026-109619</a></strong>  &#8220;RCE via stale global state&#8221;. We&#8217;ll walk through how they work, down to the source lines, and then answer the question we kept asking ourselves while reproducing this in the lab: <strong>why did this class of bug exist in PHP for two decades before anyone could turn it into a pre-auth RCE?</strong></p><h2><strong>Background: how PHP runs a web request</strong></h2><p>Both bugs only make sense once you hold three facts about PHP&#8217;s execution model in mind at once. None of them is exotic, they&#8217;re just rarely stated together.</p><p><strong>First, a web request has a strict lifecycle.</strong> The SAPI (mod_php, FPM, cgi) accepts a connection and calls <code>php_request_startup()</code>: extensions get their RINIT, output layers activate, and critically the request body is parsed, before any application code runs. The script executes, then <code>php_request_shutdown()</code> tears everything down. On the happy path this cycle is airtight: startup completes, shutdown cleans up, in that order.</p><p><strong>Second, workers are long-lived but requests are not.</strong> A mod_php child or FPM worker serves thousands of requests in one process lifetime. At every request boundary the <em>request heap</em>: every <code>emalloc</code>&#8216;d string, array, object of that request is freed wholesale. But the process&#8217;s <em>global state</em> (executor globals <code>EG</code>, session globals <code>PS</code>, core globals <code>PG</code>) persists across requests. Anything that stores a request-heap pointer into one of those globals creates a pointer that outlives the memory it points at, unless something resets it in between. Normally, something does.</p><p><strong>Third, </strong><code>memory_limit</code><strong> is enforced by unwinding, not by refusing.</strong> PHP&#8217;s allocator (ZendMM) tracks every request allocation against a per-request cap. When the cap is hit, the engine doesn&#8217;t return an error code to the caller: it <em>aborts the request</em> by jumping. The jump mechanism is <code>zend_bailout()</code>, the <code>longjmp()</code> half of PHP&#8217;s <code>zend_try</code>/<code>zend_catch</code> error-control flow, which has been the engine&#8217;s answer to &#8220;C has no exceptions&#8221; since the PHP 4 era. An abort can therefore land at essentially any allocation site in a million lines of C, including, as it turns out, in the middle of request startup, where the state being built is reachable from shutdown but not yet valid.</p><p>Hold those three together: a lifecycle that assumes orderly startup-then-shutdown, globals that outlive requests, and an abort that can fire anywhere, then you can see the shape of the problem before we name the CVEs: <strong>if the abort fires during startup, the orderly part of the lifecycle never happens, but the globals and the shutdown code still do.</strong></p><h2><strong>What the two CVEs are&#8230;</strong></h2><p><strong>CVE-2026-109618, the longjmp problem:</strong> When PHP exhausts its <code>memory_limit</code>, the allocator historically raised a &#8220;safe error&#8221;: it printed a fatal error inside a <code>zend_try { }</code> block and then called <code>zend_bailout()</code> , which is a <code>longjmp()</code>, <code>zend_mm_safe_error</code>. A longjmp out of a context that was mid-initialization leaves that context&#8217;s memory half-built. And if the request gets <em>shut down anyway</em>, the shutdown code walks that half-built state as if it were valid. On Apache mod_php, that&#8217;s exactly what happens: the request shutdown routine runs unconditionally, even when request startup failed.</p><p><strong>CVE-2026-109619, the stale global problem.</strong> PHP workers are persistent: one process serves thousands of requests, and its global state (executor globals, session globals) survives between them. Some operations <em>publish</em> request-owned pointers into those worker globals and some operations <em>consume</em> them before the next request&#8217;s normal initialization would have replaced them. If a fatal error interrupts that publish-and-reset dance, the global keeps pointing into memory that&#8217;s already been freed with the request. The next request that touches the global, before initialization, dereferences freed memory: a use-after-free whose contents an attacker can reclaim. The mental model that finally made this click for us: <strong>the bug doesn&#8217;t live in one request. It lives between two requests, carried by a worker&#8217;s globals.</strong></p><h2><strong>The bug in code</strong></h2><p>Three functions carry the whole bug. The first is the allocator&#8217;s OOM handler, the mechanism behind the abort we just described. This is the old handler, removed by the patch:</p><pre><code><code>static ZEND_COLD ZEND_NORETURN void zend_mm_safe_error(zend_mm_heap *heap,
&#9;const char *format, size_t limit, /* ... */ size_t size)
{
&#9;heap-&gt;overflow = 1;
&#9;zend_try {
&#9;&#9;zend_error_noreturn(E_ERROR, format, limit, /* ... */ size);
&#9;} zend_catch {
&#9;} zend_end_try();
&#9;heap-&gt;overflow = 0;
&#9;zend_bailout();          /* &lt;-- longjmp out of the current context */
&#9;exit(1);
}

src/php-8.5.11-src/Zend/zend_alloc.c:395-420</code></code></pre><p>The bug is in the control flow, not in any single line. The function raises a fatal error <em>through PHP&#8217;s error machinery</em>, which itself allocates memory, hence the <code>heap-&gt;overflow</code> guard, and then calls <code>zend_bailout()</code>, the <code>longjmp()</code> from the background section: an unwinding jump to whatever <code>zend_try</code> frame is nearest. The implicit assumption is that any context reaching OOM can safely unwind from that point. For most contexts, that holds. For request startup, it doesn&#8217;t, and the chain section shows exactly who trips over the wreckage.</p><p>The patch replaces this with a <em>delayed</em> OOM (record the error, raise a flag, let the VM handle it at the next safe point) and a hard limit that simply terminates:</p><pre><code><code>/* new in 8.5.12: soft OOM is deferred, not unwound */
EG(delayed_oom).message = zend_string_init(buf, strlen(buf), true);
EG(delayed_oom).state   = ZEND_OOM_STATE_PENDING;
zend_atomic_bool_store_ex(&amp;EG(vm_interrupt), true);   /* handled at safe point */

/* new in 8.5.12: hard limit kills the process instead of unwinding */
_exit(137);

src/php-8.5.11-src/Zend/zend_alloc.c</code></code></pre><p>No <code>longjmp</code>, no half-built state for shutdown to trip over. The error still gets reported, it&#8217;s just no longer delivered by jumping out of whatever the engine was mid-way through.</p><p>The second function is the consumer side of the stale-state bug, one layer up in the session module. It&#8217;s the code that dereferences one of those dangling worker globals, <em>before</em> the next request&#8217;s initialization would have replaced it:</p><pre><code><code>static void php_session_track_init(void)
{
&#9;zval session_vars;
&#9;zend_string *var_name = ZSTR_INIT_LITERAL("_SESSION", 0);
&#9;/* Unconditionally destroy existing array -- possible dirty data */
&#9;zend_delete_global_variable(var_name);

&#9;if (!Z_ISUNDEF(PS(http_session_vars))) {
&#9;&#9;zval_ptr_dtor(&amp;PS(http_session_vars));   /* &lt;-- stale pointer */
&#9;}
&#9;/* ... */
}

src/php-8.5.11-src/ext/session/session.c:253-262</code></code></pre><p>The comment even hints at the hazard &#8220;possible dirty data&#8221; but the code only guards against <em>stale arrays</em>, not against the pointer itself being stale. <code>PS(http_session_vars)</code> is a <em>worker</em> global; after the abnormal request death it points into the freed request heap, and <code>zval_ptr_dtor</code> runs on it anyway. That&#8217;s the exact instruction our gdb session caught freeing a live <code>$_POST</code> key string.</p><p>The third is the 8.5-specific producer/consumer pair in <code>Zend/zend.c</code>, the same shape in a different subsystem. PHP 8.5 made fatal-error backtraces default-on, and that convenience feature stores request-owned data in a worker global:</p><pre><code><code>/* producer: any fatal error stores a request-owned backtrace
   into a WORKER global (8.5 default: fatal_error_backtraces=1) */
} else if (EG(fatal_error_backtrace_on) &amp;&amp; (type &amp; E_FATAL_ERRORS)) {
&#9;zend_fetch_debug_backtrace(&amp;EG(last_fatal_error_backtrace), 0, ...);
}

/* consumer: the NEXT error report &#8212; fatal or not &#8212; drops it */
zval_ptr_dtor(&amp;EG(last_fatal_error_backtrace));
ZVAL_UNDEF(&amp;EG(last_fatal_error_backtrace));

src/php-8.5.11-src/Zend/zend.c</code></code></pre><p>Producer and consumer, three lines apart in the same file; and 8.5.11 never resets the global at shutdown. The 8.5.12 patch adds the reset with a comment that reads like a confession: <em>&#8220;may be used by zend_error_zstr_at before init_executor&#8221;</em>. When the fix&#8217;s own comment describes the bug, you know the maintainers understood exactly what they were closing.</p><h2><strong>How it works: the chain, stage by stage</strong></h2><p>Everything hinges on one constraint, easy to miss in prose: <strong>the producer and consumer requests must land on the same worker</strong>, that&#8217;s what turns two harmless-looking requests into one use-after-free.</p><h3><strong>Stage 1. Making PHP run out of memory without breaking any rules</strong></h3><p>Every PHP install has two size limits for incoming data. <code>post_max_size</code> (8 MB by default) caps how big a request body may be, and <code>memory_limit</code> (128 MB by default) caps how much memory PHP may use while processing it. The trick is to blow past the second without ever tripping the first and the advisory describes three ways to do it, all of which we confirmed in source and in the patch&#8217;s own regression tests.</p><p>The first exploits a bookkeeping gap. <code>post_max_size</code> is checked against the <code>Content-Length</code> header, the number the client <em>declares</em>. A request sent in chunks (HTTP &#8220;chunked&#8221; encoding) simply has no such header, so the check passes and PHP keeps reading whatever shows up. We&#8217;ve watched real servers happily parse chunked bodies this way.</p><p>The second is a compression trick. One part of a multipart form may carry an effectively unlimited number of header lines. Each line costs about 12 bytes on the wire, but inside PHP each one becomes a copied key, a value buffer, and a linked-list entry, roughly eight times the space. So 19 MB of wire headers is enough to exhaust the default 128 MB memory limit. The patch added a regression test for exactly this scenario.</p><p>The third rides a PHP feature most sites never use: upload progress. When it&#8217;s on (the default), every file in an upload makes PHP decode and re-encode the entire session, thousands of times over for a large upload, until memory runs out [V].</p><p>The result looks like this:</p><pre><code><code>Fatal error: Allowed memory size of 134217728 bytes exhausted
(tried to allocate 4096 bytes) in Unknown on line 0</code></code></pre><p>The telling part is <code>in Unknown on line 0</code>: no script was running. PHP died <em>before the application even started</em>; the memory ran out while PHP was still reading the request. The first time we saw that line come back from a server we didn&#8217;t build, it was a strange feeling: the machine is telling you, in its own error format, that you&#8217;ve exhausted it before its code even woke up.</p><h3><strong>Stage 2. The cleanup that shouldn&#8217;t happen, but does</strong></h3><p>When PHP aborts a request mid-startup, what happens next depends entirely on the web server setup and this is the pivot of the whole exploit:</p><ul><li><p><strong>Apache with mod_php</strong> runs its normal end-of-request cleanup <em>anyway</em>, even though startup failed. That cleanup now walks over data that was only half-built.</p></li><li><p><strong>PHP-FPM</strong> does the opposite: the worker process simply exits, taking the broken state with it. Nothing dangerous survives.</p></li><li><p><strong>php-cgi</strong> behaves like FPM.</p></li></ul><p>That&#8217;s why the vendor rates mod_php highest, and why the generic, works-anywhere exploit is a mod_php exploit. FPM can still be exploited, but only through the second bug&#8217;s application-phase path (below).</p><h3><strong>Stage 3. The worker remembers things it shouldn&#8217;t</strong></h3><p>Here&#8217;s where the second bug comes in. PHP workers are long-lived: one process handles thousands of requests, and some of its internal bookkeeping: session data, IDs survives between them. During request startup, PHP&#8217;s upload-progress feature writes a few pointers into that long-lived memory: where this request&#8217;s session data lives, what its session ID is. Normally, cleanup wipes those pointers at the end of the request. On PHP 8.5 there&#8217;s a second way in: any fatal error saves a debugging backtrace into the same kind of long-lived storage.</p><h3><strong>Stage 4. The cleanup that never runs</strong></h3><p>The cleanup that would erase those pointers never gets its chance. The abort happened before the parser could signal &#8220;request finished,&#8221; and the session module&#8217;s shutdown routine doesn&#8217;t run because extensions were never activated. So mod_php frees all of the request&#8217;s memory and the worker&#8217;s long-lived pointers now aim at memory nobody owns anymore. That&#8217;s the whole bug in one sentence.</p><h3><strong>Stage 5. The next request stumbles into the trap</strong></h3><p>The attacker sends a second, ordinary-looking request to the same worker. It only needs two ordinary form fields: no files, no oversized anything. Because of how PHP processes uploads, this request touches those stale pointers <em>before</em> the normal startup code would have replaced them. The engine dereferences memory that was freed one request ago.</p><h3><strong>Watching it happen</strong></h3><p>This is where we stop summarizing the advisory and show what we saw ourselves. We built a lab based on PHP 8.5.11, mod_php, a single worker, completely default settings and attached a debugger. The stale pointer didn&#8217;t point at garbage. It pointed at memory the <em>next request had already reused</em> for its own data, specifically, the name of one of its form fields. PHP then &#8220;cleaned up&#8221; that pointer anyway, freeing memory the running request was actively using. Seeing that in a debugger was the moment the advisory stopped being abstract.</p><p>You don&#8217;t even need a debugger, the corruption is visible over plain HTTP. After the two requests, the server&#8217;s response contains fragments of raw memory: internal headers, addresses, leftover bytes. That&#8217;s an information leak, delivered in the response body.</p><p>Two properties make this practical rather than theoretical. First, PHP&#8217;s memory manager restarts each request from the same memory layout, so the corruption reproduces identically run after run. That determinism surprised us; we expected heap exploitation to be flakier. Second, the attacker can choose what lands in the freed memory: by shaping the form fields of the second request, we made the leaked bytes spell out <em>our own marker strings</em> . The advisory&#8217;s &#8220;strategically crafted heap layout&#8221; is exactly this.</p><h2><strong>Detection</strong></h2><p>If you&#8217;re defending PHP infrastructure, here&#8217;s what to watch:</p><ol><li><p><strong>Log signature (precondition firing).</strong> <code>Allowed memory size of ... in Unknown on line 0</code> showing up at request startup, especially if it repeats, means someone is exercising the CVE-2026-109618 precondition against you. We&#8217;ve verified this signature is produced by the attack shape above on vulnerable builds.</p></li><li><p><strong>Configuration exposure.</strong> The patch&#8217;s own regression tests enumerate the risky settings: <code>session.upload_progress.enabled=1</code> (default-on, the pre-init consumer), finite <code>memory_limit</code> with default <code>post_max_size</code> (the controllable abort), and on 8.5 <code>fatal_error_backtraces=1</code> (the backtrace producer/consumer pair). Any of these on an unpatched, internet-facing stack is exposure.</p></li><li><p><strong>Runtime (post-exploitation).</strong> The end state of this exploit class is a webshell: alert on PHP worker processes (<code>php-fpm</code>, <code>httpd</code>, <code>apache2</code>, <code>php-cgi</code>) spawning shells, interpreters, or download tools, the Huntress/Florian Roth <code>process-watcher</code> pattern.</p></li></ol><h3><strong>Why it survived for two decades</strong></h3><p>The reachability of this class is an <em>intersection</em>: a SAPI that mishandles failed startup (or an app-phase fatal), a default-on pre-init feature, a heap layout an attacker can shape, and a consumer that fires before initialization. Remove any one and the bug is a crash or a leak, not RCE. Fuzzers struggle with it precisely because the state spans <em>two requests on the same worker</em>, the php-src fuzzer SAPI exercises single requests. And the people best positioned to find it (Stefan Esser&#8217;s 2007&#8211;2010 era of engine-level UAF research) moved on; the 2017 policy decision to stop treating <code>unserialize()</code> bugs as security issues tells you where the community&#8217;s attention went instead, away from engine-internals memory corruption.</p><p>Why <em>now</em>? In Project Zero&#8217;s RCA template terms &#8220;thoughts on how this vuln might have been found&#8221; the honest answer has three parts. First, the fuzzer blind spot: the vulnerable state spans <em>two requests on the same worker</em>, and php-src&#8217;s fuzzer SAPI exercises single requests, so coverage tooling structurally misses it. Second, variant analysis: the bug class (producer publishes request-owned state into worker globals, interruption skips the reset, consumer fires pre-init) is enumerable once someone names it, the session-upload-progress and fatal-backtrace pairs are instances of the same shape. Third, the reporter credits on the advisories lcfr-hax, sagitzadik-wiz (Wiz), tomash-areion, amyb-oai alongside Calif.IO&#8217;s published &#8220;Month of AI-Discovered Bugs&#8221; methodology, suggest the discovery cost of this bug class just dropped. The bugs were always there; the search changed. We&#8217;d put money on more engine-level UAFs surfacing this way in the next couple of years, not because PHP got worse, but because looking got cheaper.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.vyprsec.ai/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading VyPr AI! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[4 CVEs in GStreamer: A Security Research Story]]></title><description><![CDATA[It started with a bug report that led somewhere unexpected.]]></description><link>https://blog.vyprsec.ai/p/4-cves-in-gstreamer-a-security-research</link><guid isPermaLink="false">https://blog.vyprsec.ai/p/4-cves-in-gstreamer-a-security-research</guid><dc:creator><![CDATA[Syafiq Al Atiiq]]></dc:creator><pubDate>Sun, 28 Dec 2025 15:37:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-Mb3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-Mb3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-Mb3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!-Mb3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!-Mb3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!-Mb3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-Mb3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png" width="400" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:400,&quot;bytes&quot;:1739043,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vyprsec.substack.com/i/182750055?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-Mb3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!-Mb3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!-Mb3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!-Mb3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d6888b0-ce9a-4187-8902-f348f17a26fb_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The best defense isn't building without holes; it's finding them first. Image generated by Nano Banana.</figcaption></figure></div><p>It started with a bug report that led somewhere unexpected.</p><p>Our AI had flagged something in GStreamer&#8217;s MIDI parser. Valgrind was reporting invalid memory reads. The pipeline was dying with &#8220;Internal data stream error.&#8221; We had found something worth investigating.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.vyprsec.ai/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading VyPr AI! Subscribe to receive notifications of new posts.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h3>The First Report</h3><p>We filed a confidential security report to GStreamer: &#8220;MIDI Parser Buffer Overflow.&#8221; Within hours, one of the core maintainers responded.</p><p>&#8220;That&#8217;s not a crash but expected error behaviour on a broken file,&#8221; they wrote. &#8220;I can&#8217;t reproduce this here. There&#8217;s no OOB read with git main or 1.26.8.&#8221;</p><p>Interesting. The latest version seemed clean. But then they added something that caught our attention: &#8220;Also there are clear possibilities for OOB reads in the code though, none of which should be triggered by your exploit file. I&#8217;ll provide a fix for those later here.&#8221;</p><p>They couldn&#8217;t reproduce our specific issue on the latest version, but they&#8217;d spotted other potential bugs while reviewing the code.</p><h3>Down the Version Rabbit Hole</h3><p>We went back to our test environment. Something wasn&#8217;t adding up. We ran the same exploit against different GStreamer versions:</p><p>&#8226; 1.24.2 (shipped in Ubuntu LTS): Valgrind showed OOB reads</p><p>&#8226; 1.26.0: Valgrind showed OOB reads  </p><p>&#8226; 1.26.8: Clean. No warnings.</p><p>The bug was real. It just wasn&#8217;t new. Someone had already fixed it between 1.26.0 and 1.26.8.</p><p>We reported back with our findings. The response: &#8220;The second commit in <a href="https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/8396">!8396</a> fixes the problem here... That fix is included in 1.26.2 and 1.24.13.&#8221;</p><p>So the bug had been patched upstream six months ago. But here&#8217;s the thing: we weren&#8217;t testing on bleeding-edge GStreamer. We were testing on Ubuntu 24.04 LTS, the version most people actually use. And Ubuntu 24.04 ships GStreamer 1.24.2, not 1.24.13.</p><p>The upstream fix existed. But millions of Ubuntu users were still running vulnerable code.</p><p>That was worth digging deeper.</p><p>And then came the interesting part.</p><h3>The Patch That Found More Bugs</h3><p>The maintainer had attached a patch to our bug report. Not just a fix for the issue we reported, but fixes for other OOB reads they&#8217;d spotted while reviewing the code. Our report had prompted them to audit the MIDI parser more carefully.</p><p>&#8220;Your report at least helped improving other parts of the code,&#8221; they wrote.</p><p>We decided to test the patch. Could we trigger those &#8220;other&#8221; OOB reads they&#8217;d identified?</p><p>Yes. Yes we could.</p><p>Within minutes, we had proof-of-concept files that triggered the new vulnerabilities on GStreamer 1.26.8. We shared them back. The maintainer confirmed they worked and that the patch caught them.</p><p>&#8220;I wonder if you can also get it to read more than 1 byte after the end,&#8221; they asked. Challenge accepted.</p><h3>The Final Count</h3><p>The back-and-forth with the GStreamer team led us to four distinct Out-of-Bounds Read vulnerabilities:</p><p>&#8226; <a href="https://www.cve.org/CVERecord?id=CVE-2025-67326">CVE-2025-67326</a>: Missing event data length validation</p><p>&#8226; <a href="https://www.cve.org/CVERecord?id=CVE-2025-67327">CVE-2025-67327</a>: Off-by-one error in the variable-length parser</p><p>&#8226; <a href="https://www.cve.org/CVERecord?id=CVE-2025-67328">CVE-2025-67328</a>: Missing boundary check in SysEx event parsing</p><p>&#8226; <a href="https://www.cve.org/CVERecord?id=CVE-2025-67329">CVE-2025-67329:</a> Missing boundary check in meta-event handling</p><p>Two of them (67326 and 67327) affect GStreamer 1.26.x and are fixed in 1.26.10. The other two (67328 and 67329) were already fixed in 1.24.13 and 1.26.2, but we requested CVEs anyway because Ubuntu LTS is still shipping 1.24.2.</p><p>We&#8217;ve published full technical write-ups for each vulnerability, including proof-of-concept scripts you can use to reproduce them:</p><p>&#8226; <a href="https://cve.vyprsec.ai/cve-2025-67326/">https://cve.vyprsec.ai/cve-2025-67326/</a></p><p>&#8226; <a href="https://cve.vyprsec.ai/cve-2025-67327/">https://cve.vyprsec.ai/cve-2025-67327/</a></p><p>&#8226; <a href="https://cve.vyprsec.ai/cve-2025-67328/">https://cve.vyprsec.ai/cve-2025-67328/</a></p><p>&#8226; <a href="https://cve.vyprsec.ai/cve-2025-67329/">https://cve.vyprsec.ai/cve-2025-67329/</a></p><p>Each page includes the exact Python script to generate a malicious MIDI file, the commands to run, and the Valgrind output showing the memory violation.</p><h3>Why Should You Care?</h3><p>You might be thinking: &#8220;Who even uses MIDI files anymore?&#8221;</p><p>Fair question. But GStreamer doesn&#8217;t care what file you meant to open. If your file manager tries to generate a thumbnail, if your media player scans your library, if any application auto-processes media files, GStreamer&#8217;s MIDI parser might get invoked.</p><p>GStreamer is everywhere:</p><p>&#8226; GNOME and KDE desktops</p><p>&#8226; Media players like Totem and Rhythmbox</p><p>&#8226; Video editors like Pitivi and OpenShot</p><p>&#8226; Embedded systems and IoT devices</p><p>The attack is trivial: drop a 30-byte file somewhere it&#8217;ll get processed. That&#8217;s it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7jqF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7jqF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 424w, https://substackcdn.com/image/fetch/$s_!7jqF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 848w, https://substackcdn.com/image/fetch/$s_!7jqF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!7jqF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!7jqF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg" width="524" height="499" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:499,&quot;width&quot;:524,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53542,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vyprsec.substack.com/i/182750055?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7jqF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 424w, https://substackcdn.com/image/fetch/$s_!7jqF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 848w, https://substackcdn.com/image/fetch/$s_!7jqF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!7jqF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48595699-16ac-421e-a4c5-146d0edc7eca_524x499.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The MIDI parser choosing the path of least resistance</figcaption></figure></div><h3>What This Proves</h3><p>This was a story of persistence and collaboration.</p><p>We didn&#8217;t stop at the first response. We dug deeper, tested across multiple versions, and when the maintainer shared a patch for related issues, we built proof-of-concept exploits (<strong>autonomously</strong>) that confirmed the vulnerabilities were real and exploitable. <strong>Within minutes&#8230;</strong></p><p>In the end, we:</p><p>&#8226; Identified that Ubuntu LTS was shipping a vulnerable version</p><p>&#8226; Created working PoC exploits for four separate code paths</p><p>&#8226; Handled the CVE registration process with MITRE</p><p>&#8226; Published detailed technical write-ups so others can learn and verify</p><p>The GStreamer team was responsive and professional throughout. They handled the code fixes. We handled the research, documentation, and disclosure coordination. That&#8217;s how security research should work.</p><h3>The Practical Takeaway</h3><p>If you&#8217;re running Linux, check your GStreamer version:</p><p>gst-launch-1.0 --version</p><p>Fixed versions:</p><p>&#8226; 1.24.13 or later</p><p>&#8226; 1.26.2 or later (1.26.10 for all four CVEs)</p><p>As of late December 2025, most distributions haven&#8217;t shipped these updates. Track them here:</p><p>&#8226; Ubuntu: <a href="https://ubuntu.com/security/cves">https://ubuntu.com/security/cves</a></p><p>&#8226; Debian: <a href="https://security-tracker.debian.org/tracker/">https://security-tracker.debian.org/tracker/</a></p><p>&#8226; Fedora: <a href="https://bodhi.fedoraproject.org/">https://bodhi.fedoraproject.org/</a></p><h3>References</h3><p>&#8226; GStreamer Security Advisory SA-2025-0008: <a href="https://gstreamer.freedesktop.org/security/sa-2025-0008.html">https://gstreamer.freedesktop.org/security/sa-2025-0008.html</a></p><p>&#8226; GStreamer Security Advisory SA-2025-0009: <a href="https://gstreamer.freedesktop.org/security/sa-2025-0009.html">https://gstreamer.freedesktop.org/security/sa-2025-0009.html</a></p><p>&#8226; Our CVE write-ups with PoC scripts: <a href="https://cve.vyprsec.ai">https://cve.vyprsec.ai</a></p><h3>About VyPr AI</h3><p>We build AI that finds security vulnerabilities. Every report starts a conversation, and sometimes that conversation leads somewhere unexpected.</p><p>These four CVEs came from following a trail of clues across multiple GStreamer versions. That&#8217;s security research: persistent, collaborative, and always evolving.</p><p>Interested in what we might find in your software? Reach out at contact@vyprsec.ai</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.vyprsec.ai/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading VyPr AI! Subscribe to receive notifications of new posts.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>